Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions lib/src/core/model/flag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -57,6 +64,7 @@ class Flag {
{int? id,
required Feature feature,
String? stateValue,
Object? value,
bool? enabled,
int? environment,
int? identity,
Expand All @@ -68,6 +76,7 @@ class Flag {
id: id,
feature: feature,
stateValue: stateValue,
value: value,
enabled: enabled,
environment: environment,
identity: identity,
Expand All @@ -94,15 +103,18 @@ class Flag {
enabled: enabled);
}

factory Flag.fromJson(Map<String, dynamic> json) => _$FlagFromJson(json);
factory Flag.fromJson(Map<String, dynamic> json) =>
_$FlagFromJson(json).copyWith(value: json['feature_state_value']);

Map<String, dynamic> toJson() => _$FlagToJson(this);
Map<String, dynamic> 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,
Expand All @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions test/core/models/flag_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,71 @@ void main() {
});
});

group('[Typed value]', () {
Map<String, dynamic> flagJson(Object? value) => <String, dynamic>{
'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<String, dynamic>);
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(
Expand Down
Loading