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:
@@ -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
@@ -146,7 +146,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.1"
|
version: "1.15.1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
@@ -246,7 +246,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -390,7 +390,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ dependencies:
|
|||||||
audioplayers: ^6.7.1
|
audioplayers: ^6.7.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
http: ^1.6.0
|
||||||
|
crypto: ^3.0.7
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user