From e7afda4bd2f831409e34b395c86e8723620fb52a Mon Sep 17 00:00:00 2001 From: Dillon Nys Date: Mon, 19 Feb 2024 15:04:47 -0800 Subject: [PATCH] chore(core): Migrate JsonValue to extension types --- .../lib/src/serialization/json_value.dart | 263 ++++-------------- .../celest_core/test/json_value_test.dart | 103 +++---- 2 files changed, 85 insertions(+), 281 deletions(-) diff --git a/packages/celest_core/lib/src/serialization/json_value.dart b/packages/celest_core/lib/src/serialization/json_value.dart index 0a03c831..2445a685 100644 --- a/packages/celest_core/lib/src/serialization/json_value.dart +++ b/packages/celest_core/lib/src/serialization/json_value.dart @@ -1,7 +1,3 @@ -import 'dart:collection'; -import 'dart:convert'; - -import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; // ignore_for_file: avoid_positional_boolean_parameters @@ -21,8 +17,15 @@ import 'package:meta/meta.dart'; /// this class does. This is because [int] and [double] are not interchangeable /// in Dart and we want to preserve the type information. @immutable -sealed class JsonValue { - const JsonValue._(); +extension type const JsonValue._(Object value) { + factory JsonValue(Object value) { + return switch (value) { + String() || num() || bool() || List() || Map() => value as JsonValue, + _ => throw FormatException( + 'Unsupported JSON value: $value (${value.runtimeType})', + ), + }; + } /// Creates a [JsonString] from [value]. const factory JsonValue.string(String value) = JsonString; @@ -42,185 +45,64 @@ sealed class JsonValue { /// Creates a [JsonMap] from [value]. const factory JsonValue.map(Map value) = JsonMap; - /// Creates a [JsonValue] from [value]. - static JsonValue? from(Object? value) { - return switch (value) { - null => null, - JsonValue() => value, - String() => JsonString(value), - int() => JsonInt(value), - double() => JsonDouble(value), - bool() => JsonBool(value), - List() => JsonList(value), - Map() => JsonMap(value), - Map() => JsonMap(value.cast()), - _ => throw FormatException( - 'Unsupported JSON value: $value (${value.runtimeType})', - ), - }; - } - - /// The wrapped JSON value. - Object? get wrapped; - List get _path; - T _expect(String key, Object? value) { if (value is T) { return value; } - throw FormatException( - 'Expected $T for key "${[..._path, key].join('.')}" but got: $value', - ); + throw FormatException('Expected $T for key "$key" but got: $value'); } - - @override - bool operator ==(Object other) => - identical(this, other) || other is JsonValue && wrapped == other.wrapped; - - @override - int get hashCode => wrapped.hashCode; - - @override - String toString() => wrapped.toString(); } -/// A [JsonValue] which is guaranteed to be a [String]. -final class JsonString extends JsonValue { - /// Creates a [JsonString] from [wrapped]. - const JsonString(this.wrapped) - : _path = const [], - super._(); +/// A [JsonValue] which represents a [String]. +extension type const JsonString(String value) implements JsonValue, String {} - const JsonString._(this.wrapped, [this._path = const []]) : super._(); +/// A [JsonValue] which represents a [num]. +extension type const JsonNum(num value) implements JsonValue, num { + /// Converts this to a [JsonInt]. + JsonInt toInt() => JsonInt(value.toInt()); - @override - final String wrapped; - - @override - final List _path; + /// Converts this to a [JsonDouble]. + JsonDouble toDouble() => JsonDouble(value.toDouble()); } -/// A [JsonValue] which is guaranteed to be a [num]. -mixin JsonNum on JsonValue { - @override - num get wrapped; - - /// Converts this [JsonNum] to an [JsonInt]. - JsonInt toInt() => JsonInt._(wrapped.toInt(), _path); +/// A [JsonValue] which represents an [int]. +extension type const JsonInt(int value) implements JsonNum, int { + @redeclare + JsonInt toInt() => this; - /// Converts this [JsonNum] to an [JsonDouble]. - JsonDouble toDouble() => JsonDouble._(wrapped.toDouble(), _path); + @redeclare + JsonDouble toDouble() => JsonDouble(value.toDouble()); } -/// A [JsonValue] which is guaranteed to be an [int]. -final class JsonInt extends JsonValue with JsonNum { - /// Creates a [JsonInt] from [wrapped]. - const JsonInt(this.wrapped) - : _path = const [], - super._(); - - const JsonInt._(this.wrapped, [this._path = const []]) : super._(); +/// A [JsonValue] which represents a [double]. +extension type const JsonDouble(double value) implements JsonNum, double { + @redeclare + JsonInt toInt() => JsonInt(value.toInt()); - @override - final int wrapped; - - @override - final List _path; + @redeclare + JsonDouble toDouble() => this; } -/// A [JsonValue] which is guaranteed to be a [double]. -final class JsonDouble extends JsonValue with JsonNum { - /// Creates a [JsonDouble] from [wrapped]. - const JsonDouble(this.wrapped) - : _path = const [], - super._(); - - const JsonDouble._(this.wrapped, [this._path = const []]) : super._(); +/// A [JsonValue] which represents a [bool]. +extension type const JsonBool(bool value) implements JsonValue, bool {} - @override - final double wrapped; - - @override - final List _path; +/// A [JsonValue] which represents a [List]. +extension type const JsonList._(List value) + implements JsonValue, List { + const JsonList(List value) : this._(value as List); } -/// A [JsonValue] which is guaranteed to be a [bool]. -final class JsonBool extends JsonValue { - /// Creates a [JsonBool] from [wrapped]. - const JsonBool(this.wrapped) - : _path = const [], - super._(); - - const JsonBool._(this.wrapped, [this._path = const []]) : super._(); - - @override - final bool wrapped; - - @override - final List _path; -} - -/// A [JsonValue] which is guaranteed to be a [List]. -final class JsonList extends JsonValue with ListMixin { - /// Creates a [JsonList] from [wrapped]. - const JsonList(this.wrapped) - : _path = const [], - super._(); - - const JsonList._(this.wrapped, [this._path = const []]) : super._(); - - @override - final List wrapped; - - @override - final List _path; - - @override - int get length => wrapped.length; - - @override - set length(int length) => wrapped.length = length; - - @override - JsonValue? operator [](int index) => JsonValue.from(wrapped[index]); - - @override - void operator []=(int index, JsonValue? value) { - wrapped[index] = value?.wrapped; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is JsonList && _deepEquals(wrapped, other.wrapped); - - @override - int get hashCode => _deepHash(wrapped); - - @override - String toString() => _jsonEncoder.convert(wrapped); -} - -/// A [JsonValue] which is guaranteed to be a [Map]. -final class JsonMap extends JsonValue with MapMixin { - /// Creates a [JsonMap] from [wrapped]. - const JsonMap(this.wrapped) - : _path = const [], - super._(); - - const JsonMap._(this.wrapped, [this._path = const []]) : super._(); - - @override - final Map wrapped; - - @override - final List _path; +/// A [JsonValue] which represents a [Map]. +extension type const JsonMap._(Map value) + implements JsonValue, Map { + const JsonMap(Map value) + : this._(value as Map); /// Returns the string associated with [key] or `null` if [key] is not in the /// map. JsonString? optionalString(String key) { - if (wrapped[key] case final value?) { - return JsonString._(_expect(key, value), [..._path, key]); + if (value[key] case final value?) { + return JsonString(_expect(key, value)); } return null; } @@ -234,8 +116,8 @@ final class JsonMap extends JsonValue with MapMixin { /// Returns the int associated with [key] or `null` if [key] is not in the /// map. JsonInt? optionalInt(String key) { - if (wrapped[key] case final value?) { - return JsonInt._(_expect(key, value).toInt(), [..._path, key]); + if (value[key] case final value?) { + return JsonInt(_expect(key, value).toInt()); } return null; } @@ -249,8 +131,8 @@ final class JsonMap extends JsonValue with MapMixin { /// Returns the double associated with [key] or `null` if [key] is not in the /// map. JsonDouble? optionalDouble(String key) { - if (wrapped[key] case final value?) { - return JsonDouble._(_expect(key, value).toDouble(), [..._path, key]); + if (value[key] case final value?) { + return JsonDouble(_expect(key, value).toDouble()); } return null; } @@ -264,8 +146,8 @@ final class JsonMap extends JsonValue with MapMixin { /// Returns the bool associated with [key] or `null` if [key] is not in the /// map. JsonBool? optionalBool(String key) { - if (wrapped[key] case final value?) { - return JsonBool._(_expect(key, value), [..._path, key]); + if (value[key] case final value?) { + return JsonBool(_expect(key, value)); } return null; } @@ -279,8 +161,8 @@ final class JsonMap extends JsonValue with MapMixin { /// Returns the list associated with [key] or `null` if [key] is not in the /// map. JsonList? optionalList(String key) { - if (wrapped[key] case final value?) { - return JsonList._(_expect(key, value), [..._path, key]); + if (value[key] case final value?) { + return JsonList(_expect(key, value)); } return null; } @@ -294,14 +176,8 @@ final class JsonMap extends JsonValue with MapMixin { /// Returns the map associated with [key] or `null` if [key] is not in the /// map. JsonMap? optionalMap(String key) { - if (wrapped[key] case final value?) { - return JsonMap._( - switch (value) { - Map() => value, - _ => _expect>(key, value).cast(), - }, - [..._path, key], - ); + if (value[key] case final value?) { + return JsonMap(_expect(key, value)); } return null; } @@ -311,37 +187,4 @@ final class JsonMap extends JsonValue with MapMixin { JsonMap requiredMap(String key) { return _expect(key, optionalMap(key)); } - - @override - JsonValue? operator [](Object? key) => JsonValue.from(wrapped[key]); - - @override - void operator []=(String key, JsonValue? value) { - wrapped[key] = value?.wrapped; - } - - @override - void clear() => wrapped.clear(); - - @override - Iterable get keys => wrapped.keys; - - @override - JsonValue? remove(Object? key) => JsonValue.from(wrapped.remove(key)); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is JsonMap && _deepEquals(wrapped, other.wrapped); - - @override - int get hashCode => _deepHash(wrapped); - - @override - String toString() => _jsonEncoder.convert(wrapped); } - -const _jsonEncoder = JsonEncoder.withIndent(' '); -bool _deepEquals(Object? a, Object? b) => - const DeepCollectionEquality().equals(a, b); -int _deepHash(Object? a) => const DeepCollectionEquality().hash(a); diff --git a/packages/celest_core/test/json_value_test.dart b/packages/celest_core/test/json_value_test.dart index 612ae7c2..5894a3c4 100644 --- a/packages/celest_core/test/json_value_test.dart +++ b/packages/celest_core/test/json_value_test.dart @@ -8,50 +8,46 @@ void main() { group('JsonValue', () { test('primitives', () { expect( - JsonValue.from('abc'), - isA().having((j) => j.wrapped, 'value', 'abc'), + JsonValue('abc'), + isA().having((j) => j.value, 'value', 'abc'), ); expect( - JsonValue.from(123), - isA().having((j) => j.wrapped, 'value', 123), + JsonValue(123), + isA().having((j) => j.value, 'value', 123), ); expect( - JsonValue.from(123.456), - isA().having((j) => j.wrapped, 'value', 123.456), + JsonValue(123.456), + isA().having((j) => j.value, 'value', 123.456), ); expect( - JsonValue.from(true), - isA().having((j) => j.wrapped, 'value', true), + JsonValue(true), + isA().having((j) => j.value, 'value', true), ); expect( - JsonValue.from(null), - isNull, - ); - expect( - JsonValue.from([1, 2, 3]), + JsonValue([1, 2, 3]), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', [1, 2, 3], ), ); expect( - JsonValue.from({'a': 1, 'b': 2, 'c': 3}), + JsonValue({'a': 1, 'b': 2, 'c': 3}), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', {'a': 1, 'b': 2, 'c': 3}, ), ); expect( - () => JsonValue.from(RegExp('unsupported')), + () => JsonValue(RegExp('unsupported')), throwsA(isA()), ); }); test('JsonList', () { expect( - JsonValue.from([1, 2, 3]), + JsonValue([1, 2, 3]), orderedEquals([ JsonInt(1), JsonInt(2), @@ -59,14 +55,12 @@ void main() { ]), ); expect( - JsonValue.from([ + JsonValue([ 'a', 1, 1.23, true, null, - [1, 2, 3], - {'a': 1}, ]), orderedEquals([ JsonString('a'), @@ -74,24 +68,20 @@ void main() { JsonDouble(1.23), JsonBool(true), null, - JsonList([1, 2, 3]), - JsonMap({ - 'a': 1, - }), ]), ); }); test('JsonMap', () { expect( - JsonValue.from({'a': 1, 'b': 2, 'c': 3}), + JsonValue({'a': 1, 'b': 2, 'c': 3}), equals({ 'a': JsonInt(1), 'b': JsonInt(2), 'c': JsonInt(3), }), ); - final allTypes = { + final allTypes = { 'a': 'a', 'b': 1, 'c': 1.23, @@ -100,7 +90,7 @@ void main() { 'f': [1, 2, 3], 'g': {'a': 1}, }; - final allTypesJson = JsonValue.from(allTypes) as JsonMap; + final allTypesJson = JsonValue(allTypes) as JsonMap; expect( allTypesJson, equals({ @@ -115,41 +105,27 @@ void main() { }), }), ); - expect( - allTypesJson.values, - orderedEquals([ - JsonString('a'), - JsonInt(1), - JsonDouble(1.23), - JsonBool(true), - null, - JsonList([1, 2, 3]), - JsonMap({ - 'a': 1, - }), - ]), - ); expect( allTypesJson.optionalString('a'), - isA().having((j) => j.wrapped, 'value', 'a'), + isA().having((j) => j.value, 'value', 'a'), ); expect( allTypesJson.optionalInt('b'), - isA().having((j) => j.wrapped, 'value', 1), + isA().having((j) => j.value, 'value', 1), ); expect( allTypesJson.optionalDouble('c'), - isA().having((j) => j.wrapped, 'value', 1.23), + isA().having((j) => j.value, 'value', 1.23), ); expect( allTypesJson.optionalBool('d'), - isA().having((j) => j.wrapped, 'value', true), + isA().having((j) => j.value, 'value', true), ); expect( allTypesJson.optionalList('f'), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', [1, 2, 3], ), @@ -157,7 +133,7 @@ void main() { expect( allTypesJson.optionalMap('g'), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', {'a': 1}, ), @@ -165,24 +141,24 @@ void main() { expect( allTypesJson.requiredString('a'), - isA().having((j) => j.wrapped, 'value', 'a'), + isA().having((j) => j.value, 'value', 'a'), ); expect( allTypesJson.requiredInt('b'), - isA().having((j) => j.wrapped, 'value', 1), + isA().having((j) => j.value, 'value', 1), ); expect( allTypesJson.requiredDouble('c'), - isA().having((j) => j.wrapped, 'value', 1.23), + isA().having((j) => j.value, 'value', 1.23), ); expect( allTypesJson.requiredBool('d'), - isA().having((j) => j.wrapped, 'value', true), + isA().having((j) => j.value, 'value', true), ); expect( allTypesJson.requiredList('f'), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', [1, 2, 3], ), @@ -190,7 +166,7 @@ void main() { expect( allTypesJson.requiredMap('g'), isA().having( - (j) => j.wrapped, + (j) => j.value, 'value', {'a': 1}, ), @@ -235,29 +211,14 @@ void main() { .requiredMap('aMap') .requiredMap('aNestedMap') .requiredString('aNestedKey'), - isA().having((j) => j.wrapped, 'value', 'abc'), + isA().having((j) => j.value, 'value', 'abc'), ); expect( nestedJson .requiredMap('aMap') .optionalMap('aNestedMap') ?.optionalString('aNestedKey'), - isA().having((j) => j.wrapped, 'value', 'abc'), - ); - expect( - () => nestedJson - .requiredMap('aMap') - .requiredMap('aNestedMap') - .requiredString('aNonExistentKey'), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('aMap.aNestedMap.aNonExistentKey'), - ), - ), - reason: 'Accessing a non-existent key on a nested map should throw ' - 'an exception with the path to the requested key.', + isA().having((j) => j.value, 'value', 'abc'), ); }); });