From c7bdb9b9c953a33573590476b57fd83ec257ab2f Mon Sep 17 00:00:00 2001 From: airkjw Date: Fri, 12 Jun 2026 13:02:17 +0900 Subject: [PATCH] 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 --- lib/data/remote/manifest.dart | 71 +++++++++++++++++++++++++++++ pubspec.lock | 6 +-- pubspec.yaml | 3 ++ test/data/remote/manifest_test.dart | 50 ++++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 lib/data/remote/manifest.dart create mode 100644 test/data/remote/manifest_test.dart diff --git a/lib/data/remote/manifest.dart b/lib/data/remote/manifest.dart new file mode 100644 index 0000000..dbb25c2 --- /dev/null +++ b/lib/data/remote/manifest.dart @@ -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 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), + ]; + 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 seasons; + + Map 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 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 toJson() => { + 'seasonId': seasonId, + 'version': version, + 'packUrl': packUrl, + 'sha256': sha256, + }; +} diff --git a/pubspec.lock b/pubspec.lock index 3c4d887..cf6931a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index c1f5c9f..d120b03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/data/remote/manifest_test.dart b/test/data/remote/manifest_test.dart new file mode 100644 index 0000000..1562d4b --- /dev/null +++ b/test/data/remote/manifest_test.dart @@ -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'); + }); +}