Skip to content

Commit

Permalink
feat: package:coder (#128)
Browse files Browse the repository at this point in the history
Adds `package:coder`, a general-purpose serialization framework based on
Swift Codable.
  • Loading branch information
dnys1 authored May 8, 2024
1 parent 68c9bf2 commit 019c983
Show file tree
Hide file tree
Showing 21 changed files with 2,704 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/coder.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions packages/coder/.gitignore
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions packages/coder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.0.1

- Initial release
46 changes: 46 additions & 0 deletions packages/coder/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/coder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Coding

A general-purpose serialization framework for Dart, inspired by Swift's [Codable](https://developer.apple.com/documentation/swift/codable).
1 change: 1 addition & 0 deletions packages/coder/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
68 changes: 68 additions & 0 deletions packages/coder/example/example.dart
Original file line number Diff line number Diff line change
@@ -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<MyClass>(typeName: 'MyClass');

static MyClass decode<V extends Object?>(V value, Decoder<V> 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<V>(MyClass instance, Encoder<V> 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<V>(Encoder<V> protocol) => protocol.encode(this, as: self);
}

final coding = GlobalCoder(
staticConfig: const {
MyClass.self: CoderConfig<MyClass>(
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));
}
220 changes: 220 additions & 0 deletions packages/coder/lib/coder.dart
Original file line number Diff line number Diff line change
@@ -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<Typeref, CoderConfig> {
factory GlobalCoder({
/* TODO: @mustBeConst */ Map<Typeref, CoderConfig> staticConfig,
}) = _GlobalCoder;

const factory GlobalCoder.static(
/* TODO: @mustBeConst */ Map<Typeref, CoderConfig> config,
) = _StaticGlobalCoder;

CoderConfig<T> configFor<T extends Object>({Typeref<T>? type});
}

final class _StaticGlobalCoder extends UnmodifiableMapBase<Typeref, CoderConfig>
with GlobalCoder {
const _StaticGlobalCoder(this._config);

final Map<Typeref, CoderConfig> _config;

@override
CoderConfig<Object>? operator [](Object? key) => _config[key];

@override
Iterable<Typeref<Object>> get keys => _config.keys;

@override
CoderConfig<T> configFor<T extends Object>({Typeref<T>? type}) {
if (type == null) {
throw ArgumentError('Static type must be specified');
}
final config = _config[type] as CoderConfig<T>?;
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<Typeref, CoderConfig> {
_GlobalCoder({
/* TODO: @mustBeConst */ Map<Typeref, CoderConfig> staticConfig = const {},
}) : _staticConfig = staticConfig {
_runtimeConfig.addAll({
const Typeref<String>(): CoderConfig.string,
const Typeref<int>(): CoderConfig.int$,
const Typeref<double>(): CoderConfig.double$,
const Typeref<bool>(): CoderConfig.bool$,
const Typeref<DateTime>(): CoderConfig.dateTime,
});
}

final Map<Typeref, CoderConfig> _staticConfig;
final Map<Typeref, CoderConfig> _runtimeConfig = HashMap(
equals: (t1, t2) => identical(t1.type, t2.type),
hashCode: (t) => t.type.hashCode,
isValidKey: (t) => t.type != null,
);

@override
CoderConfig<T> configFor<T extends Object>({Typeref<T>? type}) {
assert(T != Object, 'Type must be specified');
type ??= Typeref<T>();
final config = this[type] as CoderConfig<T>?;
if (config == null) {
throw ArgumentError(
'No config registered for "${type.typeName}". '
'Did you add the Coding config to the registry?',
);
}
return config;
}

@override
CoderConfig<Object>? operator [](Object? key) =>
_runtimeConfig[key] ?? _staticConfig[key];

@override
void operator []=(Typeref<Object> key, CoderConfig<Object> value) {
_runtimeConfig[key] = value;
}

@override
void clear() => _runtimeConfig.clear();

@override
Iterable<Typeref<Object>> 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<Object>? 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 extends Object> = T Function<V extends Object?>(
V, Decoder<V>);
typedef TypeEncoder<T extends Object> = V Function<V extends Object?>(
T, Encoder<V>);

abstract interface class CoderKeys<Field extends Object> {
const factory CoderKeys.identity() = _CoderKeysIdentity<Field>;

String keyFor(Field field);
}

final class _CoderKeysIdentity<Field extends Object>
implements CoderKeys<Field> {
const _CoderKeysIdentity();

@override
String keyFor(Field field) => field.toString();
}

abstract mixin class CoderConfig<T extends Object> {
const factory CoderConfig({
TypeEncoder<T>? encode,
TypeDecoder<T>? decode,
}) = _CoderConfig<T>;

static final CoderConfig<String> string = CoderConfig<String>(
encode: <V>(value, encoder) => encoder.encodeString(value),
decode: <V>(value, decoder) => decoder.decodeString(value),
);
static final CoderConfig<int> int$ = CoderConfig<int>(
encode: <V>(value, encoder) => encoder.encodeInt(value),
decode: <V>(value, decoder) => decoder.decodeInt(value),
);
static final CoderConfig<double> double$ = CoderConfig<double>(
encode: <V>(value, encoder) => encoder.encodeDouble(value),
decode: <V>(value, decoder) => decoder.decodeDouble(value),
);
static final CoderConfig<bool> bool$ = CoderConfig<bool>(
encode: <V>(value, encoder) => encoder.encodeBool(value),
decode: <V>(value, decoder) => decoder.decodeBool(value),
);
static final CoderConfig<DateTime> dateTime = CoderConfig<DateTime>(
encode: <V>(value, encoder) => encoder.encodeDateTime(value),
decode: <V>(value, decoder) => decoder.decodeDateTime(value),
);

V encode<V extends Object?>(
T instance,
Encoder<V> encoder,
);
T decode<K extends Object?, V extends Object?>(
Object? value,
Decoder decoder,
);
}

final class _CoderConfig<T extends Object> with CoderConfig<T> {
const _CoderConfig({
TypeEncoder<T>? encode,
TypeDecoder<T>? decode,
}) : assert(
encode != null || decode != null,
'Either encode or decode must be provided',
),
_encode = encode ?? _noEncoder,
_decode = decode ?? _noDecoder;

static Never _noDecoder<V extends Object?>(V value, Decoder<V> decoder) {
throw UnimplementedError('No decoder registered for type');
}

static Never _noEncoder<V extends Object?>(Object value, Encoder<V> encoder) {
throw UnimplementedError('No encoder registered for type');
}

final TypeEncoder<T> _encode;
final TypeDecoder<T> _decode;

@override
V encode<V extends Object?>(
T instance,
Encoder<V> encoder,
) =>
_encode(instance, encoder);

@override
T decode<K extends Object?, V extends Object?>(
Object? value,
Decoder decoder,
) =>
_decode(value, decoder);
}
Loading

0 comments on commit 019c983

Please sign in to comment.