diff --git a/lib/src/core/model/flag.dart b/lib/src/core/model/flag.dart index 27fdd5a..ed83ba4 100644 --- a/lib/src/core/model/flag.dart +++ b/lib/src/core/model/flag.dart @@ -17,6 +17,11 @@ class Flag { fromJson: stringFromJson, toJson: stringToJson) final String? stateValue; + + /// The flag's value with its JSON type. + /// Falls back to [stateValue] when the flag was not parsed from JSON. + @JsonKey(includeFromJson: false, includeToJson: false) + final Object? value; final bool? enabled; final int? environment; final int? identity; @@ -37,13 +42,15 @@ class Flag { {this.id, required this.feature, this.stateValue, + Object? value, this.enabled, this.environment, this.identity, this.featureSegment, this.variant, this.reason, - this.experiment}); + this.experiment}) + : value = value ?? stateValue; String get key => feature.name; @override @@ -57,6 +64,7 @@ class Flag { {int? id, required Feature feature, String? stateValue, + Object? value, bool? enabled, int? environment, int? identity, @@ -68,6 +76,7 @@ class Flag { id: id, feature: feature, stateValue: stateValue, + value: value, enabled: enabled, environment: environment, identity: identity, @@ -94,15 +103,18 @@ class Flag { enabled: enabled); } - factory Flag.fromJson(Map json) => _$FlagFromJson(json); + factory Flag.fromJson(Map json) => + _$FlagFromJson(json).copyWith(value: json['feature_state_value']); - Map toJson() => _$FlagToJson(this); + Map toJson() => + _$FlagToJson(this)..['feature_state_value'] = value; String asString() => jsonEncode(toJson()); Flag copyWith( {int? id, Feature? feature, String? stateValue, + Object? value, bool? enabled, int? environment, int? identity, @@ -114,6 +126,8 @@ class Flag { id: id ?? this.id, feature: feature ?? this.feature, stateValue: stateValue ?? this.stateValue, + // A new stateValue also replaces value. + value: value ?? (stateValue == null ? this.value : null), enabled: enabled ?? this.enabled, environment: environment ?? this.environment, identity: identity ?? this.identity, diff --git a/test/core/models/flag_test.dart b/test/core/models/flag_test.dart index ed705b7..19a094c 100644 --- a/test/core/models/flag_test.dart +++ b/test/core/models/flag_test.dart @@ -197,6 +197,71 @@ void main() { }); }); + group('[Typed value]', () { + Map flagJson(Object? value) => { + 'feature': {'id': 1, 'name': 'max_items'}, + 'enabled': true, + 'feature_state_value': value, + }; + + for (final (value, stateValue) in [ + (42, '42'), + (1.5, '1.5'), + (true, 'true'), + ('welcome', 'welcome'), + (null, null), + ]) { + test( + 'When feature_state_value is ${value.runtimeType}, then value ' + 'keeps its type', () { + final flag = Flag.fromJson(flagJson(value)); + expect(flag.value, value); + expect(flag.value.runtimeType, value.runtimeType); + expect(flag.stateValue, stateValue); + }); + + test( + 'When a ${value.runtimeType} flag round-trips through toJson, ' + 'then value keeps its type', () { + final restored = Flag.fromJson( + jsonDecode(Flag.fromJson(flagJson(value)).asString()) + as Map); + expect(restored.value, value); + expect(restored.value.runtimeType, value.runtimeType); + expect(restored.stateValue, stateValue); + }); + } + + test('When a flag is restored from storage, then value keeps its type', + () async { + final store = StorageProvider(InMemoryStorage(), password: 'pa5w0rD'); + await store.saveAll([Flag.fromJson(flagJson(42))]); + final restored = await store.read('max_items'); + expect(restored!.value, 42); + expect(restored.stateValue, '42'); + }); + + test('When cached before typed values, then value is the cached string', + () { + final flag = Flag.fromJson(flagJson('42')); + expect(flag.value, '42'); + expect(flag.stateValue, '42'); + }); + + test('When a flag is created without a value, then value is stateValue', + () { + expect(Flag.seed('feature', value: '1.0.0').value, '1.0.0'); + expect(Flag.seed('feature').value, isNull); + }); + + test('When copyWith sets only stateValue, then value follows it', () { + final flag = Flag.fromJson(flagJson(42)); + expect(flag.copyWith(stateValue: '43').value, '43'); + expect(flag.copyWith(enabled: false).value, 42); + expect(flag.copyWith(value: 43).value, 43); + }); + }); + group('[FlagAndTraits]', () { test('When response successfuly parsed', () { final identity = FlagsAndTraits.fromJson(