From 749d20b98e3763dc7aea1623b6fcdd3f2a9f349f Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Sun, 4 Aug 2024 00:26:55 -0700 Subject: [PATCH] refactor(conduit): refactoring codable to typeforge --- packages/typeforge/lib/cast.dart | 29 + packages/typeforge/lib/codable.dart | 31 +- packages/typeforge/lib/src/base_cast.dart | 118 ++++ packages/typeforge/lib/src/coding.dart | 80 +++ .../typeforge/lib/src/collection_cast.dart | 129 ++++ packages/typeforge/lib/src/constants.dart | 74 +++ packages/typeforge/lib/src/keyed_archive.dart | 580 ++++++++++++++++++ packages/typeforge/lib/src/list_archive.dart | 233 +++++++ .../typeforge/lib/src/primitive_cast.dart | 108 ++++ .../typeforge/lib/src/reference_resolver.dart | 79 +++ packages/typeforge/lib/src/referenceable.dart | 46 ++ packages/typeforge/lib/src/special_cast.dart | 109 ++++ packages/typeforge/lib/src/utility_cast.dart | 77 +++ packages/typeforge/test/.gitkeep | 0 packages/typeforge/test/decode_test.dart | 464 ++++++++++++++ packages/typeforge/test/encode_test.dart | 412 +++++++++++++ 16 files changed, 2563 insertions(+), 6 deletions(-) delete mode 100644 packages/typeforge/test/.gitkeep create mode 100644 packages/typeforge/test/decode_test.dart create mode 100644 packages/typeforge/test/encode_test.dart diff --git a/packages/typeforge/lib/cast.dart b/packages/typeforge/lib/cast.dart index e69de29..7433900 100644 --- a/packages/typeforge/lib/cast.dart +++ b/packages/typeforge/lib/cast.dart @@ -0,0 +1,29 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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'; diff --git a/packages/typeforge/lib/codable.dart b/packages/typeforge/lib/codable.dart index fe85d2c..1c15775 100644 --- a/packages/typeforge/lib/codable.dart +++ b/packages/typeforge/lib/codable.dart @@ -1,8 +1,27 @@ -/// Support for doing something awesome. +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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'; diff --git a/packages/typeforge/lib/src/base_cast.dart b/packages/typeforge/lib/src/base_cast.dart index e69de29..93d1729 100644 --- a/packages/typeforge/lib/src/base_cast.dart +++ b/packages/typeforge/lib/src/base_cast.dart @@ -0,0 +1,118 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 { +/// @override +/// MyType _cast(dynamic from, String context, dynamic key) { +/// // Custom casting logic here +/// } +/// } +/// ``` +abstract class Cast { + /// 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); +} diff --git a/packages/typeforge/lib/src/coding.dart b/packages/typeforge/lib/src/coding.dart index e69de29..2c2cc69 100644 --- a/packages/typeforge/lib/src/coding.dart +++ b/packages/typeforge/lib/src/coding.dart @@ -0,0 +1,80 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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>? 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); +} diff --git a/packages/typeforge/lib/src/collection_cast.dart b/packages/typeforge/lib/src/collection_cast.dart index e69de29..ced998b 100644 --- a/packages/typeforge/lib/src/collection_cast.dart +++ b/packages/typeforge/lib/src/collection_cast.dart @@ -0,0 +1,129 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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]. +/// +/// This class extends [Cast>] and implements the [safeCast] method +/// to perform type checking and conversion to [core.Map]. +/// +/// 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 +/// mapCast.cast("not a map"); // Throws FailedCast +/// ``` +class Map extends Cast> { + final Cast _key; + final Cast _value; + const Map(Cast key, Cast value) + : _key = key, + _value = value; + @override + core.Map safeCast(dynamic from, core.String context, dynamic key) { + if (from is core.Map) { + final result = {}; + 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]. +/// +/// This class extends [Cast>] and implements the [safeCast] method +/// to perform type checking and conversion to [core.Map]. +/// +/// The class uses a [Cast] 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 +/// stringMapCast.cast("not a map"); // Throws FailedCast +/// ``` +class StringMap extends Cast> { + final Cast _value; + const StringMap(Cast value) : _value = value; + @override + core.Map safeCast( + dynamic from, + core.String context, + dynamic key, + ) { + if (from is core.Map) { + final result = {}; + for (final core.String key in from.keys as core.Iterable) { + 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]. +/// +/// This class extends [Cast>] and implements the [safeCast] method +/// to perform type checking and conversion to [core.List]. +/// +/// The class uses a [Cast] 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 +/// listCast.cast("not a list"); // Throws FailedCast +/// ``` +class List extends Cast> { + final Cast _entry; + const List(Cast entry) : _entry = entry; + @override + core.List safeCast(dynamic from, core.String context, dynamic key) { + if (from is core.List) { + final length = from.length; + final result = core.List.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"); + } +} diff --git a/packages/typeforge/lib/src/constants.dart b/packages/typeforge/lib/src/constants.dart index e69de29..b0cd0af 100644 --- a/packages/typeforge/lib/src/constants.dart +++ b/packages/typeforge/lib/src/constants.dart @@ -0,0 +1,74 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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(); diff --git a/packages/typeforge/lib/src/keyed_archive.dart b/packages/typeforge/lib/src/keyed_archive.dart index e69de29..8fef8a1 100644 --- a/packages/typeforge/lib/src/keyed_archive.dart +++ b/packages/typeforge/lib/src/keyed_archive.dart @@ -0,0 +1,580 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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], 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 + 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 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. + /// It's intended for internal use within the [KeyedArchive] class. + KeyedArchive._empty() : _map = {}; + + /// 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` 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 _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": ["foo", "bar"] + /// }; + /// final archive = KeyedArchive.unarchive(dynamicObject); + /// archive.castValues({ + /// "key": cast.List(cast.String) + /// }); + /// + /// // This now becomes a valid assignment + /// List key = archive.decode("key"); + /// + /// This method takes a [schema] parameter of type `Map?`, 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? 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] containing all the keys in the archive. + @override + Iterable 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]. + /// + /// This method recursively converts the contents of the archive to primitive types: + /// - [KeyedArchive] instances are converted to [Map] + /// - [ListArchive] instances are converted to [List] + /// - 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] containing the primitive representation + /// of this archive. + Map toPrimitive() { + final out = {}; + _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( + 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(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(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? decodeObjects(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(); + } + + /// 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? decodeObjectMap( + String key, + T Function() inflate, + ) { + final v = _getValue(key); + if (v == null) { + return null; + } + + if (v is! Map) { + throw ArgumentError( + "Cannot decode key '$key' as 'Map', 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] 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] representation of the [object], or null if [object] is null. + Map? _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? 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(String key, Map? value) { + if (value == null) return; + final object = KeyedArchive({}); + value.forEach((k, v) { + object[k] = _encodedObject(v); + }); + + _map[key] = object; + } +} diff --git a/packages/typeforge/lib/src/list_archive.dart b/packages/typeforge/lib/src/list_archive.dart index e69de29..778bd00 100644 --- a/packages/typeforge/lib/src/list_archive.dart +++ b/packages/typeforge/lib/src/list_archive.dart @@ -0,0 +1,233 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 + 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 _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 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 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] 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 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], 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) { + return KeyedArchive(e); + } else if (e is List) { + return ListArchive.from(e); + } + return e; +} diff --git a/packages/typeforge/lib/src/primitive_cast.dart b/packages/typeforge/lib/src/primitive_cast.dart index e69de29..62c2df1 100644 --- a/packages/typeforge/lib/src/primitive_cast.dart +++ b/packages/typeforge/lib/src/primitive_cast.dart @@ -0,0 +1,108 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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] 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 { + 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] 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 { + 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] 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 { + 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] 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 { + 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"); +} diff --git a/packages/typeforge/lib/src/reference_resolver.dart b/packages/typeforge/lib/src/reference_resolver.dart index e69de29..ba4651b 100644 --- a/packages/typeforge/lib/src/reference_resolver.dart +++ b/packages/typeforge/lib/src/reference_resolver.dart @@ -0,0 +1,79 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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(document, + (KeyedArchive? objectPtr, pathSegment) { + if (objectPtr != null) { + return objectPtr[pathSegment] as KeyedArchive?; + } else { + return null; + } + }); + + return folded; + } +} diff --git a/packages/typeforge/lib/src/referenceable.dart b/packages/typeforge/lib/src/referenceable.dart index e69de29..57746ff 100644 --- a/packages/typeforge/lib/src/referenceable.dart +++ b/packages/typeforge/lib/src/referenceable.dart @@ -0,0 +1,46 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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); +} diff --git a/packages/typeforge/lib/src/special_cast.dart b/packages/typeforge/lib/src/special_cast.dart index e69de29..2833060 100644 --- a/packages/typeforge/lib/src/special_cast.dart +++ b/packages/typeforge/lib/src/special_cast.dart @@ -0,0 +1,109 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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] 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 extends Cast { + final Cast _left; + final Cast _right; + const OneOf(Cast left, Cast 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] 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((s) => s.length, StringCast()); +/// final result = stringLengthCast.cast("hello"); // Returns 5 +/// ``` +class Apply extends Cast { + final Cast _first; + final T Function(S) _transform; + const Apply(T Function(S) transform, Cast 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]. +/// +/// This class extends [Cast>] and implements the [safeCast] method +/// to perform type checking and conversion to [async.Future]. +/// +/// The class uses a [Cast] 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 +/// futureCast.cast("not a future"); // Throws FailedCast +/// ``` +class Future extends Cast> { + final Cast _value; + const Future(Cast value) : _value = value; + @override + async.Future 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"); + } +} diff --git a/packages/typeforge/lib/src/utility_cast.dart b/packages/typeforge/lib/src/utility_cast.dart index e69de29..4016dde 100644 --- a/packages/typeforge/lib/src/utility_cast.dart +++ b/packages/typeforge/lib/src/utility_cast.dart @@ -0,0 +1,77 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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] 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 { + const AnyCast(); + @override + dynamic safeCast(dynamic from, core.String context, dynamic key) => from; +} + +/// A cast operation for converting dynamic values to [core.Map] with specific key-value casts. +/// +/// This class extends [Cast>] and implements the [safeCast] method +/// to perform type checking and conversion to [core.Map] based on a predefined +/// map of key-specific casts. +/// +/// The class uses a [core.Map>] 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({ +/// 'age': IntCast(), +/// 'name': StringCast(), +/// }); +/// final result = keyedCast.cast({'age': 30, 'name': 'John', 'city': 'New York'}); +/// // Returns Map with 'age' as int, 'name' as String, and 'city' preserved as-is +/// ``` +class Keyed extends Cast> { + Iterable get keys => _map.keys; + final core.Map> _map; + const Keyed(core.Map> map) : _map = map; + @override + core.Map safeCast(dynamic from, core.String context, dynamic key) { + final core.Map result = {}; + if (from is core.Map) { + for (final K key in from.keys as core.Iterable) { + 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"); + } +} diff --git a/packages/typeforge/test/.gitkeep b/packages/typeforge/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/typeforge/test/decode_test.dart b/packages/typeforge/test/decode_test.dart new file mode 100644 index 0000000..93ca841 --- /dev/null +++ b/packages/typeforge/test/decode_test.dart @@ -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 type", () { + final archive = getJSONArchive({ + "key": [1, "2"] + }); + final List? l = archive.decode("key"); + expect(l, [1, "2"]); + }); + + test("Can decode Map", () { + 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 from Map", () { + final archive = getJSONArchive({ + "key": {"key": "val"} + }); + archive.castValues({"key": const cast.Map(cast.string, cast.string)}); + final Map? d = archive.decode("key"); + expect(d, {"key": "val"}); + }); + + test("Can decode Map>", () { + final archive = getJSONArchive({ + "key": { + "key": ["val"] + } + }); + archive.castValues( + {"key": const cast.Map(cast.string, cast.List(cast.string))}, + ); + final Map>? d = archive.decode("key"); + expect(d, { + "key": ["val"] + }); + }); + + test("Can decode Map> 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>? d = archive.decode("key"); + expect(d, { + "key": [null, null] + }); + }); + + test("Can decode Map>>", () { + 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>>? d = archive.decode("key"); + expect(d, { + "key": { + "key": ["val", null] + } + }); + }); + }); + + group("Primitive list decode", () { + test("Can decode List from List", () { + final archive = getJSONArchive({ + "key": ["val", null] + }); + archive.castValues({"key": const cast.List(cast.string)}); + final List? d = archive.decode("key"); + expect(d, ["val", null]); + }); + + test("Can decode List>>", () { + final archive = getJSONArchive({ + "key": [ + { + "key": ["val", null] + }, + null + ] + }); + archive.castValues({ + "key": const cast.List(cast.Map(cast.string, cast.List(cast.string))) + }); + final List>?>? 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? 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, + allowReferences: allowReferences, + ); +} + +class Parent extends Coding { + String? name; + Child? child; + List? children; + Map? childMap; + List? things; + + @override + Map> 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) {} +} diff --git a/packages/typeforge/test/encode_test.dart b/packages/typeforge/test/encode_test.dart new file mode 100644 index 0000000..f407476 --- /dev/null +++ b/packages/typeforge/test/encode_test.dart @@ -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 type", () { + final out = encode((obj) { + obj.encode("key", [1, "2"]); + }); + + expect(out, { + "key": [1, "2"] + }); + }); + + test("Can encode Map", () { + 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; + 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, true); + expect(encoded["archive"] is KeyedArchive, false); + expect(encoded["list"] is List, true); + expect(encoded["list"] is ListArchive, false); + + expect(encoded["list"][0], "value"); + + expect(encoded["list"][1] is Map, true); + + expect(encoded["list"][1] is KeyedArchive, false); + + expect(encoded["list"][2] is List, true); + + expect(encoded["list"][2] is ListArchive, false); + }); +} + +Map? encode(void Function(KeyedArchive object) encoder) { + final archive = KeyedArchive({}); + encoder(archive); + return json.decode(json.encode(archive)) as Map?; +} + +class Container extends Coding { + Container(this.root, this.definitions); + + Container._(); + + Parent? root; + Map? 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? children; + Map? childMap; + List? 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); + } +}