add(conduit): refactoring conduit
This commit is contained in:
parent
7a095316dc
commit
8000137ded
20 changed files with 2769 additions and 0 deletions
7
packages/typeforge/.gitignore
vendored
Normal file
7
packages/typeforge/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# https://dart.dev/guides/libraries/private-files
|
||||||
|
# Created by `dart pub`
|
||||||
|
.dart_tool/
|
||||||
|
|
||||||
|
# Avoid committing pubspec.lock for library packages; see
|
||||||
|
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
pubspec.lock
|
3
packages/typeforge/CHANGELOG.md
Normal file
3
packages/typeforge/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
147
packages/typeforge/README.md
Normal file
147
packages/typeforge/README.md
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>
|
||||||
|
|
||||||
|
# protevus_typeforge
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/conduit.dart/dart-codable.svg?branch=master)](https://travis-ci.org/conduit.dart/dart-codable)
|
||||||
|
|
||||||
|
A library for encoding and decoding dynamic data into Dart objects.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
Data objects extend `Coding`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class Person extends Coding {
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void decode(KeyedArchive object) {
|
||||||
|
// must call super
|
||||||
|
super.decode(object);
|
||||||
|
|
||||||
|
name = object.decode("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void encode(KeyedArchive object) {
|
||||||
|
object.encode("name", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An object that extends `Coding` can be read from JSON:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final json = json.decode(...);
|
||||||
|
final archive = KeyedArchive.unarchive(json);
|
||||||
|
final person = Person()..decode(archive);
|
||||||
|
```
|
||||||
|
|
||||||
|
Objects that extend `Coding` may also be written to JSON:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final person = Person()..name = "Bob";
|
||||||
|
final archive = KeyedArchive.archive(person);
|
||||||
|
final json = json.encode(archive);
|
||||||
|
```
|
||||||
|
|
||||||
|
`Coding` objects can encode or decode other `Coding` objects, including lists of `Coding` objects and maps where `Coding` objects are values. You must provide a closure that instantiates the `Coding` object being decoded.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class Team extends Coding {
|
||||||
|
|
||||||
|
List<Person> members;
|
||||||
|
Person manager;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void decode(KeyedArchive object) {
|
||||||
|
super.decode(object); // must call super
|
||||||
|
|
||||||
|
members = object.decodeObjects("members", () => Person());
|
||||||
|
manager = object.decodeObject("manager", () => Person());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void encode(KeyedArchive object) {
|
||||||
|
object.encodeObject("manager", manager);
|
||||||
|
object.encodeObjects("members", members);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Type Casting
|
||||||
|
|
||||||
|
Types with primitive type arguments (e.g., `List<String>` or `Map<String, int>`) are a particular pain point when decoding. Override `castMap` in `Coding` to perform type coercion.
|
||||||
|
You must import `package:protevus_typeforge/cast.dart as cast` and prefix type names with `cast`.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:protevus_typeforge/cast.dart' as cast;
|
||||||
|
class Container extends Coding {
|
||||||
|
List<String> things;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, cast.Cast<dynamic>> get castMap => {
|
||||||
|
"things": cast.List(cast.String)
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void decode(KeyedArchive object) {
|
||||||
|
super.decode(object);
|
||||||
|
|
||||||
|
things = object.decode("things");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void encode(KeyedArchive object) {
|
||||||
|
object.encode("things", things);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Document References
|
||||||
|
|
||||||
|
`Coding` objects may be referred to multiple times in a document without duplicating their structure. An object is referenced with the `$key` key.
|
||||||
|
For example, consider the following JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"components": {
|
||||||
|
"thing": {
|
||||||
|
"name": "The Thing"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/thing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above, the decoded value of `data` inherits all properties from `/components/thing`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$ref": "#/components/thing",
|
||||||
|
"name": "The Thing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may create references in your in-memory data structures through the `Coding.referenceURI`.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final person = Person()..referenceURI = Uri(path: "/teams/engineering/manager");
|
||||||
|
```
|
||||||
|
|
||||||
|
The above person is encoded as:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$ref": "#/teams/engineering/manager"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may have cyclical references.
|
||||||
|
|
||||||
|
See the specification for [JSON Schema](http://json-schema.org) and the `$ref` keyword for more details.
|
||||||
|
|
30
packages/typeforge/analysis_options.yaml
Normal file
30
packages/typeforge/analysis_options.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
29
packages/typeforge/lib/cast.dart
Normal file
29
packages/typeforge/lib/cast.dart
Normal 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';
|
27
packages/typeforge/lib/codable.dart
Normal file
27
packages/typeforge/lib/codable.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
///
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
export 'src/referenceable.dart';
|
||||||
|
export 'src/coding.dart';
|
||||||
|
export 'src/keyed_archive.dart';
|
||||||
|
export 'src/list_archive.dart';
|
||||||
|
export 'src/reference_resolver.dart';
|
118
packages/typeforge/lib/src/base_cast.dart
Normal file
118
packages/typeforge/lib/src/base_cast.dart
Normal 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);
|
||||||
|
}
|
80
packages/typeforge/lib/src/coding.dart
Normal file
80
packages/typeforge/lib/src/coding.dart
Normal 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);
|
||||||
|
}
|
129
packages/typeforge/lib/src/collection_cast.dart
Normal file
129
packages/typeforge/lib/src/collection_cast.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
74
packages/typeforge/lib/src/constants.dart
Normal file
74
packages/typeforge/lib/src/constants.dart
Normal 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();
|
580
packages/typeforge/lib/src/keyed_archive.dart
Normal file
580
packages/typeforge/lib/src/keyed_archive.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
233
packages/typeforge/lib/src/list_archive.dart
Normal file
233
packages/typeforge/lib/src/list_archive.dart
Normal 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;
|
||||||
|
}
|
108
packages/typeforge/lib/src/primitive_cast.dart
Normal file
108
packages/typeforge/lib/src/primitive_cast.dart
Normal 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");
|
||||||
|
}
|
79
packages/typeforge/lib/src/reference_resolver.dart
Normal file
79
packages/typeforge/lib/src/reference_resolver.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
46
packages/typeforge/lib/src/referenceable.dart
Normal file
46
packages/typeforge/lib/src/referenceable.dart
Normal 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);
|
||||||
|
}
|
109
packages/typeforge/lib/src/special_cast.dart
Normal file
109
packages/typeforge/lib/src/special_cast.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
77
packages/typeforge/lib/src/utility_cast.dart
Normal file
77
packages/typeforge/lib/src/utility_cast.dart
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
17
packages/typeforge/pubspec.yaml
Normal file
17
packages/typeforge/pubspec.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: protevus_typeforge
|
||||||
|
description: The Typeforge (casting) package for the Protevus Platform
|
||||||
|
version: 0.0.1
|
||||||
|
homepage: https://protevus.com
|
||||||
|
documentation: https://docs.protevus.com
|
||||||
|
repository: https://git.protevus.com/protevus/platform
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.4.0 <4.0.0'
|
||||||
|
|
||||||
|
# Add regular dependencies here.
|
||||||
|
dependencies:
|
||||||
|
meta: ^1.3.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^3.0.0
|
||||||
|
test: ^1.24.0
|
464
packages/typeforge/test/decode_test.dart
Normal file
464
packages/typeforge/test/decode_test.dart
Normal 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) {}
|
||||||
|
}
|
412
packages/typeforge/test/encode_test.dart
Normal file
412
packages/typeforge/test/encode_test.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue