refactor(conduit): refactoring codable to typeforge

This commit is contained in:
Patrick Stewart 2024-08-04 00:26:55 -07:00
parent 844c19e8fe
commit 749d20b98e
16 changed files with 2563 additions and 6 deletions

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/// The `cast` library provides a collection of utilities for type casting and conversion in Dart.
///
/// This library exports several modules that offer different casting functionalities:
/// - `base_cast.dart`: Contains base casting operations.
/// - `primitive_cast.dart`: Provides casting methods for primitive data types.
/// - `collection_cast.dart`: Offers casting utilities for collections.
/// - `special_cast.dart`: Includes casting operations for special data types.
/// - `utility_cast.dart`: Contains additional utility functions for casting.
/// - `constants.dart`: Defines constants used across the casting operations.
///
/// These modules collectively provide a comprehensive set of tools for handling
/// various type conversion scenarios in Dart applications.
library cast;
export 'src/base_cast.dart';
export 'src/primitive_cast.dart';
export 'src/collection_cast.dart';
export 'src/special_cast.dart';
export 'src/utility_cast.dart';
export 'src/constants.dart';

View file

@ -1,8 +1,27 @@
/// Support for doing something awesome.
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/// The `codable` library provides functionality for encoding and decoding objects.
///
/// More dartdocs go here.
library;
/// This library exports several core components:
/// - `referenceable.dart`: Defines objects that can be referenced.
/// - `coding.dart`: Contains encoding and decoding interfaces.
/// - `keyed_archive.dart`: Implements a key-value storage for encoded objects.
/// - `list_archive.dart`: Implements a list-based storage for encoded objects.
/// - `reference_resolver.dart`: Handles resolving references within encoded data.
///
/// These components work together to provide a robust system for object serialization
/// and deserialization, supporting both simple and complex data structures.
library codable;
// TODO: Export any libraries intended for clients of this package.
export 'src/referenceable.dart';
export 'src/coding.dart';
export 'src/keyed_archive.dart';
export 'src/list_archive.dart';
export 'src/reference_resolver.dart';

View file

@ -0,0 +1,118 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:core' as core;
import 'dart:core' hide Map, String, int;
/// Represents an exception thrown when a cast operation fails.
///
/// This class is used to provide detailed information about the context
/// and reason for a failed cast operation.
///
/// [context] represents the location or scope where the cast failed.
/// [key] is an optional identifier for the specific element that failed to cast.
/// [message] provides additional details about the failure.
class FailedCast implements core.Exception {
dynamic context;
dynamic key;
core.String message;
FailedCast(this.context, this.key, this.message);
@override
core.String toString() {
if (key == null) {
return "Failed cast at $context: $message";
}
return "Failed cast at $context $key: $message";
}
}
/// An abstract class representing a type cast operation.
///
/// This class defines the structure for implementing type casting
/// from dynamic types to a specific type T.
///
/// The [cast] method is the public API for performing the cast,
/// while [safeCast] is the internal implementation that can be
/// overridden by subclasses to define specific casting behavior.
///
/// Usage:
/// ```dart
/// class MyCustomCast extends Cast<MyType> {
/// @override
/// MyType _cast(dynamic from, String context, dynamic key) {
/// // Custom casting logic here
/// }
/// }
/// ```
abstract class Cast<T> {
/// Constructs a new [Cast] instance.
///
/// This constructor is declared as `const` to allow for compile-time
/// constant instances of [Cast] subclasses. This can be beneficial for
/// performance and memory usage in certain scenarios.
const Cast();
/// Performs a safe cast operation from a dynamic type to type T.
///
/// This method wraps the [safeCast] method with additional error handling:
/// - If a [FailedCast] exception is thrown, it's rethrown as-is.
/// - For any other exception, it's caught and wrapped in a new [FailedCast] exception.
///
/// Parameters:
/// [from]: The value to be cast.
/// [context]: A string describing the context where the cast is performed.
/// [key]: An optional identifier for the specific element being cast.
///
/// Returns:
/// The cast value of type T.
///
/// Throws:
/// [FailedCast]: If the cast fails, either from [safeCast] or from wrapping another exception.
T _safeCast(dynamic from, core.String context, dynamic key) {
try {
return safeCast(from, context, key);
} on FailedCast {
rethrow;
} catch (e) {
throw FailedCast(context, key, e.toString());
}
}
/// Performs a safe cast operation from a dynamic type to type T.
///
/// This method is a convenience wrapper around [_safeCast] that provides
/// a default context of "toplevel" and a null key.
///
/// Parameters:
/// [from]: The value to be cast.
///
/// Returns:
/// The cast value of type T.
///
/// Throws:
/// [FailedCast]: If the cast operation fails.
T cast(dynamic from) => _safeCast(from, "toplevel", null);
/// Performs a safe cast operation from a dynamic type to type T.
///
/// This method should be implemented by subclasses to define the specific
/// casting behavior for the type T.
///
/// Parameters:
/// [from]: The value to be cast.
/// [context]: A string describing the context where the cast is performed.
/// [key]: An optional identifier for the specific element being cast.
///
/// Returns:
/// The cast value of type T.
///
/// Throws:
/// [FailedCast]: If the cast operation fails.
T safeCast(dynamic from, core.String context, dynamic key);
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:meta/meta.dart';
import 'package:protevus_typeforge/cast.dart' as cast;
import 'package:protevus_typeforge/codable.dart';
/// Abstract class representing a coding mechanism.
///
/// This class provides a framework for encoding and decoding objects.
/// It includes a [referenceURI] property and a [castMap] getter for type casting.
///
/// The [decode] method is used to populate the object's properties from a [KeyedArchive].
/// It must be called by subclasses, hence the @mustCallSuper annotation.
///
/// The [encode] method is abstract and must be implemented by subclasses to define
/// how the object should be encoded into a [KeyedArchive].
abstract class Coding {
/// The URI reference for this coding object.
///
/// This property holds a [Uri] that can be used as a reference or identifier
/// for the coded object. It may represent the location or source of the data,
/// or serve as a unique identifier within a larger system.
///
/// The [referenceURI] is typically set during decoding and can be accessed
/// or modified as needed. It may be null if no reference is available or required.
Uri? referenceURI;
/// A map of property names to their corresponding cast functions.
///
/// This getter returns a [Map] where the keys are strings representing
/// property names, and the values are [cast.Cast] functions for those properties.
/// The cast functions are used to convert decoded values to their appropriate types.
///
/// By default, this getter returns `null`, indicating that no custom casting
/// is required. Subclasses can override this getter to provide specific
/// casting behavior for their properties.
///
/// Returns `null` if no custom casting is needed, or a [Map] of property
/// names to cast functions if custom casting is required.
Map<String, cast.Cast<dynamic>>? get castMap => null;
/// Decodes the object from a [KeyedArchive].
///
/// This method is responsible for populating the object's properties from the
/// provided [KeyedArchive]. It performs two main actions:
///
/// 1. Sets the [referenceURI] of this object to the [referenceURI] of the
/// provided [KeyedArchive].
/// 2. Applies any necessary type casting to the values in the [KeyedArchive]
/// using the [castMap] defined for this object.
///
/// This method is marked with [@mustCallSuper], indicating that subclasses
/// overriding this method must call the superclass implementation.
///
/// [object] The [KeyedArchive] containing the encoded data to be decoded.
@mustCallSuper
void decode(KeyedArchive object) {
referenceURI = object.referenceURI;
object.castValues(castMap);
}
/// Encodes the object into a [KeyedArchive].
///
/// This abstract method must be implemented by subclasses to define
/// how the object should be encoded into a [KeyedArchive]. The implementation
/// should write all relevant properties of the object to the provided [object].
///
/// [object] The [KeyedArchive] to which the object's data should be encoded.
///
/// Note that the [referenceURI] of the object is not automatically written
/// to the [KeyedArchive]. See note in [KeyedArchive._encodedObject].
void encode(KeyedArchive object);
}

View file

@ -0,0 +1,129 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:core' as core;
import 'dart:core' hide Map, String, int;
import 'package:protevus_typeforge/cast.dart';
/// A cast operation for converting dynamic values to [core.Map<K, V>].
///
/// This class extends [Cast<core.Map<K, V>>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.Map<K, V>].
///
/// The class uses two separate [Cast] instances:
/// - [_key] for casting the keys of the input map to type K
/// - [_value] for casting the values of the input map to type V
///
/// The [safeCast] method checks if the input [from] is already a [core.Map].
/// If it is, it creates a new map, casting each key-value pair using the
/// respective [_key] and [_value] casts. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final mapCast = Map(StringCast(), IntCast());
/// final result = mapCast.cast({"a": 1, "b": 2}); // Returns Map<String, int>
/// mapCast.cast("not a map"); // Throws FailedCast
/// ```
class Map<K, V> extends Cast<core.Map<K, V>> {
final Cast<K> _key;
final Cast<V> _value;
const Map(Cast<K> key, Cast<V> value)
: _key = key,
_value = value;
@override
core.Map<K, V> safeCast(dynamic from, core.String context, dynamic key) {
if (from is core.Map) {
final result = <K, V>{};
for (final key in from.keys) {
final newKey = _key.safeCast(key, "map entry", key);
result[newKey] = _value.safeCast(from[key], "map entry", key);
}
return result;
}
return throw FailedCast(context, key, "not a map");
}
}
/// A cast operation for converting dynamic values to [core.Map<core.String, V>].
///
/// This class extends [Cast<core.Map<core.String, V>>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.Map<core.String, V>].
///
/// The class uses a [Cast<V>] instance [_value] for casting the values of the input map to type V.
///
/// The [safeCast] method checks if the input [from] is already a [core.Map].
/// If it is, it creates a new map with [core.String] keys and values of type V,
/// casting each value using the [_value] cast. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final stringMapCast = StringMap(IntCast());
/// final result = stringMapCast.cast({"a": 1, "b": 2}); // Returns Map<String, int>
/// stringMapCast.cast("not a map"); // Throws FailedCast
/// ```
class StringMap<V> extends Cast<core.Map<core.String, V>> {
final Cast<V> _value;
const StringMap(Cast<V> value) : _value = value;
@override
core.Map<core.String, V> safeCast(
dynamic from,
core.String context,
dynamic key,
) {
if (from is core.Map) {
final result = <core.String, V>{};
for (final core.String key in from.keys as core.Iterable<core.String>) {
result[key] = _value.safeCast(from[key], "map entry", key);
}
return result;
}
return throw FailedCast(context, key, "not a map");
}
}
/// A cast operation for converting dynamic values to [core.List<E?>].
///
/// This class extends [Cast<core.List<E?>>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.List<E?>].
///
/// The class uses a [Cast<E>] instance [_entry] for casting each element of the input list to type E.
///
/// The [safeCast] method checks if the input [from] is already a [core.List].
/// If it is, it creates a new list of nullable E elements, casting each non-null
/// element using the [_entry] cast and preserving null values. If not, it throws
/// a [FailedCast] exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final listCast = List(IntCast());
/// final result = listCast.cast([1, 2, null, 3]); // Returns List<int?>
/// listCast.cast("not a list"); // Throws FailedCast
/// ```
class List<E> extends Cast<core.List<E?>> {
final Cast<E> _entry;
const List(Cast<E> entry) : _entry = entry;
@override
core.List<E?> safeCast(dynamic from, core.String context, dynamic key) {
if (from is core.List) {
final length = from.length;
final result = core.List<E?>.filled(length, null);
for (core.int i = 0; i < length; ++i) {
if (from[i] != null) {
result[i] = _entry.safeCast(from[i], "list entry", i);
} else {
result[i] = null;
}
}
return result;
}
return throw FailedCast(context, key, "not a list");
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_typeforge/cast.dart';
/// A constant instance of [AnyCast] that can be used for casting any dynamic value.
///
/// This constant provides a convenient way to use the [AnyCast] functionality
/// without needing to create a new instance each time. It can be used in situations
/// where type-checking is not required, and you want to allow any type to pass through.
///
/// Example usage:
/// ```dart
/// final result = any.cast(someValue); // Returns someValue unchanged, regardless of its type
/// ```
const any = AnyCast();
/// A constant instance of [BoolCast] that can be used for casting dynamic values to [core.bool].
///
/// This constant provides a convenient way to use the [BoolCast] functionality
/// without needing to create a new instance each time. It can be used to perform
/// boolean type checking and casting operations.
///
/// Example usage:
/// ```dart
/// final result = bool.cast(true); // Returns true
/// bool.cast("not a bool"); // Throws FailedCast
/// ```
const bool = BoolCast();
/// A constant instance of [IntCast] that can be used for casting dynamic values to [core.int].
///
/// This constant provides a convenient way to use the [IntCast] functionality
/// without needing to create a new instance each time. It can be used to perform
/// integer type checking and casting operations.
///
/// Example usage:
/// ```dart
/// final result = int.cast(42); // Returns 42
/// int.cast("not an int"); // Throws FailedCast
/// ```
const int = IntCast();
/// A constant instance of [DoubleCast] that can be used for casting dynamic values to [core.double].
///
/// This constant provides a convenient way to use the [DoubleCast] functionality
/// without needing to create a new instance each time. It can be used to perform
/// double type checking and casting operations.
///
/// Example usage:
/// ```dart
/// final result = double.cast(3.14); // Returns 3.14
/// double.cast("not a double"); // Throws FailedCast
/// ```
const double = DoubleCast();
/// A constant instance of [StringCast] that can be used for casting dynamic values to [core.String].
///
/// This constant provides a convenient way to use the [StringCast] functionality
/// without needing to create a new instance each time. It can be used to perform
/// string type checking and casting operations.
///
/// Example usage:
/// ```dart
/// final result = string.cast("Hello"); // Returns "Hello"
/// string.cast(42); // Throws FailedCast
/// ```
const string = StringCast();

View file

@ -0,0 +1,580 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:collection';
import 'package:protevus_typeforge/cast.dart' as cast;
import 'package:protevus_typeforge/codable.dart';
/// A container for a dynamic data object that can be decoded into [Coding] objects.
///
/// A [KeyedArchive] is a [Map], but it provides additional behavior for decoding [Coding] objects
/// and managing JSON Schema references ($ref) through methods like [decode], [decodeObject], etc.
///
/// You create a [KeyedArchive] by invoking [KeyedArchive.unarchive] and passing data decoded from a
/// serialization format like JSON and YAML. A [KeyedArchive] is then provided as an argument to
/// a [Coding] subclass' [Coding.decode] method.
///
/// final json = json.decode(...);
/// final archive = KeyedArchive.unarchive(json);
/// final person = Person()..decode(archive);
///
/// You may also create [KeyedArchive]s from [Coding] objects so that they can be serialized.
///
/// final person = Person()..name = "Bob";
/// final archive = KeyedArchive.archive(person);
/// final json = json.encode(archive);
///
/// This class extends [Object] and mixes in [MapBase<String, dynamic>], allowing it to be used as a Map.
/// It also implements [Referenceable], providing functionality for handling references within the archive.
///
/// The constructor is not typically used directly; instead, use the [KeyedArchive.unarchive]
/// or [KeyedArchive.archive] methods to create instances of [KeyedArchive].
class KeyedArchive extends Object
with MapBase<String, dynamic>
implements Referenceable {
/// Use [unarchive] instead.
KeyedArchive(this._map) {
_recode();
}
/// Unarchives [data] into a [KeyedArchive] that can be used by [Coding.decode] to deserialize objects.
///
/// Each [Map] in [data] (including [data] itself) is converted to a [KeyedArchive].
/// Each [List] in [data] is converted to a [ListArchive]. These conversions occur for deeply nested maps
/// and lists.
///
/// If [allowReferences] is true, JSON Schema references will be traversed and decoded objects
/// will contain values from the referenced object. This flag defaults to false.
KeyedArchive.unarchive(this._map, {bool allowReferences = false}) {
_recode();
if (allowReferences) {
resolveOrThrow(ReferenceResolver(this));
}
}
/// Archives a [Coding] object into a [Map] that can be serialized into formats like JSON or YAML.
///
/// Note that the return value of this method, as well as all other [Map] and [List] objects
/// embedded in the return value, are instances of [KeyedArchive] and [ListArchive]. These types
/// implement [Map] and [List], respectively.
///
/// If [allowReferences] is true, JSON Schema references in the emitted document will be validated.
/// Defaults to false.
static Map<String, dynamic> archive(
Coding root, {
bool allowReferences = false,
}) {
final archive = KeyedArchive({});
root.encode(archive);
if (allowReferences) {
archive.resolveOrThrow(ReferenceResolver(archive));
}
return archive.toPrimitive();
}
/// Private constructor that creates an empty [KeyedArchive].
///
/// This constructor initializes the internal [_map] with an empty Map<String, dynamic>.
/// It's intended for internal use within the [KeyedArchive] class.
KeyedArchive._empty() : _map = <String, dynamic>{};
/// A reference to another object in the same document.
///
/// This property represents a URI reference to another object within the same document.
/// It is used to establish relationships between objects in a hierarchical structure.
///
/// Assign values to this property using the default [Uri] constructor and its path argument.
/// This property is serialized as a [Uri] fragment, e.g. `#/components/all`.
///
/// Example:
///
/// final object = new MyObject()
/// ..referenceURI = Uri(path: "/other/object");
/// archive.encodeObject("object", object);
///
Uri? referenceURI;
/// The internal map that stores the key-value pairs of this [KeyedArchive].
///
/// This map is used to store the actual data of the archive. It is of type
/// `Map<String, dynamic>` to allow for flexibility in the types of values
/// that can be stored. The keys are always strings, representing the names
/// of the properties, while the values can be of any type.
///
/// This map is manipulated by various methods of the [KeyedArchive] class,
/// such as the [] operator, decode methods, and encode methods. It's also
/// used when converting the archive to primitive types or when resolving
/// references.
Map<String, dynamic> _map;
/// Stores the inflated (decoded) object associated with this archive.
///
/// This property is used to cache the decoded object after it has been
/// inflated from the archive data. It allows for efficient retrieval
/// of the decoded object in subsequent accesses, avoiding repeated
/// decoding operations.
///
/// The type is [Coding?] to accommodate both null values (when no object
/// has been inflated yet) and any object that implements the [Coding] interface.
Coding? _inflated;
/// A reference to another [KeyedArchive] object.
///
/// This property is used to handle JSON Schema references ($ref).
/// When a reference is resolved, this property holds the referenced [KeyedArchive] object.
/// It allows the current archive to access values from the referenced object
/// when a key is not found in the current archive's map.
KeyedArchive? _objectReference;
/// Typecast the values in this archive.
///
/// Prefer to override [Coding.castMap] instead of using this method directly.
///
/// This method will recursively type values in this archive to the desired type
/// for a given key. Use this method (or [Coding.castMap]) for decoding `List` and `Map`
/// types, where the values are not `Coding` objects.
///
/// You must `import 'package:codable/cast.dart' as cast;`.
///
/// Usage:
///
/// final dynamicObject = {
/// "key": <dynamic>["foo", "bar"]
/// };
/// final archive = KeyedArchive.unarchive(dynamicObject);
/// archive.castValues({
/// "key": cast.List(cast.String)
/// });
///
/// // This now becomes a valid assignment
/// List<String> key = archive.decode("key");
///
/// This method takes a [schema] parameter of type `Map<String, cast.Cast>?`, which defines
/// the types to cast for each key in the archive. If [schema] is null, the method returns
/// without performing any casting. The method uses a flag [_casted] to ensure it only
/// performs the casting once. It creates a [cast.Keyed] object with the provided schema
/// and uses it to cast the values in both the main [_map] and the [_objectReference] map
/// (if it exists). This ensures type safety and consistency across the entire archive structure.
void castValues(Map<String, cast.Cast>? schema) {
if (schema == null) {
return;
}
if (_casted) return;
_casted = true;
final caster = cast.Keyed(schema);
_map = caster.cast(_map);
if (_objectReference != null) {
_objectReference!._map = caster.cast(_objectReference!._map);
}
}
/// A flag indicating whether the values in this archive have been cast.
///
/// This boolean is used to ensure that the [castValues] method is only
/// called once on this archive. It is set to true after the first call
/// to [castValues], preventing redundant type casting operations.
bool _casted = false;
/// Sets the value associated with the given [key] in this [KeyedArchive].
///
/// This operator allows you to assign values to keys in the archive as if it were a regular map.
/// The [key] must be a [String], and [value] can be of any type.
///
/// Example:
/// archive['name'] = 'John Doe';
/// archive['age'] = 30;
///
/// Note that this method directly modifies the internal [_map] of the archive.
/// It does not perform any type checking or conversion on the [value].
@override
void operator []=(covariant String key, dynamic value) {
_map[key] = value;
}
/// Retrieves the value associated with the given [key] from this [KeyedArchive].
///
/// This operator allows you to access values in the archive as if it were a regular map.
/// The [key] must be a [String].
///
/// If the key is found in the current archive's map, its value is returned.
/// If not found and this archive has an [_objectReference], it attempts to retrieve
/// the value from the referenced object.
///
/// Example:
/// var name = archive['name'];
/// var age = archive['age'];
///
/// Returns the value associated with [key], or null if the key is not found.
@override
dynamic operator [](covariant Object key) => _getValue(key as String);
/// Returns an [Iterable] of all the keys in the archive.
///
/// This getter provides access to all the keys stored in the internal [_map]
/// of the [KeyedArchive]. It allows iteration over all keys without exposing
/// the underlying map structure.
///
/// Returns: An [Iterable<String>] containing all the keys in the archive.
@override
Iterable<String> get keys => _map.keys;
/// Removes all entries from this [KeyedArchive].
///
/// After this call, the archive will be empty.
/// This method directly calls the [clear] method on the internal [_map].
@override
void clear() => _map.clear();
/// Removes the entry for the given [key] from this [KeyedArchive] and returns its value.
///
/// This method removes the key-value pair associated with [key] from the internal map
/// of this [KeyedArchive]. If [key] was in the archive, its associated value is returned.
/// If [key] was not in the archive, null is returned.
///
/// The [key] should be a [String], as this is a [KeyedArchive]. However, the method
/// accepts [Object?] to comply with the [MapBase] interface it implements.
///
/// Returns the value associated with [key] before it was removed, or null if [key]
/// was not in the archive.
@override
dynamic remove(Object? key) => _map.remove(key);
/// Converts this [KeyedArchive] to a primitive [Map<String, dynamic>].
///
/// This method recursively converts the contents of the archive to primitive types:
/// - [KeyedArchive] instances are converted to [Map<String, dynamic>]
/// - [ListArchive] instances are converted to [List<dynamic>]
/// - Other values are left as-is
///
/// This is useful when you need to serialize the archive to a format like JSON
/// that doesn't support custom object types.
///
/// Returns a new [Map<String, dynamic>] containing the primitive representation
/// of this archive.
Map<String, dynamic> toPrimitive() {
final out = <String, dynamic>{};
_map.forEach((key, val) {
if (val is KeyedArchive) {
out[key] = val.toPrimitive();
} else if (val is ListArchive) {
out[key] = val.toPrimitive();
} else {
out[key] = val;
}
});
return out;
}
/// Retrieves the value associated with the given [key] from this [KeyedArchive].
///
/// This method first checks if the key exists in the current archive's internal map.
/// If found, it returns the associated value.
/// If the key is not found in the current archive, and this archive has an [_objectReference],
/// it attempts to retrieve the value from the referenced object recursively.
///
/// Parameters:
/// [key] - The string key to look up in the archive.
///
/// Returns:
/// The value associated with the [key] if found, or null if the key is not present
/// in either the current archive or any referenced archives.
dynamic _getValue(String key) {
if (_map.containsKey(key)) {
return _map[key];
}
return _objectReference?._getValue(key);
}
/// Recodes the internal map of this [KeyedArchive].
///
/// This method performs the following operations:
/// 1. Creates a [cast.Map] caster for string keys and any values.
/// 2. Iterates through all keys in the internal map.
/// 3. For each key-value pair:
/// - If the value is a [Map], it's converted to a [KeyedArchive].
/// - If the value is a [List], it's converted to a [ListArchive].
/// - If the key is "$ref", it sets the [referenceURI] by parsing the value.
///
/// This method is called during initialization to ensure proper structure
/// and typing of the archive's contents.
void _recode() {
const caster = cast.Map(cast.string, cast.any);
final keys = _map.keys.toList();
for (final key in keys) {
final val = _map[key];
if (val is Map) {
_map[key] = KeyedArchive(caster.cast(val));
} else if (val is List) {
_map[key] = ListArchive.from(val);
} else if (key == r"$ref") {
referenceURI = Uri.parse(Uri.parse(val.toString()).fragment);
}
}
}
/// Validates and resolves references within this [KeyedArchive] and its nested objects.
///
/// This method is automatically invoked by both [KeyedArchive.unarchive] and [KeyedArchive.archive].
@override
void resolveOrThrow(ReferenceResolver coder) {
if (referenceURI != null) {
_objectReference = coder.resolve(referenceURI!);
if (_objectReference == null) {
throw ArgumentError(
"Invalid document. Reference '#${referenceURI!.path}' does not exist in document.",
);
}
}
_map.forEach((key, val) {
if (val is KeyedArchive) {
val.resolveOrThrow(coder);
} else if (val is ListArchive) {
val.resolveOrThrow(coder);
}
});
}
/// Decodes a [KeyedArchive] into an object of type [T] that extends [Coding].
///
/// This method is responsible for inflating (decoding) an object from its archived form.
/// If the [raw] archive is null, the method returns null.
///
/// If the archive has not been inflated before (i.e., [_inflated] is null),
/// it creates a new instance using the [inflate] function, decodes the archive
/// into this new instance, and caches it in [_inflated] for future use.
///
/// Parameters:
/// [raw]: The [KeyedArchive] containing the encoded object data.
/// [inflate]: A function that returns a new instance of [T].
///
/// Returns:
/// The decoded object of type [T], or null if [raw] is null.
T? _decodedObject<T extends Coding?>(
KeyedArchive? raw,
T Function() inflate,
) {
if (raw == null) {
return null;
}
if (raw._inflated == null) {
raw._inflated = inflate();
raw._inflated!.decode(raw);
}
return raw._inflated as T?;
}
/// Returns the object associated with [key] in this [KeyedArchive].
///
/// If [T] is inferred to be a [Uri] or [DateTime],
/// the associated object is assumed to be a [String] and an appropriate value is parsed
/// from that string.
///
/// If this object is a reference to another object (via [referenceURI]), this object's key-value
/// pairs will be searched first. If [key] is not found, the referenced object's key-values pairs are searched.
/// If no match is found, null is returned.
T? decode<T>(String key) {
final v = _getValue(key);
if (v == null) {
return null;
}
if (T == Uri) {
return Uri.parse(v.toString()) as T;
} else if (T == DateTime) {
return DateTime.parse(v.toString()) as T;
}
return v as T?;
}
/// Decodes and returns an instance of [T] associated with the given [key] in this [KeyedArchive].
///
/// [inflate] must create an empty instance of [T]. The value associated with [key]
/// must be a [KeyedArchive] (a [Map]). The values of the associated object are read into
/// the empty instance of [T].
T? decodeObject<T extends Coding>(String key, T Function() inflate) {
final val = _getValue(key);
if (val == null) {
return null;
}
if (val is! KeyedArchive) {
throw ArgumentError(
"Cannot decode key '$key' into '$T', because the value is not a Map. Actual value: '$val'.",
);
}
return _decodedObject(val, inflate);
}
/// Decodes and returns a list of objects of type [T] associated with the given [key] in this [KeyedArchive].
///
/// [inflate] must create an empty instance of [T]. The value associated with [key]
/// must be a [ListArchive] (a [List] of [Map]). For each element of the archived list,
/// [inflate] is invoked and each object in the archived list is decoded into
/// the instance of [T].
List<T?>? decodeObjects<T extends Coding>(String key, T? Function() inflate) {
final val = _getValue(key);
if (val == null) {
return null;
}
if (val is! List) {
throw ArgumentError(
"Cannot decode key '$key' as 'List<$T>', because value is not a List. Actual value: '$val'.",
);
}
return val
.map((v) => _decodedObject(v as KeyedArchive?, inflate))
.toList()
.cast<T?>();
}
/// Decodes and returns a map of objects of type [T] associated with the given [key] in this [KeyedArchive].
///
/// [inflate] must create an empty instance of [T]. The value associated with [key]
/// must be a [KeyedArchive] (a [Map]), where each value is a [T].
/// For each key-value pair of the archived map, [inflate] is invoked and
/// each value is decoded into the instance of [T].
Map<String, T?>? decodeObjectMap<T extends Coding>(
String key,
T Function() inflate,
) {
final v = _getValue(key);
if (v == null) {
return null;
}
if (v is! Map<String, dynamic>) {
throw ArgumentError(
"Cannot decode key '$key' as 'Map<String, $T>', because value is not a Map. Actual value: '$v'.",
);
}
return {
for (var k in v.keys) k: _decodedObject(v[k] as KeyedArchive?, inflate)
};
}
/// Encodes a [Coding] object into a [Map<String, dynamic>] representation.
///
/// This method creates a [KeyedArchive] from the given [object] and returns its
/// internal map representation. If the [object] has a [referenceURI], it is
/// encoded as a '$ref' key in the resulting map.
///
/// If [object] is null, this method returns null.
///
/// Note: There is a known limitation where overridden values from a reference
/// object are not currently being emitted. This is due to the complexity of
/// handling cyclic references between objects.
///
/// Parameters:
/// [object]: The [Coding] object to be encoded.
///
/// Returns:
/// A [Map<String, dynamic>] representation of the [object], or null if [object] is null.
Map<String, dynamic>? _encodedObject(Coding? object) {
if (object == null) {
return null;
}
final json = KeyedArchive._empty()
.._map = {}
..referenceURI = object.referenceURI;
if (json.referenceURI != null) {
json._map[r"$ref"] = Uri(fragment: json.referenceURI!.path).toString();
} else {
object.encode(json);
}
return json;
}
/// Encodes [value] into this object for [key].
///
/// This method adds a key-value pair to the internal map of the [KeyedArchive].
/// The [key] is always a [String], while [value] can be of any type.
///
/// If [value] is null, no value is encoded and the [key] will not be present
/// in the resulting archive.
void encode(String key, dynamic value) {
if (value == null) {
return;
}
if (value is DateTime) {
_map[key] = value.toIso8601String();
} else if (value is Uri) {
_map[key] = value.toString();
} else {
_map[key] = value;
}
}
/// Encodes a [Coding] object into this object for [key].
///
/// This method takes a [Coding] object [value] and encodes it into the archive
/// under the specified [key]. If [value] is null, no action is taken and the method returns early.
///
/// The encoding process involves:
/// 1. Checking if the [value] is null.
/// 2. If not null, it uses the private [_encodedObject] method to convert the [Coding] object
/// into a format suitable for storage in the archive.
/// 3. The encoded object is then stored in the archive's internal map ([_map]) using the provided [key].
///
/// This method is useful for adding complex objects that implement the [Coding] interface
/// to the archive, allowing for structured data storage and later retrieval.
///
/// Parameters:
/// [key]: A [String] that serves as the identifier for the encoded object in the archive.
/// [value]: A [Coding] object to be encoded and stored. Can be null.
///
/// Example:
/// ```dart
/// final person = Person(name: "John", age: 30);
/// archive.encodeObject("person", person);
/// ```
void encodeObject(String key, Coding? value) {
if (value == null) {
return;
}
_map[key] = _encodedObject(value);
}
/// Encodes a list of [Coding] objects into this archive for the given [key].
///
/// This invokes [Coding.encode] on each object in [value] and adds the list of objects
/// to this archive for the key [key].
void encodeObjects(String key, List<Coding?>? value) {
if (value == null) {
return;
}
_map[key] = ListArchive.from(value.map((v) => _encodedObject(v)).toList());
}
/// Encodes a map of [Coding] objects into this archive for the given [key].
///
/// This invokes [Coding.encode] on each value in [value] and adds the map of objects
/// to this archive for the key [key].
void encodeObjectMap<T extends Coding>(String key, Map<String, T?>? value) {
if (value == null) return;
final object = KeyedArchive({});
value.forEach((k, v) {
object[k] = _encodedObject(v);
});
_map[key] = object;
}
}

View file

@ -0,0 +1,233 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:collection';
import 'package:protevus_typeforge/codable.dart';
/// A list of values in a [KeyedArchive].
///
/// This object is a [List] that has additional behavior for encoding and decoding [Coding] objects.
/// It provides functionality to store and manipulate a list of dynamic values, with special handling
/// for nested maps and lists. The class implements [Referenceable], allowing it to resolve references
/// within its contents when used in conjunction with a [ReferenceResolver].
///
/// The [ListArchive] can be created empty or initialized from an existing list. When initialized
/// from a list, it automatically converts nested maps and lists to [KeyedArchive] and [ListArchive]
/// instances respectively, providing a consistent interface for complex nested structures.
///
/// This class is particularly useful when working with serializable data structures that may
/// contain nested objects or arrays, as it preserves the structure while allowing for easy
/// manipulation and serialization.
class ListArchive extends Object
with ListBase<dynamic>
implements Referenceable {
/// The internal list that stores the dynamic values of this [ListArchive].
///
/// This list can contain various types of elements, including primitive types,
/// [KeyedArchive] instances (for nested maps), and other [ListArchive] instances
/// (for nested lists). It is used to maintain the structure and order of the
/// archived data while providing the necessary functionality for the [ListArchive].
final List<dynamic> _inner;
/// Creates an empty [ListArchive].
///
/// This constructor initializes a new [ListArchive] instance with an empty internal list.
/// The resulting [ListArchive] is ready to accept new elements through its various
/// list manipulation methods inherited from [ListBase].
ListArchive() : _inner = [];
/// Creates a [ListArchive] from an existing [List] of dynamic values.
///
/// This constructor takes a [List] of dynamic values as input and initializes
/// a new [ListArchive] instance. It processes each element of the input list,
/// converting any nested [Map] to [KeyedArchive] and nested [List] to [ListArchive].
/// This conversion is done using the [_toAtchiveType] function.
///
/// The resulting [ListArchive] maintains the structure of the original list
/// but with enhanced functionality for handling nested data structures.
///
/// Parameters:
/// [raw]: The input [List] of dynamic values to be converted into a [ListArchive].
///
/// Returns:
/// A new [ListArchive] instance containing the processed elements from the input list.
ListArchive.from(List<dynamic> raw)
: _inner = raw.map(_toAtchiveType).toList();
/// Returns the element at the specified [index] in the list.
///
/// This operator overrides the default list indexing behavior to access
/// elements in the internal [_inner] list.
///
/// Parameters:
/// [index]: An integer index of the element to retrieve.
///
/// Returns:
/// The element at the specified [index] in the list.
///
/// Throws:
/// [RangeError] if the [index] is out of bounds.
@override
dynamic operator [](int index) => _inner[index];
/// Returns the length of the internal list.
///
/// This getter overrides the [length] property from [ListBase] to provide
/// the correct length of the internal [_inner] list.
///
/// Returns:
/// An integer representing the number of elements in the [ListArchive].
@override
int get length => _inner.length;
/// Sets the length of the internal list.
///
/// This setter overrides the [length] property from [ListBase] to allow
/// modification of the internal [_inner] list's length. Setting the length
/// can be used to truncate the list or extend it with null values.
///
/// Parameters:
/// [length]: The new length to set for the list.
///
/// Throws:
/// [RangeError] if [length] is negative.
/// [UnsupportedError] if the list is fixed-length.
@override
set length(int length) {
_inner.length = length;
}
/// Sets the value at the specified [index] in the list.
///
/// This operator overrides the default list indexing assignment behavior to
/// modify elements in the internal [_inner] list.
///
/// Parameters:
/// [index]: An integer index of the element to set.
/// [val]: The new value to be assigned at the specified [index].
///
/// Throws:
/// [RangeError] if the [index] is out of bounds.
/// [UnsupportedError] if the list is fixed-length.
@override
void operator []=(int index, dynamic val) {
_inner[index] = val;
}
/// Adds a single element to the end of this list.
///
/// This method overrides the [add] method from [ListBase] to add an element
/// to the internal [_inner] list.
///
/// Parameters:
/// [element]: The element to be added to the list. Can be of any type.
///
/// The list grows by one element.
@override
void add(dynamic element) {
_inner.add(element);
}
/// Adds all elements of the given [iterable] to the end of this list.
///
/// This method overrides the [addAll] method from [ListBase] to add multiple
/// elements to the internal [_inner] list.
///
/// Parameters:
/// [iterable]: An [Iterable] of elements to be added to the list. The elements
/// can be of any type.
///
/// The list grows by the length of the [iterable].
@override
void addAll(Iterable<dynamic> iterable) {
_inner.addAll(iterable);
}
/// Converts the [ListArchive] to a list of primitive values.
///
/// This method traverses the [ListArchive] and converts its contents to a list
/// of primitive values. It recursively processes nested [KeyedArchive] and
/// [ListArchive] instances, ensuring that the entire structure is converted
/// to basic Dart types.
///
/// Returns:
/// A [List<dynamic>] containing the primitive representation of the [ListArchive].
/// - [KeyedArchive] instances are converted to [Map]s.
/// - [ListArchive] instances are converted to [List]s.
/// - Other values are left as-is.
///
/// This method is useful for serialization purposes or when you need to
/// convert the [ListArchive] to a format that can be easily serialized
/// or transmitted.
List<dynamic> toPrimitive() {
final out = [];
for (final val in _inner) {
if (val is KeyedArchive) {
out.add(val.toPrimitive());
} else if (val is ListArchive) {
out.add(val.toPrimitive());
} else {
out.add(val);
}
}
return out;
}
/// Resolves references within this [ListArchive] using the provided [ReferenceResolver].
///
/// This method iterates through all elements in the internal list ([_inner]) and
/// resolves references for nested [KeyedArchive] and [ListArchive] instances.
/// It's part of the [Referenceable] interface implementation, allowing for
/// deep resolution of references in complex nested structures.
///
/// Parameters:
/// [coder]: A [ReferenceResolver] used to resolve references within the archive.
///
/// Throws:
/// May throw exceptions if reference resolution fails, as implied by the method name.
///
/// This method is typically called during the decoding process to ensure all
/// references within the archive structure are properly resolved.
@override
void resolveOrThrow(ReferenceResolver coder) {
for (final i in _inner) {
if (i is KeyedArchive) {
i.resolveOrThrow(coder);
} else if (i is ListArchive) {
i.resolveOrThrow(coder);
}
}
}
}
/// Converts a dynamic value to an archive type if necessary.
///
/// This function takes a dynamic value and converts it to an appropriate archive type:
/// - If the input is a [Map<String, dynamic>], it's converted to a [KeyedArchive].
/// - If the input is a [List], it's converted to a [ListArchive].
/// - For all other types, the input is returned as-is.
///
/// This function is used internally by [ListArchive] to ensure that nested structures
/// (maps and lists) are properly converted to their respective archive types when
/// creating a new [ListArchive] instance.
///
/// Parameters:
/// [e]: The dynamic value to be converted.
///
/// Returns:
/// The input value converted to an appropriate archive type, or the original value
/// if no conversion is necessary.
dynamic _toAtchiveType(dynamic e) {
if (e is Map<String, dynamic>) {
return KeyedArchive(e);
} else if (e is List) {
return ListArchive.from(e);
}
return e;
}

View file

@ -0,0 +1,108 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:core' as core;
import 'dart:core' hide Map, String, int;
import 'package:protevus_typeforge/cast.dart';
/// A cast operation for converting dynamic values to [core.int].
///
/// This class extends [Cast<core.int>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.int].
///
/// The [safeCast] method checks if the input [from] is already a [core.int].
/// If it is, it returns the value unchanged. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final intCast = IntCast();
/// final result = intCast.cast(42); // Returns 42
/// intCast.cast("not an int"); // Throws FailedCast
/// ```
class IntCast extends Cast<core.int> {
const IntCast();
@override
core.int safeCast(dynamic from, core.String context, dynamic key) =>
from is core.int
? from
: throw FailedCast(context, key, "$from is not an int");
}
/// A cast operation for converting dynamic values to [core.double].
///
/// This class extends [Cast<core.double>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.double].
///
/// The [safeCast] method checks if the input [from] is already a [core.double].
/// If it is, it returns the value unchanged. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final doubleCast = DoubleCast();
/// final result = doubleCast.cast(3.14); // Returns 3.14
/// doubleCast.cast("not a double"); // Throws FailedCast
/// ```
class DoubleCast extends Cast<core.double> {
const DoubleCast();
@override
core.double safeCast(dynamic from, core.String context, dynamic key) =>
from is core.double
? from
: throw FailedCast(context, key, "$from is not an double");
}
/// A cast operation for converting dynamic values to [core.String].
///
/// This class extends [Cast<core.String>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.String].
///
/// The [safeCast] method checks if the input [from] is already a [core.String].
/// If it is, it returns the value unchanged. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final stringCast = StringCast();
/// final result = stringCast.cast("Hello"); // Returns "Hello"
/// stringCast.cast(42); // Throws FailedCast
/// ```
class StringCast extends Cast<core.String> {
const StringCast();
@override
core.String safeCast(dynamic from, core.String context, dynamic key) =>
from is core.String
? from
: throw FailedCast(context, key, "$from is not a String");
}
/// A cast operation for converting dynamic values to [core.bool].
///
/// This class extends [Cast<core.bool>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.bool].
///
/// The [safeCast] method checks if the input [from] is already a [core.bool].
/// If it is, it returns the value unchanged. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final boolCast = BoolCast();
/// final result = boolCast.cast(true); // Returns true
/// boolCast.cast("not a bool"); // Throws FailedCast
/// ```
class BoolCast extends Cast<core.bool> {
const BoolCast();
@override
core.bool safeCast(dynamic from, core.String context, dynamic key) =>
from is core.bool
? from
: throw FailedCast(context, key, "$from is not a bool");
}

View file

@ -0,0 +1,79 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_typeforge/codable.dart';
/// A class for resolving references within a document structure.
///
/// This class provides functionality to resolve references within a document
/// represented by a [KeyedArchive]. It allows for navigation through the
/// document structure using URI-style references.
///
/// The [ReferenceResolver] is particularly useful in scenarios where you need
/// to traverse complex, nested document structures and resolve references
/// to specific parts of the document.
///
/// Usage:
/// ```dart
/// final document = KeyedArchive(...); // Your document structure
/// final resolver = ReferenceResolver(document);
/// final resolved = resolver.resolve(Uri.parse('#/definitions/child'));
/// ```
///
/// The [resolve] method is the primary way to use this class. It takes a [Uri]
/// reference and returns the corresponding [KeyedArchive] from the document,
/// or null if the reference cannot be resolved.
class ReferenceResolver {
/// Creates a new [ReferenceResolver] instance.
///
/// The [ReferenceResolver] is used to resolve references within a document
/// structure represented by a [KeyedArchive].
///
/// Parameters:
/// [document] - The document to resolve references within. This
/// [KeyedArchive] represents the entire document structure that will be
/// used to resolve references.
ReferenceResolver(this.document);
/// The document to resolve references within.
///
/// This [KeyedArchive] represents the entire document structure
/// that will be used to resolve references.
final KeyedArchive document;
/// Resolves a reference URI to a [KeyedArchive] within the document.
///
/// This method takes a [Uri] [ref] and traverses the document structure
/// to find the corresponding [KeyedArchive]. It uses the path segments
/// of the URI to navigate through the nested structure of the document.
///
/// Parameters:
/// [ref] - A [Uri] representing the reference to resolve.
///
/// Returns:
/// A [KeyedArchive] corresponding to the resolved reference, or null
/// if the reference cannot be resolved within the document structure.
///
/// Example:
/// If [ref] is '#/definitions/child', this method will attempt to
/// navigate to document['definitions']['child'] and return the
/// corresponding [KeyedArchive].
KeyedArchive? resolve(Uri ref) {
final folded = ref.pathSegments.fold<KeyedArchive?>(document,
(KeyedArchive? objectPtr, pathSegment) {
if (objectPtr != null) {
return objectPtr[pathSegment] as KeyedArchive?;
} else {
return null;
}
});
return folded;
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_typeforge/codable.dart';
/// This abstract class serves as a contract for objects that need to be
/// resolved based on references. It defines a common interface for resolution
/// operations, allowing for consistent handling of referenceable objects
/// throughout the system.
///
/// Implementations of this class should ensure that:
/// - They provide a meaningful implementation of [resolveOrThrow].
/// - They handle potential errors during resolution and throw appropriate exceptions.
/// - They interact correctly with the provided [ReferenceResolver].
///
/// Example usage:
/// ```dart
/// class ConcreteReferenceable implements Referenceable {
/// @override
/// void resolveOrThrow(ReferenceResolver resolver) {
/// // Implementation of reference resolution
/// }
/// }
abstract class Referenceable {
/// Resolves the references within this object using the provided [resolver].
///
/// This method is responsible for resolving any references or dependencies
/// that this object might have. It should use the [resolver] to look up and
/// resolve these references.
///
/// If the resolution process encounters any errors or fails to resolve
/// necessary references, this method should throw an appropriate exception.
///
/// Parameters:
/// [resolver]: The [ReferenceResolver] instance to use for resolving references.
///
/// Throws:
/// An exception if the resolution process fails or encounters errors.
void resolveOrThrow(ReferenceResolver resolver);
}

View file

@ -0,0 +1,109 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async' as async;
import 'dart:core' as core;
import 'dart:core' hide Map, String, int;
import 'package:protevus_typeforge/cast.dart';
/// A cast operation that attempts to cast a dynamic value to either type S or type T.
///
/// This class extends [Cast<dynamic>] and provides a mechanism to attempt casting
/// to two different types in sequence. It first tries to cast to type S using the
/// [_left] cast, and if that fails, it attempts to cast to type T using the [_right] cast.
///
/// The [safeCast] method first attempts to use the [_left] cast. If it succeeds, the result
/// is returned. If it fails (by throwing a [FailedCast] exception), the method then
/// attempts to use the [_right] cast and returns its result.
///
/// This class is useful when you have a value that could be one of two different types
/// and you want to handle both cases.
///
/// Usage:
/// ```dart
/// final oneOfCast = OneOf(IntCast(), StringCast());
/// final resultInt = oneOfCast.cast(42); // Returns 42 as int
/// final resultString = oneOfCast.cast("hello"); // Returns "hello" as String
/// oneOfCast.cast(true); // Throws FailedCast
/// ```
class OneOf<S, T> extends Cast<dynamic> {
final Cast<S> _left;
final Cast<T> _right;
const OneOf(Cast<S> left, Cast<T> right)
: _left = left,
_right = right;
@override
dynamic safeCast(dynamic from, core.String context, dynamic key) {
try {
return _left.safeCast(from, context, key);
} on FailedCast {
return _right.safeCast(from, context, key);
}
}
}
/// A cast operation that applies a transformation function after casting to an intermediate type.
///
/// This class extends [Cast<T>] and combines two operations:
/// 1. Casting the input to type S using the [_first] cast operation.
/// 2. Applying a transformation function [_transform] to convert the result from S to T.
///
/// [S] is the intermediate type after the first cast.
/// [T] is the final type after applying the transformation.
///
/// The [safeCast] method first uses [_first] to cast the input to type S,
/// then applies [_transform] to convert the result to type T.
///
/// This class is useful when you need to perform a cast followed by a type conversion
/// or when you want to apply some transformation logic after casting.
///
/// Usage:
/// ```dart
/// final stringLengthCast = Apply<String, int>((s) => s.length, StringCast());
/// final result = stringLengthCast.cast("hello"); // Returns 5
/// ```
class Apply<S, T> extends Cast<T> {
final Cast<S> _first;
final T Function(S) _transform;
const Apply(T Function(S) transform, Cast<S> first)
: _transform = transform,
_first = first;
@override
T safeCast(dynamic from, core.String context, dynamic key) =>
_transform(_first.safeCast(from, context, key));
}
/// A cast operation for converting dynamic values to [async.Future<E>].
///
/// This class extends [Cast<async.Future<E>>] and implements the [safeCast] method
/// to perform type checking and conversion to [async.Future<E>].
///
/// The class uses a [Cast<E>] instance [_value] for casting the value inside the Future to type E.
///
/// The [safeCast] method checks if the input [from] is already an [async.Future].
/// If it is, it returns a new Future that applies the [_value] cast to the result of the original Future.
/// If not, it throws a [FailedCast] exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final futureCast = Future(IntCast());
/// final result = futureCast.cast(Future.value(42)); // Returns Future<int>
/// futureCast.cast("not a future"); // Throws FailedCast
/// ```
class Future<E> extends Cast<async.Future<E>> {
final Cast<E> _value;
const Future(Cast<E> value) : _value = value;
@override
async.Future<E> safeCast(dynamic from, core.String context, dynamic key) {
if (from is async.Future) {
return from.then(_value.cast);
}
return throw FailedCast(context, key, "not a Future");
}
}

View file

@ -0,0 +1,77 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:core' as core;
import 'dart:core' hide Map, String, int;
import 'package:protevus_typeforge/cast.dart';
/// A cast operation that accepts and returns any dynamic value without modification.
///
/// This class extends [Cast<dynamic>] and provides a no-op cast operation.
/// It's useful when you want to allow any type to pass through without
/// performing any type checking or transformation.
///
/// The [safeCast] method simply returns the input value as-is, regardless of its type.
///
/// Example usage:
/// ```dart
/// final anyCast = AnyCast();
/// final result = anyCast.cast(someValue); // Returns someValue unchanged
/// ```
class AnyCast extends Cast<dynamic> {
const AnyCast();
@override
dynamic safeCast(dynamic from, core.String context, dynamic key) => from;
}
/// A cast operation for converting dynamic values to [core.Map<K, V>] with specific key-value casts.
///
/// This class extends [Cast<core.Map<K, V>>] and implements the [safeCast] method
/// to perform type checking and conversion to [core.Map<K, V>] based on a predefined
/// map of key-specific casts.
///
/// The class uses a [core.Map<K, Cast<V>>] to define custom casts for specific keys.
/// Keys not present in this map will be cast as-is.
///
/// The [keys] getter provides access to the keys of the internal cast map.
///
/// The [safeCast] method checks if the input [from] is a [core.Map]. If it is,
/// it creates a new map, applying the specific casts for keys present in [_map]
/// and preserving other key-value pairs as-is. If not, it throws a [FailedCast]
/// exception with appropriate context information.
///
/// Usage:
/// ```dart
/// final keyedCast = Keyed<String, dynamic>({
/// 'age': IntCast(),
/// 'name': StringCast(),
/// });
/// final result = keyedCast.cast({'age': 30, 'name': 'John', 'city': 'New York'});
/// // Returns Map<String, dynamic> with 'age' as int, 'name' as String, and 'city' preserved as-is
/// ```
class Keyed<K, V> extends Cast<core.Map<K, V>> {
Iterable<K> get keys => _map.keys;
final core.Map<K, Cast<V>> _map;
const Keyed(core.Map<K, Cast<V>> map) : _map = map;
@override
core.Map<K, V> safeCast(dynamic from, core.String context, dynamic key) {
final core.Map<K, V> result = {};
if (from is core.Map) {
for (final K key in from.keys as core.Iterable<K>) {
if (_map.containsKey(key)) {
result[key] = _map[key]!.safeCast(from[key], "map entry", key);
} else {
result[key] = from[key] as V;
}
}
return result;
}
throw FailedCast(context, key, "not a map");
}
}

View file

@ -0,0 +1,464 @@
import 'dart:convert';
import 'package:protevus_typeforge/cast.dart' as cast;
import 'package:protevus_typeforge/codable.dart';
import 'package:test/test.dart';
void main() {
group("Primitive decode", () {
test("Can decode primitive type", () {
final archive = getJSONArchive({"key": 2});
final int? val = archive.decode("key");
expect(val, 2);
});
test("Can decode List<dynamic> type", () {
final archive = getJSONArchive({
"key": [1, "2"]
});
final List<dynamic>? l = archive.decode("key");
expect(l, [1, "2"]);
});
test("Can decode Map<String, dynamic>", () {
final archive = getJSONArchive({
"key": {"key": "val"}
});
final KeyedArchive? d = archive.decode("key");
expect(d, {"key": "val"});
});
test("Can decode URI", () {
final archive = getJSONArchive({"key": "https://host.com"});
final Uri? d = archive.decode("key");
expect(d!.host, "host.com");
});
test("Can decode DateTime", () {
final date = DateTime.now();
final archive = getJSONArchive({"key": date.toIso8601String()});
final DateTime? d = archive.decode("key");
expect(d!.isAtSameMomentAs(date), true);
});
test("If value is null, return null from decode", () {
final archive = getJSONArchive({"key": null});
final int? val = archive.decode("key");
expect(val, isNull);
});
test("If archive does not contain key, return null from decode", () {
final archive = getJSONArchive({});
final int? val = archive.decode("key");
expect(val, isNull);
});
});
group("Primitive map decode", () {
test("Can decode Map<String, String> from Map<String, dynamic>", () {
final archive = getJSONArchive({
"key": {"key": "val"}
});
archive.castValues({"key": const cast.Map(cast.string, cast.string)});
final Map<String, String>? d = archive.decode("key");
expect(d, {"key": "val"});
});
test("Can decode Map<String, List<String>>", () {
final archive = getJSONArchive({
"key": {
"key": ["val"]
}
});
archive.castValues(
{"key": const cast.Map(cast.string, cast.List(cast.string))},
);
final Map<String, List<String?>>? d = archive.decode("key");
expect(d, {
"key": ["val"]
});
});
test("Can decode Map<String, List<String?>> where elements are null", () {
final archive = getJSONArchive({
"key": {
"key": [null, null]
}
});
archive.castValues(
{"key": const cast.Map(cast.string, cast.List(cast.string))},
);
final Map<String, List<String?>>? d = archive.decode("key");
expect(d, {
"key": [null, null]
});
});
test("Can decode Map<String, Map<String, List<String?>>>", () {
final archive = getJSONArchive({
"key": {
"key": {
"key": ["val", null]
}
}
});
archive.castValues({
"key": const cast.Map(
cast.string,
cast.Map(cast.string, cast.List(cast.string)),
)
});
final Map<String, Map<String, List<String?>>>? d = archive.decode("key");
expect(d, {
"key": {
"key": ["val", null]
}
});
});
});
group("Primitive list decode", () {
test("Can decode List<String> from List<dynamic>", () {
final archive = getJSONArchive({
"key": ["val", null]
});
archive.castValues({"key": const cast.List(cast.string)});
final List<String?>? d = archive.decode("key");
expect(d, ["val", null]);
});
test("Can decode List<Map<String, List<String>>>", () {
final archive = getJSONArchive({
"key": [
{
"key": ["val", null]
},
null
]
});
archive.castValues({
"key": const cast.List(cast.Map(cast.string, cast.List(cast.string)))
});
final List<Map<String, List<String?>>?>? d = archive.decode("key");
expect(d, [
{
"key": ["val", null]
},
null
]);
});
});
group("Coding objects", () {
test("Can decode Coding object", () {
final archive = getJSONArchive({
"key": {"name": "Bob"}
});
final Parent p = archive.decodeObject("key", () => Parent())!;
expect(p.name, "Bob");
expect(p.child, isNull);
expect(p.children, isNull);
expect(p.childMap, isNull);
});
test("If coding object is paired with non-Map, an exception is thrown", () {
final archive = getJSONArchive({
"key": [
{"name": "Bob"}
]
});
try {
archive.decodeObject("key", () => Parent());
fail('unreachable');
} on ArgumentError {
// no action required
}
});
test("Can decode list of Coding objects", () {
final archive = getJSONArchive({
"key": [
{"name": "Bob"},
null,
{"name": "Sally"}
]
});
final List<Parent?>? p = archive.decodeObjects("key", () => Parent());
expect(p![0]!.name, "Bob");
expect(p[1], isNull);
expect(p[2]!.name, "Sally");
});
test(
"If coding object list is paired with non-List, an exception is thrown",
() {
final archive = getJSONArchive({
"key": {"name": "Bob"}
});
try {
archive.decodeObjects("key", () => Parent());
fail('unreachable');
} on ArgumentError {
// no op
}
});
test(
"If any element of coding list is not a coding object, an exception is thrown",
() {
final archive = getJSONArchive({
"key": [
{"name": "Bob"},
'foo'
]
});
try {
archive.decodeObjects("key", () => Parent());
fail('unreachable');
} on TypeError {
// no op
}
});
test("Can decode map of Coding objects", () {
final archive = getJSONArchive({
"key": {
"1": {"name": "Bob"},
"2": null
}
});
final map = archive.decodeObjectMap("key", () => Parent())!;
expect(map.length, 2);
expect(map["1"]!.name, "Bob");
expect(map["2"], isNull);
});
test("If coding object map is paired with non-Map, an exception is thrown",
() {
final archive = getJSONArchive({"key": []});
try {
archive.decodeObjectMap("key", () => Parent());
fail('unreachable');
} on ArgumentError {
// no op
}
});
test(
"If any element of coding map is not a coding object, an exception is thrown",
() {
final archive = getJSONArchive({
"key": {"1": "2"}
});
try {
archive.decodeObjectMap("key", () => Parent());
fail('unreachable');
} on TypeError {
// no op
}
});
});
group("Deep Coding objects", () {
test("Can decode single nested object", () {
final archive = getJSONArchive({
"key": {
"name": "Bob",
"child": {"name": "Sally"}
}
});
final o = archive.decodeObject("key", () => Parent())!;
expect(o.name, "Bob");
expect(o.child!.name, "Sally");
expect(o.childMap, isNull);
expect(o.children, isNull);
});
test("Can decode list of nested objects", () {
final archive = getJSONArchive({
"key": {
"name": "Bob",
"children": [
{"name": "Sally"}
]
}
});
final o = archive.decodeObject("key", () => Parent())!;
expect(o.name, "Bob");
expect(o.child, isNull);
expect(o.childMap, isNull);
expect(o.children!.length, 1);
expect(o.children?.first?.name, "Sally");
});
test("Can decode map of nested objects", () {
final archive = getJSONArchive({
"key": {
"name": "Bob",
"childMap": {
"sally": {"name": "Sally"}
}
}
});
final o = archive.decodeObject("key", () => Parent())!;
expect(o.name, "Bob");
expect(o.children, isNull);
expect(o.child, isNull);
expect(o.childMap!.length, 1);
expect(o.childMap!["sally"]!.name, "Sally");
});
});
group("Coding object references", () {
test("Parent can contain reference to child in single object decode", () {
final archive = getJSONArchive(
{
"child": {"name": "Sally"},
"parent": {
"name": "Bob",
"child": {"\$ref": "#/child"}
}
},
allowReferences: true,
);
final p = archive.decodeObject("parent", () => Parent())!;
expect(p.name, "Bob");
expect(p.child!.name, "Sally");
expect(p.child!.parent, isNull);
});
test(
"If reference doesn't exist, an error is thrown when creating document",
() {
try {
getJSONArchive(
{
"parent": {
"name": "Bob",
"child": {"\$ref": "#/child"}
}
},
allowReferences: true,
);
fail("unreachable");
} on ArgumentError catch (e) {
expect(e.toString(), contains("/child"));
}
});
test("Parent can contain reference to child in a list of objects", () {
final archive = getJSONArchive(
{
"child": {"name": "Sally"},
"parent": {
"name": "Bob",
"children": [
{"\$ref": "#/child"},
{"name": "fred"}
]
}
},
allowReferences: true,
);
final p = archive.decodeObject("parent", () => Parent())!;
expect(p.name, "Bob");
expect(p.children?.first?.name, "Sally");
expect(p.children?.last?.name, "fred");
});
test("Cyclical references are resolved", () {
final archive = getJSONArchive(
{
"child": {
"name": "Sally",
"parent": {"\$ref": "#/parent"}
},
"parent": {
"name": "Bob",
"children": [
{"\$ref": "#/child"},
{"name": "fred"}
]
}
},
allowReferences: true,
);
final p = archive.decodeObject("parent", () => Parent())!;
expect(p.name, "Bob");
expect(p.children?.first?.name, "Sally");
expect(p.children?.first?.parent!.name, "Bob");
expect(p.children?.last?.name, "fred");
expect(p.hashCode, isNot(p.children?.first?.parent.hashCode));
});
test("Can override castMap to coerce values", () {
final archive = getJSONArchive({
"key": {
"name": "Bob",
"things": ["value"]
}
});
final p = archive.decodeObject("key", () => Parent())!;
expect(p.things, ["value"]);
});
});
}
/// Strips type info from data
KeyedArchive getJSONArchive(dynamic data, {bool allowReferences = false}) {
return KeyedArchive.unarchive(
json.decode(json.encode(data)) as Map<String, dynamic>,
allowReferences: allowReferences,
);
}
class Parent extends Coding {
String? name;
Child? child;
List<Child?>? children;
Map<String, Child?>? childMap;
List<String?>? things;
@override
Map<String, cast.Cast<dynamic>> get castMap {
return {"things": const cast.List(cast.string)};
}
@override
void decode(KeyedArchive object) {
super.decode(object);
name = object.decode("name");
child = object.decodeObject("child", () => Child());
children = object.decodeObjects("children", () => Child());
childMap = object.decodeObjectMap("childMap", () => Child());
things = object.decode("things");
}
@override
void encode(KeyedArchive object) {}
}
class Child extends Coding {
String? name;
Parent? parent;
@override
void decode(KeyedArchive object) {
super.decode(object);
name = object.decode("name");
parent = object.decodeObject("parent", () => Parent());
}
@override
void encode(KeyedArchive object) {}
}

View file

@ -0,0 +1,412 @@
import 'dart:convert';
import 'package:protevus_typeforge/codable.dart';
import 'package:test/test.dart';
void main() {
group("Primitive encode", () {
test("Can encode primitive type", () {
final out = encode((obj) {
obj.encode("int", 1);
obj.encode("string", "1");
});
expect(out, {"int": 1, "string": "1"});
});
test("Can encode List<dynamic> type", () {
final out = encode((obj) {
obj.encode("key", [1, "2"]);
});
expect(out, {
"key": [1, "2"]
});
});
test("Can encode Map<String, dynamic>", () {
final out = encode((obj) {
obj.encode("key", {"1": 1, "2": "2"});
});
expect(out, {
"key": {"1": 1, "2": "2"}
});
});
test("Can encode URI", () {
final out = encode((obj) {
obj.encode("key", Uri.parse("https://host.com"));
});
expect(out, {"key": "https://host.com"});
});
test("Can encode DateTime", () {
final out = encode((obj) {
obj.encode("key", DateTime(2000));
});
expect(out, {"key": DateTime(2000).toIso8601String()});
});
test("If value is null, do not include key", () {
final out = encode((obj) {
obj.encode("key", null);
});
expect(out, {});
});
});
group("Coding objects", () {
test("Can encode Coding object", () {
final out = encode((object) {
object.encodeObject("key", Parent("Bob"));
});
expect(out, {
"key": {"name": "Bob"}
});
});
test("Can encode list of Coding objects", () {
final out = encode((object) {
object.encodeObject(
"key",
Parent("Bob", children: [Child("Fred"), null, Child("Sally")]),
);
});
expect(out, {
"key": {
"name": "Bob",
"children": [
{"name": "Fred"},
null,
{"name": "Sally"}
]
}
});
});
test("Can encode map of Coding objects", () {
final out = encode((object) {
object.encodeObject(
"key",
Parent(
"Bob",
childMap: {
"fred": Child("Fred"),
"null": null,
"sally": Child("Sally")
},
),
);
});
expect(out, {
"key": {
"name": "Bob",
"childMap": {
"fred": {"name": "Fred"},
"null": null,
"sally": {"name": "Sally"}
}
}
});
});
});
group("Coding object references", () {
test("Parent can contain reference to child in single object encode", () {
final container = Container(
Parent(
"Bob",
child: Child._()..referenceURI = Uri(path: "/definitions/child"),
),
{"child": Child("Sally")},
);
final out = KeyedArchive.archive(container, allowReferences: true);
expect(out, {
"definitions": {
"child": {"name": "Sally"}
},
"root": {
"name": "Bob",
"child": {"\$ref": "#/definitions/child"}
}
});
});
test(
"If reference doesn't exist, an error is thrown when creating document",
() {
final container = Container(
Parent(
"Bob",
child: Child._()..referenceURI = Uri(path: "/definitions/child"),
),
{},
);
try {
KeyedArchive.archive(container, allowReferences: true);
fail('unreachable');
} on ArgumentError catch (e) {
expect(e.toString(), contains("#/definitions/child"));
}
});
test(
"If reference doesn't exist in objectMap, an error is thrown when creating document",
() {
final container = Container(
Parent(
"Bob",
childMap: {
"c": Child._()..referenceURI = Uri(path: "/definitions/child")
},
),
{},
);
try {
KeyedArchive.archive(container, allowReferences: true);
fail('unreachable');
} on ArgumentError catch (e) {
expect(e.toString(), contains("#/definitions/child"));
}
});
test(
"If reference doesn't exist in objectList, an error is thrown when creating document",
() {
final container = Container(
Parent(
"Bob",
children: [Child._()..referenceURI = Uri(path: "/definitions/child")],
),
{},
);
try {
KeyedArchive.archive(container, allowReferences: true);
fail('unreachable');
} on ArgumentError catch (e) {
expect(e.toString(), contains("#/definitions/child"));
}
});
test("Parent can contain reference to child in a list of objects", () {
final container = Container(
Parent(
"Bob",
children: [
Child("Sally"),
Child._()..referenceURI = Uri(path: "/definitions/child")
],
),
{"child": Child("Fred")},
);
final out = KeyedArchive.archive(container, allowReferences: true);
expect(out, {
"definitions": {
"child": {"name": "Fred"}
},
"root": {
"name": "Bob",
"children": [
{"name": "Sally"},
{"\$ref": "#/definitions/child"}
]
}
});
});
test("Parent can contain reference to child in a map of objects", () {
final container = Container(
Parent(
"Bob",
childMap: {
"sally": Child("Sally"),
"ref": Child._()..referenceURI = Uri(path: "/definitions/child")
},
),
{"child": Child("Fred")},
);
final out = KeyedArchive.archive(container, allowReferences: true);
expect(out, {
"definitions": {
"child": {"name": "Fred"}
},
"root": {
"name": "Bob",
"childMap": {
"sally": {"name": "Sally"},
"ref": {"\$ref": "#/definitions/child"}
}
}
});
});
test("Cyclical references are resolved", () {
final container = Container(
Parent(
"Bob",
children: [
Child("Sally"),
Child._()..referenceURI = Uri(path: "/definitions/child")
],
),
{
"child": Child(
"Fred",
parent: Parent._()..referenceURI = Uri(path: "/root"),
)
});
final out = KeyedArchive.archive(container, allowReferences: true);
final expected = {
"definitions": {
"child": {
"name": "Fred",
"parent": {"\$ref": "#/root"}
}
},
"root": {
"name": "Bob",
"children": [
{"name": "Sally"},
{"\$ref": "#/definitions/child"},
]
}
};
expect(out, expected);
// we'll also ensure that writing it out and reading it back in
// works, to complete the lifecycle of a document. we are ensuring
// that no state is accumulated in decoding that impacts encoding
// and ensure that our data is valid json
final washedData = json.decode(json.encode(out)) as Map<String, dynamic>;
final doc = KeyedArchive.unarchive(washedData);
final decodedContainer = Container._()..decode(doc);
final reencodedArchive = KeyedArchive.archive(decodedContainer);
expect(reencodedArchive, expected);
});
});
test("toPrimitive does not include keyed archives or lists", () {
final archive = KeyedArchive.unarchive({
"value": "v",
"archive": {"key": "value"},
"list": [
"value",
{"key": "value"},
["value"]
]
});
final encoded = archive.toPrimitive();
expect(encoded["value"], "v");
expect(encoded["archive"] is Map<String, dynamic>, true);
expect(encoded["archive"] is KeyedArchive, false);
expect(encoded["list"] is List<dynamic>, true);
expect(encoded["list"] is ListArchive, false);
expect(encoded["list"][0], "value");
expect(encoded["list"][1] is Map<String, dynamic>, true);
expect(encoded["list"][1] is KeyedArchive, false);
expect(encoded["list"][2] is List<dynamic>, true);
expect(encoded["list"][2] is ListArchive, false);
});
}
Map<String, dynamic>? encode(void Function(KeyedArchive object) encoder) {
final archive = KeyedArchive({});
encoder(archive);
return json.decode(json.encode(archive)) as Map<String, dynamic>?;
}
class Container extends Coding {
Container(this.root, this.definitions);
Container._();
Parent? root;
Map<String, Coding?>? definitions;
@override
void decode(KeyedArchive object) {
super.decode(object);
root = object.decodeObject("root", () => Parent._());
definitions = object.decodeObjectMap("definitions", () => Child._());
}
@override
void encode(KeyedArchive object) {
object.encodeObject("root", root);
object.encodeObjectMap("definitions", definitions);
}
}
class Parent extends Coding {
Parent(this.name, {this.child, this.children, this.childMap, this.things});
Parent._();
String? name;
Child? child;
List<Child?>? children;
Map<String, Child?>? childMap;
List<String>? things;
@override
void decode(KeyedArchive object) {
super.decode(object);
name = object.decode("name");
child = object.decodeObject("child", () => Child._());
children = object.decodeObjects("children", () => Child._());
childMap = object.decodeObjectMap("childMap", () => Child._());
}
@override
void encode(KeyedArchive object) {
object.encode("name", name);
object.encodeObject("child", child);
object.encodeObjects("children", children);
object.encodeObjectMap("childMap", childMap);
object.encode("things", things);
}
}
class Child extends Coding {
Child(this.name, {this.parent});
Child._();
String? name;
Parent? parent;
@override
void decode(KeyedArchive object) {
super.decode(object);
name = object.decode("name");
parent = object.decodeObject("parent", () => Parent._());
}
@override
void encode(KeyedArchive object) {
object.encode("name", name);
object.encodeObject("parent", parent);
}
}