Skip to content

Commit

Permalink
chore(core): Migrate JsonValue to extension types
Browse files Browse the repository at this point in the history
  • Loading branch information
dnys1 committed Feb 19, 2024
1 parent 0673120 commit e7afda4
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 281 deletions.
263 changes: 53 additions & 210 deletions packages/celest_core/lib/src/serialization/json_value.dart
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -42,185 +45,64 @@ sealed class JsonValue {
/// Creates a [JsonMap] from [value].
const factory JsonValue.map(Map<String, Object?> 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<Object?>() => JsonList(value),
Map<String, Object?>() => JsonMap(value),
Map() => JsonMap(value.cast()),
_ => throw FormatException(
'Unsupported JSON value: $value (${value.runtimeType})',
),
};
}

/// The wrapped JSON value.
Object? get wrapped;
List<String> get _path;

T _expect<T>(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<String> _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<String> _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<String> _path;
/// A [JsonValue] which represents a [List].
extension type const JsonList._(List<JsonValue?> value)
implements JsonValue, List<JsonValue?> {
const JsonList(List<Object?> value) : this._(value as List<JsonValue?>);
}

/// 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<String> _path;
}

/// A [JsonValue] which is guaranteed to be a [List].
final class JsonList extends JsonValue with ListMixin<JsonValue?> {
/// Creates a [JsonList] from [wrapped].
const JsonList(this.wrapped)
: _path = const [],
super._();

const JsonList._(this.wrapped, [this._path = const []]) : super._();

@override
final List<Object?> wrapped;

@override
final List<String> _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<String, JsonValue?> {
/// Creates a [JsonMap] from [wrapped].
const JsonMap(this.wrapped)
: _path = const [],
super._();

const JsonMap._(this.wrapped, [this._path = const []]) : super._();

@override
final Map<String, Object?> wrapped;

@override
final List<String> _path;
/// A [JsonValue] which represents a [Map].
extension type const JsonMap._(Map<String, JsonValue?> value)
implements JsonValue, Map<String, JsonValue?> {
const JsonMap(Map<String, Object?> value)
: this._(value as Map<String, JsonValue?>);

/// 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;
}
Expand All @@ -234,8 +116,8 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
/// 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<num>(key, value).toInt(), [..._path, key]);
if (value[key] case final value?) {
return JsonInt(_expect<num>(key, value).toInt());
}
return null;
}
Expand All @@ -249,8 +131,8 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
/// 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<num>(key, value).toDouble(), [..._path, key]);
if (value[key] case final value?) {
return JsonDouble(_expect<num>(key, value).toDouble());
}
return null;
}
Expand All @@ -264,8 +146,8 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
/// 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;
}
Expand All @@ -279,8 +161,8 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
/// 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;
}
Expand All @@ -294,14 +176,8 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
/// 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<String, Object?>() => value,
_ => _expect<Map<Object?, Object?>>(key, value).cast(),
},
[..._path, key],
);
if (value[key] case final value?) {
return JsonMap(_expect(key, value));
}
return null;
}
Expand All @@ -311,37 +187,4 @@ final class JsonMap extends JsonValue with MapMixin<String, JsonValue?> {
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<String> 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);
Loading

0 comments on commit e7afda4

Please sign in to comment.