feat: remote manifest model and content dependencies

Add http, crypto, path_provider deps; introduce RemoteManifest /
ManifestSeason immutable models with fromJson/toJson, schema-version
guard (FormatException on unsupported schema), and fallback for missing
current field. 3/3 TDD tests pass, flutter analyze clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:02:17 +09:00
parent 63ac8c6b9e
commit c7bdb9b9c9
4 changed files with 127 additions and 3 deletions
+71
View File
@@ -0,0 +1,71 @@
/// Index of remotely available seasons, served as a static JSON next to the
/// pack files. The client compares (seasonId, version) against its cache.
class RemoteManifest {
const RemoteManifest({
required this.schemaVersion,
required this.minAppBuild,
required this.current,
required this.seasons,
});
static const int supportedSchema = 1;
factory RemoteManifest.fromJson(Map<String, dynamic> json) {
final schema = json['schemaVersion'] as int;
if (schema > supportedSchema) {
throw FormatException('Unsupported manifest schema: $schema');
}
final seasons = [
for (final s in json['seasons'] as List)
ManifestSeason.fromJson(s as Map<String, dynamic>),
];
return RemoteManifest(
schemaVersion: schema,
minAppBuild: (json['minAppBuild'] as int?) ?? 1,
current: (json['current'] as String?) ??
(seasons.isNotEmpty ? seasons.last.seasonId : ''),
seasons: seasons,
);
}
final int schemaVersion;
final int minAppBuild;
final String current;
final List<ManifestSeason> seasons;
Map<String, dynamic> toJson() => {
'schemaVersion': schemaVersion,
'minAppBuild': minAppBuild,
'current': current,
'seasons': [for (final s in seasons) s.toJson()],
};
}
class ManifestSeason {
const ManifestSeason({
required this.seasonId,
required this.version,
required this.packUrl,
required this.sha256,
});
factory ManifestSeason.fromJson(Map<String, dynamic> json) =>
ManifestSeason(
seasonId: json['seasonId'] as String,
version: json['version'] as int,
packUrl: json['packUrl'] as String,
sha256: json['sha256'] as String,
);
final String seasonId;
final int version;
final String packUrl;
final String sha256;
Map<String, dynamic> toJson() => {
'seasonId': seasonId,
'version': version,
'packUrl': packUrl,
'sha256': sha256,
};
}
+3 -3
View File
@@ -146,7 +146,7 @@ packages:
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -246,7 +246,7 @@ packages:
source: hosted
version: "2.1.3"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@@ -390,7 +390,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+3
View File
@@ -39,6 +39,9 @@ dependencies:
audioplayers: ^6.7.1
flutter_localizations:
sdk: flutter
http: ^1.6.0
crypto: ^3.0.7
path_provider: ^2.1.5
dev_dependencies:
flutter_test:
+50
View File
@@ -0,0 +1,50 @@
import 'package:block_seasons/data/remote/manifest.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final json = {
'schemaVersion': 1,
'minAppBuild': 1,
'current': 'season_002',
'seasons': [
{
'seasonId': 'season_001',
'version': 1,
'packUrl': 'seasons/season_001/pack.json',
'sha256': 'abc123',
},
{
'seasonId': 'season_002',
'version': 3,
'packUrl': 'seasons/season_002/pack.json',
'sha256': 'def456',
},
],
};
test('round-trips through json', () {
final manifest = RemoteManifest.fromJson(json);
expect(manifest.schemaVersion, 1);
expect(manifest.minAppBuild, 1);
expect(manifest.current, 'season_002');
expect(manifest.seasons, hasLength(2));
expect(manifest.seasons[1].seasonId, 'season_002');
expect(manifest.seasons[1].version, 3);
expect(manifest.seasons[1].packUrl, 'seasons/season_002/pack.json');
expect(manifest.seasons[1].sha256, 'def456');
expect(RemoteManifest.fromJson(manifest.toJson()).toJson(),
manifest.toJson());
});
test('rejects unsupported schema', () {
expect(
() => RemoteManifest.fromJson({...json, 'schemaVersion': 99}),
throwsFormatException,
);
});
test('missing current falls back to last season id', () {
final m = RemoteManifest.fromJson({...json}..remove('current'));
expect(m.current, 'season_002');
});
}