diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml new file mode 100644 index 00000000..de6ce9d7 --- /dev/null +++ b/.github/workflows/coder.yaml @@ -0,0 +1,36 @@ +name: coder +on: + pull_request: + paths: + - ".github/workflows/coder.yaml" + - "packages/coder/**" + +# Prevent duplicate runs due to Graphite +# https://graphite.dev/docs/troubleshooting#why-are-my-actions-running-twice +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || ''}} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Git Checkout + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # 4.1.5 + - name: Setup Flutter + uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # 2.16.0 + with: + cache: true + - name: Get Packages + working-directory: packages/coder + run: dart pub get + - name: Analyze + working-directory: packages/coder + run: dart analyze + - name: Format + working-directory: packages/coder + run: dart format --set-exit-if-changed . + - name: Test + working-directory: packages/coder + run: dart test diff --git a/packages/coder/.gitignore b/packages/coder/.gitignore new file mode 100644 index 00000000..3cceda55 --- /dev/null +++ b/packages/coder/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/coder/CHANGELOG.md b/packages/coder/CHANGELOG.md new file mode 100644 index 00000000..339d42b2 --- /dev/null +++ b/packages/coder/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- Initial release diff --git a/packages/coder/LICENSE b/packages/coder/LICENSE new file mode 100644 index 00000000..d8101f5c --- /dev/null +++ b/packages/coder/LICENSE @@ -0,0 +1,46 @@ +Copyright (c) 2024 Teo, Inc. (Celest) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Subject to the terms and conditions of this license, each copyright holder and +contributor hereby grants to those receiving rights under this license a +perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except for failure to satisfy the conditions of this license) patent license to +make, have made, use, offer to sell, sell, import, and otherwise transfer this +software, where such license applies only to those patent claims, already +acquired or hereafter acquired, licensable by such copyright holder or +contributor that are necessarily infringed by: + +(a) their Contribution(s) (the licensed copyrights of copyright holders and +non-copyrightable additions of contributors, in source or binary form) alone; or + +(b) combination of their Contribution(s) with the work of authorship to which +such Contribution(s) was added by such copyright holder or contributor, if, at +the time the Contribution is added, such addition causes such combination to be +necessarily infringed. The patent license shall not apply to any other +combinations which include the Contribution. + +Except as expressly stated above, no rights or licenses from any copyright +holder or contributor is granted under this license, whether expressly, by +implication, estoppel or otherwise. + +DISCLAIMER + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/coder/README.md b/packages/coder/README.md new file mode 100644 index 00000000..cc8c5106 --- /dev/null +++ b/packages/coder/README.md @@ -0,0 +1,3 @@ +# Coding + +A general-purpose serialization framework for Dart, inspired by Swift's [Codable](https://developer.apple.com/documentation/swift/codable). diff --git a/packages/coder/analysis_options.yaml b/packages/coder/analysis_options.yaml new file mode 100644 index 00000000..572dd239 --- /dev/null +++ b/packages/coder/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/packages/coder/example/example.dart b/packages/coder/example/example.dart new file mode 100644 index 00000000..d0f84c5c --- /dev/null +++ b/packages/coder/example/example.dart @@ -0,0 +1,68 @@ +import 'package:coder/coder.dart'; + +final class MyClass { + MyClass({ + required this.myBool, + required this.myInt, + required this.myDouble, + required this.myString, + required this.myDateTime, + }); + + final bool myBool; + final int myInt; + final double myDouble; + final String myString; + final DateTime myDateTime; + + static const self = Typeref(typeName: 'MyClass'); + + static MyClass decode(V value, Decoder decoder) { + final container = decoder.container(value); + final myBool = container.decodeBool('myBool'); + final myInt = container.decodeInt('myInt'); + final myDouble = container.decodeDouble('myDouble'); + final myString = container.decodeString('myString'); + final myDateTime = container.decodeDateTime('myDateTime'); + return MyClass( + myBool: myBool, + myInt: myInt, + myDouble: myDouble, + myString: myString, + myDateTime: myDateTime, + ); + } + + static V encode(MyClass instance, Encoder encoder) { + final container = encoder.container(); + container.encodeBool('myBool', instance.myBool); + container.encodeInt('myInt', instance.myInt); + container.encodeDouble('myDouble', instance.myDouble); + container.encodeString('myString', instance.myString); + container.encodeDateTime('myDateTime', instance.myDateTime); + return container.value; + } + + V encodeWith(Encoder protocol) => protocol.encode(this, as: self); +} + +final coding = GlobalCoder( + staticConfig: const { + MyClass.self: CoderConfig( + encode: MyClass.encode, + decode: MyClass.decode, + ), + }, +); + +void main() { + final instance = MyClass( + myBool: true, + myInt: 42, + myDouble: 3.14, + myString: 'Hello, World!', + myDateTime: DateTime.now(), + ); + print(coding.json.encode(instance, as: MyClass.self)); + print(coding.formData.encode(instance, as: MyClass.self)); +} diff --git a/packages/coder/lib/coder.dart b/packages/coder/lib/coder.dart new file mode 100644 index 00000000..3a315e5c --- /dev/null +++ b/packages/coder/lib/coder.dart @@ -0,0 +1,220 @@ +library; + +import 'dart:collection'; + +import 'package:coder/src/decoder.dart'; +import 'package:coder/src/encoder.dart'; +import 'package:coder/src/form_data/form_data_coder.dart'; +import 'package:coder/src/form_fields/form_fields_coder.dart'; +import 'package:coder/src/json/json_coder.dart'; +import 'package:coder/src/typeref.dart'; + +export 'src/coder.dart'; +export 'src/decoder.dart'; +export 'src/encoder.dart'; +export 'src/form_fields/form_fields_encoder.dart'; +export 'src/json/json_coder.dart'; +export 'src/json/json_decoder.dart'; +export 'src/json/json_encoder.dart'; +export 'src/typeref.dart'; + +final GlobalCoder coder = GlobalCoder(); + +abstract mixin class GlobalCoder implements Map { + factory GlobalCoder({ + /* TODO: @mustBeConst */ Map staticConfig, + }) = _GlobalCoder; + + const factory GlobalCoder.static( + /* TODO: @mustBeConst */ Map config, + ) = _StaticGlobalCoder; + + CoderConfig configFor({Typeref? type}); +} + +final class _StaticGlobalCoder extends UnmodifiableMapBase + with GlobalCoder { + const _StaticGlobalCoder(this._config); + + final Map _config; + + @override + CoderConfig? operator [](Object? key) => _config[key]; + + @override + Iterable> get keys => _config.keys; + + @override + CoderConfig configFor({Typeref? type}) { + if (type == null) { + throw ArgumentError('Static type must be specified'); + } + final config = _config[type] as CoderConfig?; + if (config == null) { + throw ArgumentError( + 'No config registered for "${type.typeName}". ' + 'Did you add the Coding config to the registry?', + ); + } + return config; + } +} + +final class _GlobalCoder with GlobalCoder, MapMixin { + _GlobalCoder({ + /* TODO: @mustBeConst */ Map staticConfig = const {}, + }) : _staticConfig = staticConfig { + _runtimeConfig.addAll({ + const Typeref(): CoderConfig.string, + const Typeref(): CoderConfig.int$, + const Typeref(): CoderConfig.double$, + const Typeref(): CoderConfig.bool$, + const Typeref(): CoderConfig.dateTime, + }); + } + + final Map _staticConfig; + final Map _runtimeConfig = HashMap( + equals: (t1, t2) => identical(t1.type, t2.type), + hashCode: (t) => t.type.hashCode, + isValidKey: (t) => t.type != null, + ); + + @override + CoderConfig configFor({Typeref? type}) { + assert(T != Object, 'Type must be specified'); + type ??= Typeref(); + final config = this[type] as CoderConfig?; + if (config == null) { + throw ArgumentError( + 'No config registered for "${type.typeName}". ' + 'Did you add the Coding config to the registry?', + ); + } + return config; + } + + @override + CoderConfig? operator [](Object? key) => + _runtimeConfig[key] ?? _staticConfig[key]; + + @override + void operator []=(Typeref key, CoderConfig value) { + _runtimeConfig[key] = value; + } + + @override + void clear() => _runtimeConfig.clear(); + + @override + Iterable> get keys => HashSet( + equals: (t1, t2) => identical(t1, t2) || identical(t1.type, t2.type), + hashCode: (t) => Object.hash(t.typeName, t.type), + ) + ..addAll(_staticConfig.keys) + ..addAll(_runtimeConfig.keys); + + @override + CoderConfig? remove(Object? key) { + return _runtimeConfig.remove(key); + } +} + +extension CoreCoders on GlobalCoder { + JsonCoder get json => JsonCoder(coder: this); + FormDataCoder get formData => FormDataCoder(coder: this); + FormFieldsCoder get formFields => FormFieldsCoder(coder: this); +} + +typedef TypeDecoder = T Function( + V, Decoder); +typedef TypeEncoder = V Function( + T, Encoder); + +abstract interface class CoderKeys { + const factory CoderKeys.identity() = _CoderKeysIdentity; + + String keyFor(Field field); +} + +final class _CoderKeysIdentity + implements CoderKeys { + const _CoderKeysIdentity(); + + @override + String keyFor(Field field) => field.toString(); +} + +abstract mixin class CoderConfig { + const factory CoderConfig({ + TypeEncoder? encode, + TypeDecoder? decode, + }) = _CoderConfig; + + static final CoderConfig string = CoderConfig( + encode: (value, encoder) => encoder.encodeString(value), + decode: (value, decoder) => decoder.decodeString(value), + ); + static final CoderConfig int$ = CoderConfig( + encode: (value, encoder) => encoder.encodeInt(value), + decode: (value, decoder) => decoder.decodeInt(value), + ); + static final CoderConfig double$ = CoderConfig( + encode: (value, encoder) => encoder.encodeDouble(value), + decode: (value, decoder) => decoder.decodeDouble(value), + ); + static final CoderConfig bool$ = CoderConfig( + encode: (value, encoder) => encoder.encodeBool(value), + decode: (value, decoder) => decoder.decodeBool(value), + ); + static final CoderConfig dateTime = CoderConfig( + encode: (value, encoder) => encoder.encodeDateTime(value), + decode: (value, decoder) => decoder.decodeDateTime(value), + ); + + V encode( + T instance, + Encoder encoder, + ); + T decode( + Object? value, + Decoder decoder, + ); +} + +final class _CoderConfig with CoderConfig { + const _CoderConfig({ + TypeEncoder? encode, + TypeDecoder? decode, + }) : assert( + encode != null || decode != null, + 'Either encode or decode must be provided', + ), + _encode = encode ?? _noEncoder, + _decode = decode ?? _noDecoder; + + static Never _noDecoder(V value, Decoder decoder) { + throw UnimplementedError('No decoder registered for type'); + } + + static Never _noEncoder(Object value, Encoder encoder) { + throw UnimplementedError('No encoder registered for type'); + } + + final TypeEncoder _encode; + final TypeDecoder _decode; + + @override + V encode( + T instance, + Encoder encoder, + ) => + _encode(instance, encoder); + + @override + T decode( + Object? value, + Decoder decoder, + ) => + _decode(value, decoder); +} diff --git a/packages/coder/lib/src/coder.dart b/packages/coder/lib/src/coder.dart new file mode 100644 index 00000000..e6ffafa8 --- /dev/null +++ b/packages/coder/lib/src/coder.dart @@ -0,0 +1,18 @@ +import 'package:coder/coder.dart'; + +abstract mixin class Coder { + Encoder get encoder; + Decoder get decoder; + + V encode( + T value, { + required Typeref as, + }) => + encoder.encode(value, as: as); + + T decode( + V value, { + required Typeref as, + }) => + decoder.decode(value, as: as); +} diff --git a/packages/coder/lib/src/decoder.dart b/packages/coder/lib/src/decoder.dart new file mode 100644 index 00000000..c8775a7a --- /dev/null +++ b/packages/coder/lib/src/decoder.dart @@ -0,0 +1,76 @@ +import 'package:coder/coder.dart'; + +abstract mixin class Decoder { + const Decoder(); + + KeyedDecodingContainer container( + V value, { + CoderKeys? keyedBy, + }); + SingleValueDecodingContainer singleValueContainer(V value); + UnkeyedDecodingContainer unkeyedContainer(V value); + + T decode(V value, {Typeref? as}); + bool decodeBool(V value); + int decodeInt(V value); + double decodeDouble(V value); + String decodeString(V value); + DateTime decodeDateTime(V value); +} + +abstract mixin class SingleValueDecodingContainer { + Decoder get decoder; + + T decode({Typeref? as}); + bool decodeNull(); + bool decodeBool(); + int decodeInt(); + double decodeDouble(); + String decodeString(); + DateTime decodeDateTime(); + + V get value; +} + +abstract interface class KeyedDecodingContainer { + Decoder get decoder; + + KeyedDecodingContainer + nestedContainer( + K key, { + CoderKeys? keyedBy, + }); + SingleValueDecodingContainer nestedSingleValueContainer(K key); + UnkeyedDecodingContainer nestedUnkeyedContainer(K key); + + T decode(K key, {Typeref? as}); + bool decodeNull(K key); + bool decodeBool(K key); + int decodeInt(K key); + double decodeDouble(K key); + String decodeString(K key); + DateTime decodeDateTime(K key); + + V get value; +} + +abstract interface class UnkeyedDecodingContainer { + Decoder get decoder; + + KeyedDecodingContainer nestedContainer({ + CoderKeys? keyedBy, + }); + SingleValueDecodingContainer nestedSingleValueContainer(); + UnkeyedDecodingContainer nestedUnkeyedContainer(); + + T decode({Typeref? as}); + bool decodeNull(); + String decodeString(); + int decodeInt(); + double decodeDouble(); + bool decodeBool(); + DateTime decodeDateTime(); + + V get value; +} diff --git a/packages/coder/lib/src/encoder.dart b/packages/coder/lib/src/encoder.dart new file mode 100644 index 00000000..a55bd052 --- /dev/null +++ b/packages/coder/lib/src/encoder.dart @@ -0,0 +1,99 @@ +import 'package:coder/coder.dart'; + +abstract mixin class Encoder { + const Encoder(); + + KeyedEncodingContainer container({ + CoderKeys? keyedBy, + }); + SingleValueEncodingContainer singleValueContainer(); + UnkeyedEncodingContainer unkeyedContainer(); + + V encode(T value, {Typeref? as}); + V encodeNull(); + V encodeBool(bool value); + V encodeInt(int value); + V encodeDouble(double value); + V encodeString(String value); + V encodeDateTime(DateTime value); + V encodePrimitive(Object value); +} + +abstract mixin class SingleValueEncodingContainer { + Encoder get encoder; + + void encode(T value, {Typeref? as}); + void encodeNull(); + void encodeBool(bool value); + void encodeInt(int value); + void encodeDouble(double value); + void encodeString(String value); + void encodeDateTime(DateTime value); + void encodePrimitive(Object value); + + V get value; + set value(covariant V value); +} + +abstract interface class KeyedEncodingContainer { + Encoder get encoder; + + KeyedEncodingContainer + nestedContainer( + K key, { + CoderKeys? keyedBy, + }); + SingleValueEncodingContainer nestedSingleValueContainer(K key); + UnkeyedEncodingContainer nestedUnkeyedContainer(K key); + + void encode(K key, T value, {Typeref? as}); + void encodeNull(K key, [Null value]); + void encodeString(K key, String value); + void encodeInt(K key, int value); + void encodeDouble(K key, double value); + void encodeBool(K key, bool value); + void encodeDateTime(K key, DateTime value); + void encodePrimitive(K key, Object value); + void encodeList( + K key, + void Function(UnkeyedEncodingContainer container) callback, + ); + void encodeMap( + K key, + void Function(KeyedEncodingContainer container) callback, { + CoderKeys? keyedBy, + }); + + V get value; + set value(covariant V value); +} + +abstract interface class UnkeyedEncodingContainer { + Encoder get encoder; + + KeyedEncodingContainer nestedContainer({ + CoderKeys? keyedBy, + }); + SingleValueEncodingContainer nestedSingleValueContainer(); + UnkeyedEncodingContainer nestedUnkeyedContainer(); + + void encode(T value, {Typeref? as}); + void encodeNull(); + void encodeBool(bool value); + void encodeInt(int value); + void encodeDouble(double value); + void encodeString(String value); + void encodeDateTime(DateTime value); + void encodePrimitive(Object value); + void encodeList( + void Function(UnkeyedEncodingContainer container) callback, + ); + void encodeMap( + void Function(KeyedEncodingContainer container) callback, { + CoderKeys? keyedBy, + }); + + V get value; + set value(covariant V value); +} diff --git a/packages/coder/lib/src/form_data/form_data_coder.dart b/packages/coder/lib/src/form_data/form_data_coder.dart new file mode 100644 index 00000000..7e95dec3 --- /dev/null +++ b/packages/coder/lib/src/form_data/form_data_coder.dart @@ -0,0 +1,14 @@ +import 'package:coder/coder.dart'; +import 'package:coder/src/form_data/form_data_encoder.dart'; + +final class FormDataCoder with Coder { + FormDataCoder({ + required GlobalCoder coder, + }) : encoder = FormDataEncoder(coder: coder); + + @override + final FormDataEncoder encoder; + + @override + Decoder get decoder => throw UnimplementedError(); +} diff --git a/packages/coder/lib/src/form_data/form_data_encoder.dart b/packages/coder/lib/src/form_data/form_data_encoder.dart new file mode 100644 index 00000000..f631989d --- /dev/null +++ b/packages/coder/lib/src/form_data/form_data_encoder.dart @@ -0,0 +1,452 @@ +import 'package:coder/coder.dart'; + +final class FormDataEncoder extends Encoder { + FormDataEncoder({ + required GlobalCoder coder, + String? key, + }) : _key = key, + _coder = coder; + + final String? _key; + final GlobalCoder _coder; + + @override + SingleValueEncodingContainer singleValueContainer() { + return _FormValueEncodingContainer(encoder: this); + } + + @override + UnkeyedEncodingContainer unkeyedContainer() { + if (_key == null) { + throw StateError('Cannot create an unkeyed value container.'); + } + return _FormFieldListContainer( + key: _key, + encoder: this, + ); + } + + @override + KeyedEncodingContainer container({ + CoderKeys? keyedBy, + }) { + return _FormFieldsEncodingContainer._( + key: _key, + encoder: this, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + String encode(T value, {Typeref? as}) { + final config = _coder.configFor(type: as); + return config.encode(value, this); + } + + @override + String encodeBool(bool value) { + return value.toString(); + } + + @override + String encodeDateTime(DateTime value) { + return value.millisecondsSinceEpoch.toString(); + } + + @override + String encodeDouble(double value) { + return value.toString(); + } + + @override + String encodeInt(int value) { + return value.toString(); + } + + @override + String encodeNull() { + return ''; + } + + @override + String encodeString(String value) { + return Uri.encodeQueryComponent(value); + } + + @override + String encodePrimitive(Object value) { + if (value is String) { + return encodeString(value); + } + return value.toString(); + } + + FormDataEncoder _withKey(String key) => FormDataEncoder( + coder: _coder, + key: key, + ); +} + +final class _FormFieldsEncodingContainer + implements KeyedEncodingContainer { + _FormFieldsEncodingContainer._({ + required this.encoder, + required CoderKeys coderKeys, + StringBuffer? sink, + String? key, + }) : _key = key, + _buffer = sink ?? StringBuffer(), + _coderKeys = coderKeys; + + final String? _key; + final StringBuffer _buffer; + final CoderKeys _coderKeys; + + @override + final FormDataEncoder encoder; + + @override + String get value => _buffer.toString(); + + @override + set value(String value) { + _buffer + ..clear() + ..write(value); + } + + String _nextKey(Key key) { + final codingKey = _coderKeys.keyFor(key); + return _key == null ? codingKey : '$_key[$codingKey]'; + } + + @override + void encode( + Key key, + T value, { + Typeref? as, + }) { + final valueKey = _nextKey(key); + _buffer.writeEncodedComponent( + encoder._withKey(valueKey).encode(value, as: as), + ); + } + + @override + void encodeNull(Key key, [Null value]) { + _buffer.writeComponent(_nextKey(key), encoder.encodeNull()); + } + + @override + void encodeBool(Key key, bool value) { + _buffer.writeComponent(_nextKey(key), encoder.encodeBool(value)); + } + + @override + void encodeDouble(Key key, double value) { + _buffer.writeComponent(_nextKey(key), encoder.encodeDouble(value)); + } + + @override + void encodeInt(Key key, int value) { + _buffer.writeComponent(_nextKey(key), encoder.encodeInt(value)); + } + + @override + void encodeString(Key key, String value) { + _buffer.writeComponent(_nextKey(key), encoder.encodeString(value)); + } + + @override + void encodeDateTime(Key key, DateTime value) { + _buffer.writeComponent(_nextKey(key), encoder.encodeDateTime(value)); + } + + @override + void encodePrimitive(Key key, Object value) { + _buffer.writeComponent(_nextKey(key), encoder.encodePrimitive(value)); + } + + @override + void encodeList( + Key key, + void Function(UnkeyedEncodingContainer container) callback, + ) { + final container = _FormFieldListContainer( + encoder: encoder, + key: _nextKey(key), + ); + callback(container); + _buffer.writeEncodedComponent(container.value); + } + + @override + void encodeMap( + Key key, + void Function(_FormFieldsEncodingContainer container) callback, { + CoderKeys? keyedBy, + }) { + final coderKeys = keyedBy ?? CoderKeys.identity(); + final container = _FormFieldsEncodingContainer._( + encoder: encoder, + key: _nextKey(key), + coderKeys: coderKeys, + ); + callback(container); + _buffer.writeEncodedComponent(container.value); + } + + @override + KeyedEncodingContainer + nestedContainer( + Key key, { + CoderKeys? keyedBy, + }) { + final coderKeys = keyedBy ?? CoderKeys.identity(); + return _FormFieldsEncodingContainer._( + encoder: encoder, + coderKeys: coderKeys, + key: _nextKey(key), + sink: _buffer, + ); + } + + @override + SingleValueEncodingContainer nestedSingleValueContainer(Key key) { + final valueyKey = _nextKey(key); + return _FormValueEncodingContainer( + encoder: encoder._withKey(valueyKey), + sink: (value) => _buffer.writeComponent(valueyKey, value), + ); + } + + @override + UnkeyedEncodingContainer nestedUnkeyedContainer(Key key) { + return _FormFieldListContainer( + encoder: encoder, + key: _nextKey(key), + sink: _buffer, + ); + } +} + +final class _FormFieldListContainer + implements UnkeyedEncodingContainer { + _FormFieldListContainer({ + required FormDataEncoder encoder, + required this.key, + StringBuffer? sink, + }) : buffer = sink ?? StringBuffer(), + encoder = encoder._withKey(key); + + final String key; + final StringBuffer buffer; + + @override + final FormDataEncoder encoder; + + @override + String get value => buffer.toString(); + + @override + set value(String value) { + buffer + ..clear() + ..write(value); + } + + var _index = 0; + + String get _nextKey => '$key[${_index++}]'; + + @override + void encode(T value, {Typeref? as}) { + buffer.writeEncodedComponent( + encoder._withKey(_nextKey).encode(value, as: as), + ); + } + + @override + void encodeNull([Null value]) { + buffer.writeComponent(_nextKey, encoder.encodeNull()); + } + + @override + void encodeBool(bool value) { + buffer.writeComponent(_nextKey, encoder.encodeBool(value)); + } + + @override + void encodeDouble(double value) { + buffer.writeComponent(_nextKey, encoder.encodeDouble(value)); + } + + @override + void encodeInt(int value) { + buffer.writeComponent(_nextKey, encoder.encodeInt(value)); + } + + @override + void encodeString(String value) { + buffer.writeComponent(_nextKey, encoder.encodeString(value)); + } + + @override + void encodeDateTime(DateTime value) { + buffer.writeComponent(_nextKey, encoder.encodeDateTime(value)); + } + + @override + void encodePrimitive(Object value) { + buffer.writeComponent(_nextKey, encoder.encodePrimitive(value)); + } + + @override + void encodeList(void Function(_FormFieldListContainer container) callback) { + final valueKey = _nextKey; + final container = _FormFieldListContainer( + encoder: encoder._withKey(valueKey), + key: valueKey, + ); + callback(container); + buffer.writeEncodedComponent(container.value); + } + + @override + void encodeMap( + void Function(KeyedEncodingContainer container) + callback, { + CoderKeys? keyedBy, + }) { + final container = _FormFieldsEncodingContainer._( + encoder: encoder, + key: _nextKey, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + callback(container); + buffer.writeEncodedComponent(container.value); + } + + @override + KeyedEncodingContainer nestedContainer({ + CoderKeys? keyedBy, + }) { + final coderKeys = keyedBy ?? CoderKeys.identity(); + return _FormFieldsEncodingContainer._( + encoder: encoder, + coderKeys: coderKeys, + key: _nextKey, + sink: buffer, + ); + } + + @override + SingleValueEncodingContainer nestedSingleValueContainer() { + final key = _nextKey; + return _FormValueEncodingContainer( + encoder: encoder._withKey(key), + sink: buffer.writeEncodedComponent, + ); + } + + @override + UnkeyedEncodingContainer nestedUnkeyedContainer() { + return _FormFieldListContainer( + encoder: encoder, + key: _nextKey, + sink: buffer, + ); + } +} + +typedef _FormValueSink = void Function(String value); + +final class _FormValueEncodingContainer + implements SingleValueEncodingContainer { + _FormValueEncodingContainer({ + required this.encoder, + this.sink, + }); + + @override + final FormDataEncoder encoder; + + final _FormValueSink? sink; + + late final String _value; + + @override + String get value => _value; + + @override + set value(String value) { + _value = value; + sink?.call(value); + } + + @override + void encode(T value, {Typeref? as}) { + this.value = encoder.encode(value, as: as); + sink?.call(this.value); + } + + @override + void encodeNull([Null value]) { + this.value = encoder.encodeNull(); + sink?.call(this.value); + } + + @override + void encodeBool(bool value) { + this.value = encoder.encodeBool(value); + sink?.call(this.value); + } + + @override + void encodeDouble(double value) { + this.value = encoder.encodeDouble(value); + sink?.call(this.value); + } + + @override + void encodeInt(int value) { + this.value = encoder.encodeInt(value); + sink?.call(this.value); + } + + @override + void encodeString(String value) { + this.value = encoder.encodeString(value); + sink?.call(this.value); + } + + @override + void encodeDateTime(DateTime value) { + this.value = encoder.encodeDateTime(value); + sink?.call(this.value); + } + + @override + void encodePrimitive(Object value) { + this.value = encoder.encodePrimitive(value); + sink?.call(this.value); + } +} + +extension on StringBuffer { + void writeComponent(String key, String value) { + if (isNotEmpty) { + writeCharCode(0x26); // '&' + } + write(Uri.encodeQueryComponent(key)); + writeCharCode(0x3D); // '=' + write(value); + } + + void writeEncodedComponent(String component) { + if (isNotEmpty) { + writeCharCode(0x26); // '&' + } + write(component); + } +} diff --git a/packages/coder/lib/src/form_fields/form_fields_coder.dart b/packages/coder/lib/src/form_fields/form_fields_coder.dart new file mode 100644 index 00000000..1490e7b5 --- /dev/null +++ b/packages/coder/lib/src/form_fields/form_fields_coder.dart @@ -0,0 +1,13 @@ +import 'package:coder/coder.dart'; + +final class FormFieldsCoder with Coder { + FormFieldsCoder({ + required GlobalCoder coder, + }) : encoder = FormFieldsEncoder(coder: coder); + + @override + final FormFieldsEncoder encoder; + + @override + Decoder get decoder => throw UnimplementedError(); +} diff --git a/packages/coder/lib/src/form_fields/form_fields_encoder.dart b/packages/coder/lib/src/form_fields/form_fields_encoder.dart new file mode 100644 index 00000000..85e689ab --- /dev/null +++ b/packages/coder/lib/src/form_fields/form_fields_encoder.dart @@ -0,0 +1,426 @@ +import 'package:coder/coder.dart'; + +final class FormFieldsEncoder extends Encoder { + FormFieldsEncoder({ + required GlobalCoder coder, + String? key, + Map? fields, + }) : _key = key, + _fields = fields, + _coder = coder; + + final String? _key; + final Map? _fields; + final GlobalCoder _coder; + + @override + SingleValueEncodingContainer singleValueContainer() { + if (_key == null || _fields == null) { + throw StateError('Cannot create an unkeyed value container.'); + } + return _FormValueEncodingContainer( + encoder: this, + sink: (value) { + if (value is String) { + _fields[_key] = value; + } + }, + ); + } + + @override + UnkeyedEncodingContainer unkeyedContainer() { + if (_key == null || _fields == null) { + throw StateError('Cannot create an unkeyed value container.'); + } + return _FormFieldListContainer( + encoder: this, + key: _key, + fields: _fields, + ); + } + + @override + FormFieldsEncodingContainer container({ + CoderKeys? keyedBy, + }) { + return FormFieldsEncodingContainer._( + encoder: this, + key: _key, + coderKeys: keyedBy ?? CoderKeys.identity(), + fields: _fields, + ); + } + + @override + Object encode(T value, {Typeref? as}) { + final config = _coder.configFor(type: as); + return config.encode(value, this); + } + + @override + String encodeBool(bool value) { + return value.toString(); + } + + @override + String encodeDateTime(DateTime value) { + return value.millisecondsSinceEpoch.toString(); + } + + @override + String encodeDouble(double value) { + return value.toString(); + } + + @override + String encodeInt(int value) { + return value.toString(); + } + + @override + String encodeNull() { + return ''; + } + + @override + String encodeString(String value) { + return value; + } + + @override + String encodePrimitive(Object value) { + return value.toString(); + } + + FormFieldsEncoder _nested({ + required String key, + required Map fields, + }) => + FormFieldsEncoder(coder: _coder, key: key, fields: fields); +} + +final class FormFieldsEncodingContainer + implements KeyedEncodingContainer { + FormFieldsEncodingContainer._({ + required this.encoder, + required CoderKeys coderKeys, + Map? fields, + String? key, + }) : _key = key, + _fields = fields ?? {}, + _coderKeys = coderKeys; + + final String? _key; + final Map _fields; + final CoderKeys _coderKeys; + + @override + final FormFieldsEncoder encoder; + + @override + Map get value => _fields; + + @override + set value(Map value) { + _fields + ..clear() + ..addAll(value); + } + + String _nextKey(Key key) { + final codingKey = _coderKeys.keyFor(key); + return _key == null ? codingKey : '$_key[$codingKey]'; + } + + @override + void encode( + Key key, + T value, { + Typeref? as, + }) { + final valueKey = _nextKey(key); + encoder._nested(key: valueKey, fields: _fields).encode(value, as: as); + } + + @override + void encodeNull(Key key, [Null value]) { + _fields[_nextKey(key)] = encoder.encodeNull(); + } + + @override + void encodeBool(Key key, bool value) { + _fields[_nextKey(key)] = encoder.encodeBool(value); + } + + @override + void encodeDouble(Key key, double value) { + _fields[_nextKey(key)] = encoder.encodeDouble(value); + } + + @override + void encodeInt(Key key, int value) { + _fields[_nextKey(key)] = encoder.encodeInt(value); + } + + @override + void encodeString(Key key, String value) { + _fields[_nextKey(key)] = encoder.encodeString(value); + } + + @override + void encodeDateTime(Key key, DateTime value) { + _fields[_nextKey(key)] = encoder.encodeDateTime(value); + } + + @override + void encodePrimitive(Key key, Object value) { + _fields[_nextKey(key)] = encoder.encodePrimitive(value); + } + + @override + void encodeList( + Key key, + void Function(UnkeyedEncodingContainer container) callback, + ) { + callback(nestedUnkeyedContainer(key)); + } + + @override + void encodeMap( + Key key, + void Function(FormFieldsEncodingContainer container) callback, { + CoderKeys? keyedBy, + }) { + callback(nestedContainer(key, keyedBy: keyedBy)); + } + + @override + FormFieldsEncodingContainer + nestedContainer( + Key key, { + CoderKeys? keyedBy, + }) { + final coderKeys = keyedBy ?? CoderKeys.identity(); + final valueKey = _nextKey(key); + return FormFieldsEncodingContainer._( + encoder: encoder._nested(key: valueKey, fields: value), + coderKeys: coderKeys, + key: valueKey, + fields: _fields, + ); + } + + @override + SingleValueEncodingContainer nestedSingleValueContainer(Key key) { + final valueKey = _nextKey(key); + return _FormValueEncodingContainer( + encoder: encoder._nested(key: valueKey, fields: value), + sink: (value) { + if (value is String) { + _fields[valueKey] = value; + } + }, + ); + } + + @override + UnkeyedEncodingContainer nestedUnkeyedContainer(Key key) { + final valueKey = _nextKey(key); + return _FormFieldListContainer( + encoder: encoder._nested(key: valueKey, fields: _fields), + key: valueKey, + fields: _fields, + ); + } +} + +final class _FormFieldListContainer + implements UnkeyedEncodingContainer { + _FormFieldListContainer({ + required this.encoder, + required this.key, + Map? fields, + }) : value = fields ?? {}; + + final String key; + + @override + final FormFieldsEncoder encoder; + + @override + final Map value; + + @override + set value(Map value) { + this.value.addAll(value); + } + + var _index = 0; + + String get _nextKey => '$key[${_index++}]'; + + @override + void encode(T value, {Typeref? as}) { + encoder._nested(key: _nextKey, fields: this.value).encode(value, as: as); + } + + @override + void encodeNull([Null value]) { + this.value[_nextKey] = encoder.encodeNull(); + } + + @override + void encodeBool(bool value) { + this.value[_nextKey] = encoder.encodeBool(value); + } + + @override + void encodeDouble(double value) { + this.value[_nextKey] = encoder.encodeDouble(value); + } + + @override + void encodeInt(int value) { + this.value[_nextKey] = encoder.encodeInt(value); + } + + @override + void encodeString(String value) { + this.value[_nextKey] = encoder.encodeString(value); + } + + @override + void encodeDateTime(DateTime value) { + this.value[_nextKey] = encoder.encodeDateTime(value); + } + + @override + void encodePrimitive(Object value) { + this.value[_nextKey] = encoder.encodePrimitive(value); + } + + @override + void encodeList( + void Function(UnkeyedEncodingContainer container) callback, + ) { + callback(nestedUnkeyedContainer()); + } + + @override + void encodeMap( + void Function(FormFieldsEncodingContainer container) callback, { + CoderKeys? keyedBy, + }) { + callback(nestedContainer(keyedBy: keyedBy)); + } + + @override + FormFieldsEncodingContainer nestedContainer({ + CoderKeys? keyedBy, + }) { + final coderKeys = keyedBy ?? CoderKeys.identity(); + return FormFieldsEncodingContainer._( + encoder: encoder, + coderKeys: coderKeys, + key: _nextKey, + fields: value, + ); + } + + @override + SingleValueEncodingContainer nestedSingleValueContainer() { + final key = _nextKey; + return _FormValueEncodingContainer( + encoder: encoder._nested(key: key, fields: value), + sink: (value) { + if (value is String) { + this.value[key] = value; + } + }, + ); + } + + @override + UnkeyedEncodingContainer nestedUnkeyedContainer() { + return _FormFieldListContainer( + encoder: encoder, + key: _nextKey, + fields: value, + ); + } +} + +typedef _FormValueSink = void Function(Object value); + +final class _FormValueEncodingContainer + implements SingleValueEncodingContainer { + _FormValueEncodingContainer({ + required this.encoder, + this.sink, + }); + + @override + final FormFieldsEncoder encoder; + + final _FormValueSink? sink; + + late final Object _value; + + @override + Object get value => _value; + + @override + set value(Object value) { + _value = value; + sink?.call(value); + } + + @override + void encode(T value, {Typeref? as}) { + this.value = encoder.encode(value, as: as); + sink?.call(this.value); + } + + @override + void encodeNull([Null value]) { + this.value = encoder.encodeNull(); + sink?.call(this.value); + } + + @override + void encodeBool(bool value) { + this.value = encoder.encodeBool(value); + sink?.call(this.value); + } + + @override + void encodeDouble(double value) { + this.value = encoder.encodeDouble(value); + sink?.call(this.value); + } + + @override + void encodeInt(int value) { + this.value = encoder.encodeInt(value); + sink?.call(this.value); + } + + @override + void encodeString(String value) { + this.value = encoder.encodeString(value); + sink?.call(this.value); + } + + @override + void encodeDateTime(DateTime value) { + this.value = encoder.encodeDateTime(value); + sink?.call(this.value); + } + + @override + void encodePrimitive(Object value) { + this.value = encoder.encodePrimitive(value); + sink?.call(this.value); + } +} diff --git a/packages/coder/lib/src/json/json_coder.dart b/packages/coder/lib/src/json/json_coder.dart new file mode 100644 index 00000000..ea090f51 --- /dev/null +++ b/packages/coder/lib/src/json/json_coder.dart @@ -0,0 +1,14 @@ +import 'package:coder/coder.dart'; + +final class JsonCoder with Coder { + JsonCoder({ + required GlobalCoder coder, + }) : encoder = JsonEncoder(coder: coder), + decoder = JsonDecoder(coder: coder); + + @override + final JsonEncoder encoder; + + @override + final JsonDecoder decoder; +} diff --git a/packages/coder/lib/src/json/json_decoder.dart b/packages/coder/lib/src/json/json_decoder.dart new file mode 100644 index 00000000..2a18155e --- /dev/null +++ b/packages/coder/lib/src/json/json_decoder.dart @@ -0,0 +1,289 @@ +import 'package:coder/coder.dart'; + +final class JsonDecoder extends Decoder { + JsonDecoder({ + required GlobalCoder coder, + }) : _coder = coder; + + final GlobalCoder _coder; + + @override + T decode(Object? value, {Typeref? as}) { + final config = _coder.configFor(type: as); + return config.decode(value, this); + } + + @override + bool decodeBool(Object? value) => value as bool; + + @override + DateTime decodeDateTime(Object? value) { + final milliseconds = (value as num).toInt(); + return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true); + } + + @override + double decodeDouble(Object? value) { + return (value as num).toDouble(); + } + + @override + int decodeInt(Object? value) { + return (value as num).toInt(); + } + + @override + String decodeString(Object? value) { + return value as String; + } + + @override + JsonMapDecodingContainer container( + Object? value, { + CoderKeys? keyedBy, + }) { + return JsonMapDecodingContainer._( + decoder: this, + value: (value as Map).cast(), + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + JsonValueDecodingContainer singleValueContainer( + Object? value, + ) { + return JsonValueDecodingContainer._( + decoder: this, + value: value, + ); + } + + @override + JsonListDecodingContainer unkeyedContainer(Object? value) { + return JsonListDecodingContainer._( + decoder: this, + value: (value as List).cast(), + ); + } +} + +final class JsonMapDecodingContainer + implements KeyedDecodingContainer { + JsonMapDecodingContainer._({ + required this.decoder, + required this.value, + required CoderKeys coderKeys, + }) : _coderKeys = coderKeys; + + final CoderKeys _coderKeys; + + @override + final JsonDecoder decoder; + + @override + final Map value; + + @override + T decode( + Key key, { + Typeref? as, + }) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decode(value[codingKey], as: as); + } + + @override + bool decodeBool(Key key) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decodeBool(value[codingKey]); + } + + @override + DateTime decodeDateTime(Key key) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decodeDateTime(value[codingKey]); + } + + @override + double decodeDouble(Key key) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decodeDouble(value[codingKey]); + } + + @override + int decodeInt(Key key) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decodeInt(value[codingKey]); + } + + @override + bool decodeNull(Key key) { + final codingKey = _coderKeys.keyFor(key); + return value.containsKey(codingKey) && value[codingKey] == null; + } + + @override + String decodeString(Key key) { + final codingKey = _coderKeys.keyFor(key); + return decoder.decodeString(value[codingKey]); + } + + @override + JsonMapDecodingContainer nestedContainer( + Key key, { + CoderKeys? keyedBy, + }) { + final codingKey = _coderKeys.keyFor(key); + return JsonMapDecodingContainer._( + decoder: decoder, + value: (value[codingKey] as Map).cast(), + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + JsonListDecodingContainer nestedUnkeyedContainer(Key key) { + final codingKey = _coderKeys.keyFor(key); + return JsonListDecodingContainer._( + decoder: decoder, + value: value[codingKey] as List, + ); + } + + @override + JsonValueDecodingContainer nestedSingleValueContainer(Key key) { + final codingKey = _coderKeys.keyFor(key); + return JsonValueDecodingContainer._( + decoder: decoder, + value: value[codingKey], + ); + } +} + +final class JsonListDecodingContainer + implements UnkeyedDecodingContainer { + JsonListDecodingContainer._({ + required this.decoder, + required this.value, + }); + + @override + final JsonDecoder decoder; + + @override + final List value; + + var _index = 0; + + @override + T decode({Typeref? as}) { + return decoder.decode(value[_index++], as: as); + } + + @override + bool decodeBool() { + return decoder.decodeBool(value[_index++]); + } + + @override + DateTime decodeDateTime() { + return decoder.decodeDateTime(value[_index++]); + } + + @override + double decodeDouble() { + return decoder.decodeDouble(value[_index++]); + } + + @override + int decodeInt() { + return decoder.decodeInt(value[_index++]); + } + + @override + bool decodeNull() { + return value[_index++] == null; + } + + @override + String decodeString() { + return decoder.decodeString(value[_index++]); + } + + @override + JsonMapDecodingContainer nestedContainer({ + CoderKeys? keyedBy, + }) { + return JsonMapDecodingContainer._( + decoder: decoder, + value: (value[_index++] as Map).cast(), + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + JsonListDecodingContainer nestedUnkeyedContainer() { + return JsonListDecodingContainer._( + decoder: decoder, + value: value[_index++] as List, + ); + } + + @override + JsonValueDecodingContainer nestedSingleValueContainer() { + return JsonValueDecodingContainer._( + decoder: decoder, + value: value[_index++], + ); + } +} + +final class JsonValueDecodingContainer + implements SingleValueDecodingContainer { + JsonValueDecodingContainer._({ + required this.decoder, + required this.value, + }); + + @override + final JsonDecoder decoder; + + @override + final Object? value; + + @override + T decode({Typeref? as}) { + return decoder.decode(value, as: as); + } + + @override + bool decodeBool() { + return decoder.decodeBool(value); + } + + @override + DateTime decodeDateTime() { + return decoder.decodeDateTime(value); + } + + @override + double decodeDouble() { + return decoder.decodeDouble(value); + } + + @override + int decodeInt() { + return decoder.decodeInt(value); + } + + @override + bool decodeNull() { + return value == null; + } + + @override + String decodeString() { + return decoder.decodeString(value); + } +} diff --git a/packages/coder/lib/src/json/json_encoder.dart b/packages/coder/lib/src/json/json_encoder.dart new file mode 100644 index 00000000..1e0f0058 --- /dev/null +++ b/packages/coder/lib/src/json/json_encoder.dart @@ -0,0 +1,356 @@ +import 'package:coder/coder.dart'; + +final class JsonEncoder extends Encoder { + JsonEncoder({ + required GlobalCoder coder, + }) : _coder = coder; + + final GlobalCoder _coder; + + @override + JsonValueEncodingContainer singleValueContainer() { + return JsonValueEncodingContainer._(encoder: this); + } + + @override + JsonListEncodingContainer unkeyedContainer() { + return JsonListEncodingContainer._(encoder: this); + } + + @override + JsonMapEncodingContainer container({ + CoderKeys? keyedBy, + }) { + return JsonMapEncodingContainer._( + encoder: this, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + Object? encode( + T value, { + Typeref? as, + }) { + final config = _coder.configFor(type: as); + return config.encode(value, this); + } + + @override + Object? encodeBool(bool value) => value; + + @override + Object? encodeDateTime(DateTime value) => + value.toUtc().millisecondsSinceEpoch; + + @override + Object? encodeDouble(double value) => value; + + @override + Object? encodeInt(int value) => value; + + @override + Object? encodeNull() => null; + + @override + Object? encodeString(String value) => value; + + @override + Object? encodePrimitive(Object value) => value; +} + +final class JsonMapEncodingContainer + implements KeyedEncodingContainer { + JsonMapEncodingContainer._({ + required this.encoder, + required CoderKeys coderKeys, + Map? value, + }) : _value = value ?? {}, + _coderKeys = coderKeys; + + @override + final JsonEncoder encoder; + + final CoderKeys _coderKeys; + + @override + JsonValueEncodingContainer nestedSingleValueContainer(Key key) { + return JsonValueEncodingContainer._( + encoder: encoder, + sink: (value) => _value[_coderKeys.keyFor(key)] = value, + ); + } + + @override + JsonMapEncodingContainer nestedContainer( + Key key, { + CoderKeys? keyedBy, + }) { + final value = {}; + _value[_coderKeys.keyFor(key)] = value; + return JsonMapEncodingContainer._( + encoder: encoder, + value: value, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + JsonListEncodingContainer nestedUnkeyedContainer(Key key) { + final value = []; + _value[_coderKeys.keyFor(key)] = value; + return JsonListEncodingContainer._(encoder: encoder, value: value); + } + + Map _value; + + @override + Map get value => _value; + + @override + set value(Map object) => _value = object; + + @override + void encode( + Key key, + T value, { + Typeref? as, + }) { + _value[_coderKeys.keyFor(key)] = encoder.encode(value, as: as); + } + + @override + void encodeNull(Key key, [Null value]) { + _value[_coderKeys.keyFor(key)] = null; + } + + @override + void encodeBool(Key key, bool value) { + _value[_coderKeys.keyFor(key)] = value; + } + + @override + void encodeDouble(Key key, double value) { + _value[_coderKeys.keyFor(key)] = value; + } + + @override + void encodeInt(Key key, int value) { + _value[_coderKeys.keyFor(key)] = value; + } + + @override + void encodeString(Key key, String value) { + _value[_coderKeys.keyFor(key)] = value; + } + + @override + void encodeDateTime(Key key, DateTime value) { + _value[_coderKeys.keyFor(key)] = value.millisecondsSinceEpoch; + } + + @override + void encodePrimitive(Key key, Object value) { + _value[_coderKeys.keyFor(key)] = value; + } + + @override + void encodeList( + Key key, + void Function(JsonListEncodingContainer container) callback, + ) { + final container = JsonListEncodingContainer._(encoder: encoder); + callback(container); + _value[_coderKeys.keyFor(key)] = container.value; + } + + @override + void encodeMap( + Key key, + void Function(JsonMapEncodingContainer container) callback, { + CoderKeys? keyedBy, + }) { + final container = JsonMapEncodingContainer._( + encoder: encoder, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + callback(container); + _value[_coderKeys.keyFor(key)] = container.value; + } +} + +final class JsonListEncodingContainer + implements UnkeyedEncodingContainer { + JsonListEncodingContainer._({ + required this.encoder, + List? value, + }) : _value = value ?? []; + + @override + final JsonEncoder encoder; + + @override + JsonValueEncodingContainer nestedSingleValueContainer() { + return JsonValueEncodingContainer._( + encoder: encoder, + sink: (value) => _value.add(value), + ); + } + + @override + JsonMapEncodingContainer + nestedContainer({ + CoderKeys? keyedBy, + }) { + final value = {}; + _value.add(value); + return JsonMapEncodingContainer._( + encoder: encoder, + value: value, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + } + + @override + JsonListEncodingContainer nestedUnkeyedContainer() { + final value = []; + _value.add(value); + return JsonListEncodingContainer._(encoder: encoder, value: value); + } + + List _value; + + @override + List get value => _value; + + @override + set value(List object) => _value = object; + + @override + void encode(T value, {Typeref? as}) { + _value.add(encoder.encode(value, as: as)); + } + + @override + void encodeNull([Null value]) { + _value.add(null); + } + + @override + void encodeBool(bool value) { + _value.add(value); + } + + @override + void encodeDouble(double value) { + _value.add(value); + } + + @override + void encodeInt(int value) { + _value.add(value); + } + + @override + void encodeString(String value) { + _value.add(value); + } + + @override + void encodeDateTime(DateTime value) { + _value.add(encoder.encodeDateTime(value)); + } + + @override + void encodePrimitive(Object value) { + _value.add(value); + } + + @override + void encodeList( + void Function(JsonListEncodingContainer container) callback, + ) { + final container = JsonListEncodingContainer._(encoder: encoder); + callback(container); + _value.add(container.value); + } + + @override + void encodeMap( + void Function(JsonMapEncodingContainer container) callback, { + CoderKeys? keyedBy, + }) { + final container = JsonMapEncodingContainer._( + encoder: encoder, + coderKeys: keyedBy ?? CoderKeys.identity(), + ); + callback(container); + _value.add(container.value); + } +} + +typedef _JsonValueSink = void Function(Object? value); + +final class JsonValueEncodingContainer + implements SingleValueEncodingContainer { + JsonValueEncodingContainer._({ + required this.encoder, + _JsonValueSink? sink, + }) : _sink = sink; + + final _JsonValueSink? _sink; + + @override + final JsonEncoder encoder; + + @override + late final Object? value; + + @override + void encode(T value, {Typeref? as}) { + this.value = encoder.encode(value, as: as); + _sink?.call(this.value); + } + + @override + void encodeNull([Null value]) { + this.value = null; + _sink?.call(this.value); + } + + @override + void encodeBool(bool value) { + this.value = value; + _sink?.call(this.value); + } + + @override + void encodeDouble(double value) { + this.value = value; + _sink?.call(this.value); + } + + @override + void encodeInt(int value) { + this.value = value; + _sink?.call(this.value); + } + + @override + void encodeString(String value) { + this.value = value; + _sink?.call(this.value); + } + + @override + void encodeDateTime(DateTime value) { + this.value = encoder.encodeDateTime(value); + _sink?.call(this.value); + } + + @override + void encodePrimitive(Object value) { + this.value = value; + _sink?.call(this.value); + } +} diff --git a/packages/coder/lib/src/typeref.dart b/packages/coder/lib/src/typeref.dart new file mode 100644 index 00000000..8f8f0706 --- /dev/null +++ b/packages/coder/lib/src/typeref.dart @@ -0,0 +1,83 @@ +import 'package:meta/meta.dart'; + +@immutable +sealed class Typeref { + const factory Typeref({ + String? typeName, + }) = _StaticTyperef; + + const factory Typeref.extensionType({ + required String typeName, + }) = ExtensionTyperef._; + + factory Typeref.runtime({ + required Type runtimeType, + String? typeName, + }) = _RuntimeTyperef; + + const Typeref._(); + + /// A stringified representation of the type, useful for debugging. + String get typeName; + + /// The referenced type object, if meaningful at runtime. + /// + /// For extension type references, this will always be `null`. + Type? get type; + + @override + String toString() => typeName; +} + +final class _StaticTyperef extends Typeref { + const _StaticTyperef({ + String? typeName, + }) : _typeName = typeName, + super._(); + + final String? _typeName; + + @override + Type get type => T; + + @override + String get typeName => _typeName ?? T.toString(); +} + +/// A reference to a static extension type. +final class ExtensionTyperef extends Typeref { + const ExtensionTyperef._({ + required this.typeName, + }) : super._(); + + @override + Type? get type => null; + + @override + final String typeName; +} + +final class _RuntimeTyperef extends Typeref { + _RuntimeTyperef({ + required Type runtimeType, + String? typeName, + }) : _runtimeType = runtimeType, + typeName = typeName ?? runtimeType.toString(), + super._(); + + final Type _runtimeType; + + @override + Type get type => _runtimeType; + + @override + final String typeName; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _RuntimeTyperef && identical(_runtimeType, other._runtimeType); + + @override + int get hashCode => _runtimeType.hashCode; +} diff --git a/packages/coder/pubspec.yaml b/packages/coder/pubspec.yaml new file mode 100644 index 00000000..517ad7ba --- /dev/null +++ b/packages/coder/pubspec.yaml @@ -0,0 +1,14 @@ +name: coder +description: A general-purpose serialization framework for Dart. +repository: https://github.com/celest-dev/celest/tree/main/packages/coder +version: 0.0.1 + +environment: + sdk: ^3.3.0 + +dependencies: + meta: ^1.10.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/packages/coder/test/coder_test.dart b/packages/coder/test/coder_test.dart new file mode 100644 index 00000000..dc873afc --- /dev/null +++ b/packages/coder/test/coder_test.dart @@ -0,0 +1,466 @@ +import 'package:coder/coder.dart'; +import 'package:test/test.dart'; + +final class MyClass { + MyClass({ + required this.myBool, + required this.myInt, + required this.myDouble, + required this.myString, + required this.myDateTime, + }); + + final bool myBool; + final int myInt; + final double myDouble; + final String myString; + final DateTime myDateTime; + + static const Typeref self = Typeref(typeName: 'MyClass'); + + static MyClass decode(V value, Decoder decoder) { + final container = decoder.container(value); + final myBool = container.decodeBool('myBool'); + final myInt = container.decodeInt('myInt'); + final myDouble = container.decodeDouble('myDouble'); + final myString = container.decodeString('myString'); + final myDateTime = container.decodeDateTime('myDateTime'); + return MyClass( + myBool: myBool, + myInt: myInt, + myDouble: myDouble, + myString: myString, + myDateTime: myDateTime, + ); + } + + static V encode(MyClass instance, Encoder encoder) { + final container = encoder.container(); + container.encodeBool('myBool', instance.myBool); + container.encodeInt('myInt', instance.myInt); + container.encodeDouble('myDouble', instance.myDouble); + container.encodeString('myString', instance.myString); + container.encodeDateTime('myDateTime', instance.myDateTime); + return container.value; + } +} + +final coding = GlobalCoder( + staticConfig: const { + MyClass.self: CoderConfig( + encode: MyClass.encode, + decode: MyClass.decode, + ) + }, +); + +void main() { + group('JsonEncoder', () { + final jsonEncoder = coding.json.encoder; + + test('can encode single values', () { + final bool = jsonEncoder.singleValueContainer(); + bool.encodeBool(true); + expect(bool.value, true); + + final int = jsonEncoder.singleValueContainer(); + int.encodeInt(42); + expect(int.value, 42); + + final double = jsonEncoder.singleValueContainer(); + double.encodeDouble(3.14); + expect(double.value, 3.14); + + final string = jsonEncoder.singleValueContainer(); + string.encodeString('Hello, World!'); + expect(string.value, 'Hello, World!'); + + final nullValue = jsonEncoder.singleValueContainer(); + nullValue.encodeNull(); + expect(nullValue.value, null); + }); + + test('can encode list values', () { + final container = jsonEncoder.unkeyedContainer(); + container.encodeBool(true); + container.encodeInt(42); + container.encodeDouble(3.14); + container.encodeString('Hello, World!'); + container.encodeNull(); + + final sublist = container.nestedUnkeyedContainer(); + sublist.encodeBool(true); + sublist.encodeInt(42); + sublist.encodeDouble(3.14); + sublist.encodeString('Hello, World!'); + sublist.encodeNull(); + + final submap = container.nestedContainer(); + submap.encodeBool('bool', true); + submap.encodeInt('int', 42); + submap.encodeDouble('double', 3.14); + submap.encodeString('string', 'Hello, World!'); + submap.encodeNull('null'); + + final myClass = container.nestedSingleValueContainer(); + myClass.encode( + MyClass( + myBool: true, + myInt: 42, + myDouble: 3.14, + myString: 'Hello, World!', + myDateTime: DateTime.utc(2000), + ), + as: MyClass.self, + ); + + expect( + container.value, + [ + true, + 42, + 3.14, + 'Hello, World!', + null, + [true, 42, 3.14, 'Hello, World!', null], + { + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + }, + { + 'myBool': true, + 'myInt': 42, + 'myDouble': 3.14, + 'myString': 'Hello, World!', + 'myDateTime': 946684800000, + }, + ], + ); + }); + + test('can encode map values', () { + final container = jsonEncoder.container(); + container.encodeBool('bool', true); + container.encodeInt('int', 42); + container.encodeDouble('double', 3.14); + container.encodeString('string', 'Hello, World!'); + container.encodeNull('null'); + + final list = container.nestedUnkeyedContainer('list'); + list.encodeBool(true); + list.encodeInt(42); + list.encodeDouble(3.14); + list.encodeString('Hello, World!'); + list.encodeNull(); + + final map = container.nestedContainer('map'); + map.encodeBool('bool', true); + map.encodeInt('int', 42); + map.encodeDouble('double', 3.14); + map.encodeString('string', 'Hello, World!'); + map.encodeNull('null'); + + final myClass = container.nestedSingleValueContainer('myClass'); + myClass.encode( + MyClass( + myBool: true, + myInt: 42, + myDouble: 3.14, + myString: 'Hello, World!', + myDateTime: DateTime.utc(2000), + ), + as: MyClass.self, + ); + + expect(container.value, { + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + 'list': [true, 42, 3.14, 'Hello, World!', null], + 'map': { + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + }, + 'myClass': { + 'myBool': true, + 'myInt': 42, + 'myDouble': 3.14, + 'myString': 'Hello, World!', + 'myDateTime': 946684800000, + }, + }); + }); + }); + + group('JsonDecoder', () { + final jsonDecoder = coding.json.decoder; + + test('can decode single values', () { + final bool = jsonDecoder.singleValueContainer(true); + expect(bool.decodeBool(), true); + + final int = jsonDecoder.singleValueContainer(42); + expect(int.decodeInt(), 42); + + final double = jsonDecoder.singleValueContainer(3.14); + expect(double.decodeDouble(), 3.14); + + final string = jsonDecoder.singleValueContainer('Hello, World!'); + expect(string.decodeString(), 'Hello, World!'); + + final nullValue = jsonDecoder.singleValueContainer(null); + expect(nullValue.decodeNull(), true); + }); + + test('can decode list values', () { + final container = jsonDecoder.unkeyedContainer([ + true, + 42, + 3.14, + 'Hello, World!', + null, + [true, 42, 3.14, 'Hello, World!', null], + { + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + }, + { + 'myBool': true, + 'myInt': 42, + 'myDouble': 3.14, + 'myString': 'Hello, World!', + 'myDateTime': 946684800000, + }, + ]); + + expect(container.decodeBool(), true); + expect(container.decodeInt(), 42); + expect(container.decodeDouble(), 3.14); + expect(container.decodeString(), 'Hello, World!'); + expect(container.decodeNull(), true); + + final sublist = container.nestedUnkeyedContainer(); + expect(sublist.decodeBool(), true); + expect(sublist.decodeInt(), 42); + expect(sublist.decodeDouble(), 3.14); + expect(sublist.decodeString(), 'Hello, World!'); + expect(sublist.decodeNull(), true); + + final submap = container.nestedContainer(); + expect(submap.decodeBool('bool'), true); + expect(submap.decodeInt('int'), 42); + expect(submap.decodeDouble('double'), 3.14); + expect(submap.decodeString('string'), 'Hello, World!'); + expect(submap.decodeNull('null'), true); + + final myClass = container.nestedSingleValueContainer(); + final myClassValue = myClass.decode(as: MyClass.self); + expect(myClassValue.myBool, true); + expect(myClassValue.myInt, 42); + expect(myClassValue.myDouble, 3.14); + expect(myClassValue.myString, 'Hello, World!'); + }); + + test('can decode map values', () { + final container = jsonDecoder.container({ + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + 'list': [true, 42, 3.14, 'Hello, World!', null], + 'map': { + 'bool': true, + 'int': 42, + 'double': 3.14, + 'string': 'Hello, World!', + 'null': null, + }, + 'myClass': { + 'myBool': true, + 'myInt': 42, + 'myDouble': 3.14, + 'myString': 'Hello, World!', + 'myDateTime': 946684800000, + }, + }); + + expect(container.decodeBool('bool'), true); + expect(container.decodeInt('int'), 42); + expect(container.decodeDouble('double'), 3.14); + expect(container.decodeString('string'), 'Hello, World!'); + expect(container.decodeNull('null'), true); + + final list = container.nestedUnkeyedContainer('list'); + expect(list.decodeBool(), true); + expect(list.decodeInt(), 42); + expect(list.decodeDouble(), 3.14); + expect(list.decodeString(), 'Hello, World!'); + expect(list.decodeNull(), true); + + final map = container.nestedContainer('map'); + expect(map.decodeBool('bool'), true); + expect(map.decodeInt('int'), 42); + expect(map.decodeDouble('double'), 3.14); + expect(map.decodeString('string'), 'Hello, World!'); + expect(map.decodeNull('null'), true); + + final myClass = container.nestedSingleValueContainer('myClass'); + final myClassValue = myClass.decode(as: MyClass.self); + expect(myClassValue.myBool, true); + expect(myClassValue.myInt, 42); + expect(myClassValue.myDouble, 3.14); + expect(myClassValue.myString, 'Hello, World!'); + expect(myClassValue.myDateTime, DateTime.utc(2000)); + }); + }); + + const expectedFormValues = { + 'bool': 'true', + 'double': '1.0', + 'int': '1', + 'string': 'string', + 'datetime': '946684800000', + 'myClass[myBool]': 'true', + 'myClass[myInt]': '42', + 'myClass[myDouble]': '3.14', + 'myClass[myString]': 'Hello, World!', + 'myClass[myDateTime]': '946684800000', + 'list[0]': 'true', + 'list[1]': '1', + 'list[2]': 'string', + 'list[3][bool]': 'true', + 'list[3][double]': '1.0', + 'list[3][int]': '1', + 'list[3][string]': 'string', + 'list[3][datetime]': '946684800000', + 'list[3][myClass][myBool]': 'true', + 'list[3][myClass][myInt]': '42', + 'list[3][myClass][myDouble]': '3.14', + 'list[3][myClass][myString]': 'Hello, World!', + 'list[3][myClass][myDateTime]': '946684800000', + 'list[4][myBool]': 'true', + 'list[4][myInt]': '42', + 'list[4][myDouble]': '3.14', + 'list[4][myString]': 'Hello, World!', + 'list[4][myDateTime]': '946684800000', + 'list[5][0]': 'true', + 'list[5][1]': '1', + 'list[5][2]': 'string', + 'list[5][3][bool]': 'true', + 'list[5][3][double]': '1.0', + 'list[5][3][int]': '1', + 'list[5][3][string]': 'string', + 'list[5][3][datetime]': '946684800000', + 'list[5][3][myClass][myBool]': 'true', + 'list[5][3][myClass][myInt]': '42', + 'list[5][3][myClass][myDouble]': '3.14', + 'list[5][3][myClass][myString]': 'Hello, World!', + 'list[5][3][myClass][myDateTime]': '946684800000', + 'list[5][4][myBool]': 'true', + 'list[5][4][myInt]': '42', + 'list[5][4][myDouble]': '3.14', + 'list[5][4][myString]': 'Hello, World!', + 'list[5][4][myDateTime]': '946684800000', + 'fields[bool]': 'true', + 'fields[double]': '1.0', + 'fields[int]': '1', + 'fields[string]': 'string', + 'fields[datetime]': '946684800000', + 'fields[myClass][myBool]': 'true', + 'fields[myClass][myInt]': '42', + 'fields[myClass][myDouble]': '3.14', + 'fields[myClass][myString]': 'Hello, World!', + 'fields[myClass][myDateTime]': '946684800000', + 'fields[list][0]': 'true', + 'fields[list][1]': '1', + 'fields[list][2]': 'string', + 'fields[list][3][bool]': 'true', + 'fields[list][3][double]': '1.0', + 'fields[list][3][int]': '1', + 'fields[list][3][string]': 'string', + 'fields[list][3][datetime]': '946684800000', + 'fields[list][3][myClass][myBool]': 'true', + 'fields[list][3][myClass][myInt]': '42', + 'fields[list][3][myClass][myDouble]': '3.14', + 'fields[list][3][myClass][myString]': 'Hello, World!', + 'fields[list][3][myClass][myDateTime]': '946684800000', + 'fields[list][4][myBool]': 'true', + 'fields[list][4][myInt]': '42', + 'fields[list][4][myDouble]': '3.14', + 'fields[list][4][myString]': 'Hello, World!', + 'fields[list][4][myDateTime]': '946684800000', + }; + + void encodeValues(KeyedEncodingContainer container) { + container.encodeBool('bool', true); + container.encodeDouble('double', 1.0); + container.encodeInt('int', 1); + container.encodeString('string', 'string'); + container.encodeDateTime('datetime', DateTime.utc(2000)); + container.encode( + 'myClass', + MyClass( + myBool: true, + myInt: 42, + myDouble: 3.14, + myString: 'Hello, World!', + myDateTime: DateTime.utc(2000), + ), + as: MyClass.self, + ); + } + + void encodeList(UnkeyedEncodingContainer container) { + container.encodeBool(true); + container.encodeInt(1); + container.encodeString('string'); + container.encodeMap(encodeValues); + final myClass = container.nestedSingleValueContainer(); + myClass.encode( + MyClass( + myBool: true, + myInt: 42, + myDouble: 3.14, + myString: 'Hello, World!', + myDateTime: DateTime.utc(2000), + ), + as: MyClass.self, + ); + } + + Object encode(Encoder encoder) { + final container = encoder.container(); + encodeValues(container); + container.encodeList('list', (container) { + encodeList(container); + container.encodeList(encodeList); + }); + container.encodeMap('fields', (fields) { + encodeValues(fields); + fields.encodeList('list', encodeList); + }); + return container.value; + } + + test('FormDataEncoder', () { + final encoded = encode(coding.formData.encoder) as String; + expect(Uri.splitQueryString(encoded), expectedFormValues); + }); + + test('FormFieldsEncoder', () { + final encoded = encode(coding.formFields.encoder) as Map; + expect(encoded, expectedFormValues); + }); +}