mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
fix(codec): replace codec and map codec structs with Encode and Decode traits and their field versions (#2051)
* removed codecs in favor of the `Encode` and `Decode` traits and their derivatives * placed more exports in the `pumpkin-codecs` lib * split primitives into their own folder and added some tests * added `Either` support and added a simple test for it * fixed linting for tests
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2892,7 +2892,6 @@ dependencies = [
|
||||
name = "pumpkin-codecs"
|
||||
version = "0.1.0-dev+26.1"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"either",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
|
||||
@@ -7,7 +7,6 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde_json.workspace = true
|
||||
dashmap.workspace = true
|
||||
tracing.workspace = true
|
||||
either.workspace = true
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::lifecycle::Lifecycle;
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A trait to provide basic functionality for an implementation of a *map* [`Codec`] or of a [`MapCodec`].
|
||||
pub trait BaseMapCodec {
|
||||
/// The key type of this map codec.
|
||||
type Key: Display + Eq + Hash;
|
||||
type KeyCodec: Codec<Value = Self::Key> + 'static;
|
||||
|
||||
/// The value (element) type of this map codec.
|
||||
type Element;
|
||||
type ElementCodec: Codec<Value = Self::Element> + 'static;
|
||||
|
||||
fn key_codec(&self) -> &'static Self::KeyCodec;
|
||||
fn element_codec(&self) -> &'static Self::ElementCodec;
|
||||
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &HashMap<Self::Key, Self::Element>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
mut prefix: impl StructBuilder<Value = T>,
|
||||
) -> impl StructBuilder<Value = T> {
|
||||
for (key, element) in input {
|
||||
prefix = prefix.add_key_result_value_result(
|
||||
self.key_codec().encode_start(key, ops),
|
||||
self.element_codec().encode_start(element, ops),
|
||||
);
|
||||
}
|
||||
prefix
|
||||
}
|
||||
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<HashMap<Self::Key, Self::Element>> {
|
||||
let mut read_map: HashMap<Self::Key, Self::Element> = HashMap::new();
|
||||
let mut failed: Vec<(T, T)> = vec![];
|
||||
|
||||
let result = input.iter().fold(
|
||||
DataResult::new_success_with_lifecycle((), Lifecycle::Stable),
|
||||
|r, (k, e)| {
|
||||
// First, we try to parse the key and value.
|
||||
let key_result = self.key_codec().parse(k.clone(), ops);
|
||||
let element_result = self.element_codec().parse(e.clone(), ops);
|
||||
|
||||
let entry_result =
|
||||
key_result.apply_2_and_make_stable(|kr, er| (kr, er), element_result);
|
||||
let accumulated = r.add_message(&entry_result);
|
||||
let entry = entry_result.into_result_or_partial();
|
||||
|
||||
if let Some((key, element)) = entry {
|
||||
// If this parses successfully, we try adding it to our map.
|
||||
if read_map.contains_key(&key) {
|
||||
// There was already a value for this key.
|
||||
failed.push((k, e.clone()));
|
||||
return accumulated.add_message::<()>(&DataResult::new_error(format!(
|
||||
"Duplicate entry for key: {key}"
|
||||
)));
|
||||
}
|
||||
read_map.insert(key, element);
|
||||
} else {
|
||||
// Could not parse.
|
||||
failed.push((k, e.clone()));
|
||||
}
|
||||
|
||||
accumulated
|
||||
},
|
||||
);
|
||||
|
||||
let errors = ops.create_map(failed);
|
||||
|
||||
result
|
||||
.with_complete_or_partial(read_map)
|
||||
.map_error(|e| format!("{e} (Missed inputs: {errors})"))
|
||||
}
|
||||
}
|
||||
@@ -1,595 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codecs::either::{EitherCodec, new_either_codec};
|
||||
use crate::codecs::lazy::{LazyCodec, new_lazy_codec};
|
||||
use crate::codecs::list::{ListCodec, new_list_codec};
|
||||
use crate::codecs::primitive::{
|
||||
BoolCodec, ByteBufferCodec, ByteCodec, DoubleCodec, FloatCodec, IntCodec, IntStreamCodec,
|
||||
LongCodec, LongStreamCodec, ShortCodec, StringCodec,
|
||||
};
|
||||
use crate::codecs::range::RangeCodec;
|
||||
use crate::codecs::range::new_range_codec;
|
||||
use crate::codecs::unbounded_map::{UnboundedMapCodec, new_unbounded_map_codec};
|
||||
use crate::codecs::validated::{ValidatedCodec, new_validated_codec};
|
||||
use crate::coders::{
|
||||
ComappedEncoderImpl, Decoder, Encoder, FlatComappedEncoderImpl, FlatMappedDecoderImpl,
|
||||
MappedDecoderImpl, comap, decoder_field, encoder_field, flat_comap, flat_map, map,
|
||||
};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_codec::ComposedMapCodec;
|
||||
use crate::map_codecs::field_coders::{FieldDecoder, FieldEncoder};
|
||||
use crate::map_codecs::optional_field::{
|
||||
DefaultValueProviderMapCodec, OptionalFieldMapCodec, new_default_value_provider_map_codec,
|
||||
new_optional_field_map_codec,
|
||||
};
|
||||
use crate::map_codecs::simple::{SimpleMapCodec, new_simple_map_codec};
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A type of *codec* describing the way to **encode from and decode to** something of a type `Value` (`Value` -> `?` and `?` -> `Value`).
|
||||
///
|
||||
/// # Usage
|
||||
/// This trait is the main way serialization/deserialization can be handled easily.
|
||||
/// - To encode something, use [`Codec::encode_start`]
|
||||
/// - To decode something, use [`Codec::parse`].
|
||||
///
|
||||
/// To use these methods, use a [`DynamicOps`] instance to tell the intermediate format to encode to/decode from:
|
||||
///
|
||||
/// # Primitive Codecs
|
||||
/// This trait's module (`codec`) provides many common codecs that can be used for more complex codec types:
|
||||
/// - [`BYTE_CODEC`], [`SHORT_CODEC`], [`INT_CODEC`], [`LONG_CODEC`], [`BOOL_CODEC`], [`FLOAT_CODEC`] and [`DOUBLE_CODEC`] for Java primitive types.
|
||||
/// - [`STRING_CODEC`] for `String`s.
|
||||
/// - [`BYTE_CODEC`], [`USHORT_CODEC`], [`UINT_CODEC`] and [`ULONG_CODEC`] for unsigned versions of Java primitive number types (`u8`, `u16`, `u32` and `u64`).
|
||||
/// - [`BYTE_BUFFER_CODEC`] for byte buffers (equivalent to `Box<[u8]>`).
|
||||
/// - [`INT_STREAM_CODEC`] and [`LONG_STREAM_CODEC`] for Java's `int` and `long` stream codecs (equivalent to `Vec<i32>` and `Vec<i64>`).
|
||||
///
|
||||
/// # Creating a Codec
|
||||
/// There are a few codec types that can be created for custom types. **Keep in mind that codecs are meant
|
||||
/// to be static instances, and they should not be created at runtime. Codecs are also immutable,
|
||||
/// which means they cannot be modified after they are created.** Usually, codecs are declared
|
||||
/// using `pub static`.
|
||||
///
|
||||
/// ## Lists
|
||||
/// Use one of the following with the required arguments:
|
||||
/// - [`list`]: Creates a list codec of a given codec with the provided minimum and maximum size limits.
|
||||
/// - [`limited_list`]: Creates a list codec of a given codec with the provided maximum size limit.
|
||||
/// - [`unbounded_list`]: Creates a list codec of a given codec with no size limit.
|
||||
///
|
||||
/// ## Ranges
|
||||
/// A codec can also only accept a range of values of some number type. You can use one of the following for that:
|
||||
/// - [`int_range`]: For `int`s.
|
||||
/// - [`float_range`]: For `float`s.
|
||||
/// - [`double_range`]: For `double`s.
|
||||
///
|
||||
/// ## Structs
|
||||
/// Use the [`crate::struct_codec!`] macro to generate a codec implementation for a struct.
|
||||
/// A struct codec can work with up to 16 [`Field`]s, which each take a [`MapCodec`]
|
||||
/// and a getter. A `MapCodec` is simply an object that works with one or more keys of a provided map.
|
||||
/// Most of them used will be [`FieldMapCodec`]s, which only work with one singular key.
|
||||
///
|
||||
/// A field `FieldMapCodec` can be created with one of the following:
|
||||
/// - [`field`]: Provides a *required* field with the provided codec and name.
|
||||
/// - [`optional_field`]: Provides an *optional* field with the provided codec and name. Since this type of `MapCodec`
|
||||
/// has **no default value**, it encodes into an [`Option`].
|
||||
/// - [`optional_field_with_default`]: Provides an *optional* field with the provided codec and name, along with a default value factory
|
||||
/// for when the value does not exist while decoding.
|
||||
/// - [`lenient_optional_field`] and [`lenient_optional_field_with_default`] for lenient versions of the above two optional field methods.
|
||||
///
|
||||
/// To create a `Field` object using a `MapCodec`, use [`for_getter`] (which takes a `MapCodec` to own)
|
||||
/// or, in more specific cases, [`for_getter_ref`] (which takes a static `MapCodec` pointer) to include a getter method
|
||||
/// to tell the codec how to get some value (for encoding) from a struct instance.
|
||||
/// These `Field`s can then be placed in the `struct_codec` body, one for each pair, along with a constructor function at the end
|
||||
/// to tell the codec how to create an instance (for decoding) with the provided values. See the documentation
|
||||
/// of the `struct_codec!` macro for a basic example for defining a struct codec.
|
||||
///
|
||||
/// ## Unbounded Maps
|
||||
/// Use the [`unbounded_map`] function to create a codec encoding/decoding a `HashMap` of any arbitrary key.
|
||||
/// **Unbounded map codecs only support keys that can encode from/decode to strings.**
|
||||
///
|
||||
/// ## Either
|
||||
/// Use the [`either`] function to create a codec that can use one of two provided codecs to serialize/deserialize
|
||||
/// an [`Either`].
|
||||
///
|
||||
/// # Transformers
|
||||
/// A map codec of a type `B` can be implemented by *transforming* another codec of type `A` to work with type `B`.
|
||||
/// The following methods can be used depending on the equivalence relation between the two types:
|
||||
/// - [`xmap`]
|
||||
/// - [`comap_flat_map`]
|
||||
/// - [`flat_map_comap`]
|
||||
/// - [`flat_xmap`]
|
||||
///
|
||||
/// For example, the unsigned types use `flat_xmap` to convert between the `i_` and `u_` types.
|
||||
///
|
||||
/// # Validator Codecs
|
||||
/// The [`validate`] function returns a codec wrapper that validates a value before encoding and after decoding.
|
||||
/// A validated codec takes a function that can either return an [`Ok`] for a success,
|
||||
/// or an [`Err`] with the provided message to place in a `DataResult`.
|
||||
///
|
||||
/// [`MapCodec`]: super::map_codec::MapCodec
|
||||
/// [`for_getter`]: super::map_codec::for_getter
|
||||
/// [`for_getter_ref`]: super::map_codec::for_getter_ref
|
||||
/// [`Field`]: super::struct_codecs::Field
|
||||
///
|
||||
/// [`Either`]: crate::util::either::Either
|
||||
pub trait Codec: Encoder + Decoder {}
|
||||
|
||||
// Any struct implementing Encoder<Value = A> and Decoder<Value = A> will also implement Codec<Value = A>.
|
||||
impl<T> Codec for T where T: Encoder + Decoder {}
|
||||
|
||||
/// A codec allowing an arbitrary encoder and decoder.
|
||||
pub struct ComposedCodec<E: Encoder + 'static, D: Decoder<Value = E::Value> + 'static> {
|
||||
encoder: E,
|
||||
decoder: D,
|
||||
}
|
||||
|
||||
impl<E: Encoder, D: Decoder<Value = E::Value>> HasValue for ComposedCodec<E, D> {
|
||||
type Value = E::Value;
|
||||
}
|
||||
|
||||
impl<E: Encoder, D: Decoder<Value = E::Value>> Encoder for ComposedCodec<E, D> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
self.encoder.encode(input, ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Encoder, D: Decoder<Value = E::Value>> Decoder for ComposedCodec<E, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.decoder.decode(input, ops)
|
||||
}
|
||||
}
|
||||
|
||||
// Primitive codecs
|
||||
|
||||
macro_rules! define_const_codec {
|
||||
($name:ident, $codec_ty:ident, $ty:ident, $java_ty:ident) => {
|
||||
#[doc = concat!("A primitive codec for Java's `", stringify!($java_ty), "` (`", stringify!($ty), "` in Rust).")]
|
||||
pub const $name: $codec_ty = $codec_ty;
|
||||
};
|
||||
(box $name:ident, $codec_ty:ident, $vec_ty:ident, $java_ty:ident) => {
|
||||
#[doc = concat!("A primitive codec for Java's `", stringify!($java_ty), "`.")]
|
||||
///
|
||||
#[doc = concat!("This actually stores a [`Box<[", stringify!($vec_ty), "]>`].")]
|
||||
#[doc = concat!("This is useful for *packed* `", stringify!($vec_ty), "`s in a single array.")]
|
||||
pub const $name: $codec_ty = $codec_ty;
|
||||
};
|
||||
(vec $name:ident, $codec_ty:ident, $vec_ty:ident, $java_ty:ident) => {
|
||||
#[doc = concat!("A primitive codec for Java's `", stringify!($java_ty), "`.")]
|
||||
///
|
||||
#[doc = concat!("This actually stores a [`Vec<", stringify!($vec_ty), ">`].")]
|
||||
#[doc = concat!("This is useful for *packed* `", stringify!($vec_ty), "`s in a single array.")]
|
||||
pub const $name: $codec_ty = $codec_ty;
|
||||
};
|
||||
}
|
||||
|
||||
define_const_codec!(BOOL_CODEC, BoolCodec, bool, boolean);
|
||||
|
||||
define_const_codec!(BYTE_CODEC, ByteCodec, i8, byte);
|
||||
define_const_codec!(SHORT_CODEC, ShortCodec, i16, short);
|
||||
define_const_codec!(INT_CODEC, IntCodec, i32, int);
|
||||
define_const_codec!(LONG_CODEC, LongCodec, i64, long);
|
||||
define_const_codec!(FLOAT_CODEC, FloatCodec, f32, float);
|
||||
define_const_codec!(DOUBLE_CODEC, DoubleCodec, f64, double);
|
||||
|
||||
define_const_codec!(STRING_CODEC, StringCodec, String, String);
|
||||
|
||||
define_const_codec!(box BYTE_BUFFER_CODEC, ByteBufferCodec, i8, ByteBuffer);
|
||||
|
||||
define_const_codec!(vec INT_STREAM_CODEC, IntStreamCodec, i32, IntStream);
|
||||
define_const_codec!(vec LONG_STREAM_CODEC, LongStreamCodec, i64, LongStream);
|
||||
|
||||
// Unsigned types
|
||||
|
||||
/// Helper macro to generate a [`Codec`] of unsigned number types using `flat_xmap` of their signed counterparts.
|
||||
macro_rules! impl_unsigned_transformer_codec {
|
||||
($name:ident, $signed_codec_type:ident, $unsigned_codec_type:ident, $unsigned_prim:ident, $signed_prim:ident, $transformed_codec:ident) => {
|
||||
#[doc = concat!("The codec type for the [`", stringify!($unsigned_prim), "`] data type.")]
|
||||
pub type $unsigned_codec_type = FlatXmapCodec<$unsigned_prim, $signed_codec_type>;
|
||||
|
||||
#[doc = concat!("A [`Codec`] for `", stringify!($unsigned_prim), "`, which is a transformer codec of [`", stringify!($transformed_codec), "`].")]
|
||||
///
|
||||
/// Be wary that
|
||||
#[doc = concat!("if any encoded value exceeds [`", stringify!($signed_prim), "::MAX`], or if any decoded value is negative, this codec will return an error [`DataResult`].")]
|
||||
pub static $name: $unsigned_codec_type = flat_xmap(
|
||||
&$transformed_codec,
|
||||
|i| <$unsigned_prim>::try_from(i)
|
||||
.map_or_else(|_| DataResult::new_error(concat!("Could not fit ", stringify!($signed_prim), " into ", stringify!($unsigned_prim))), DataResult::new_success),
|
||||
|u| <$signed_prim>::try_from(*u)
|
||||
.map_or_else(|_| DataResult::new_error(concat!("Could not fit ", stringify!($unsigned_prim), " into ", stringify!($signed_prim))), DataResult::new_success),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
impl_unsigned_transformer_codec!(UBYTE_CODEC, ByteCodec, UbyteCodec, u8, i8, BYTE_CODEC);
|
||||
impl_unsigned_transformer_codec!(USHORT_CODEC, ShortCodec, UshortCodec, u16, i16, SHORT_CODEC);
|
||||
impl_unsigned_transformer_codec!(UINT_CODEC, IntCodec, UintCodec, u32, i32, INT_CODEC);
|
||||
impl_unsigned_transformer_codec!(ULONG_CODEC, LongCodec, UlongCodec, u64, i64, LONG_CODEC);
|
||||
|
||||
// Modifier methods
|
||||
|
||||
/// Creates a [`LazyCodec`] with a *function pointer* that returns a new [`Codec`], which will be called on first use.
|
||||
pub const fn lazy<C: Codec>(f: fn() -> C) -> LazyCodec<C> {
|
||||
new_lazy_codec(f)
|
||||
}
|
||||
|
||||
/// Creates a [`ListCodec`] of another [`Codec`] with the provided minimum and maximum size.
|
||||
pub const fn list<C: Codec>(codec: &'static C, min_size: usize, max_size: usize) -> ListCodec<C> {
|
||||
new_list_codec(codec, min_size, max_size)
|
||||
}
|
||||
|
||||
/// Creates a [`ListCodec`] of another [`Codec`] with the provided maximum size.
|
||||
pub const fn limited_list<C: Codec>(codec: &'static C, max_size: usize) -> ListCodec<C> {
|
||||
new_list_codec(codec, 0, max_size)
|
||||
}
|
||||
|
||||
/// Creates a [`ListCodec`] of another [`Codec`], which allows any size.
|
||||
pub const fn unbounded_list<C: Codec>(codec: &'static C) -> ListCodec<C> {
|
||||
new_list_codec(codec, 0, usize::MAX)
|
||||
}
|
||||
|
||||
/// Helper macro to generate the shorthand types and functions of the transformer [`Codec`] methods.
|
||||
macro_rules! make_codec_transformation_function {
|
||||
($name:ident, $short_type:ident, $encoder_type:ident, $decoder_type:ident, $encoder_func:ident, $decoder_func:ident, $to_func_result:ty, $from_func_result:ty, $a_equivalency:literal, $s_equivalency:literal) => {
|
||||
pub type $short_type<S, C> = ComposedCodec<$encoder_type<S, C>, $decoder_type<S, C>>;
|
||||
|
||||
#[doc = "Transforms a [`Codec`] of type `A` to another [`Codec`] of type `S`."]
|
||||
///
|
||||
/// - `to` is the function called on `A` after decoding to convert it to `S`.
|
||||
/// - `from` is the function called on `S` before encoding to convert it to `A`.
|
||||
///
|
||||
/// Use this if:
|
||||
#[doc = concat!("- `A` is **", $a_equivalency, "** to `S`.")]
|
||||
#[doc = concat!("- `S` is **", $s_equivalency, "** to `A`.")]
|
||||
#[doc = ""]
|
||||
#[doc = "A type `A` is *fully equivalent* to `B` if *A can always successfully be converted to B*."]
|
||||
pub const fn $name<A, C: Codec<Value = A>, S>(codec: &'static C, to: fn(A) -> $to_func_result, from: fn(&S) -> $from_func_result) -> $short_type<S, C> {
|
||||
ComposedCodec {
|
||||
encoder: $encoder_func(codec, from),
|
||||
decoder: $decoder_func(codec, to)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Transformer functions
|
||||
|
||||
make_codec_transformation_function!(
|
||||
xmap,
|
||||
XmapCodec,
|
||||
ComappedEncoderImpl,
|
||||
MappedDecoderImpl,
|
||||
comap,
|
||||
map,
|
||||
S,
|
||||
A,
|
||||
"equivalent",
|
||||
"equivalent"
|
||||
);
|
||||
|
||||
make_codec_transformation_function!(
|
||||
comap_flat_map,
|
||||
ComapFlatMapCodec,
|
||||
ComappedEncoderImpl,
|
||||
FlatMappedDecoderImpl,
|
||||
comap,
|
||||
flat_map,
|
||||
DataResult<S>,
|
||||
A,
|
||||
"partially equivalent",
|
||||
"equivalent"
|
||||
);
|
||||
|
||||
make_codec_transformation_function!(
|
||||
flat_map_comap,
|
||||
FlatMapComapCodec,
|
||||
FlatComappedEncoderImpl,
|
||||
MappedDecoderImpl,
|
||||
flat_comap,
|
||||
map,
|
||||
S,
|
||||
DataResult<A>,
|
||||
"equivalent",
|
||||
"partially equivalent"
|
||||
);
|
||||
|
||||
make_codec_transformation_function!(
|
||||
flat_xmap,
|
||||
FlatXmapCodec,
|
||||
FlatComappedEncoderImpl,
|
||||
FlatMappedDecoderImpl,
|
||||
flat_comap,
|
||||
flat_map,
|
||||
DataResult<S>,
|
||||
DataResult<A>,
|
||||
"partially equivalent",
|
||||
"partially equivalent"
|
||||
);
|
||||
|
||||
/// Returns a transformer codec that validates a value before encoding and after decoding by calling a function,
|
||||
/// which provides a [`DataResult`] depending on that value's validity.
|
||||
///
|
||||
/// `validator` is a function that takes the pointer of a value and returns a [`Result`].
|
||||
/// - If the returned result is an [`Ok`], the codec works as normal.
|
||||
/// - Otherwise, it always returns a non-result with the message [`String`].
|
||||
pub const fn validate<C: Codec>(
|
||||
codec: &'static C,
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
) -> ValidatedCodec<C> {
|
||||
new_validated_codec(codec, validator)
|
||||
}
|
||||
|
||||
// Range codec functions
|
||||
|
||||
macro_rules! make_codec_range_function {
|
||||
($func_name:ident, $shorthand_name:ident, $ty:ty, $codec:ident, $singleton_codec:ident, $java_type:ident) => {
|
||||
pub type $shorthand_name = RangeCodec<$codec>;
|
||||
|
||||
#[doc = concat!("Returns a version of [`", stringify!($singleton_codec), "`] for `", stringify!($ty), "`s (or `", stringify!($java_type), "`s in Java) constrained to a minimum *(inclusive)* and maximum *(inclusive)* value.")]
|
||||
pub const fn $func_name(min: $ty, max: $ty) -> $shorthand_name {
|
||||
new_range_codec(&$singleton_codec, min, max)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
make_codec_range_function!(int_range, IntRangeCodec, i32, IntCodec, INT_CODEC, int);
|
||||
make_codec_range_function!(
|
||||
float_range,
|
||||
FloatRangeCodec,
|
||||
f32,
|
||||
FloatCodec,
|
||||
FLOAT_CODEC,
|
||||
float
|
||||
);
|
||||
make_codec_range_function!(
|
||||
double_range,
|
||||
DoubleRangeCodec,
|
||||
f64,
|
||||
DoubleCodec,
|
||||
DOUBLE_CODEC,
|
||||
double
|
||||
);
|
||||
|
||||
// Map codec functions
|
||||
|
||||
/// Creates a [`SimpleMapCodec`] with the provided key codec, value (element) codec and the possible key values.
|
||||
pub const fn simple_map<K: Codec, V: Codec, Key: Keyable>(
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
keyable: Key,
|
||||
) -> SimpleMapCodec<K, V, Key>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
new_simple_map_codec(key_codec, element_codec, keyable)
|
||||
}
|
||||
|
||||
/// Creates an [`UnboundedMapCodec`] with the provided key and value (element) codec.
|
||||
pub const fn unbounded_map<K: Codec, V: Codec>(
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
) -> UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
new_unbounded_map_codec(key_codec, element_codec)
|
||||
}
|
||||
|
||||
/// Creates an [`EitherCodec`] with the provided left and right codecs to tell the way to serialize/deserialize
|
||||
/// their respective types.
|
||||
pub const fn either<L: Codec, R: Codec>(
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
) -> EitherCodec<L, R> {
|
||||
new_either_codec(left_codec, right_codec)
|
||||
}
|
||||
|
||||
// Struct codec functions
|
||||
|
||||
/// Creates a structure [`Codec`]. This macro supports up to *16* [`Field`]s.
|
||||
///
|
||||
/// Struct codec types are usually pretty large. To combat this, use `pub type ... = ...` to
|
||||
/// only store the complicated type once and never use it again. Rust can easily infer the type
|
||||
/// for you after you define your codec.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use pumpkin_codecs::codec::*;
|
||||
/// use pumpkin_codecs::map_codec::*;
|
||||
/// use pumpkin_codecs::codecs::primitive::*;
|
||||
/// use pumpkin_codecs::struct_codecs::*;
|
||||
/// use pumpkin_codecs::struct_codec;
|
||||
///
|
||||
/// // An example struct to make a codec for.
|
||||
/// pub struct Person {
|
||||
/// name: String,
|
||||
/// age: u32
|
||||
/// }
|
||||
///
|
||||
/// // Type to avoid writing this struct codec's type again.
|
||||
/// pub type PersonCodec = StructCodec2<Person, FieldMapCodec<StringCodec>, FieldMapCodec<UintCodec>>;
|
||||
///
|
||||
/// // The actual codec.
|
||||
/// pub static PERSON_CODEC: PersonCodec = struct_codec!(
|
||||
/// for_getter(field(&STRING_CODEC, "name"), |person: &Person| &person.name),
|
||||
/// for_getter(field(&UINT_CODEC, "age"), |person: &Person| &person.age),
|
||||
/// |name, age| Person {name, age}
|
||||
/// );
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! struct_codec {
|
||||
($f1:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_1($f1, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_2($f1, $f2, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_3($f1, $f2, $f3, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_4($f1, $f2, $f3, $f4, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_5($f1, $f2, $f3, $f4, $f5, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_6($f1, $f2, $f3, $f4, $f5, $f6, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_7($f1, $f2, $f3, $f4, $f5, $f6, $f7, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_8($f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_9($f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_10($f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_11(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f,
|
||||
)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f12:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_12(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f12, $f,
|
||||
)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f12:expr, $f13:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_13(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f12, $f13, $f,
|
||||
)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f12:expr, $f13:expr, $f14:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_14(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f12, $f13, $f14, $f,
|
||||
)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f12:expr, $f13:expr, $f14:expr, $f15:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_15(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f12, $f13, $f14, $f15, $f,
|
||||
)
|
||||
};
|
||||
($f1:expr, $f2:expr, $f3:expr, $f4:expr, $f5:expr, $f6:expr, $f7:expr, $f8:expr, $f9:expr, $f10:expr, $f11:expr, $f12:expr, $f13:expr, $f14:expr, $f15:expr, $f16:expr, $f:expr $(,)?) => {
|
||||
$crate::struct_codecs::struct_16(
|
||||
$f1, $f2, $f3, $f4, $f5, $f6, $f7, $f8, $f9, $f10, $f11, $f12, $f13, $f14, $f15, $f16,
|
||||
$f,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
// Field functions
|
||||
|
||||
/// A type of [`MapCodec`] to encode/decode for a single field of a map with the help of a [`Codec`].
|
||||
pub type FieldMapCodec<C> = ComposedMapCodec<
|
||||
FieldEncoder<<C as HasValue>::Value, C>,
|
||||
FieldDecoder<<C as HasValue>::Value, C>,
|
||||
>;
|
||||
|
||||
/// Creates a [`MapCodec`] for a field which relies on the provided [`Codec`] for serialization/deserialization.
|
||||
pub const fn field<C: Codec>(codec: &'static C, name: &'static str) -> FieldMapCodec<C> {
|
||||
ComposedMapCodec {
|
||||
encoder: encoder_field(name, codec),
|
||||
decoder: decoder_field(name, codec),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`MapCodec`] for an optional field which relies on the provided [`Codec`] for serialization/deserialization.
|
||||
///
|
||||
/// Since this `MapCodec` has no 'default value', this is equivalent to encoding an [`Option`].
|
||||
/// The returned `MapCodec` is also *not lenient*, meaning that it will not give a complete (successful) result
|
||||
/// if the decoded field value is an error [`DataResult`] (partial or no result). Most of the time, you will
|
||||
/// want a *non-lenient* field.
|
||||
pub const fn optional_field<C: Codec>(
|
||||
codec: &'static C,
|
||||
name: &'static str,
|
||||
) -> OptionalFieldMapCodec<C> {
|
||||
new_optional_field_map_codec(codec, name, false)
|
||||
}
|
||||
|
||||
/// Creates a [`MapCodec`] for an optional field which relies on the provided [`Codec`] for serialization/deserialization.
|
||||
///
|
||||
/// Since this `MapCodec` has no 'default value', this is equivalent to encoding an [`Option`].
|
||||
/// The returned `MapCodec` is also *lenient*, meaning that it will still give a complete (successful) result
|
||||
/// if the decoded field value is an error [`DataResult`] (partial or no result). Most of the time, you will
|
||||
/// want a *non-lenient* field.
|
||||
pub const fn lenient_optional_field<C: Codec>(
|
||||
codec: &'static C,
|
||||
name: &'static str,
|
||||
) -> OptionalFieldMapCodec<C> {
|
||||
new_optional_field_map_codec(codec, name, true)
|
||||
}
|
||||
|
||||
pub type DefaultedFieldCodec<C> =
|
||||
DefaultValueProviderMapCodec<<C as HasValue>::Value, OptionalFieldMapCodec<C>>;
|
||||
|
||||
/// Creates a [`MapCodec`] for an optional field which relies on the provided [`Codec`] for serialization/deserialization, along with a default value factory.
|
||||
///
|
||||
/// The factory provided is used for equality checks and for creating a new default value
|
||||
/// for when no value is found. *If the encoded value is equal to the default value (provided via the factory), it is omitted.*
|
||||
///
|
||||
/// The returned `MapCodec` is also *not lenient*, meaning that it will not give a complete (successful) result
|
||||
/// if the decoded field value is an error [`DataResult`] (partial or no result). Most of the time, you will
|
||||
/// want a *non-lenient* field.
|
||||
pub const fn optional_field_with_default<C: Codec>(
|
||||
codec: &'static C,
|
||||
name: &'static str,
|
||||
factory: fn() -> C::Value,
|
||||
) -> DefaultedFieldCodec<C>
|
||||
where
|
||||
<C as HasValue>::Value: PartialEq + Clone,
|
||||
{
|
||||
new_default_value_provider_map_codec(new_optional_field_map_codec(codec, name, false), factory)
|
||||
}
|
||||
|
||||
/// Creates a [`MapCodec`] for an optional field which relies on the provided [`Codec`] for serialization/deserialization, along with a default value factory.
|
||||
///
|
||||
/// The factory provided is used for equality checks and for creating a new default value
|
||||
/// for when no value is found. *If the encoded value is equal to the default value (provided via the factory), it is omitted.*
|
||||
///
|
||||
/// The returned `MapCodec` is also *lenient*, meaning that it will still give a complete (successful) result
|
||||
/// if the decoded field value is an error [`DataResult`] (partial or no result). Most of the time, you will
|
||||
/// want a *non-lenient* field.
|
||||
pub const fn lenient_optional_field_with_default<C: Codec>(
|
||||
codec: &'static C,
|
||||
name: &'static str,
|
||||
factory: fn() -> C::Value,
|
||||
) -> DefaultedFieldCodec<C>
|
||||
where
|
||||
<C as HasValue>::Value: PartialEq + Clone,
|
||||
{
|
||||
new_default_value_provider_map_codec(new_optional_field_map_codec(codec, name, true), factory)
|
||||
}
|
||||
|
||||
// Assertion functions
|
||||
|
||||
/// Asserts that the decoding of some value by a [`DynamicOps`] via a [`Codec`] is a success/error.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use pumpkin_codecs::assert_decode;
|
||||
/// # use serde_json::json;
|
||||
/// # use pumpkin_codecs::json_ops;
|
||||
/// # use pumpkin_codecs::codec;
|
||||
/// # use pumpkin_codecs::coders::Decoder;
|
||||
///
|
||||
/// assert_decode!(codec::INT_CODEC, json!(2), &json_ops::INSTANCE, is_success);
|
||||
/// assert_decode!(codec::STRING_CODEC, json!("hello"), &json_ops::INSTANCE, is_success);
|
||||
/// assert_decode!(codec::FLOAT_CODEC, json!(true), &json_ops::INSTANCE, is_error);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! assert_decode {
|
||||
($codec:expr, $value:expr, $ops:expr, $assertion:ident) => {{
|
||||
assert!($codec.decode($value, $ops).$assertion());
|
||||
}};
|
||||
}
|
||||
91
pumpkin-codecs/src/codec/either.rs
Normal file
91
pumpkin-codecs/src/codec/either.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::{DataResult, Decode, DynamicOps, Encode};
|
||||
use either::Either;
|
||||
|
||||
impl<L: Encode, R: Encode> Encode for Either<L, R> {
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
|
||||
match self {
|
||||
Self::Left(l) => l.encode(ops, prefix),
|
||||
Self::Right(r) => r.encode(ops, prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: Decode, R: Decode> Decode for Either<L, R> {
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
|
||||
let left = L::decode(input.clone(), ops).map(|(l, t)| (Self::Left(l), t));
|
||||
|
||||
// If the left result is a success, return that.
|
||||
if left.is_success() {
|
||||
return left;
|
||||
}
|
||||
|
||||
let right = R::decode(input, ops).map(|(r, t)| (Self::Right(r), t));
|
||||
|
||||
// If the right result is a success, return that.
|
||||
if right.is_success() {
|
||||
return right;
|
||||
}
|
||||
|
||||
// Since no result is a complete success by this point, we look for partial results.
|
||||
|
||||
if left.has_result_or_partial() {
|
||||
return left;
|
||||
}
|
||||
|
||||
if right.has_result_or_partial() {
|
||||
return right;
|
||||
}
|
||||
|
||||
DataResult::new_error(format!(
|
||||
"Failed to parse either. First: {}; Second: {}",
|
||||
left.get_message().unwrap(),
|
||||
right.get_message().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::json_ops::JsonOps;
|
||||
use crate::{assert_decode, assert_encode_success};
|
||||
use either::Either;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
assert_encode_success!(Either::<i32, String>::Left(5), JsonOps, json!(5));
|
||||
|
||||
assert_encode_success!(
|
||||
Either::<i32, String>::Right("I am some text.".to_string()),
|
||||
JsonOps,
|
||||
json!("I am some text.")
|
||||
);
|
||||
|
||||
// Decoding
|
||||
|
||||
assert_decode!(
|
||||
Either<i32, String>,
|
||||
json!(-238),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
Either<u32, String>,
|
||||
json!(-238),
|
||||
JsonOps,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
Either<u32, String>,
|
||||
json!("hello"),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
Either<u32, String>,
|
||||
json!(true),
|
||||
JsonOps,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
208
pumpkin-codecs/src/codec/list.rs
Normal file
208
pumpkin-codecs/src/codec/list.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use crate::list_builder::ListBuilder;
|
||||
use crate::{DataResult, Decode, DynamicOps, Encode, FlatTryFrom, Lifecycle};
|
||||
|
||||
/// A wrapped [`Vec`] that can only contain a size of elements between `MIN` and `MAX` (inclusive).
|
||||
pub struct BoundedVec<T, const MIN: usize, const MAX: usize>(Vec<T>);
|
||||
|
||||
impl<T, const MIN: usize, const MAX: usize> From<BoundedVec<T, MIN, MAX>> for Vec<T> {
|
||||
fn from(value: BoundedVec<T, MIN, MAX>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const MIN: usize, const MAX: usize> FlatTryFrom<Vec<T>> for BoundedVec<T, MIN, MAX> {
|
||||
fn flat_try_from(value: Vec<T>) -> DataResult<Self> {
|
||||
let size = value.len();
|
||||
if size < MIN {
|
||||
create_too_short_error(MIN, MAX, size)
|
||||
} else if size > MAX {
|
||||
create_too_long_error(MIN, MAX, size)
|
||||
} else {
|
||||
DataResult::new_success(Self(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_too_short_error<T>(min: usize, max: usize, size: usize) -> DataResult<T> {
|
||||
DataResult::new_error(format!(
|
||||
"List is too short: {size}, expected range [{min}-{max}]"
|
||||
))
|
||||
}
|
||||
|
||||
fn create_too_long_error<T>(min: usize, max: usize, size: usize) -> DataResult<T> {
|
||||
DataResult::new_error(format!(
|
||||
"List is too long: {size}, expected range [{min}-{max}]"
|
||||
))
|
||||
}
|
||||
|
||||
impl<T, const MIN: usize, const MAX: usize> Encode for BoundedVec<T, MIN, MAX>
|
||||
where
|
||||
T: Encode,
|
||||
{
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
|
||||
let size = self.0.len();
|
||||
if size < MIN {
|
||||
create_too_short_error(MIN, MAX, size)
|
||||
} else if size > MAX {
|
||||
create_too_long_error(MIN, MAX, size)
|
||||
} else {
|
||||
let mut builder = ops.list_builder();
|
||||
for e in &self.0 {
|
||||
builder = builder.add_data_result(e.encode_start(ops));
|
||||
}
|
||||
builder.build(prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const MIN: usize, const MAX: usize> Decode for BoundedVec<T, MIN, MAX>
|
||||
where
|
||||
T: Decode,
|
||||
{
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
|
||||
let iter = ops.get_iter(input).with_lifecycle(Lifecycle::Stable);
|
||||
iter.flat_map(|i| {
|
||||
let mut total_count = 0;
|
||||
let mut elements: Vec<T> = vec![];
|
||||
let mut failed: Vec<O::Value> = vec![];
|
||||
// This is used to keep track of the overall `DataResult`.
|
||||
// If any one element has a partial result, this turns into a partial result.
|
||||
// If any one element has no result, this turns into a non-result.
|
||||
let mut result = DataResult::new_success(());
|
||||
|
||||
for element in i {
|
||||
total_count += 1;
|
||||
if elements.len() >= MAX {
|
||||
failed.push(element.clone());
|
||||
continue;
|
||||
}
|
||||
let element_result = T::decode(element.clone(), ops);
|
||||
result = result.add_message(&element_result);
|
||||
if let Some(element) = element_result.into_result_or_partial() {
|
||||
elements.push(element.0);
|
||||
}
|
||||
}
|
||||
|
||||
if total_count < MIN {
|
||||
return create_too_short_error(MIN, MAX, total_count);
|
||||
}
|
||||
|
||||
let pair = (Self(elements), ops.create_list(failed));
|
||||
if total_count > MAX {
|
||||
result = create_too_long_error(MIN, MAX, total_count);
|
||||
}
|
||||
result.with_complete_or_partial(pair)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encode for Vec<T>
|
||||
where
|
||||
T: Encode,
|
||||
{
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
|
||||
let mut builder = ops.list_builder();
|
||||
for e in self {
|
||||
builder = builder.add_data_result(e.encode_start(ops));
|
||||
}
|
||||
builder.build(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Decode for Vec<T>
|
||||
where
|
||||
T: Decode,
|
||||
{
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
|
||||
let iter = ops.get_iter(input).with_lifecycle(Lifecycle::Stable);
|
||||
iter.flat_map(|i| {
|
||||
let mut elements: Self = vec![];
|
||||
let mut result = DataResult::new_success(());
|
||||
|
||||
for element in i {
|
||||
let element_result = T::decode(element.clone(), ops);
|
||||
result = result.add_message(&element_result);
|
||||
if let Some(element) = element_result.into_result_or_partial() {
|
||||
elements.push(element.0);
|
||||
}
|
||||
}
|
||||
|
||||
let pair = (elements, ops.create_list(Vec::new()));
|
||||
result.with_complete_or_partial(pair)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::assert_decode;
|
||||
use crate::assert_encode_success;
|
||||
use crate::json_ops::JsonOps;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
assert_encode_success!(vec![1, 2], JsonOps, json!([1, 2]));
|
||||
let vec: Vec<i32> = vec![];
|
||||
assert_encode_success!(vec, JsonOps, json!([]));
|
||||
assert_encode_success!(vec![-3, 192, 182], JsonOps, json!([-3, 192, 182]));
|
||||
|
||||
assert_encode_success!(
|
||||
vec!["a".to_string(), "b".to_string()],
|
||||
JsonOps,
|
||||
json!(["a", "b"])
|
||||
);
|
||||
assert_encode_success!(vec!["one".to_string()], JsonOps, json!(["one"]));
|
||||
assert_encode_success!(
|
||||
vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
JsonOps,
|
||||
json!(["1", "2", "3"])
|
||||
);
|
||||
|
||||
assert_encode_success!(vec![1, 2], JsonOps, json!([1, 2]));
|
||||
|
||||
assert_encode_success!(vec![true, false], JsonOps, json!([true, false]));
|
||||
assert_encode_success!(
|
||||
vec![vec![true, false], vec![true, false]],
|
||||
JsonOps,
|
||||
json!([[true, false], [true, false]])
|
||||
);
|
||||
assert_encode_success!(
|
||||
vec![vec![vec![true, true], vec![false, false]]],
|
||||
JsonOps,
|
||||
json!([[[true, true], [false, false]]])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
type NumberGrid = Vec<Vec<f64>>;
|
||||
|
||||
assert_decode!(Vec<i16>, json!([1, 2, 3]), JsonOps, is_success);
|
||||
assert_decode!(Vec<i16>, json!([1, 2, 6, 24, 120]), JsonOps, is_success);
|
||||
assert_decode!(Vec<i16>, json!(["string", "b"]), JsonOps, is_error);
|
||||
assert_decode!(Vec<i16>, json!(false), JsonOps, is_error);
|
||||
|
||||
assert_decode!(NumberGrid, json!([[0, 0.5, 1.0]]), JsonOps, is_success);
|
||||
assert_decode!(
|
||||
NumberGrid,
|
||||
json!([[0, 0.5, 1.0], [1, 4, 5], [-293.4, 1, 293]]),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
NumberGrid,
|
||||
json!([[0, 0.5, 1.0], [1, false, 5], [-293.4, 1, 293]]),
|
||||
JsonOps,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
NumberGrid,
|
||||
json!([[1, 1.5, 2.0], [-20]]),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(NumberGrid, json!([[]]), JsonOps, is_success);
|
||||
assert_decode!(NumberGrid, json!([[[[]]]]), JsonOps, is_error);
|
||||
}
|
||||
}
|
||||
264
pumpkin-codecs/src/codec/map.rs
Normal file
264
pumpkin-codecs/src/codec/map.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use crate::{DataResult, Decode, DynamicOps, Encode, Lifecycle};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::hash::{BuildHasher, Hash};
|
||||
|
||||
fn encode_base_hash_map<
|
||||
O: DynamicOps,
|
||||
K: Encode + Eq + Hash + Display,
|
||||
V: Encode,
|
||||
S: BuildHasher + Default,
|
||||
>(
|
||||
map: &HashMap<K, V, S>,
|
||||
ops: &'static O,
|
||||
mut prefix: impl StructBuilder<Value = O::Value>,
|
||||
) -> impl StructBuilder<Value = O::Value> {
|
||||
for (key, element) in map {
|
||||
prefix =
|
||||
prefix.add_key_result_value_result(key.encode_start(ops), element.encode_start(ops));
|
||||
}
|
||||
prefix
|
||||
}
|
||||
|
||||
fn decode_base_hash_map<
|
||||
O: DynamicOps,
|
||||
K: Decode + Eq + Hash + Display,
|
||||
V: Decode,
|
||||
S: BuildHasher + Default,
|
||||
>(
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static O,
|
||||
) -> DataResult<HashMap<K, V, S>> {
|
||||
let mut read_map: HashMap<K, V, S> = HashMap::with_hasher(S::default());
|
||||
let mut failed: Vec<(O::Value, O::Value)> = vec![];
|
||||
|
||||
let result = input.iter().fold(
|
||||
DataResult::new_success_with_lifecycle((), Lifecycle::Stable),
|
||||
|r, (k, e)| {
|
||||
// First, we try to parse the key and value.
|
||||
let key_result = K::parse(k.clone(), ops);
|
||||
let element_result = V::parse(e.clone(), ops);
|
||||
|
||||
let entry_result =
|
||||
key_result.apply_2_and_make_stable(|kr, er| (kr, er), element_result);
|
||||
let accumulated = r.add_message(&entry_result);
|
||||
let entry = entry_result.into_result_or_partial();
|
||||
|
||||
if let Some((key, element)) = entry {
|
||||
// If this parses successfully, we try adding it to our map.
|
||||
if read_map.contains_key(&key) {
|
||||
// There was already a value for this key.
|
||||
failed.push((k, e.clone()));
|
||||
return accumulated.add_message::<()>(&DataResult::new_error(format!(
|
||||
"Duplicate entry for key: {key}"
|
||||
)));
|
||||
}
|
||||
read_map.insert(key, element);
|
||||
} else {
|
||||
// Could not parse.
|
||||
failed.push((k, e.clone()));
|
||||
}
|
||||
|
||||
accumulated
|
||||
},
|
||||
);
|
||||
|
||||
let errors = ops.create_map(failed);
|
||||
|
||||
result
|
||||
.with_complete_or_partial(read_map)
|
||||
.map_error(|e| format!("{e} (Missed inputs: {errors})"))
|
||||
}
|
||||
|
||||
impl<K, V, S: BuildHasher + Default> Encode for HashMap<K, V, S>
|
||||
where
|
||||
K: Encode + Eq + Hash + Display,
|
||||
V: Encode,
|
||||
{
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
|
||||
encode_base_hash_map::<O, K, V, S>(self, ops, ops.map_builder()).build(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, S: BuildHasher + Default> Decode for HashMap<K, V, S>
|
||||
where
|
||||
K: Decode + Eq + Hash + Display,
|
||||
V: Decode,
|
||||
{
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
|
||||
ops.get_map(&input)
|
||||
.with_lifecycle(Lifecycle::Stable)
|
||||
.flat_map(|map| decode_base_hash_map(&map, ops))
|
||||
.map(|r| (r, input))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::*;
|
||||
use crate::json_ops::JsonOps;
|
||||
use crate::{assert_decode, assert_encode_success};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[test]
|
||||
fn simple_encoding() {
|
||||
let mut map = HashMap::<String, i32>::new();
|
||||
|
||||
map.insert("Amy".to_string(), 10);
|
||||
map.insert("Leo".to_string(), 24);
|
||||
map.insert("Patrick".to_string(), -65);
|
||||
|
||||
assert_encode_success!(map, JsonOps, json!({"Amy": 10, "Leo": 24, "Patrick": -65}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_integer_map() {
|
||||
// A basic implementation to check if a number is prime.
|
||||
fn is_prime(number: u32) -> bool {
|
||||
if number < 2 {
|
||||
return false;
|
||||
}
|
||||
for i in 2..number {
|
||||
if number.is_multiple_of(i) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A `u32` wrapper that encodes a `String`.
|
||||
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq, Hash)]
|
||||
struct StringInteger(u32);
|
||||
|
||||
impl Display for StringInteger {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for StringInteger {
|
||||
fn encode<O: DynamicOps>(
|
||||
&self,
|
||||
ops: &'static O,
|
||||
prefix: O::Value,
|
||||
) -> DataResult<O::Value> {
|
||||
// This will always succeed.
|
||||
self.0.to_string().encode(ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for StringInteger {
|
||||
fn decode<O: DynamicOps>(
|
||||
input: O::Value,
|
||||
ops: &'static O,
|
||||
) -> DataResult<(Self, O::Value)> {
|
||||
String::decode(input, ops).flat_map(|s| {
|
||||
// Try to parse an integer.
|
||||
s.0.parse().map_or_else(
|
||||
|_| DataResult::new_error("Could not parse String"),
|
||||
|i| DataResult::new_success((Self(i), s.1)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let mut map = HashMap::<StringInteger, bool>::new();
|
||||
|
||||
// Calculate the map for the first 20 numbers.
|
||||
for i in 1..=20 {
|
||||
map.insert(StringInteger(i), is_prime(i));
|
||||
}
|
||||
|
||||
assert_encode_success!(
|
||||
map,
|
||||
JsonOps,
|
||||
json!({
|
||||
"1": false, "2": true, "3": true, "4": false, "5": true, "6": false, "7": true, "8": false, "9": false, "10": false,
|
||||
"11": true, "12": false, "13": true, "14": false, "15": false, "16": false, "17": true, "18": false, "19": true, "20": false
|
||||
})
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
HashMap<StringInteger, bool>,
|
||||
json!({
|
||||
"1": true, "2": true, "3": true, "4": false, "5": false
|
||||
}),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn letter_frequency() {
|
||||
/// A wrapper of a `String` that only allows a single letter.
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash)]
|
||||
struct Letter(String);
|
||||
|
||||
fn check_letter(string: String) -> DataResult<Letter> {
|
||||
// Try to parse a single letter.
|
||||
if string.len() == 1 {
|
||||
DataResult::new_success(Letter(string))
|
||||
} else {
|
||||
DataResult::new_error(format!("Not a letter: {string}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Letter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for Letter {
|
||||
fn encode<O: DynamicOps>(
|
||||
&self,
|
||||
ops: &'static O,
|
||||
prefix: O::Value,
|
||||
) -> DataResult<O::Value> {
|
||||
// This will always succeed.
|
||||
check_letter(self.0.clone()).flat_map(|l| l.0.encode(ops, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for Letter {
|
||||
fn decode<O: DynamicOps>(
|
||||
input: O::Value,
|
||||
ops: &'static O,
|
||||
) -> DataResult<(Self, O::Value)> {
|
||||
String::decode(input, ops).flat_map(|(s, v)| check_letter(s).map(|l| (l, v)))
|
||||
}
|
||||
}
|
||||
|
||||
type LetterData = HashMap<Letter, u64>;
|
||||
|
||||
let mut map = LetterData::new();
|
||||
map.insert(Letter("b".to_string()), 62);
|
||||
map.insert(Letter("z".to_string()), 2342);
|
||||
|
||||
assert_encode_success!(map, JsonOps, json!({"b": 62, "z": 2342}));
|
||||
|
||||
let mut map = LetterData::new();
|
||||
map.insert(Letter("d".to_string()), 12452);
|
||||
map.insert(Letter("candy".to_string()), 2342);
|
||||
|
||||
assert!(map.encode_start(&JsonOps).is_error());
|
||||
|
||||
assert_decode!(
|
||||
LetterData,
|
||||
json!({"a": 13, "c": 34, "x": 1, "e": 21}),
|
||||
JsonOps,
|
||||
is_success
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
LetterData,
|
||||
json!({"b": 45, "w": 10, "l": 90, "word": 5}),
|
||||
JsonOps,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
142
pumpkin-codecs/src/codec/mod.rs
Normal file
142
pumpkin-codecs/src/codec/mod.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
mod either;
|
||||
pub mod list;
|
||||
pub mod map;
|
||||
pub mod optional_field;
|
||||
pub(crate) mod primitive;
|
||||
|
||||
use crate::codec::optional_field::OptionalFieldDecode;
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use crate::{DataResult, DynamicOps};
|
||||
|
||||
/// A trait for something that can be encoded by a [`DynamicOps`] to its format.
|
||||
pub trait Encode {
|
||||
/// Encodes this value to a value represented by the provided [`DynamicOps`]
|
||||
/// with the provided prefix.
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value>;
|
||||
|
||||
/// Encodes this value to a value represented by the provided [`DynamicOps`] without a prefix.
|
||||
fn encode_start<O: DynamicOps>(&self, ops: &'static O) -> DataResult<O::Value> {
|
||||
self.encode(ops, ops.empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for something which can be added to a [`MapLike`] as a field with a provided name.
|
||||
pub trait FieldEncode {
|
||||
/// Encodes this value to a map by adding a field, whose:
|
||||
/// - key is the field's `name`.
|
||||
/// - value is the encoded value represented by the provided [`DynamicOps`].
|
||||
fn encode_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
) -> B;
|
||||
|
||||
/// Encodes this value to a map by adding a defaulted field, whose:
|
||||
/// - key is the field's `name`.
|
||||
/// - value is the encoded value represented by the provided [`DynamicOps`].
|
||||
///
|
||||
/// The field may not be encoded if `default` == `*self`.
|
||||
fn encode_defaulted_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
default: Self,
|
||||
) -> B
|
||||
where
|
||||
Self: PartialEq;
|
||||
}
|
||||
|
||||
impl<T: Encode> FieldEncode for T {
|
||||
fn encode_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
prefix.add_string_key_value_result(name, self.encode_start(ops))
|
||||
}
|
||||
|
||||
fn encode_defaulted_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
default: Self,
|
||||
) -> B
|
||||
where
|
||||
Self: PartialEq,
|
||||
{
|
||||
if default == *self {
|
||||
prefix.add_string_key_value_result(name, self.encode_start(ops))
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for something that can be decoded from the value represented by a [`DynamicOps`].
|
||||
pub trait Decode: Sized {
|
||||
/// Decodes a value of this type from a value represented by the provided [`DynamicOps`],
|
||||
/// along with the remaining data.
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)>;
|
||||
|
||||
/// Decodes a value of this type from a value represented by the provided [`DynamicOps`],
|
||||
/// without providing any other data.
|
||||
fn parse<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<Self> {
|
||||
Self::decode(input, ops).map(|(r, _)| r)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for something which can be decoded from a [`MapLike`] from a field with a provided name.
|
||||
pub trait FieldDecode: Sized {
|
||||
/// Decodes a value of this type from a map by decoding one of its fields, whose:
|
||||
/// - key is the field's `name`.
|
||||
/// - value is the value represented by a [`DynamicsOps`] that is meant to be decoded.
|
||||
fn decode_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
) -> DataResult<Self>;
|
||||
|
||||
/// Decodes a value of this type from a map by decoding one of its defaulted fields, whose:
|
||||
/// - key is the field's `name`.
|
||||
/// - value is the value represented by a [`DynamicsOps`] that is meant to be decoded.
|
||||
///
|
||||
/// If a value could not be decoded, the `default` value is returned.
|
||||
/// This method has an extra `lenient` parameter. If it is `true`, errors
|
||||
/// while trying to decode an explicit value, and the default value will be decoded instead.
|
||||
fn decode_defaulted_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
default: Self,
|
||||
lenient: bool,
|
||||
) -> DataResult<Self>;
|
||||
}
|
||||
|
||||
impl<T: Decode> FieldDecode for T {
|
||||
fn decode_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
) -> DataResult<Self> {
|
||||
input.get_str(name).map_or_else(
|
||||
|| DataResult::new_error(format!("No key {name} in map")),
|
||||
|v| Self::parse(v.clone(), ops),
|
||||
)
|
||||
}
|
||||
|
||||
fn decode_defaulted_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
default: Self,
|
||||
lenient: bool,
|
||||
) -> DataResult<Self> {
|
||||
let decoded_option = Option::decode_optional_field::<O>(name, input, ops, lenient);
|
||||
decoded_option.map(|o| o.unwrap_or(default))
|
||||
}
|
||||
}
|
||||
77
pumpkin-codecs/src/codec/optional_field.rs
Normal file
77
pumpkin-codecs/src/codec/optional_field.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::codec::FieldEncode;
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use crate::{DataResult, Decode, DynamicOps, Encode};
|
||||
|
||||
/// A trait for something which can be added to a [`MapLike`] as an optional field with a provided name.
|
||||
pub trait OptionalFieldEncode {
|
||||
/// Encodes this value to a map by adding an optional field, whose:
|
||||
/// - key is the field's `name`.
|
||||
/// - value is the encoded value represented by the provided [`DynamicOps`].
|
||||
fn encode_optional_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
) -> B;
|
||||
}
|
||||
|
||||
impl<T> OptionalFieldEncode for Option<T>
|
||||
where
|
||||
T: Encode,
|
||||
{
|
||||
fn encode_optional_field<O: DynamicOps, B: StructBuilder<Value = O::Value>>(
|
||||
&self,
|
||||
name: &'static str,
|
||||
ops: &'static O,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
if let Some(value) = self {
|
||||
value.encode_field(name, ops, prefix)
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to decode an optional field of a [`MapLike`] into a
|
||||
/// value of the implementing type.
|
||||
///
|
||||
/// There is no `OptionalFieldEncode` variant of this trait; just
|
||||
/// use [`FieldEncode::encode_field`] for encoding an optional field.
|
||||
pub trait OptionalFieldDecode: Sized {
|
||||
/// Decodes an optional field from a map, similar to [`FieldDecode::decode_field`].
|
||||
///
|
||||
/// However, this method has an extra `lenient` parameter. If it is `true`, errors
|
||||
/// while decoding a `Some` option will not occur, and a `None` will be decoded instead.
|
||||
fn decode_optional_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
lenient: bool,
|
||||
) -> DataResult<Self>;
|
||||
}
|
||||
|
||||
impl<T> OptionalFieldDecode for Option<T>
|
||||
where
|
||||
T: Decode,
|
||||
{
|
||||
fn decode_optional_field<O: DynamicOps>(
|
||||
name: &'static str,
|
||||
input: &impl MapLike<Value = O::Value>,
|
||||
ops: &'static impl DynamicOps<Value = O::Value>,
|
||||
lenient: bool,
|
||||
) -> DataResult<Self> {
|
||||
input.get_str(name).map_or_else(
|
||||
|| DataResult::new_success(None),
|
||||
|value| {
|
||||
let result = T::parse(value.clone(), ops);
|
||||
if result.is_error() && lenient {
|
||||
DataResult::new_success(None)
|
||||
} else {
|
||||
result.map(Some)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
229
pumpkin-codecs/src/codec/primitive.rs
Normal file
229
pumpkin-codecs/src/codec/primitive.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use crate::codec::primitive::sealed::Primitive;
|
||||
use crate::{DataResult, Decode, DynamicOps, Encode};
|
||||
|
||||
mod sealed {
|
||||
use super::{DataResult, DynamicOps};
|
||||
|
||||
/// Sealed trait to easily implement `Encode` and `Decode` for
|
||||
/// primitive DFU types.
|
||||
pub trait Primitive: Sized {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value;
|
||||
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self>;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Primitive> Encode for T {
|
||||
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
|
||||
ops.merge_into_primitive(prefix, self.primitive_encode(ops))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Primitive> Decode for T {
|
||||
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
|
||||
T::primitive_decode(ops, input).map(|r| (r, ops.empty()))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_number {
|
||||
($ty:ty, $create_func:ident) => {
|
||||
impl Primitive for $ty {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
|
||||
ops.$create_func(*self)
|
||||
}
|
||||
fn primitive_decode<O: DynamicOps>(
|
||||
ops: &'static O,
|
||||
input: O::Value,
|
||||
) -> DataResult<Self> {
|
||||
ops.get_number(&input).map(|n| <$ty>::from(n))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_number_and_unsigned {
|
||||
($ty:ty, $uty:ty, $create_func:ident) => {
|
||||
impl_number!($ty, $create_func);
|
||||
|
||||
// Unsigned type
|
||||
impl Encode for $uty {
|
||||
fn encode<O: DynamicOps>(
|
||||
&self,
|
||||
ops: &'static O,
|
||||
prefix: O::Value,
|
||||
) -> DataResult<O::Value> {
|
||||
<$ty>::try_from(*self).map_or_else(
|
||||
|_| {
|
||||
DataResult::new_error(concat!(
|
||||
"Could not fit ",
|
||||
stringify!($uty),
|
||||
" into ",
|
||||
stringify!($ty)
|
||||
))
|
||||
},
|
||||
|i| i.encode(ops, prefix),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Decode for $uty {
|
||||
fn decode<O: DynamicOps>(
|
||||
input: O::Value,
|
||||
ops: &'static O,
|
||||
) -> DataResult<(Self, O::Value)> {
|
||||
<$ty>::parse(input, ops).flat_map(|i| {
|
||||
<$uty>::try_from(i).map_or_else(
|
||||
|_| {
|
||||
DataResult::new_error(concat!(
|
||||
"Could not fit ",
|
||||
stringify!($ty),
|
||||
" into ",
|
||||
stringify!($uty)
|
||||
))
|
||||
},
|
||||
|u| DataResult::new_success((u, ops.empty())),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_number_and_unsigned!(i8, u8, create_byte);
|
||||
impl_number_and_unsigned!(i16, u16, create_short);
|
||||
impl_number_and_unsigned!(i32, u32, create_int);
|
||||
impl_number_and_unsigned!(i64, u64, create_long);
|
||||
|
||||
impl_number!(f32, create_float);
|
||||
impl_number!(f64, create_double);
|
||||
|
||||
impl Primitive for bool {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
|
||||
ops.create_bool(*self)
|
||||
}
|
||||
|
||||
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self> {
|
||||
ops.get_bool(&input)
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive for String {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
|
||||
ops.create_string(self.as_str())
|
||||
}
|
||||
|
||||
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self> {
|
||||
ops.get_string(&input)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! stream_struct {
|
||||
($stream:ident, $ty:ty, $create_func:ident, $get_func:ident) => {
|
||||
#[doc = concat!("A [`Vec<", stringify!($ty), ">`] wrapper that has built-in DFU support for encoding and decoding.")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct $stream(pub Vec<$ty>);
|
||||
|
||||
impl From<Vec<$ty>> for $stream {
|
||||
fn from(value: Vec<$ty>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$stream> for Vec<$ty> {
|
||||
fn from(value: $stream) -> Vec<$ty> {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive for $stream {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
|
||||
ops.$create_func(self.0.clone())
|
||||
}
|
||||
|
||||
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self> {
|
||||
ops.$get_func(input).map(From::from)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
stream_struct!(IntStream, i32, create_int_list, get_int_list);
|
||||
stream_struct!(LongStream, i64, create_long_list, get_long_list);
|
||||
|
||||
/// A [`Box<[u8]>`] wrapper that has built-in DFU support for encoding and decoding.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ByteBuffer(pub Box<[u8]>);
|
||||
|
||||
impl From<Box<[u8]>> for ByteBuffer {
|
||||
fn from(value: Box<[u8]>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<[u8; N]> for ByteBuffer {
|
||||
fn from(value: [u8; N]) -> Self {
|
||||
Self(Box::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ByteBuffer> for Box<[u8]> {
|
||||
fn from(value: ByteBuffer) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive for ByteBuffer {
|
||||
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
|
||||
ops.create_byte_buffer(self.0.to_vec())
|
||||
}
|
||||
|
||||
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self> {
|
||||
ops.get_byte_buffer(input).map(From::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::json_ops::JsonOps;
|
||||
use crate::{ByteBuffer, IntStream, LongStream, assert_decode, assert_encode_success};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
assert_encode_success!(3, JsonOps, json!(3));
|
||||
assert_encode_success!(-68i8, JsonOps, json!(-68));
|
||||
assert_encode_success!(-913813743, JsonOps, json!(-913813743));
|
||||
assert_encode_success!("Hello, world!".to_string(), JsonOps, json!("Hello, world!"));
|
||||
assert_encode_success!(String::new(), JsonOps, json!(""));
|
||||
assert_encode_success!(ByteBuffer::from([1u8, 2u8, 3u8]), JsonOps, json!([1, 2, 3]));
|
||||
assert_encode_success!(
|
||||
IntStream::from(vec![3, 6, 9, 11, 15]),
|
||||
JsonOps,
|
||||
json!([3, 6, 9, 11, 15])
|
||||
);
|
||||
assert_encode_success!(
|
||||
LongStream::from(vec![4, 6, 9, 12]),
|
||||
JsonOps,
|
||||
json!([4, 6, 9, 12])
|
||||
);
|
||||
|
||||
assert_encode_success!(3u8, JsonOps, json!(3));
|
||||
assert_encode_success!(923482312u64, JsonOps, json!(923482312));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
assert_decode!(i32, json!(-2), JsonOps, is_success);
|
||||
assert_decode!(bool, json!("hello"), JsonOps, is_error);
|
||||
assert_decode!(bool, json!(0), JsonOps, is_error);
|
||||
|
||||
assert_decode!(IntStream, json!([1, 2, 3]), JsonOps, is_success);
|
||||
assert_decode!(LongStream, json!([]), JsonOps, is_success);
|
||||
assert_decode!(ByteBuffer, json!(["not a number"]), JsonOps, is_error);
|
||||
|
||||
assert_decode!(String, json!("cool"), JsonOps, is_success);
|
||||
assert_decode!(String, json!(1), JsonOps, is_error);
|
||||
|
||||
assert_decode!(u32, json!(-45), JsonOps, is_error);
|
||||
assert_decode!(u64, json!(-132541235), JsonOps, is_error);
|
||||
assert_decode!(u64, json!(132541235), JsonOps, is_success);
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use either::Either;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A codec that can serialize/deserialize one of two types, with a codec for each one.
|
||||
///
|
||||
/// This evaluates the left codec first, and if the [`DataResult`] for it is invalid,
|
||||
/// it evaluates the right codec.
|
||||
pub struct EitherCodec<L: Codec + 'static, R: Codec + 'static> {
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
}
|
||||
|
||||
impl<L: Codec, R: Codec> HasValue for EitherCodec<L, R> {
|
||||
type Value = Either<L::Value, R::Value>;
|
||||
}
|
||||
|
||||
impl<L: Codec, R: Codec> Encoder for EitherCodec<L, R> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
match &input {
|
||||
Either::Left(l) => self.left_codec.encode(l, ops, prefix),
|
||||
Either::Right(r) => self.right_codec.encode(r, ops, prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: Codec, R: Codec> Decoder for EitherCodec<L, R> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
let left = self
|
||||
.left_codec
|
||||
.decode(input.clone(), ops)
|
||||
.map(|(l, t)| (Either::Left(l), t));
|
||||
|
||||
// If the left result is a success, return that.
|
||||
if left.is_success() {
|
||||
return left;
|
||||
}
|
||||
|
||||
let right = self
|
||||
.right_codec
|
||||
.decode(input, ops)
|
||||
.map(|(r, t)| (Either::Right(r), t));
|
||||
|
||||
// If the right result is a success, return that.
|
||||
if right.is_success() {
|
||||
return right;
|
||||
}
|
||||
|
||||
// Since no result is a complete success by this point, we look for partial results.
|
||||
|
||||
if left.has_result_or_partial() {
|
||||
return left;
|
||||
}
|
||||
|
||||
if right.has_result_or_partial() {
|
||||
return right;
|
||||
}
|
||||
|
||||
DataResult::new_error(format!(
|
||||
"Failed to parse either. First: {}; Second: {}",
|
||||
left.get_message().unwrap(),
|
||||
right.get_message().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `EitherCodec` with the provided left and right codecs for serializing/deserializing both possible types.
|
||||
pub(crate) const fn new_either_codec<L: Codec, R: Codec>(
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
) -> EitherCodec<L, R> {
|
||||
EitherCodec {
|
||||
left_codec,
|
||||
right_codec,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::{
|
||||
DOUBLE_CODEC, FieldMapCodec, INT_CODEC, STRING_CODEC, either, field, unbounded_map,
|
||||
};
|
||||
use crate::codecs::either::EitherCodec;
|
||||
use crate::codecs::primitive::{DoubleCodec, IntCodec, StringCodec};
|
||||
use crate::codecs::unbounded_map::UnboundedMapCodec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::json_ops;
|
||||
use crate::map_codec::for_getter;
|
||||
use crate::struct_codec;
|
||||
use crate::struct_codecs::StructCodec2;
|
||||
use either::Either;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
pub static EITHER_INT_STRING_CODEC: EitherCodec<IntCodec, StringCodec> =
|
||||
either(&INT_CODEC, &STRING_CODEC);
|
||||
|
||||
// Encoding
|
||||
|
||||
assert_eq!(
|
||||
EITHER_INT_STRING_CODEC
|
||||
.encode_start(&Either::Left(5), &json_ops::INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
json!(5)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
EITHER_INT_STRING_CODEC
|
||||
.encode_start(
|
||||
&Either::Right("I am some text.".to_string()),
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
json!("I am some text.")
|
||||
);
|
||||
|
||||
// Decoding
|
||||
|
||||
assert_eq!(
|
||||
EITHER_INT_STRING_CODEC
|
||||
.parse(json!(-238), &json_ops::INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
Either::Left(-238)
|
||||
);
|
||||
assert_eq!(
|
||||
EITHER_INT_STRING_CODEC
|
||||
.parse(json!("hello"), &json_ops::INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
Either::Right("hello".to_string())
|
||||
);
|
||||
assert!(
|
||||
EITHER_INT_STRING_CODEC
|
||||
.parse(json!(true), &json_ops::INSTANCE)
|
||||
.get_message()
|
||||
.expect("Decoding should fail")
|
||||
.starts_with("Failed to parse either.")
|
||||
);
|
||||
}
|
||||
|
||||
// A situation where two codecs could possibly decode valid but different values.
|
||||
// This test only checks for decoding.
|
||||
#[test]
|
||||
fn intersecting_codecs() {
|
||||
/// A type to store a complex number (a number with both a real and imaginary part).
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
struct ComplexNumber(f64, f64);
|
||||
|
||||
pub type ComplexNumberCodec =
|
||||
StructCodec2<ComplexNumber, FieldMapCodec<DoubleCodec>, FieldMapCodec<DoubleCodec>>;
|
||||
pub static COMPLEX_NUMBER_CODEC: ComplexNumberCodec = struct_codec!(
|
||||
for_getter(field(&DoubleCodec, "real"), |n| &n.0),
|
||||
for_getter(field(&DoubleCodec, "imaginary"), |n| &n.1),
|
||||
ComplexNumber
|
||||
);
|
||||
|
||||
pub type DoubleMapCodec = UnboundedMapCodec<StringCodec, DoubleCodec>;
|
||||
pub static DOUBLE_MAP_CODEC: DoubleMapCodec = unbounded_map(&STRING_CODEC, &DOUBLE_CODEC);
|
||||
|
||||
pub static COMPLEX_NUMBER_FIRST_EITHER_CODEC: EitherCodec<
|
||||
ComplexNumberCodec,
|
||||
DoubleMapCodec,
|
||||
> = either(&COMPLEX_NUMBER_CODEC, &DOUBLE_MAP_CODEC);
|
||||
pub static DOUBLE_MAP_FIRST_EITHER_CODEC: EitherCodec<DoubleMapCodec, ComplexNumberCodec> =
|
||||
either(&DOUBLE_MAP_CODEC, &COMPLEX_NUMBER_CODEC);
|
||||
|
||||
// We expect a complex number first, as that is the first to be checked.
|
||||
assert_eq!(
|
||||
COMPLEX_NUMBER_FIRST_EITHER_CODEC
|
||||
.parse(
|
||||
json!(
|
||||
{
|
||||
"real": 10.1,
|
||||
"imaginary": -4.5
|
||||
}
|
||||
),
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed")
|
||||
.expect_left("Expected complex number"),
|
||||
ComplexNumber(10.1, -4.5)
|
||||
);
|
||||
|
||||
// This should be a map, as the complex number codec fails due to a missing field.
|
||||
assert_eq!(
|
||||
COMPLEX_NUMBER_FIRST_EITHER_CODEC
|
||||
.parse(
|
||||
json!(
|
||||
{
|
||||
"real": 10.1,
|
||||
"second": -4.5,
|
||||
"third": 1
|
||||
}
|
||||
),
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed")
|
||||
.expect_right("Expected double map")
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DOUBLE_MAP_FIRST_EITHER_CODEC
|
||||
.parse(
|
||||
json!(
|
||||
{
|
||||
"real": -124.6,
|
||||
"imaginary": 134,
|
||||
}
|
||||
),
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed")
|
||||
.expect_left("Expected double map")
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DOUBLE_MAP_FIRST_EITHER_CODEC
|
||||
.parse(
|
||||
json!(
|
||||
{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
"d": 4
|
||||
}
|
||||
),
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed")
|
||||
.expect_left("Expected double map")
|
||||
.len(),
|
||||
4
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use std::fmt::Display;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// A type of [`Codec`] that initializes an inner [`Codec`] on first use.
|
||||
pub struct LazyCodec<C>
|
||||
where
|
||||
C: Codec,
|
||||
{
|
||||
codec: LazyLock<C>,
|
||||
}
|
||||
|
||||
impl<C: Codec> HasValue for LazyCodec<C> {
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: Codec> Encoder for LazyCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
self.codec.encode(input, ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> Decoder for LazyCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.codec.decode(input, ops)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`LazyCodec`].
|
||||
pub(crate) const fn new_lazy_codec<C: Codec>(f: fn() -> C) -> LazyCodec<C> {
|
||||
LazyCodec {
|
||||
codec: LazyLock::new(f),
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::lifecycle::Lifecycle;
|
||||
use crate::list_builder::ListBuilder;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
/// A list codec type. For a type `A`, this codec serializes/deserializes a [`Vec<A>`].
|
||||
/// `C` is the codec used for each element of this list.
|
||||
///
|
||||
/// A `ListCodec` can also specify a minimum and maximum number of elements to allow in the list.
|
||||
#[derive(Debug)]
|
||||
pub struct ListCodec<C>
|
||||
where
|
||||
C: Codec + ?Sized + 'static,
|
||||
{
|
||||
element_codec: &'static C,
|
||||
min_size: usize,
|
||||
max_size: usize,
|
||||
}
|
||||
|
||||
impl<C: Codec> ListCodec<C> {
|
||||
fn create_too_short_error<T>(&self, size: usize) -> DataResult<T> {
|
||||
DataResult::new_error(format!(
|
||||
"List is too short: {size}, expected range [{}-{}]",
|
||||
self.min_size, self.max_size
|
||||
))
|
||||
}
|
||||
|
||||
fn create_too_long_error<T>(&self, size: usize) -> DataResult<T> {
|
||||
DataResult::new_error(format!(
|
||||
"List is too long: {size}, expected range [{}-{}]",
|
||||
self.min_size, self.max_size
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> HasValue for ListCodec<C> {
|
||||
type Value = Vec<C::Value>;
|
||||
}
|
||||
|
||||
impl<C: Codec> Encoder for ListCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
let size = input.len();
|
||||
if size < self.min_size {
|
||||
self.create_too_short_error(size)
|
||||
} else if size > self.max_size {
|
||||
self.create_too_long_error(size)
|
||||
} else {
|
||||
let mut builder = ops.list_builder();
|
||||
for e in input {
|
||||
builder = builder.add_data_result(self.element_codec.encode_start(e, ops));
|
||||
}
|
||||
builder.build(prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Decoder for ListCodec<C>
|
||||
where
|
||||
C: Codec,
|
||||
{
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
let iter = ops.get_iter(input).with_lifecycle(Lifecycle::Stable);
|
||||
iter.flat_map(|i| {
|
||||
let mut total_count = 0;
|
||||
let mut elements: Self::Value = vec![];
|
||||
let mut failed: Vec<T> = vec![];
|
||||
// This is used to keep track of the overall `DataResult`.
|
||||
// If any one element has a partial result, this turns into a partial result.
|
||||
// If any one element has no result, this turns into a non-result.
|
||||
let mut result = DataResult::new_success(());
|
||||
|
||||
for element in i {
|
||||
total_count += 1;
|
||||
if elements.len() >= self.max_size {
|
||||
failed.push(element.clone());
|
||||
continue;
|
||||
}
|
||||
let element_result = self.element_codec.decode(element.clone(), ops);
|
||||
result = result.add_message(&element_result);
|
||||
if let Some(element) = element_result.into_result_or_partial() {
|
||||
elements.push(element.0);
|
||||
}
|
||||
}
|
||||
|
||||
if total_count < self.min_size {
|
||||
return self.create_too_short_error(total_count);
|
||||
}
|
||||
|
||||
let pair = (elements, ops.create_list(failed));
|
||||
if total_count > self.max_size {
|
||||
result = self.create_too_long_error(total_count);
|
||||
}
|
||||
result.with_complete_or_partial(pair)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`ListCodec`].
|
||||
pub(crate) const fn new_list_codec<C: Codec>(
|
||||
codec: &'static C,
|
||||
min_size: usize,
|
||||
max_size: usize,
|
||||
) -> ListCodec<C> {
|
||||
ListCodec {
|
||||
element_codec: codec,
|
||||
min_size,
|
||||
max_size,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::*;
|
||||
use crate::codecs::list::ListCodec;
|
||||
use crate::codecs::primitive::{BoolCodec, DoubleCodec, IntCodec, ShortCodec, StringCodec};
|
||||
use crate::coders::Decoder;
|
||||
use crate::coders::Encoder;
|
||||
use crate::json_ops;
|
||||
use crate::{assert_decode, assert_success};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
{
|
||||
pub static INT_LIST_CODEC: ListCodec<IntCodec> = list(&INT_CODEC, 1, 3);
|
||||
|
||||
assert_success!(
|
||||
INT_LIST_CODEC.encode_start(&vec![1, 2], &json_ops::INSTANCE),
|
||||
json!([1, 2])
|
||||
);
|
||||
assert!(
|
||||
INT_LIST_CODEC
|
||||
.encode_start(&vec![], &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
assert!(
|
||||
INT_LIST_CODEC
|
||||
.encode_start(&vec![50, 52, 54, 56], &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
pub static STRING_LIST_CODEC: ListCodec<StringCodec> = limited_list(&STRING_CODEC, 2);
|
||||
|
||||
assert_success!(
|
||||
STRING_LIST_CODEC
|
||||
.encode_start(&vec!["a".to_string(), "b".to_string()], &json_ops::INSTANCE),
|
||||
json!(["a", "b"])
|
||||
);
|
||||
assert_success!(
|
||||
STRING_LIST_CODEC.encode_start(&vec!["one".to_string()], &json_ops::INSTANCE),
|
||||
json!(["one"])
|
||||
);
|
||||
assert!(
|
||||
STRING_LIST_CODEC
|
||||
.encode_start(
|
||||
&vec!["1".to_string(), "2".to_string(), "3".to_string()],
|
||||
&json_ops::INSTANCE
|
||||
)
|
||||
.is_error()
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
// The inner lists have a max size of 2, while the main list has a max size of 3.
|
||||
pub static BOOL_LIST_LIST_CODEC: ListCodec<ListCodec<BoolCodec>> =
|
||||
limited_list(&limited_list(&BOOL_CODEC, 2), 3);
|
||||
|
||||
assert_success!(
|
||||
BOOL_LIST_LIST_CODEC.encode_start(&vec![vec![true, true]], &json_ops::INSTANCE),
|
||||
json!([[true, true]])
|
||||
);
|
||||
assert_success!(
|
||||
BOOL_LIST_LIST_CODEC
|
||||
.encode_start(&vec![vec![], vec![false, true]], &json_ops::INSTANCE),
|
||||
json!([[], [false, true]])
|
||||
);
|
||||
assert!(
|
||||
BOOL_LIST_LIST_CODEC
|
||||
.encode_start(&vec![vec![true, false, true, false]], &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
{
|
||||
pub static SHORT_LIST_CODEC: ListCodec<ShortCodec> = list(&SHORT_CODEC, 2, 4);
|
||||
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!([1, 2]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!([1, 2, 6, 24]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!([1, 2, 6, 24, 120]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!([-45, 252, 1000]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!(["string", "b"]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
SHORT_LIST_CODEC,
|
||||
json!(["1", "2"]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
// The inner lists have a size of 3, while the main list has a max size of 2.
|
||||
pub static POS_LIST_CODEC: ListCodec<ListCodec<DoubleCodec>> =
|
||||
limited_list(&list(&DOUBLE_CODEC, 3, 3), 2);
|
||||
|
||||
assert_decode!(
|
||||
POS_LIST_CODEC,
|
||||
json!([[0, 0.5, 1.0]]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
POS_LIST_CODEC,
|
||||
json!([0, 0.5, 1.0]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
POS_LIST_CODEC,
|
||||
json!([[3.56, 123.4, -0.144], [12.34, 56.78]]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(POS_LIST_CODEC, json!([]), &json_ops::INSTANCE, is_success);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::map_codec::MapCodec;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A [`Codec`] implementation for a [`MapCodec`].
|
||||
///
|
||||
/// The `MapCodec` held by this `Codec` can either be *owned* or a static reference (*borrowed*).
|
||||
pub enum MapCodecCodec<C: MapCodec + 'static> {
|
||||
Owned(C),
|
||||
Borrowed(&'static C),
|
||||
}
|
||||
|
||||
impl<C: MapCodec> MapCodecCodec<C> {
|
||||
const fn codec(&self) -> &C {
|
||||
match self {
|
||||
Self::Owned(c) => c,
|
||||
Self::Borrowed(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> HasValue for MapCodecCodec<C> {
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: MapCodec> Encoder for MapCodecCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
self.codec()
|
||||
.encode(input, ops, self.codec().builder(ops))
|
||||
.build(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> Decoder for MapCodecCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.codec()
|
||||
.compressed_decode(input.clone(), ops)
|
||||
.map(|a| (a, input))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
pub mod either;
|
||||
pub mod lazy;
|
||||
pub mod list;
|
||||
pub mod map_codec;
|
||||
pub mod primitive;
|
||||
pub mod range;
|
||||
pub mod unbounded_map;
|
||||
pub mod validated;
|
||||
@@ -1,208 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
|
||||
// DFU types
|
||||
|
||||
/// Helper macro to generate the struct and [`HasValue`] trait implementation for a `PrimitiveCodec` struct.
|
||||
macro_rules! impl_primitive_codec_start {
|
||||
($name:ident, $prim:ty) => {
|
||||
/// A primitive [`Codec`] for the
|
||||
#[doc = concat!("[`", stringify!($prim), "`]")]
|
||||
/// data type.
|
||||
pub struct $name;
|
||||
|
||||
impl HasValue for $name {
|
||||
type Value = $prim;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper macro to generate an entire implementation for a number `PrimitiveCodec`.
|
||||
macro_rules! impl_primitive_number_codec {
|
||||
($name:ident, $prim:ty, $create_func:ident) => {
|
||||
impl_primitive_codec_start!($name, $prim);
|
||||
impl PrimitiveCodec for $name {
|
||||
fn read<T>(
|
||||
&self,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
input: T,
|
||||
) -> DataResult<$prim> {
|
||||
ops.get_number(&input).map(|n| <$prim>::from(n))
|
||||
}
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &$prim) -> T {
|
||||
ops.$create_func(*value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper macro to generate an entire implementation for a list `PrimitiveCodec`.
|
||||
macro_rules! impl_primitive_list_codec {
|
||||
($name:ident, $elem:ty, $get_func:ident, $create_func:ident) => {
|
||||
impl_primitive_codec_start!($name, Vec<$elem>);
|
||||
impl PrimitiveCodec for $name {
|
||||
fn read<T>(
|
||||
&self,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
input: T,
|
||||
) -> DataResult<Vec<$elem>> {
|
||||
ops.$get_func(input)
|
||||
}
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &Vec<$elem>) -> T {
|
||||
ops.$create_func(value.to_vec())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A generic primitive codec.
|
||||
trait PrimitiveCodec: Codec {
|
||||
fn read<T>(
|
||||
&self,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
input: T,
|
||||
) -> DataResult<Self::Value>;
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &Self::Value) -> T;
|
||||
}
|
||||
|
||||
impl<C: PrimitiveCodec> Encoder for C {
|
||||
fn encode<T: PartialEq>(
|
||||
&self,
|
||||
input: &<C as HasValue>::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
ops.merge_into_primitive(prefix, self.write(ops, input))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: PrimitiveCodec> Decoder for C {
|
||||
fn decode<T: PartialEq>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(<C as HasValue>::Value, T)> {
|
||||
self.read(ops, input).map(|r| (r, ops.empty()))
|
||||
}
|
||||
}
|
||||
|
||||
// Implementations
|
||||
|
||||
impl_primitive_codec_start!(BoolCodec, bool);
|
||||
impl PrimitiveCodec for BoolCodec {
|
||||
fn read<T>(&self, ops: &'static impl DynamicOps<Value = T>, input: T) -> DataResult<bool> {
|
||||
ops.get_bool(&input)
|
||||
}
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &bool) -> T {
|
||||
ops.create_bool(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl_primitive_number_codec!(ByteCodec, i8, create_byte);
|
||||
impl_primitive_number_codec!(ShortCodec, i16, create_short);
|
||||
impl_primitive_number_codec!(IntCodec, i32, create_int);
|
||||
impl_primitive_number_codec!(LongCodec, i64, create_long);
|
||||
impl_primitive_number_codec!(FloatCodec, f32, create_float);
|
||||
impl_primitive_number_codec!(DoubleCodec, f64, create_double);
|
||||
|
||||
impl_primitive_codec_start!(StringCodec, String);
|
||||
impl PrimitiveCodec for StringCodec {
|
||||
fn read<T>(&self, ops: &'static impl DynamicOps<Value = T>, input: T) -> DataResult<String> {
|
||||
ops.get_string(&input)
|
||||
}
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &String) -> T {
|
||||
ops.create_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl_primitive_codec_start!(ByteBufferCodec, Box<[u8]>);
|
||||
impl PrimitiveCodec for ByteBufferCodec {
|
||||
fn read<T>(&self, ops: &'static impl DynamicOps<Value = T>, input: T) -> DataResult<Box<[u8]>> {
|
||||
ops.get_byte_buffer(input)
|
||||
}
|
||||
|
||||
fn write<T>(&self, ops: &'static impl DynamicOps<Value = T>, value: &Box<[u8]>) -> T {
|
||||
ops.create_byte_buffer(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl_primitive_list_codec!(IntStreamCodec, i32, get_int_list, create_int_list);
|
||||
impl_primitive_list_codec!(LongStreamCodec, i64, get_long_list, create_long_list);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::*;
|
||||
use crate::coders::*;
|
||||
use crate::json_ops;
|
||||
use crate::{assert_decode, assert_success};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
assert_success!(INT_CODEC.encode_start(&3, &json_ops::INSTANCE), json!(3));
|
||||
assert_success!(
|
||||
BYTE_CODEC.encode_start(&-68i8, &json_ops::INSTANCE),
|
||||
json!(-68)
|
||||
);
|
||||
assert_success!(
|
||||
LONG_CODEC.encode_start(&-913813743, &json_ops::INSTANCE),
|
||||
json!(-913813743)
|
||||
);
|
||||
|
||||
assert_success!(
|
||||
STRING_CODEC.encode_start(&"Hello, world!".to_string(), &json_ops::INSTANCE),
|
||||
json!("Hello, world!")
|
||||
);
|
||||
assert_success!(
|
||||
STRING_CODEC.encode_start(&String::new(), &json_ops::INSTANCE),
|
||||
json!("")
|
||||
);
|
||||
|
||||
assert_success!(
|
||||
BYTE_BUFFER_CODEC.encode_start(&Box::from([1u8, 2u8, 3u8]), &json_ops::INSTANCE),
|
||||
json!([1, 2, 3])
|
||||
);
|
||||
assert_success!(
|
||||
LONG_STREAM_CODEC.encode_start(&vec![4, 6, 9, 12], &json_ops::INSTANCE),
|
||||
json!([4, 6, 9, 12])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
assert_decode!(INT_CODEC, json!(-2), &json_ops::INSTANCE, is_success);
|
||||
|
||||
assert_decode!(SHORT_CODEC, json!("hello"), &json_ops::INSTANCE, is_error);
|
||||
assert_decode!(BOOL_CODEC, json!(0), &json_ops::INSTANCE, is_error);
|
||||
|
||||
assert_decode!(
|
||||
INT_STREAM_CODEC,
|
||||
json!([1, 2, 3]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
LONG_STREAM_CODEC,
|
||||
json!([]),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
BYTE_BUFFER_CODEC,
|
||||
json!(["not a number"]),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
|
||||
assert_decode!(STRING_CODEC, json!("cool"), &json_ops::INSTANCE, is_success);
|
||||
assert_decode!(STRING_CODEC, json!(1), &json_ops::INSTANCE, is_error);
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A codec for a specific number range.
|
||||
/// - `C` is the type of codec used to serialize them (as if there was no range).
|
||||
/// - `C::Value` (the codec type) is the type of number to restrict (by providing a range), while
|
||||
pub struct RangeCodec<C: Codec + 'static>
|
||||
where
|
||||
C::Value: PartialOrd + Display + Clone,
|
||||
{
|
||||
codec: &'static C,
|
||||
min: C::Value,
|
||||
max: C::Value,
|
||||
}
|
||||
|
||||
impl<C: Codec> HasValue for RangeCodec<C>
|
||||
where
|
||||
<C as HasValue>::Value: PartialOrd + Display + Clone,
|
||||
{
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: Codec> Encoder for RangeCodec<C>
|
||||
where
|
||||
<C as HasValue>::Value: PartialOrd + Display + Clone,
|
||||
{
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
check_range(input, &self.min, &self.max).flat_map(|t| self.codec.encode(&t, ops, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> Decoder for RangeCodec<C>
|
||||
where
|
||||
<C as HasValue>::Value: PartialOrd + Display + Clone,
|
||||
{
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.codec
|
||||
.decode(input, ops)
|
||||
.flat_map(|(i, t)| check_range(&i, &self.min, &self.max).map(|n| (n, t)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper function to check whether a number is between the range `[min, max]` (both inclusive).
|
||||
fn check_range<T: PartialOrd + Display + Clone>(input: &T, min: &T, max: &T) -> DataResult<T> {
|
||||
if input >= min && input <= max {
|
||||
DataResult::new_success(input.clone())
|
||||
} else {
|
||||
DataResult::new_error(format!("Value {input} is outside range [{min}, {max}]"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn new_range_codec<A: Display + PartialOrd + Clone, C: Codec<Value = A>>(
|
||||
codec: &'static C,
|
||||
min: A,
|
||||
max: A,
|
||||
) -> RangeCodec<C> {
|
||||
RangeCodec { codec, min, max }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::*;
|
||||
use crate::coders::*;
|
||||
use crate::json_ops;
|
||||
use crate::{assert_decode, assert_success};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
{
|
||||
// A codec that does not allow negative numbers.
|
||||
pub static NON_NEGATIVE_INT_CODEC: IntRangeCodec = int_range(0, i32::MAX);
|
||||
|
||||
assert_success!(
|
||||
NON_NEGATIVE_INT_CODEC.encode_start(&3, &json_ops::INSTANCE),
|
||||
json!(3)
|
||||
);
|
||||
assert_success!(
|
||||
NON_NEGATIVE_INT_CODEC.encode_start(&6745, &json_ops::INSTANCE),
|
||||
json!(6745)
|
||||
);
|
||||
assert_success!(
|
||||
NON_NEGATIVE_INT_CODEC.encode_start(&0, &json_ops::INSTANCE),
|
||||
json!(0)
|
||||
);
|
||||
assert!(
|
||||
NON_NEGATIVE_INT_CODEC
|
||||
.encode_start(&-93, &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
// A codec accepting a double value from 0 to 100.
|
||||
pub static PERCENTAGE_CODEC: DoubleRangeCodec = double_range(0.0, 100.0);
|
||||
|
||||
assert!(
|
||||
PERCENTAGE_CODEC
|
||||
.encode_start(&16.0, &json_ops::INSTANCE)
|
||||
.is_success()
|
||||
);
|
||||
assert!(
|
||||
PERCENTAGE_CODEC
|
||||
.encode_start(&45.5, &json_ops::INSTANCE)
|
||||
.is_success()
|
||||
);
|
||||
assert!(
|
||||
PERCENTAGE_CODEC
|
||||
.encode_start(&99.999, &json_ops::INSTANCE)
|
||||
.is_success()
|
||||
);
|
||||
assert!(
|
||||
PERCENTAGE_CODEC
|
||||
.encode_start(&134.4, &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
assert_decode!(int_range(1, 5), json!(3), &json_ops::INSTANCE, is_success);
|
||||
assert_decode!(int_range(-5, 5), json!(6), &json_ops::INSTANCE, is_error);
|
||||
|
||||
assert_decode!(
|
||||
double_range(-100.0, 100.0),
|
||||
json!(45.5),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
double_range(-100.0, 100.0),
|
||||
json!(-100),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
double_range(1.0, f64::MAX),
|
||||
json!(88.44),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
double_range(1.0, f64::MAX),
|
||||
json!(0.999),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
float_range(0.04, 0.08),
|
||||
json!(0.05),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
float_range(0.006, 0.012),
|
||||
json!(0.013),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::base_map_codec::BaseMapCodec;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::lifecycle::Lifecycle;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A type of [`Codec`] for a map with no known list of keys.
|
||||
pub struct UnboundedMapCodec<K: Codec + 'static, V: Codec + 'static>
|
||||
where
|
||||
K::Value: Display + Eq + Hash,
|
||||
{
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec> BaseMapCodec for UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
type Key = K::Value;
|
||||
type KeyCodec = K;
|
||||
type Element = V::Value;
|
||||
type ElementCodec = V;
|
||||
|
||||
fn key_codec(&self) -> &'static Self::KeyCodec {
|
||||
self.key_codec
|
||||
}
|
||||
|
||||
fn element_codec(&self) -> &'static Self::ElementCodec {
|
||||
self.element_codec
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec> HasValue for UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
type Value = HashMap<K::Value, V::Value>;
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec> Encoder for UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
BaseMapCodec::encode(self, input, ops, ops.map_builder()).build(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec> Decoder for UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
ops.get_map(&input)
|
||||
.with_lifecycle(Lifecycle::Stable)
|
||||
.flat_map(|map| BaseMapCodec::decode(self, &map, ops))
|
||||
.map(|r| (r, input))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`UnboundedMapCodec`].
|
||||
pub(crate) const fn new_unbounded_map_codec<K: Codec, V: Codec>(
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
) -> UnboundedMapCodec<K, V>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
UnboundedMapCodec {
|
||||
key_codec,
|
||||
element_codec,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::assert_decode;
|
||||
use crate::codec::*;
|
||||
use crate::codecs::primitive::{BoolCodec, IntCodec, StringCodec};
|
||||
use crate::codecs::unbounded_map::UnboundedMapCodec;
|
||||
use crate::codecs::validated::ValidatedCodec;
|
||||
use crate::coders::Decoder;
|
||||
use crate::coders::Encoder;
|
||||
use crate::json_ops;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn simple_encoding() {
|
||||
pub static SCORES_CODEC: UnboundedMapCodec<StringCodec, IntCodec> =
|
||||
unbounded_map(&STRING_CODEC, &INT_CODEC);
|
||||
|
||||
let mut map = HashMap::<String, i32>::new();
|
||||
|
||||
map.insert("Amy".to_string(), 10);
|
||||
map.insert("Leo".to_string(), 24);
|
||||
map.insert("Patrick".to_string(), -65);
|
||||
|
||||
assert_eq!(
|
||||
SCORES_CODEC
|
||||
.encode_start(&map, &json_ops::INSTANCE)
|
||||
.expect("Encoding scores failed"),
|
||||
json!({"Amy": 10, "Leo": 24, "Patrick": -65})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_key_encoding() {
|
||||
// A basic implementation to check if a number is prime.
|
||||
fn is_prime(number: u32) -> bool {
|
||||
if number < 2 {
|
||||
return false;
|
||||
}
|
||||
for i in 2..number {
|
||||
if number.is_multiple_of(i) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// A codec to store whether a number is prime or not.
|
||||
// We use a transformer to keep the keys in a string form even while working with `u32` keys.
|
||||
pub static PRIME_MAP_CODEC: UnboundedMapCodec<XmapCodec<u32, StringCodec>, BoolCodec> =
|
||||
unbounded_map(
|
||||
&xmap(
|
||||
&STRING_CODEC,
|
||||
|s| s.parse().expect("Could not parse String"),
|
||||
|u: &u32| u.to_string(),
|
||||
),
|
||||
&BOOL_CODEC,
|
||||
);
|
||||
|
||||
let mut map = HashMap::<u32, bool>::new();
|
||||
|
||||
// Calculate the map for the first 20 numbers.
|
||||
for i in 1..=20 {
|
||||
map.insert(i, is_prime(i));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
PRIME_MAP_CODEC
|
||||
.encode_start(&map, &json_ops::INSTANCE)
|
||||
.expect("Encoding prime map failed"),
|
||||
json!({
|
||||
"1": false, "2": true, "3": true, "4": false, "5": true, "6": false, "7": true, "8": false, "9": false, "10": false,
|
||||
"11": true, "12": false, "13": true, "14": false, "15": false, "16": false, "17": true, "18": false, "19": true, "20": false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding() {
|
||||
// A codec storing a frequency for each letter.
|
||||
// Each key must only be 1 character long (to make it a letter).
|
||||
// There must be at least 1 key.
|
||||
pub static LETTER_FREQUENCY_CODEC: ValidatedCodec<
|
||||
UnboundedMapCodec<ValidatedCodec<StringCodec>, UlongCodec>,
|
||||
> = validate(
|
||||
&unbounded_map(
|
||||
&validate(&STRING_CODEC, |s| {
|
||||
if s.len() == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("String must be exactly 1 character long".to_string())
|
||||
}
|
||||
}),
|
||||
&ULONG_CODEC,
|
||||
),
|
||||
|m| {
|
||||
if m.is_empty() {
|
||||
Err("Map must not be empty".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
LETTER_FREQUENCY_CODEC,
|
||||
json!({"a": 13, "c": 34, "x": 1, "e": 21}),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
LETTER_FREQUENCY_CODEC,
|
||||
json!({"b": 45, "w": 10, "l": 90, "word": 5}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
LETTER_FREQUENCY_CODEC,
|
||||
json!({}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A validator codec that validates any values before encoding and after decoding.
|
||||
pub struct ValidatedCodec<C: Codec + 'static> {
|
||||
codec: &'static C,
|
||||
/// The validator function used.
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
}
|
||||
|
||||
impl<C: Codec> HasValue for ValidatedCodec<C> {
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: Codec> Encoder for ValidatedCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
(self.validator)(input).map_or_else(
|
||||
|error| DataResult::new_error(error),
|
||||
|()| self.codec.encode(input, ops, prefix),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> Decoder for ValidatedCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.codec.decode(input, ops).flat_map(|decoded| {
|
||||
(self.validator)(&decoded.0)
|
||||
.map_or_else(DataResult::new_error, |()| DataResult::new_success(decoded))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`ValidatedCodec`].
|
||||
pub(crate) const fn new_validated_codec<C: Codec>(
|
||||
codec: &'static C,
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
) -> ValidatedCodec<C> {
|
||||
ValidatedCodec { codec, validator }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::assert_decode;
|
||||
use crate::codec::*;
|
||||
use crate::codecs::primitive::{IntCodec, StringCodec};
|
||||
use crate::codecs::validated::ValidatedCodec;
|
||||
use crate::coders::Decoder;
|
||||
use crate::coders::Encoder;
|
||||
use crate::json_ops;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn even_int_validation() {
|
||||
// An `int` codec that only accepts even numbers.
|
||||
pub static EVEN_INT_CODEC: ValidatedCodec<IntCodec> = validate(&INT_CODEC, |value| {
|
||||
if value % 2 == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from("Not an even number"))
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
EVEN_INT_CODEC
|
||||
.encode_start(&2, &json_ops::INSTANCE)
|
||||
.expect("Encoding panicked"),
|
||||
json!(2)
|
||||
);
|
||||
assert_eq!(
|
||||
EVEN_INT_CODEC
|
||||
.encode_start(&-56, &json_ops::INSTANCE)
|
||||
.expect("Encoding panicked"),
|
||||
json!(-56)
|
||||
);
|
||||
assert!(
|
||||
EVEN_INT_CODEC
|
||||
.encode_start(&-135, &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
|
||||
assert_decode!(EVEN_INT_CODEC, json!(0), &json_ops::INSTANCE, is_success);
|
||||
assert_decode!(EVEN_INT_CODEC, json!(3456), &json_ops::INSTANCE, is_success);
|
||||
assert_decode!(EVEN_INT_CODEC, json!(-12345), &json_ops::INSTANCE, is_error);
|
||||
assert_decode!(EVEN_INT_CODEC, json!(153453), &json_ops::INSTANCE, is_error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_name_validation() {
|
||||
// A codec of a Minecraft player name, which has the following rules:
|
||||
// - The length must be between 3-16 characters long.
|
||||
// - They must only have alphanumeric characters and underscores.
|
||||
pub static PLAYER_NAME_CODEC: ValidatedCodec<StringCodec> = validate(&STRING_CODEC, |s| {
|
||||
if !(3..=16).contains(&s.len()) {
|
||||
return Err(String::from(
|
||||
"Player name must be between 3-16 characters long (inclusive)",
|
||||
));
|
||||
}
|
||||
if !s.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||
return Err(String::from(
|
||||
"Player name must only contain alphanumeric characters and underscores",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
assert!(
|
||||
PLAYER_NAME_CODEC
|
||||
.encode_start(&String::from("Player"), &json_ops::INSTANCE)
|
||||
.is_success()
|
||||
);
|
||||
assert!(
|
||||
PLAYER_NAME_CODEC
|
||||
.encode_start(&String::from("abcd1234"), &json_ops::INSTANCE)
|
||||
.is_success()
|
||||
);
|
||||
assert!(
|
||||
PLAYER_NAME_CODEC
|
||||
.encode_start(&String::from("has some spaces"), &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
assert!(
|
||||
PLAYER_NAME_CODEC
|
||||
.encode_start(&String::from("XxXxVeryLongNamexXxX"), &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
assert!(
|
||||
PLAYER_NAME_CODEC
|
||||
.encode_start(&String::from("ILovePizza$"), &json_ops::INSTANCE)
|
||||
.is_error()
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
PLAYER_NAME_CODEC,
|
||||
json!("Pumpkin"),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
PLAYER_NAME_CODEC,
|
||||
json!("IGoByNoNames__"),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
assert_decode!(
|
||||
PLAYER_NAME_CODEC,
|
||||
json!("#idk"),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::map_codecs::field_coders::{FieldDecoder, FieldEncoder};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A trait describing the way to encode something of a type `Value` into something else (`Value -> ?`).
|
||||
pub trait Encoder: HasValue {
|
||||
/// Encodes an input of this encoder's type (`A`) into an output of type `T`,
|
||||
/// along with the `prefix` (already encoded data).
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T>;
|
||||
|
||||
/// Encodes an input of this encoder's type (`A`) into an output of type `T`
|
||||
/// with no prefix (no already encoded data).
|
||||
fn encode_start<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<T> {
|
||||
self.encode(input, ops, ops.empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComappedEncoderImpl<B, E: Encoder + 'static> {
|
||||
encoder: &'static E,
|
||||
function: fn(&B) -> E::Value,
|
||||
}
|
||||
|
||||
impl<B, E: Encoder> HasValue for ComappedEncoderImpl<B, E> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, E: Encoder> Encoder for ComappedEncoderImpl<B, E> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
self.encoder.encode(&(self.function)(input), ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *contramapped* (*comapped*) transformation of a provided [`Encoder`].
|
||||
/// A *comapped* encoder transforms the input before encoding.
|
||||
pub(crate) const fn comap<B, E: Encoder>(
|
||||
encoder: &'static E,
|
||||
f: fn(&B) -> E::Value,
|
||||
) -> ComappedEncoderImpl<B, E> {
|
||||
ComappedEncoderImpl {
|
||||
encoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FlatComappedEncoderImpl<B, E: Encoder + 'static> {
|
||||
encoder: &'static E,
|
||||
function: fn(&B) -> DataResult<E::Value>,
|
||||
}
|
||||
|
||||
impl<B, E: Encoder> HasValue for FlatComappedEncoderImpl<B, E> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, E: Encoder> Encoder for FlatComappedEncoderImpl<B, E> {
|
||||
fn encode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: T,
|
||||
) -> DataResult<T> {
|
||||
(self.function)(input).flat_map(|a| self.encoder.encode(&a, ops, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *flat contramapped* (*flat-comapped*) transformation of a provided [`Encoder`].
|
||||
/// A *flat comapped* encoder transforms the input before encoding, but the transformation can fail.
|
||||
pub(crate) const fn flat_comap<B, E: Encoder>(
|
||||
encoder: &'static E,
|
||||
f: fn(&B) -> DataResult<E::Value>,
|
||||
) -> FlatComappedEncoderImpl<B, E> {
|
||||
FlatComappedEncoderImpl {
|
||||
encoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn encoder_field<A, E: Encoder<Value = A>>(
|
||||
name: &'static str,
|
||||
encoder: &'static E,
|
||||
) -> FieldEncoder<A, E> {
|
||||
FieldEncoder::new(name, encoder)
|
||||
}
|
||||
|
||||
/// A trait describing the way to decode something of some type to something of type `Value` (`? -> Value`).
|
||||
pub trait Decoder: HasValue {
|
||||
/// Decodes an input of this decoder's type (`A`) into an output of type `T`,
|
||||
/// keeping the remaining undecoded data as another element of the tuple.
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)>;
|
||||
|
||||
/// Decodes an input of this decoder's type (`A`) into an output of type `T`,
|
||||
/// discarding any remaining undecoded data.
|
||||
fn parse<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.decode(input, ops).map(|r| r.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MappedDecoderImpl<B, D: Decoder + 'static> {
|
||||
decoder: &'static D,
|
||||
function: fn(D::Value) -> B,
|
||||
}
|
||||
|
||||
impl<B, D: Decoder> HasValue for MappedDecoderImpl<B, D> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, D: Decoder> Decoder for MappedDecoderImpl<B, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.decoder
|
||||
.decode(input, ops)
|
||||
.map(|(a, t)| ((self.function)(a), t))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *covariant mapped* transformation of a provided [`Decoder`].
|
||||
/// A *mapped* decoder transforms the output after decoding.
|
||||
pub(crate) const fn map<B, D: Decoder>(
|
||||
decoder: &'static D,
|
||||
f: fn(D::Value) -> B,
|
||||
) -> MappedDecoderImpl<B, D> {
|
||||
MappedDecoderImpl {
|
||||
decoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FlatMappedDecoderImpl<B, D: Decoder + 'static> {
|
||||
decoder: &'static D,
|
||||
function: fn(D::Value) -> DataResult<B>,
|
||||
}
|
||||
|
||||
impl<B, D: Decoder> HasValue for FlatMappedDecoderImpl<B, D> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, D: Decoder> Decoder for FlatMappedDecoderImpl<B, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<(Self::Value, T)> {
|
||||
self.decoder
|
||||
.decode(input, ops)
|
||||
.flat_map(|(a, t)| (self.function)(a).map(|b| (b, t)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *covariant flat-mapped* transformation of a provided [`Decoder`].
|
||||
/// A *flat-mapped* decoder transforms the output after decoding, but the transformation can fail.
|
||||
pub(crate) const fn flat_map<B, D: Decoder>(
|
||||
decoder: &'static D,
|
||||
f: fn(D::Value) -> DataResult<B>,
|
||||
) -> FlatMappedDecoderImpl<B, D> {
|
||||
FlatMappedDecoderImpl {
|
||||
decoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn decoder_field<A, D: Decoder<Value = A>>(
|
||||
name: &'static str,
|
||||
decoder: &'static D,
|
||||
) -> FieldDecoder<A, D> {
|
||||
FieldDecoder::new(name, decoder)
|
||||
}
|
||||
@@ -628,8 +628,78 @@ macro_rules! assert_success {
|
||||
}};
|
||||
}
|
||||
|
||||
/// Asserts that encoding the left expression will lead to a complete result (success) whose stored result is `$right`.
|
||||
#[macro_export]
|
||||
macro_rules! assert_encode_success {
|
||||
($left:expr, $ops:expr, $right:expr $(,)?) => {{
|
||||
let result = $crate::codec::Encode::encode_start(&$left, &$ops);
|
||||
assert!(
|
||||
result.is_success(),
|
||||
"Expected a `DataResult` success, got: {:?}",
|
||||
result
|
||||
);
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
$right,
|
||||
"`DataResult` was successful but the value doesn't match"
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Asserts that decoding the left expression will lead to a `DataResult` whose provided method returns `true`.
|
||||
#[macro_export]
|
||||
macro_rules! assert_decode {
|
||||
($ty:ty, $input:expr, $ops:expr, $func:ident $(,)?) => {{
|
||||
let result = <$ty as $crate::codec::Decode>::parse($input, &$ops);
|
||||
assert!(
|
||||
result.$func(),
|
||||
concat!(
|
||||
"Expected a `DataResult` that returns `true` for ",
|
||||
stringify!($func),
|
||||
", got: {:?}"
|
||||
),
|
||||
result
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
impl<T> Default for DataResult<T> {
|
||||
fn default() -> Self {
|
||||
Self::new_error("Default DataResult")
|
||||
}
|
||||
}
|
||||
|
||||
/// A type conversion from one type to this type that may fail, resulting in a [`DataResult`].
|
||||
///
|
||||
/// Always prefer using [`FlatTryFrom`] over [`FlatTryInto`] for implementing the conversion,
|
||||
/// as an implementation of [`FlatTryInto`] will automatically work as well.
|
||||
pub trait FlatTryFrom<T>: Sized {
|
||||
/// Performs the conversion.
|
||||
fn flat_try_from(value: T) -> DataResult<Self>;
|
||||
}
|
||||
|
||||
impl<T> FlatTryFrom<T> for T {
|
||||
fn flat_try_from(value: T) -> DataResult<Self> {
|
||||
DataResult::new_success(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> FlatTryInto<U> for T
|
||||
where
|
||||
U: FlatTryFrom<T>,
|
||||
{
|
||||
#[inline]
|
||||
/// Calls `U::flat_try_from()`, which performs the conversion.
|
||||
fn flat_try_into(self) -> DataResult<U> {
|
||||
U::flat_try_from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type conversion from this type to another that may fail, resulting in a [`DataResult`].
|
||||
///
|
||||
/// Always prefer using [`FlatTryFrom`] over [`FlatTryInto`] for implementing the conversion,
|
||||
/// as an implementation of [`FlatTryInto`] will automatically work as well.
|
||||
pub trait FlatTryInto<T>: Sized {
|
||||
/// Performs the conversion.
|
||||
fn flat_try_into(self) -> DataResult<T>;
|
||||
}
|
||||
|
||||
@@ -248,11 +248,6 @@ pub trait DynamicOps {
|
||||
/// This returns the new value if successful, otherwise, this returns itself.
|
||||
fn remove(&self, input: Self::Value, key: &str) -> Self::Value;
|
||||
|
||||
/// Whether maps should be compressed under this `DynamicOps`.
|
||||
fn compress_maps(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Tries to get a value from a value represented by this `DynamicOps` using a key.
|
||||
/// Only works for values that can be [`MapLike`]-viewed.
|
||||
fn get_element<'a>(&'a self, input: &'a Self::Value, key: &str) -> DataResult<&'a Self::Value> {
|
||||
|
||||
@@ -9,18 +9,7 @@ use serde_json::{Map, Value};
|
||||
use tracing::warn;
|
||||
|
||||
/// A [`DynamicOps`] to serialize to/deserialize from JSON data.
|
||||
pub struct JsonOps {
|
||||
compressed: bool,
|
||||
}
|
||||
|
||||
/// A normal instance of [`JsonOps`], which serializes/deserializes normal JSON data.
|
||||
pub static INSTANCE: JsonOps = JsonOps { compressed: false };
|
||||
|
||||
/// A normal instance of [`JsonOps`], which serializes/deserializes compressed JSON data.
|
||||
///
|
||||
/// *Compressed* JSON data is a little more lenient with placing values at places that expect something else.
|
||||
/// This allows JSON to be compressed to a single string.
|
||||
pub static COMPRESSED: JsonOps = JsonOps { compressed: true };
|
||||
pub struct JsonOps;
|
||||
|
||||
impl JsonOps {
|
||||
/// A function to get a JSON value as a string, similar to Google's GSON's `getAsString()` method for `JsonElement`.
|
||||
@@ -53,14 +42,8 @@ impl JsonOps {
|
||||
/// Whether a JSON value is considered to be a valid key.
|
||||
///
|
||||
/// If this returns `true`, it is safe to say that calling [`get_as_string`] with `input` will always return a [`Some`].
|
||||
const fn is_valid_key(&self, input: &Value) -> bool {
|
||||
// Normal mode: has to be a string.
|
||||
// Compressed mode: can be any JSON primitive.
|
||||
if self.compressed {
|
||||
matches!(input, Value::String(_) | Value::Number(_) | Value::Bool(_))
|
||||
} else {
|
||||
matches!(input, Value::String(_))
|
||||
}
|
||||
const fn is_valid_key(input: &Value) -> bool {
|
||||
matches!(input, Value::String(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,35 +95,19 @@ impl DynamicOps for JsonOps {
|
||||
}
|
||||
|
||||
fn get_number(&self, input: &Self::Value) -> DataResult<Number> {
|
||||
match input {
|
||||
Value::Number(_) => {
|
||||
return input.try_into().map_or_else(
|
||||
|_| DataResult::new_error(format!("Not a number: {input}")),
|
||||
DataResult::new_success,
|
||||
);
|
||||
}
|
||||
Value::String(string) if self.compressed => {
|
||||
if let Ok(i) = string.parse::<i32>() {
|
||||
return DataResult::new_success(Number::Int(i));
|
||||
}
|
||||
if let Ok(l) = string.parse::<i64>() {
|
||||
return DataResult::new_success(Number::Long(l));
|
||||
}
|
||||
if let Ok(d) = string.parse::<f64>() {
|
||||
return DataResult::new_success(Number::Double(d));
|
||||
}
|
||||
return DataResult::new_error(format!("Number could not be parsed: {string}"));
|
||||
}
|
||||
_ => {}
|
||||
if let Value::Number(_) = input {
|
||||
input.try_into().map_or_else(
|
||||
|_| DataResult::new_error(format!("Not a number: {input}")),
|
||||
DataResult::new_success,
|
||||
)
|
||||
} else {
|
||||
DataResult::new_error(format!("Not a number: {input}"))
|
||||
}
|
||||
DataResult::new_error(format!("Not a number: {input}"))
|
||||
}
|
||||
|
||||
fn get_string(&self, input: &Self::Value) -> DataResult<String> {
|
||||
if matches!(input, Value::String(_))
|
||||
|| (matches!(input, Value::Number(_)) && self.compressed)
|
||||
{
|
||||
// Unwrapping is fine as only strings and numbers are possible here.
|
||||
if let Value::String(_) = input {
|
||||
// Unwrapping is fine as only strings are possible here.
|
||||
DataResult::new_success(Self::get_as_string(input).unwrap())
|
||||
} else {
|
||||
DataResult::new_error(format!("Not a string: {input}"))
|
||||
@@ -224,7 +191,7 @@ impl DynamicOps for JsonOps {
|
||||
return DataResult::new_partial_error(format!("Not a map: {map}"), map);
|
||||
}
|
||||
|
||||
if !self.is_valid_key(&key) {
|
||||
if !Self::is_valid_key(&key) {
|
||||
return DataResult::new_partial_error(format!("Key is not a string: {key}"), map);
|
||||
}
|
||||
|
||||
@@ -258,7 +225,7 @@ impl DynamicOps for JsonOps {
|
||||
let mut missed = vec![];
|
||||
|
||||
for entry in other_map_like.iter() {
|
||||
if self.is_valid_key(&entry.0) {
|
||||
if Self::is_valid_key(&entry.0) {
|
||||
output_map.insert(Self::get_as_string(&entry.0).unwrap(), entry.1.clone());
|
||||
} else {
|
||||
missed.push(entry.0);
|
||||
@@ -291,10 +258,6 @@ impl DynamicOps for JsonOps {
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_maps(&self) -> bool {
|
||||
self.compressed
|
||||
}
|
||||
|
||||
fn convert_to<U>(&self, out_ops: &impl DynamicOps<Value = U>, input: Self::Value) -> U {
|
||||
match input {
|
||||
Value::Null => out_ops.empty(),
|
||||
@@ -407,7 +370,7 @@ impl StructBuilder for JsonStructBuilder {
|
||||
type Value = Value;
|
||||
|
||||
impl_struct_builder!(builder);
|
||||
impl_string_struct_builder!(builder, INSTANCE);
|
||||
impl_string_struct_builder!(builder, JsonOps);
|
||||
}
|
||||
|
||||
impl StringStructBuilder for JsonStructBuilder {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use dashmap::DashMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
/// A cache for all [`crate::map_coders::CompressorHolder`] structs.
|
||||
///
|
||||
/// This `HashMap` stores a `KeyCompressor` for each `MapCodec` instance.
|
||||
/// This way, we don't have to use `OnceLock` in every `MapCodec`, so we can easily
|
||||
/// capture their pointers while calling other functions without any destructor
|
||||
/// compile-time errors.
|
||||
pub(crate) static KEY_COMPRESSOR_CACHE: LazyLock<DashMap<usize, Arc<KeyCompressor>>> =
|
||||
LazyLock::new(DashMap::new);
|
||||
|
||||
/// A struct to compress keys of a map by converting them to numbers (making a kind of list) and back.
|
||||
pub struct KeyCompressor {
|
||||
compress_map: HashMap<String, usize>,
|
||||
decompress_map: HashMap<usize, String>,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl KeyCompressor {
|
||||
/// Returns a new `KeyCompressor`, which can be populated later via [`KeyCompressor::populate`].
|
||||
///
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
compress_map: HashMap::new(),
|
||||
decompress_map: HashMap::new(),
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Populates a `KeyCompressor` with the calculated compressor and decompressor maps.
|
||||
pub(crate) fn populate(&mut self, keys: impl IntoIterator<Item = String>) {
|
||||
// Iterate over every key.
|
||||
keys.into_iter().for_each(|key: String| {
|
||||
if self.compress_map.contains_key(&key) {
|
||||
return;
|
||||
}
|
||||
// The index that the key will correspond to.
|
||||
let i = self.size;
|
||||
self.compress_map.insert(key.clone(), i);
|
||||
self.decompress_map.insert(i, key);
|
||||
|
||||
self.size += 1;
|
||||
});
|
||||
}
|
||||
|
||||
/// Gets the decompressed key of an index with the provided dynamic type.
|
||||
pub fn decompress_key<T>(
|
||||
&self,
|
||||
key: usize,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> Option<T> {
|
||||
self.decompress_map.get(&key).map(|s| ops.create_string(s))
|
||||
}
|
||||
|
||||
/// Gets the compressed key of the provided dynamic type.
|
||||
pub fn compress_key<T>(
|
||||
&self,
|
||||
key: &T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> Option<usize> {
|
||||
let string = ops.get_string(key).into_result()?;
|
||||
self.compress_key_str(&string)
|
||||
}
|
||||
|
||||
/// Gets the compressed key of a string value.
|
||||
pub(crate) fn compress_key_str(&self, key: &str) -> Option<usize> {
|
||||
self.compress_map.get(key).copied()
|
||||
}
|
||||
|
||||
/// Returns the size of the compressed/decompressed maps.
|
||||
#[must_use]
|
||||
pub const fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/// A trait that specifies that an object can be represented with keys, like maps or `struct` types.
|
||||
pub trait Keyable {
|
||||
/// Returns a new copy of a [`Vec`] of the keys of this `Keyable`.
|
||||
#[must_use]
|
||||
fn keys(&self) -> Vec<String>;
|
||||
}
|
||||
@@ -1,175 +1,26 @@
|
||||
extern crate core;
|
||||
|
||||
use core::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub mod base_map_codec;
|
||||
pub mod codec;
|
||||
pub mod codecs;
|
||||
pub mod coders;
|
||||
pub mod data_result;
|
||||
pub mod dynamic_ops;
|
||||
mod data_result;
|
||||
mod dynamic_ops;
|
||||
pub mod json_ops;
|
||||
pub mod key_compressor;
|
||||
pub mod keyable;
|
||||
pub mod lifecycle;
|
||||
pub mod list_builder;
|
||||
pub mod map_codec;
|
||||
pub mod map_codecs;
|
||||
pub mod map_coders;
|
||||
pub mod map_like;
|
||||
mod lifecycle;
|
||||
mod list_builder;
|
||||
mod map_like;
|
||||
pub mod struct_builder;
|
||||
pub mod struct_codecs;
|
||||
|
||||
/// A trait specifying a single type.
|
||||
/// This is used to prevent type conflicts for `Codec`s and `MapCodec`s implementing an encoder and decoder.
|
||||
pub trait HasValue {
|
||||
type Value;
|
||||
}
|
||||
pub mod codec;
|
||||
mod number;
|
||||
|
||||
/// Represents a generic number in Java.
|
||||
pub enum Number {
|
||||
Byte(i8),
|
||||
Short(i16),
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Float(f32),
|
||||
Double(f64),
|
||||
}
|
||||
pub use crate::data_result::DataResult;
|
||||
pub use crate::data_result::FlatTryFrom;
|
||||
pub use crate::data_result::FlatTryInto;
|
||||
pub use crate::dynamic_ops::DynamicOps;
|
||||
pub use crate::lifecycle::Lifecycle;
|
||||
pub use crate::list_builder::ListBuilder;
|
||||
pub use crate::map_like::MapLike;
|
||||
pub use number::Number;
|
||||
|
||||
impl From<Number> for i64 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use crate::codec::Decode;
|
||||
pub use crate::codec::Encode;
|
||||
|
||||
impl From<Number> for i32 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for i16 {
|
||||
fn from(num: Number) -> Self {
|
||||
// Similar to Java, we will first convert the number to an `i16`, and then to an `i8`.
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for i8 {
|
||||
fn from(num: Number) -> Self {
|
||||
// Similar to Java, we will first convert the number to an `i32`, and then to an `i8`.
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for u8 {
|
||||
fn from(num: Number) -> Self {
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for f32 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for f64 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Number {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Byte(v) => write!(f, "{v}"),
|
||||
Self::Short(v) => write!(f, "{v}"),
|
||||
Self::Int(v) => write!(f, "{v}"),
|
||||
Self::Long(v) => write!(f, "{v}"),
|
||||
Self::Float(v) => write!(f, "{v}"),
|
||||
Self::Double(v) => write!(f, "{v}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for serde_json::Value {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(n) => n.into(),
|
||||
Number::Short(n) => n.into(),
|
||||
Number::Int(n) => n.into(),
|
||||
Number::Long(n) => n.into(),
|
||||
Number::Float(n) => n.into(),
|
||||
Number::Double(n) => n.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error struct returned for an invalid conversion to [`Number`] from a [`serde_json::Value`].
|
||||
pub struct FromJsonValueError;
|
||||
|
||||
impl TryFrom<&serde_json::Value> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: &serde_json::Value) -> Result<Self, Self::Error> {
|
||||
num.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<serde_json::Value> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: serde_json::Value) -> Result<Self, Self::Error> {
|
||||
match num {
|
||||
serde_json::Value::Number(n) => n.try_into(),
|
||||
_ => Err(FromJsonValueError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<serde_json::Number> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: serde_json::Number) -> Result<Self, Self::Error> {
|
||||
// Try converting the number to an integer first.
|
||||
num.as_i64().map_or_else(
|
||||
// Try the float conversion.
|
||||
|| {
|
||||
num.as_f64()
|
||||
.map_or(Err(FromJsonValueError), |f| Ok(Self::Double(f)))
|
||||
},
|
||||
// Do the integer conversion.
|
||||
|n| Ok(Self::Long(n)),
|
||||
)
|
||||
}
|
||||
}
|
||||
pub use crate::codec::primitive::ByteBuffer;
|
||||
pub use crate::codec::primitive::IntStream;
|
||||
pub use crate::codec::primitive::LongStream;
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::lifecycle::Lifecycle;
|
||||
use crate::map_codecs::either::{EitherMapCodec, new_either_map_codec};
|
||||
use crate::map_codecs::validated::{ValidatedMapCodec, new_validated_map_codec};
|
||||
use crate::map_coders::{
|
||||
ComappedMapEncoderImpl, CompressorHolder, FlatComappedMapEncoderImpl, FlatMappedMapDecoderImpl,
|
||||
MapDecoder, MapEncoder, MappedMapDecoderImpl, comap, flat_comap, flat_map, map,
|
||||
};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use crate::struct_codecs::Field;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A type of *codec* which encodes/decodes fields of a map.
|
||||
///
|
||||
/// The number of keys a `MapCodec` can work with can be one or many keys.
|
||||
///
|
||||
/// **This is functionally different from [`Codec`].**
|
||||
/// The main difference is that while a `Codec` works on encoding/decoding values, a `MapCodec`
|
||||
/// works on a [`MapLike`].
|
||||
///
|
||||
/// # Using Map Codecs
|
||||
/// They can be used in struct codecs as one part of a struct.
|
||||
/// **Just like codecs, map codecs are also meant to be static instances, and they should not be created at runtime.
|
||||
/// They are also immutable, which means they cannot be modified after they are created.**
|
||||
///
|
||||
/// # Creating Map Codecs
|
||||
/// There are a few ways to create map codecs.
|
||||
///
|
||||
/// ## Field Map Codecs
|
||||
/// These are the most commonly used map codecs. The `codec` module has methods for creating them with a `Codec` instance:
|
||||
/// - [`field`]: For required fields.
|
||||
/// - [`optional_field`] and [`lenient_optional_field`]: For optional fields encoding/decoding an [`Option`] type.
|
||||
/// - [`optional_field_with_default`] and [`lenient_optional_field_with_default`]:
|
||||
/// For optional fields which have a default value for when no value is found while decoding.
|
||||
///
|
||||
/// ## Either
|
||||
/// Use [`either`] to create an [`EitherMapCodec`] that can use one of two provided codecs to serialize/deserialize
|
||||
/// an [`Either`].
|
||||
///
|
||||
/// # Transformers
|
||||
/// A map codec of a type `B` can be implemented by *transforming* another codec of type `A` to work with type `B`,
|
||||
/// similar to a `Codec`.
|
||||
/// The following methods can be used depending on the equivalence relation between the two types:
|
||||
/// - [`xmap`]
|
||||
/// - [`flat_xmap`]
|
||||
///
|
||||
/// # Validator Map Codecs
|
||||
/// The [`validate`] function returns a codec wrapper that validates a value before encoding and after decoding.
|
||||
/// A validated codec takes a function that can either return an [`Ok`] for a success,
|
||||
/// or an [`Err`] with the provided message to place in a `DataResult`.
|
||||
///
|
||||
/// [`Codec`]: super::codec::Codec
|
||||
/// [`field`]: super::codec::field
|
||||
/// [`optional_field`]: super::codec::optional_field
|
||||
/// [`lenient_optional_field`]: super::codec::lenient_optional_field
|
||||
/// [`optional_field_with_default`]: super::codec::optional_field_with_default
|
||||
/// [`lenient_optional_field_with_default`]: super::codec::lenient_optional_field_with_default
|
||||
///
|
||||
/// [`Either`]: crate::util::either::Either
|
||||
pub trait MapCodec: MapEncoder + MapDecoder {}
|
||||
|
||||
// Any struct implementing MapEncoder<Value = A> and MapDecoder<Value = A> will also implement MapCodec<Value = A>.
|
||||
impl<T> MapCodec for T where T: MapEncoder + MapDecoder {}
|
||||
|
||||
/// A map codec allowing an arbitrary encoder and decoder.
|
||||
pub struct ComposedMapCodec<E: MapEncoder + 'static, D: MapDecoder<Value = E::Value> + 'static> {
|
||||
pub(crate) encoder: E,
|
||||
pub(crate) decoder: D,
|
||||
}
|
||||
|
||||
impl<E: MapEncoder, D: MapDecoder<Value = E::Value>> HasValue for ComposedMapCodec<E, D> {
|
||||
type Value = E::Value;
|
||||
}
|
||||
|
||||
impl<E: MapEncoder, D: MapDecoder<Value = E::Value>> Keyable for ComposedMapCodec<E, D> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
let mut vec = self.encoder.keys();
|
||||
vec.extend(self.decoder.keys());
|
||||
vec
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: MapEncoder, D: MapDecoder<Value = E::Value>> CompressorHolder for ComposedMapCodec<E, D> {
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
// This could return either the encoder or decoder's compressor, but we'll stick with the encoder's.
|
||||
self.encoder.compressor()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: MapEncoder, D: MapDecoder<Value = E::Value>> MapEncoder for ComposedMapCodec<E, D> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
self.encoder.encode(input, ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: MapEncoder, D: MapDecoder<Value = E::Value>> MapDecoder for ComposedMapCodec<E, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.decoder.decode(input, ops)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a [`MapCodec`] to make its [`DataResult`]s stable.
|
||||
pub struct StableMapCodec<C: MapCodec> {
|
||||
map_codec: C,
|
||||
}
|
||||
|
||||
impl<C: MapCodec> HasValue for StableMapCodec<C> {
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: MapCodec> Keyable for StableMapCodec<C> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.map_codec.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> CompressorHolder for StableMapCodec<C> {
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
self.map_codec.compressor()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> MapEncoder for StableMapCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
self.map_codec
|
||||
.encode(input, ops, prefix)
|
||||
.set_lifecycle(Lifecycle::Stable)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> MapDecoder for StableMapCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.map_codec
|
||||
.decode(input, ops)
|
||||
.with_lifecycle(Lifecycle::Stable)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Field`] with the provided owned [`MapCodec`] and a getter,
|
||||
/// which tells the field how to get a part of a struct to serialize.
|
||||
pub const fn for_getter<T, C: MapCodec + 'static>(
|
||||
map_codec: C,
|
||||
getter: fn(&T) -> &C::Value,
|
||||
) -> Field<T, C> {
|
||||
Field::Owned(map_codec, getter)
|
||||
}
|
||||
|
||||
/// Returns a [`Field`] with the provided [`MapCodec`] reference and a getter,
|
||||
/// which tells the field how to get a part of a struct to serialize.
|
||||
pub const fn for_getter_ref<T, C: MapCodec>(
|
||||
map_codec: &'static C,
|
||||
getter: fn(&T) -> &C::Value,
|
||||
) -> Field<T, C> {
|
||||
Field::Borrowed(map_codec, getter)
|
||||
}
|
||||
|
||||
/// Returns another [`MapCodec`] of a provided `MapCodec` which provides [`DataResult`]s of the wrapped `map_codec`,
|
||||
/// but always sets their lifecycle to [`Lifecycle::Stable`].
|
||||
pub const fn stable<C: MapCodec>(map_codec: C) -> StableMapCodec<C> {
|
||||
StableMapCodec { map_codec }
|
||||
}
|
||||
|
||||
/// Helper macro to generate the shorthand types and functions of the transformer [`MapCodec`] methods.
|
||||
macro_rules! make_map_codec_transformation_function {
|
||||
($name:ident, $short_type:ident, $encoder_type:ident, $decoder_type:ident, $encoder_func:ident, $decoder_func:ident, $to_func_result:ty, $from_func_result:ty, $a_equivalency:literal, $s_equivalency:literal) => {
|
||||
pub type $short_type<S, C> = ComposedMapCodec<$encoder_type<S, C>, $decoder_type<S, C>>;
|
||||
|
||||
#[doc = "Transforms a [`MapCodec`] of type `A` to another [`MapCodec`] of type `S`."]
|
||||
///
|
||||
/// - `to` is the function called on `A` after decoding to convert it to `S`.
|
||||
/// - `from` is the function called on `S` before encoding to convert it to `A`.
|
||||
///
|
||||
/// Use this if:
|
||||
#[doc = concat!("- `A` is **", $a_equivalency, "** to `S`.")]
|
||||
#[doc = concat!("- `S` is **", $s_equivalency, "** to `A`.")]
|
||||
#[doc = ""]
|
||||
#[doc = "A type `A` is *fully equivalent* to `B` if *A can always successfully be converted to B*."]
|
||||
pub const fn $name<A, C: MapCodec<Value = A>, S>(map_codec: &'static C, to: fn(A) -> $to_func_result, from: fn(&S) -> $from_func_result) -> $short_type<S, C> {
|
||||
ComposedMapCodec {
|
||||
encoder: $encoder_func(map_codec, from),
|
||||
decoder: $decoder_func(map_codec, to)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
make_map_codec_transformation_function!(
|
||||
xmap,
|
||||
XmapMapCodec,
|
||||
ComappedMapEncoderImpl,
|
||||
MappedMapDecoderImpl,
|
||||
comap,
|
||||
map,
|
||||
S,
|
||||
A,
|
||||
"equivalent",
|
||||
"equivalent"
|
||||
);
|
||||
|
||||
make_map_codec_transformation_function!(
|
||||
flat_xmap,
|
||||
FlatXmapMapCodec,
|
||||
FlatComappedMapEncoderImpl,
|
||||
FlatMappedMapDecoderImpl,
|
||||
flat_comap,
|
||||
flat_map,
|
||||
DataResult<S>,
|
||||
DataResult<A>,
|
||||
"partially equivalent",
|
||||
"partially equivalent"
|
||||
);
|
||||
|
||||
/// Returns a transformer map codec that validates a value before encoding and after decoding by calling a function,
|
||||
/// which provides a [`DataResult`] depending on that value's validity.
|
||||
///
|
||||
/// `validator` is a function that takes the pointer of a value and returns a [`Result`].
|
||||
/// - If the returned result is an [`Ok`], the codec works as normal.
|
||||
/// - Otherwise, it always returns a non-result with the message [`String`].
|
||||
pub const fn validate<C: MapCodec>(
|
||||
codec: &'static C,
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
) -> ValidatedMapCodec<C> {
|
||||
new_validated_map_codec(codec, validator)
|
||||
}
|
||||
|
||||
/// Creates an [`EitherMapCodec`] with the provided left and right codecs to tell the way to serialize/deserialize
|
||||
/// their respective types.
|
||||
pub const fn either<L: MapCodec, R: MapCodec>(
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
) -> EitherMapCodec<L, R> {
|
||||
new_either_map_codec(left_codec, right_codec)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::impl_compressor;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_codec::MapCodec;
|
||||
use crate::map_coders::{CompressorHolder, MapDecoder, MapEncoder};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use either::Either;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A [`MapCodec`] that can serialize/deserialize one of two types, with a map codec for each one.
|
||||
///
|
||||
/// This evaluates the left map codec first, and if the [`DataResult`] for it is invalid,
|
||||
/// it evaluates the right map codec.
|
||||
pub struct EitherMapCodec<L: MapCodec + 'static, R: MapCodec + 'static> {
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
}
|
||||
|
||||
impl<L: MapCodec, R: MapCodec> HasValue for EitherMapCodec<L, R> {
|
||||
type Value = Either<L::Value, R::Value>;
|
||||
}
|
||||
|
||||
impl<L: MapCodec, R: MapCodec> Keyable for EitherMapCodec<L, R> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
let mut keys = self.left_codec.keys();
|
||||
keys.extend(self.right_codec.keys());
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: MapCodec, R: MapCodec> CompressorHolder for EitherMapCodec<L, R> {
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<L: MapCodec, R: MapCodec> MapEncoder for EitherMapCodec<L, R> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
match &input {
|
||||
Either::Left(l) => self.left_codec.encode(l, ops, prefix),
|
||||
Either::Right(r) => self.right_codec.encode(r, ops, prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: MapCodec, R: MapCodec> MapDecoder for EitherMapCodec<L, R> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
let left = self.left_codec.decode(input, ops).map(Either::Left);
|
||||
if left.is_success() {
|
||||
return left;
|
||||
}
|
||||
let right = self.right_codec.decode(input, ops).map(Either::Right);
|
||||
if right.is_success() {
|
||||
return right;
|
||||
}
|
||||
left.apply_2(|_, r| r, right)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `EitherMapCodec` with the provided left and right codecs for serializing/deserializing both possible types.
|
||||
pub(crate) const fn new_either_map_codec<L: MapCodec, R: MapCodec>(
|
||||
left_codec: &'static L,
|
||||
right_codec: &'static R,
|
||||
) -> EitherMapCodec<L, R> {
|
||||
EitherMapCodec {
|
||||
left_codec,
|
||||
right_codec,
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::impl_compressor;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_coders::{CompressorHolder, MapDecoder, MapEncoder};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A [`MapEncoder`] that knows how to encode an entire field (key + value), where the value is encoded by an [`Encoder`].
|
||||
///
|
||||
/// `A` is the type of value encoded.
|
||||
pub struct FieldEncoder<A, E: Encoder<Value = A> + 'static> {
|
||||
/// The name of the key.
|
||||
name: &'static str,
|
||||
/// The [`Encoder`] for encoding the value.
|
||||
element_encoder: &'static E,
|
||||
}
|
||||
|
||||
impl<A, E: Encoder<Value = A>> HasValue for FieldEncoder<A, E> {
|
||||
type Value = A;
|
||||
}
|
||||
|
||||
impl<A, E: Encoder<Value = A>> Keyable for FieldEncoder<A, E> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
vec![self.name.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E: Encoder<Value = A>> CompressorHolder for FieldEncoder<A, E> {
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<A, E: Encoder<Value = A>> MapEncoder for FieldEncoder<A, E> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
prefix.add_string_key_value_result(self.name, self.element_encoder.encode_start(input, ops))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E: Encoder<Value = A>> FieldEncoder<A, E> {
|
||||
/// Returns a new [`FieldEncoder`] with the provided name and [`Encoder`].
|
||||
pub(crate) const fn new(name: &'static str, element_encoder: &'static E) -> Self {
|
||||
Self {
|
||||
name,
|
||||
element_encoder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`MapDecoder`] that knows how to decode an entire field (key + value), where the value is encoded by a [`Decoder`].
|
||||
///
|
||||
/// `A` is the type of value that the decoder can decode to.
|
||||
pub struct FieldDecoder<A, D: Decoder<Value = A> + 'static> {
|
||||
/// The name of the key.
|
||||
name: &'static str,
|
||||
/// The [`Decoder`] for encoding the value.
|
||||
element_decoder: &'static D,
|
||||
}
|
||||
|
||||
impl<A, D: Decoder<Value = A>> HasValue for FieldDecoder<A, D> {
|
||||
type Value = A;
|
||||
}
|
||||
|
||||
impl<A, D: Decoder<Value = A>> Keyable for FieldDecoder<A, D> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
vec![self.name.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, D: Decoder<Value = A>> CompressorHolder for FieldDecoder<A, D> {
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<A, D: Decoder<Value = A>> MapDecoder for FieldDecoder<A, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
input.get_str(self.name).map_or_else(
|
||||
|| DataResult::new_error(format!("No key {} in map", self.name)),
|
||||
|v| self.element_decoder.parse(v.clone(), ops),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, D: Decoder<Value = A>> FieldDecoder<A, D> {
|
||||
/// Returns a new [`FieldDecoder`] with the provided name and [`Decoder`].
|
||||
pub(crate) const fn new(name: &'static str, element_decoder: &'static D) -> Self {
|
||||
Self {
|
||||
name,
|
||||
element_decoder,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod either;
|
||||
pub mod field_coders;
|
||||
pub mod optional_field;
|
||||
pub mod simple;
|
||||
pub mod validated;
|
||||
@@ -1,168 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::codec::Codec;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::impl_compressor;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_codec::MapCodec;
|
||||
use crate::map_coders::{CompressorHolder, MapDecoder, MapEncoder};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A [`MapCodec`] that describes an optional field.
|
||||
pub struct OptionalFieldMapCodec<C: Codec + 'static> {
|
||||
element_codec: &'static C,
|
||||
name: &'static str,
|
||||
/// Whether this field should give a complete result for an
|
||||
/// error result (partial or no result) of the underlying codec.
|
||||
lenient: bool,
|
||||
}
|
||||
|
||||
impl<C: Codec> HasValue for OptionalFieldMapCodec<C> {
|
||||
// The type of this `MapCodec` should be an `Option`.
|
||||
type Value = Option<C::Value>;
|
||||
}
|
||||
|
||||
impl<C: Codec> Keyable for OptionalFieldMapCodec<C> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
vec![self.name.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> CompressorHolder for OptionalFieldMapCodec<C> {
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<C: Codec> MapEncoder for OptionalFieldMapCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
if let Some(input) = input.as_ref() {
|
||||
prefix
|
||||
.add_string_key_value_result(self.name, self.element_codec.encode_start(input, ops))
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Codec> MapDecoder for OptionalFieldMapCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
input.get_str(self.name).map_or_else(
|
||||
|| DataResult::new_success(None),
|
||||
|value| {
|
||||
let result = self.element_codec.parse(value.clone(), ops);
|
||||
if result.is_error() && self.lenient {
|
||||
DataResult::new_success(None)
|
||||
} else {
|
||||
result.map(Some)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a [`MapCodec`] returning an [`Option`] type that
|
||||
/// can provide a default value to transform the `MapCodec` type into its non-`Option` type.
|
||||
pub struct DefaultValueProviderMapCodec<
|
||||
T: PartialEq + Clone,
|
||||
C: MapCodec<Value = Option<T>> + 'static,
|
||||
> {
|
||||
codec: C,
|
||||
default: fn() -> T,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone, C: MapCodec<Value = Option<T>>> HasValue
|
||||
for DefaultValueProviderMapCodec<T, C>
|
||||
{
|
||||
type Value = T;
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone, C: MapCodec<Value = Option<T>>> Keyable
|
||||
for DefaultValueProviderMapCodec<T, C>
|
||||
{
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.codec.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone, C: MapCodec<Value = Option<T>>> CompressorHolder
|
||||
for DefaultValueProviderMapCodec<T, C>
|
||||
{
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
self.codec.compressor()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone, C: MapCodec<Value = Option<T>>> MapEncoder
|
||||
for DefaultValueProviderMapCodec<T, C>
|
||||
{
|
||||
fn encode<U: Display + PartialEq + Clone, B: StructBuilder<Value = U>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = U>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
let clone = Some(input.clone());
|
||||
self.codec.encode(
|
||||
if *input == (self.default)() {
|
||||
&None
|
||||
} else {
|
||||
&clone
|
||||
},
|
||||
ops,
|
||||
prefix,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone, C: MapCodec<Value = Option<T>>> MapDecoder
|
||||
for DefaultValueProviderMapCodec<T, C>
|
||||
{
|
||||
fn decode<U: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = U>,
|
||||
ops: &'static impl DynamicOps<Value = U>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.codec
|
||||
.decode(input, ops)
|
||||
.map(|value| value.unwrap_or_else(self.default))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new [`DefaultValueProviderMapCodec`] with the provided [`Option`] [`MapCodec`] and a default value factory.
|
||||
pub(crate) const fn new_default_value_provider_map_codec<
|
||||
T: PartialEq + Clone,
|
||||
C: MapCodec<Value = Option<T>>,
|
||||
>(
|
||||
map_codec: C,
|
||||
default: fn() -> T,
|
||||
) -> DefaultValueProviderMapCodec<T, C> {
|
||||
DefaultValueProviderMapCodec {
|
||||
codec: map_codec,
|
||||
default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new [`OptionalFieldMapCodec`].
|
||||
pub(crate) const fn new_optional_field_map_codec<C: Codec>(
|
||||
element_codec: &'static C,
|
||||
name: &'static str,
|
||||
lenient: bool,
|
||||
) -> OptionalFieldMapCodec<C> {
|
||||
OptionalFieldMapCodec {
|
||||
element_codec,
|
||||
name,
|
||||
lenient,
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::base_map_codec::BaseMapCodec;
|
||||
use crate::codec::Codec;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_coders::CompressorHolder;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::impl_compressor;
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A simple [`MapCodec`] implementation of [`BaseMapCodec`].
|
||||
/// This codec has a fixed set of keys.
|
||||
pub struct SimpleMapCodec<K: Codec + 'static, V: Codec + 'static, Key: Keyable>
|
||||
where
|
||||
K::Value: Display + Eq + Hash,
|
||||
{
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
|
||||
keyable: Key,
|
||||
}
|
||||
impl<K: Codec, V: Codec, Key: Keyable> Keyable for SimpleMapCodec<K, V, Key>
|
||||
where
|
||||
K::Value: Display + Eq + Hash,
|
||||
{
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.keyable.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec, Key: Keyable> CompressorHolder for SimpleMapCodec<K, V, Key>
|
||||
where
|
||||
K::Value: Display + Eq + Hash,
|
||||
{
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<K: Codec, V: Codec, Key: Keyable> BaseMapCodec for SimpleMapCodec<K, V, Key>
|
||||
where
|
||||
K::Value: Display + Eq + Hash,
|
||||
{
|
||||
type Key = K::Value;
|
||||
type KeyCodec = K;
|
||||
type Element = V::Value;
|
||||
type ElementCodec = V;
|
||||
|
||||
fn key_codec(&self) -> &'static Self::KeyCodec {
|
||||
self.key_codec
|
||||
}
|
||||
|
||||
fn element_codec(&self) -> &'static Self::ElementCodec {
|
||||
self.element_codec
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn new_simple_map_codec<K: Codec, V: Codec, Key: Keyable>(
|
||||
key_codec: &'static K,
|
||||
element_codec: &'static V,
|
||||
keyable: Key,
|
||||
) -> SimpleMapCodec<K, V, Key>
|
||||
where
|
||||
<K as HasValue>::Value: Display + Eq + Hash,
|
||||
{
|
||||
SimpleMapCodec {
|
||||
key_codec,
|
||||
element_codec,
|
||||
keyable,
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_codec::MapCodec;
|
||||
use crate::map_coders::{CompressorHolder, MapDecoder, MapEncoder};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A validator [`MapCodec`] that validates any values before encoding and after decoding.
|
||||
pub struct ValidatedMapCodec<C: MapCodec + 'static> {
|
||||
codec: &'static C,
|
||||
/// The validator function used.
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
}
|
||||
|
||||
impl<C: MapCodec> HasValue for ValidatedMapCodec<C> {
|
||||
type Value = C::Value;
|
||||
}
|
||||
|
||||
impl<C: MapCodec> Keyable for ValidatedMapCodec<C> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.codec.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> CompressorHolder for ValidatedMapCodec<C> {
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
self.codec.compressor()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> MapEncoder for ValidatedMapCodec<C> {
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B {
|
||||
match (self.validator)(input) {
|
||||
Ok(()) => self.codec.encode(input, ops, prefix),
|
||||
Err(s) => prefix.with_errors_from(&DataResult::<()>::new_error(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MapCodec> MapDecoder for ValidatedMapCodec<C> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
let result = self.codec.decode(input, ops);
|
||||
if let Some(v) = result.result_or_partial_as_ref() {
|
||||
(self.validator)(v).map_or_else(DataResult::new_error, |()| result)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`ValidatedMapCodec`].
|
||||
pub(crate) const fn new_validated_map_codec<C: MapCodec>(
|
||||
codec: &'static C,
|
||||
validator: fn(&C::Value) -> Result<(), String>,
|
||||
) -> ValidatedMapCodec<C> {
|
||||
ValidatedMapCodec { codec, validator }
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
use crate::HasValue;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::lifecycle::Lifecycle;
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::{
|
||||
MapBuilder, ResultStructBuilder, StructBuilder, UniversalStructBuilder,
|
||||
};
|
||||
use crate::{impl_struct_builder, impl_universal_struct_builder};
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A [`StructBuilder`] for compressed map data.
|
||||
pub struct CompressedStructBuilder<'a, T, O: DynamicOps<Value = T> + 'static> {
|
||||
builder: DataResult<Vec<T>>,
|
||||
ops: &'static O,
|
||||
compressor: &'a KeyCompressor,
|
||||
}
|
||||
|
||||
impl<'a, T: Clone, O: DynamicOps<Value = T> + 'static> CompressedStructBuilder<'a, T, O> {
|
||||
#[expect(dead_code)]
|
||||
pub(crate) const fn new(ops: &'static O, compressor: &'a KeyCompressor) -> Self {
|
||||
Self {
|
||||
builder: DataResult::new_success_with_lifecycle(vec![], Lifecycle::Stable),
|
||||
ops,
|
||||
compressor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> StructBuilder for CompressedStructBuilder<'_, T, O> {
|
||||
type Value = T;
|
||||
|
||||
impl_struct_builder!(builder);
|
||||
impl_universal_struct_builder!(builder, self.ops);
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> ResultStructBuilder for CompressedStructBuilder<'_, T, O> {
|
||||
type Result = Vec<T>;
|
||||
|
||||
fn build_with_builder(
|
||||
self,
|
||||
builder: Self::Result,
|
||||
prefix: Self::Value,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.ops.merge_values_into_list(prefix, builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> UniversalStructBuilder
|
||||
for CompressedStructBuilder<'_, T, O>
|
||||
{
|
||||
fn append(
|
||||
&self,
|
||||
key: Self::Value,
|
||||
value: Self::Value,
|
||||
mut builder: Self::Result,
|
||||
) -> Self::Result {
|
||||
if let Some(i) = self.compressor.compress_key(&key, self.ops) {
|
||||
builder[i] = value;
|
||||
}
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`StructBuilder`] that could be compressed or uncompressed.
|
||||
pub enum EncoderStructBuilder<T, O: DynamicOps<Value = T> + 'static> {
|
||||
Normal(O::StructBuilder),
|
||||
Compressed(MapBuilder<T, O>),
|
||||
}
|
||||
|
||||
/// Outsources a function of [`EncoderStructBuilder`] to call the inner builder's method.
|
||||
macro_rules! delegate_encoder_struct_builder_method {
|
||||
($target:ident, $name:ident $(, $args:expr)*) => {
|
||||
match $target {
|
||||
Self::Normal(b) => Self::Normal(b.$name($($args),*)),
|
||||
Self::Compressed(b) => Self::Compressed(b.$name($($args),*)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> StructBuilder for EncoderStructBuilder<T, O> {
|
||||
type Value = T;
|
||||
|
||||
fn add_key_value(self, key: Self::Value, value: Self::Value) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, add_key_value, key, value)
|
||||
}
|
||||
|
||||
fn add_key_value_result(self, key: Self::Value, value: DataResult<Self::Value>) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, add_key_value_result, key, value)
|
||||
}
|
||||
|
||||
fn add_key_result_value_result(
|
||||
self,
|
||||
key: DataResult<Self::Value>,
|
||||
value: DataResult<Self::Value>,
|
||||
) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, add_key_result_value_result, key, value)
|
||||
}
|
||||
|
||||
fn with_errors_from<U>(self, result: &DataResult<U>) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, with_errors_from, result)
|
||||
}
|
||||
|
||||
fn add_string_key_value(self, key: &str, value: Self::Value) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, add_string_key_value, key, value)
|
||||
}
|
||||
|
||||
fn add_string_key_value_result(self, key: &str, value: DataResult<Self::Value>) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, add_string_key_value_result, key, value)
|
||||
}
|
||||
|
||||
fn set_lifecycle(self, lifecycle: Lifecycle) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, set_lifecycle, lifecycle)
|
||||
}
|
||||
|
||||
fn map_error(self, f: impl FnOnce(String) -> String) -> Self {
|
||||
delegate_encoder_struct_builder_method!(self, map_error, f)
|
||||
}
|
||||
|
||||
fn build(self, prefix: Self::Value) -> DataResult<Self::Value> {
|
||||
match self {
|
||||
Self::Normal(e) => e.build(prefix),
|
||||
Self::Compressed(e) => e.build(prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait specifying that an object holds a [`KeyCompressor`].
|
||||
pub trait CompressorHolder: Keyable {
|
||||
/// Returns the [`KeyCompressor`] of this object with the provided [`DynamicOps`].
|
||||
fn compressor(&self) -> Arc<KeyCompressor>;
|
||||
}
|
||||
|
||||
/// A different encoder that encodes a value of type `Value` for a map.
|
||||
pub trait MapEncoder: HasValue + Keyable + CompressorHolder {
|
||||
/// Encodes an input by working on a [`StructBuilder`].
|
||||
fn encode<T: Display + PartialEq + Clone, B: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: B,
|
||||
) -> B;
|
||||
|
||||
/// Returns a [`StructBuilder`] of this `MapEncoder` with the provided [`DynamicOps`].
|
||||
fn builder<'a, T: Display + Clone + 'a, O: DynamicOps<Value = T> + 'static>(
|
||||
&'a self,
|
||||
ops: &'static O,
|
||||
) -> EncoderStructBuilder<T, O> {
|
||||
if ops.compress_maps() {
|
||||
EncoderStructBuilder::Compressed(MapBuilder::new(ops))
|
||||
} else {
|
||||
EncoderStructBuilder::Normal(ops.map_builder())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A different decoder that decodes into something of type `Value` for a map.
|
||||
pub trait MapDecoder: HasValue + Keyable + CompressorHolder {
|
||||
/// Decodes a map input.
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value>;
|
||||
|
||||
fn compressed_decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: T,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
if ops.compress_maps() {
|
||||
// Since compressed maps are really just lists, we parse a list instead.
|
||||
return ops.get_iter(input).into_result().map_or_else(
|
||||
|| DataResult::new_error("Input is not a list"),
|
||||
|iter| {
|
||||
/// A [`MapLike`] for handling [`KeyCompressor`] methods.
|
||||
struct CompressorMapLikeImpl<T, O: DynamicOps<Value = T> + 'static> {
|
||||
list: Vec<T>,
|
||||
compressor: Arc<KeyCompressor>,
|
||||
ops: &'static O,
|
||||
}
|
||||
|
||||
impl<T, O: DynamicOps<Value = T>> MapLike for CompressorMapLikeImpl<T, O> {
|
||||
type Value = T;
|
||||
|
||||
fn get(&self, key: &Self::Value) -> Option<&Self::Value> {
|
||||
self.compressor
|
||||
.compress_key(key, self.ops)
|
||||
.and_then(|i| self.list.get(i))
|
||||
}
|
||||
|
||||
fn get_str(&self, key: &str) -> Option<&Self::Value> {
|
||||
self.compressor
|
||||
.compress_key_str(key)
|
||||
.and_then(|i| self.list.get(i))
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = (Self::Value, &Self::Value)> + '_ {
|
||||
self.list.iter().enumerate().filter_map(|(i, v)| {
|
||||
self.compressor.decompress_key(i, self.ops).map(|k| (k, v))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.decode(
|
||||
&CompressorMapLikeImpl {
|
||||
list: iter.collect(),
|
||||
compressor: self.compressor(),
|
||||
ops,
|
||||
},
|
||||
ops,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
ops.get_map(&input)
|
||||
.with_lifecycle(Lifecycle::Stable)
|
||||
.flat_map(|map| self.decode(&map, ops))
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper macro for generating the [`CompressorHolder::compressor`] method
|
||||
/// for structs implementing `CompressorHolder`.
|
||||
///
|
||||
/// This macro caches the [`KeyCompressor`] of this [`CompressorHolder`]
|
||||
/// in a global map.
|
||||
///
|
||||
/// Implement this in an `impl` block for `CompressorHolder`.
|
||||
#[macro_export]
|
||||
macro_rules! impl_compressor {
|
||||
() => {
|
||||
fn compressor(&self) -> std::sync::Arc<KeyCompressor> {
|
||||
// We get the unique pointer of this holder.
|
||||
let key = std::ptr::from_ref::<Self>(self) as usize;
|
||||
// Then, we get the cache or store it.
|
||||
$crate::key_compressor::KEY_COMPRESSOR_CACHE
|
||||
.entry(key)
|
||||
.or_insert_with(|| {
|
||||
let mut c = KeyCompressor::new();
|
||||
c.populate(self.keys());
|
||||
std::sync::Arc::new(c)
|
||||
})
|
||||
.value()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Transformer map encoders and decoders
|
||||
|
||||
macro_rules! impl_map_encoder_transformer {
|
||||
($name:ident, $function_return:ty) => {
|
||||
pub struct $name<B, E: MapEncoder + 'static> {
|
||||
encoder: &'static E,
|
||||
function: fn(&B) -> $function_return,
|
||||
}
|
||||
|
||||
impl<B, E: MapEncoder> HasValue for $name<B, E> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, E: MapEncoder> Keyable for $name<B, E> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.encoder.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, E: MapEncoder> CompressorHolder for $name<B, E> {
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
self.encoder.compressor()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_map_encoder_transformer!(ComappedMapEncoderImpl, E::Value);
|
||||
|
||||
impl<B, E: MapEncoder> MapEncoder for ComappedMapEncoderImpl<B, E> {
|
||||
fn encode<T: Display + PartialEq + Clone, S: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: S,
|
||||
) -> S {
|
||||
self.encoder.encode(&(self.function)(input), ops, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *contramapped* (*comapped*) transformation of a provided [`MapEncoder`].
|
||||
/// A *comapped* encoder transforms the input before encoding.
|
||||
pub(crate) const fn comap<B, E: MapEncoder>(
|
||||
encoder: &'static E,
|
||||
f: fn(&B) -> E::Value,
|
||||
) -> ComappedMapEncoderImpl<B, E> {
|
||||
ComappedMapEncoderImpl {
|
||||
encoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
impl_map_encoder_transformer!(FlatComappedMapEncoderImpl, DataResult<E::Value>);
|
||||
|
||||
impl<B, E: MapEncoder> MapEncoder for FlatComappedMapEncoderImpl<B, E> {
|
||||
fn encode<T: Display + PartialEq + Clone, S: StructBuilder<Value = T>>(
|
||||
&self,
|
||||
input: &Self::Value,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
prefix: S,
|
||||
) -> S {
|
||||
let result = (self.function)(input);
|
||||
let builder = prefix.with_errors_from(&result);
|
||||
// We want to encode either a complete or partial result if there is one.
|
||||
// Otherwise, we do nothing.
|
||||
match result {
|
||||
DataResult::Success { result: r, .. }
|
||||
| DataResult::Error {
|
||||
partial_result: Some(r),
|
||||
..
|
||||
} => self.encoder.encode(&r, ops, builder),
|
||||
DataResult::Error {
|
||||
partial_result: None,
|
||||
..
|
||||
} => builder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *flat contramapped* (*flat-comapped*) transformation of a provided [`MapEncoder`].
|
||||
/// A *flat comapped* encoder transforms the input before encoding, but the transformation can fail.
|
||||
pub(crate) const fn flat_comap<B, E: MapEncoder>(
|
||||
encoder: &'static E,
|
||||
f: fn(&B) -> DataResult<E::Value>,
|
||||
) -> FlatComappedMapEncoderImpl<B, E> {
|
||||
FlatComappedMapEncoderImpl {
|
||||
encoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_map_decoder_transformer {
|
||||
($name:ident, $function_return:ty) => {
|
||||
pub struct $name<B, D: MapDecoder + 'static> {
|
||||
decoder: &'static D,
|
||||
function: fn(D::Value) -> $function_return,
|
||||
}
|
||||
|
||||
impl<B, D: MapDecoder> HasValue for $name<B, D> {
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<B, D: MapDecoder> Keyable for $name<B, D> {
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.decoder.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, D: MapDecoder> CompressorHolder for $name<B, D> {
|
||||
fn compressor(&self) -> Arc<KeyCompressor> {
|
||||
self.decoder.compressor()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_map_decoder_transformer!(MappedMapDecoderImpl, B);
|
||||
|
||||
impl<B, D: MapDecoder> MapDecoder for MappedMapDecoderImpl<B, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.decoder.decode(input, ops).map(|a| (self.function)(a))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *covariant mapped* transformation of a provided [`MapDecoder`].
|
||||
/// A *mapped* decoder transforms the output after decoding.
|
||||
pub(crate) const fn map<B, D: MapDecoder>(
|
||||
decoder: &'static D,
|
||||
f: fn(D::Value) -> B,
|
||||
) -> MappedMapDecoderImpl<B, D> {
|
||||
MappedMapDecoderImpl {
|
||||
decoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
|
||||
impl_map_decoder_transformer!(FlatMappedMapDecoderImpl, DataResult<B>);
|
||||
|
||||
impl<B, D: MapDecoder> MapDecoder for FlatMappedMapDecoderImpl<B, D> {
|
||||
fn decode<T: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = T>,
|
||||
ops: &'static impl DynamicOps<Value = T>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.decoder
|
||||
.decode(input, ops)
|
||||
.flat_map(|a| (self.function)(a))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *covariant flat-mapped* transformation of a provided [`MapDecoder`].
|
||||
/// A *flat-mapped* decoder transforms the output after decoding, but the transformation can fail.
|
||||
pub(crate) const fn flat_map<B, D: MapDecoder>(
|
||||
decoder: &'static D,
|
||||
f: fn(D::Value) -> DataResult<B>,
|
||||
) -> FlatMappedMapDecoderImpl<B, D> {
|
||||
FlatMappedMapDecoderImpl {
|
||||
decoder,
|
||||
function: f,
|
||||
}
|
||||
}
|
||||
150
pumpkin-codecs/src/number.rs
Normal file
150
pumpkin-codecs/src/number.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use core::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Represents a generic number in Java.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum Number {
|
||||
Byte(i8),
|
||||
Short(i16),
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Float(f32),
|
||||
Double(f64),
|
||||
}
|
||||
|
||||
impl From<Number> for i64 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for i32 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for i16 {
|
||||
fn from(num: Number) -> Self {
|
||||
// Similar to Java, we will first convert the number to an `i16`, and then to an `i8`.
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for i8 {
|
||||
fn from(num: Number) -> Self {
|
||||
// Similar to Java, we will first convert the number to an `i32`, and then to an `i8`.
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for u8 {
|
||||
fn from(num: Number) -> Self {
|
||||
i32::from(num) as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for f32 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f,
|
||||
Number::Double(d) => d as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for f64 {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(b) => b as Self,
|
||||
Number::Short(s) => s as Self,
|
||||
Number::Int(i) => i as Self,
|
||||
Number::Long(l) => l as Self,
|
||||
Number::Float(f) => f as Self,
|
||||
Number::Double(d) => d,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Number {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Byte(v) => write!(f, "{v}"),
|
||||
Self::Short(v) => write!(f, "{v}"),
|
||||
Self::Int(v) => write!(f, "{v}"),
|
||||
Self::Long(v) => write!(f, "{v}"),
|
||||
Self::Float(v) => write!(f, "{v}"),
|
||||
Self::Double(v) => write!(f, "{v}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for serde_json::Value {
|
||||
fn from(num: Number) -> Self {
|
||||
match num {
|
||||
Number::Byte(n) => n.into(),
|
||||
Number::Short(n) => n.into(),
|
||||
Number::Int(n) => n.into(),
|
||||
Number::Long(n) => n.into(),
|
||||
Number::Float(n) => n.into(),
|
||||
Number::Double(n) => n.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error struct returned for an invalid conversion to [`Number`] from a [`serde_json::Value`].
|
||||
pub struct FromJsonValueError;
|
||||
|
||||
impl TryFrom<&serde_json::Value> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: &serde_json::Value) -> Result<Self, Self::Error> {
|
||||
num.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<serde_json::Value> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: serde_json::Value) -> Result<Self, Self::Error> {
|
||||
match num {
|
||||
serde_json::Value::Number(n) => n.try_into(),
|
||||
_ => Err(FromJsonValueError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<serde_json::Number> for Number {
|
||||
type Error = FromJsonValueError;
|
||||
|
||||
fn try_from(num: serde_json::Number) -> Result<Self, Self::Error> {
|
||||
// Try converting the number to an integer first.
|
||||
num.as_i64().map_or_else(
|
||||
// Try the float conversion.
|
||||
|| {
|
||||
num.as_f64()
|
||||
.map_or(Err(FromJsonValueError), |f| Ok(Self::Double(f)))
|
||||
},
|
||||
// Do the integer conversion.
|
||||
|n| Ok(Self::Long(n)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -260,15 +260,6 @@ pub struct MapBuilder<T, O: DynamicOps<Value = T> + 'static> {
|
||||
ops: &'static O,
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> MapBuilder<T, O> {
|
||||
pub(crate) const fn new(ops: &'static O) -> Self {
|
||||
Self {
|
||||
builder: DataResult::new_success_with_lifecycle(vec![], Lifecycle::Stable),
|
||||
ops,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone, O: DynamicOps<Value = T>> StructBuilder for MapBuilder<T, O> {
|
||||
type Value = T;
|
||||
|
||||
|
||||
@@ -1,740 +0,0 @@
|
||||
use crate::HasValue;
|
||||
#[allow(unused_imports)] // Only used for documentation.
|
||||
use crate::codec::Codec;
|
||||
use crate::codecs::map_codec::MapCodecCodec;
|
||||
use crate::data_result::DataResult;
|
||||
use crate::dynamic_ops::DynamicOps;
|
||||
use crate::impl_compressor;
|
||||
use crate::key_compressor::KeyCompressor;
|
||||
use crate::keyable::Keyable;
|
||||
use crate::map_codec::MapCodec;
|
||||
use crate::map_coders::{CompressorHolder, MapDecoder, MapEncoder};
|
||||
use crate::map_like::MapLike;
|
||||
use crate::struct_builder::StructBuilder;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// A single field object to build a struct codec, which either takes an *owned* or *borrowed* [`MapCodec`] and a getter.
|
||||
///
|
||||
/// - `T` is the composite type to get from.
|
||||
/// - `C` is the [`MapCodec`] for serializing/deserializing the field.
|
||||
pub enum Field<T, C: MapCodec + 'static> {
|
||||
Owned(C, fn(&T) -> &C::Value),
|
||||
Borrowed(&'static C, fn(&T) -> &C::Value),
|
||||
}
|
||||
|
||||
impl<T, C: MapCodec + 'static> Field<T, C> {
|
||||
fn getter(&self) -> &fn(&T) -> &C::Value {
|
||||
match self {
|
||||
Self::Owned(_, g) => g,
|
||||
Self::Borrowed(_, g) => g,
|
||||
}
|
||||
}
|
||||
|
||||
const fn map_codec(&self) -> &C {
|
||||
match self {
|
||||
Self::Owned(c, _) => c,
|
||||
Self::Borrowed(c, _) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro to generate a `StructMapCodecN` struct (structure codec of `N` arguments).
|
||||
/// This also creates a function to get a normal [`Codec`] from `N` fields.
|
||||
macro_rules! impl_struct_map_codec {
|
||||
(@internal_start $n:literal $name:ident $alias:ident $apply_func:ident $func_name:ident $($codec_type:ident, $field:ident),*) => {
|
||||
#[doc = concat!("A [`MapCodec`] for a map with ", stringify!($n) , " rigid field(s).")]
|
||||
///
|
||||
/// A [`Codec`] can then be made from this object.
|
||||
pub struct $name<T, C1: MapCodec + 'static $(, $codec_type: MapCodec + 'static)* > {
|
||||
field_1: Field<T, C1>,
|
||||
$( $field: Field<T, $codec_type> ,)*
|
||||
apply_function: fn(C1::Value $(, $codec_type::Value)*) -> T
|
||||
}
|
||||
|
||||
impl<T, C1: MapCodec $(, $codec_type: MapCodec)* > HasValue for $name<T, C1 $(, $codec_type)*> {
|
||||
type Value = T;
|
||||
}
|
||||
|
||||
impl<T, C1: MapCodec $(, $codec_type: MapCodec)* > Keyable for $name<T, C1 $(, $codec_type)*> {
|
||||
#[allow(unused_mut)]
|
||||
fn keys(&self) -> Vec<String> {
|
||||
let mut keys = self.field_1.map_codec().keys();
|
||||
$( keys.extend(self.$field.map_codec().keys()); )*
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C1: MapCodec $(, $codec_type: MapCodec)* > CompressorHolder for $name<T, C1 $(, $codec_type)*> {
|
||||
impl_compressor!();
|
||||
}
|
||||
|
||||
impl<T, C1: MapCodec $(, $codec_type: MapCodec)* > MapEncoder for $name<T, C1 $(, $codec_type)*> {
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn encode<U: Display + PartialEq + Clone, B: StructBuilder<Value = U>>(&self, input: &Self::Value, ops: &'static impl DynamicOps<Value=U>, prefix: B) -> B {
|
||||
let prefix =
|
||||
self.field_1.map_codec()
|
||||
.encode((self.field_1.getter())(input), ops, prefix);
|
||||
$(
|
||||
let prefix =
|
||||
self.$field.map_codec()
|
||||
.encode((self.$field.getter())(input), ops, prefix);
|
||||
)*
|
||||
prefix
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C1: MapCodec $(, $codec_type: MapCodec)* > MapDecoder for $name<T, C1 $(, $codec_type)*> {
|
||||
fn decode<U: Display + PartialEq + Clone>(
|
||||
&self,
|
||||
input: &impl MapLike<Value = U>,
|
||||
ops: &'static impl DynamicOps<Value = U>,
|
||||
) -> DataResult<Self::Value> {
|
||||
self.field_1.map_codec().decode(input, ops).$apply_func(
|
||||
self.apply_function,
|
||||
$( self.$field.map_codec().decode(input, ops), )*
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = concat!("A type alias of a struct [`Codec`] with ", stringify!($n), " field(s).")]
|
||||
pub type $alias<T, C1 $(, $codec_type)* > = MapCodecCodec<$name<T, C1 $(, $codec_type)*>>;
|
||||
};
|
||||
|
||||
($n:literal, $name:ident, $alias:ident, $apply_func:ident, $func_name:ident $(,)? $($codec_type:ident, $field:ident),*) => {
|
||||
|
||||
impl_struct_map_codec!(@internal_start $n $name $alias $apply_func $func_name $($codec_type, $field),*);
|
||||
|
||||
#[doc = concat!("Returns a struct [`Codec`] with ", stringify!($n), " field(s).")]
|
||||
pub const fn $func_name<T, C1: MapCodec $(, $codec_type: MapCodec)*>(
|
||||
field_1: Field<T, C1>,
|
||||
$($field: Field<T, $codec_type>,)*
|
||||
f: fn(C1::Value $(, $codec_type::Value)*) -> T,
|
||||
) -> $alias<T, C1 $(, $codec_type)*> {
|
||||
MapCodecCodec::Owned(
|
||||
$name {
|
||||
field_1,
|
||||
$( $field, )*
|
||||
apply_function: f
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
(expect $n:literal, $name:ident, $alias:ident, $apply_func:ident, $func_name:ident $(,)? $($codec_type:ident, $field:ident),*) => {
|
||||
|
||||
impl_struct_map_codec!(@internal_start $n $name $alias $apply_func $func_name $($codec_type, $field),*);
|
||||
|
||||
#[doc = concat!("Returns a struct [`Codec`] with ", stringify!($n), " field(s).")]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub const fn $func_name<T, C1: MapCodec $(, $codec_type: MapCodec)*>(
|
||||
field_1: Field<T, C1>,
|
||||
$($field: Field<T, $codec_type>,)*
|
||||
f: fn(C1::Value $(, $codec_type::Value)*) -> T,
|
||||
) -> $alias<T, C1 $(, $codec_type)*> {
|
||||
MapCodecCodec::Owned(
|
||||
$name {
|
||||
field_1,
|
||||
$( $field, )*
|
||||
apply_function: f
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_struct_map_codec!(1, StructMapCodec1, StructCodec1, map, struct_1,);
|
||||
impl_struct_map_codec!(
|
||||
2,
|
||||
StructMapCodec2,
|
||||
StructCodec2,
|
||||
apply_2,
|
||||
struct_2,
|
||||
C2,
|
||||
field_2
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
3,
|
||||
StructMapCodec3,
|
||||
StructCodec3,
|
||||
apply_3,
|
||||
struct_3,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
4,
|
||||
StructMapCodec4,
|
||||
StructCodec4,
|
||||
apply_4,
|
||||
struct_4,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
5,
|
||||
StructMapCodec5,
|
||||
StructCodec5,
|
||||
apply_5,
|
||||
struct_5,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
6,
|
||||
StructMapCodec6,
|
||||
StructCodec6,
|
||||
apply_6,
|
||||
struct_6,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 7,
|
||||
StructMapCodec7,
|
||||
StructCodec7,
|
||||
apply_7,
|
||||
struct_7,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 8,
|
||||
StructMapCodec8,
|
||||
StructCodec8,
|
||||
apply_8,
|
||||
struct_8,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 9,
|
||||
StructMapCodec9,
|
||||
StructCodec9,
|
||||
apply_9,
|
||||
struct_9,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 10,
|
||||
StructMapCodec10,
|
||||
StructCodec10,
|
||||
apply_10,
|
||||
struct_10,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 11,
|
||||
StructMapCodec11,
|
||||
StructCodec11,
|
||||
apply_11,
|
||||
struct_11,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 12,
|
||||
StructMapCodec12,
|
||||
StructCodec12,
|
||||
apply_12,
|
||||
struct_12,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11,
|
||||
C12,
|
||||
field_12
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 13,
|
||||
StructMapCodec13,
|
||||
StructCodec13,
|
||||
apply_13,
|
||||
struct_13,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11,
|
||||
C12,
|
||||
field_12,
|
||||
C13,
|
||||
field_13
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 14,
|
||||
StructMapCodec14,
|
||||
StructCodec14,
|
||||
apply_14,
|
||||
struct_14,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11,
|
||||
C12,
|
||||
field_12,
|
||||
C13,
|
||||
field_13,
|
||||
C14,
|
||||
field_14
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 15,
|
||||
StructMapCodec15,
|
||||
StructCodec15,
|
||||
apply_15,
|
||||
struct_15,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11,
|
||||
C12,
|
||||
field_12,
|
||||
C13,
|
||||
field_13,
|
||||
C14,
|
||||
field_14,
|
||||
C15,
|
||||
field_15
|
||||
);
|
||||
impl_struct_map_codec!(
|
||||
expect 16,
|
||||
StructMapCodec16,
|
||||
StructCodec16,
|
||||
apply_16,
|
||||
struct_16,
|
||||
C2,
|
||||
field_2,
|
||||
C3,
|
||||
field_3,
|
||||
C4,
|
||||
field_4,
|
||||
C5,
|
||||
field_5,
|
||||
C6,
|
||||
field_6,
|
||||
C7,
|
||||
field_7,
|
||||
C8,
|
||||
field_8,
|
||||
C9,
|
||||
field_9,
|
||||
C10,
|
||||
field_10,
|
||||
C11,
|
||||
field_11,
|
||||
C12,
|
||||
field_12,
|
||||
C13,
|
||||
field_13,
|
||||
C14,
|
||||
field_14,
|
||||
C15,
|
||||
field_15,
|
||||
C16,
|
||||
field_16
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::codec::*;
|
||||
use crate::codecs::list::ListCodec;
|
||||
use crate::codecs::primitive::StringCodec;
|
||||
use crate::codecs::validated::ValidatedCodec;
|
||||
use crate::coders::{Decoder, Encoder};
|
||||
use crate::json_ops;
|
||||
use crate::map_codec::for_getter;
|
||||
use crate::struct_codecs::StructCodec3;
|
||||
use crate::{assert_decode, struct_codec};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Book {
|
||||
name: String,
|
||||
author: String,
|
||||
pages: u32,
|
||||
}
|
||||
|
||||
pub type BookCodec = StructCodec3<
|
||||
Book,
|
||||
FieldMapCodec<StringCodec>,
|
||||
FieldMapCodec<StringCodec>,
|
||||
FieldMapCodec<UintCodec>,
|
||||
>;
|
||||
|
||||
pub static BOOK_CODEC: BookCodec = struct_codec!(
|
||||
for_getter(field(&STRING_CODEC, "name"), |book: &Book| &book.name),
|
||||
for_getter(field(&STRING_CODEC, "author"), |book: &Book| &book.author),
|
||||
for_getter(field(&UINT_CODEC, "pages"), |book: &Book| &book.pages),
|
||||
|name, author, pages| Book {
|
||||
name,
|
||||
author,
|
||||
pages
|
||||
}
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn book_struct() {
|
||||
let object = Book {
|
||||
name: "Sample Book".to_string(),
|
||||
author: "Sample Author".to_string(),
|
||||
pages: 16,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
BOOK_CODEC
|
||||
.encode_start(&object, &json_ops::INSTANCE)
|
||||
.expect("Could not encode book"),
|
||||
json![{
|
||||
"name": "Sample Book",
|
||||
"author": "Sample Author",
|
||||
"pages": 16
|
||||
}]
|
||||
);
|
||||
|
||||
assert_eq!(BOOK_CODEC.parse(json!({"name": "The Great Gatsby", "author": "F. Scott Fitzgerald", "pages": 180}), &json_ops::INSTANCE).expect("Parsing book object failed"),
|
||||
Book {
|
||||
name: "The Great Gatsby".to_string(),
|
||||
author: "F. Scott Fitzgerald".to_string(),
|
||||
pages: 180
|
||||
}
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOK_CODEC,
|
||||
json!({"name": "Untitled Book", "pages": 345}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
assert_decode!(
|
||||
BOOK_CODEC,
|
||||
json!({"name": "Untitled Book 2", "author": "Untitled Author", "pages": "98"}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn bookshelf_struct() {
|
||||
// A struct for a bookshelf.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Bookshelf {
|
||||
id: u32,
|
||||
// Optional, defaults to no books.
|
||||
books: Vec<Book>,
|
||||
capacity: u32,
|
||||
}
|
||||
|
||||
pub type BookshelfCodec = ValidatedCodec<
|
||||
StructCodec3<
|
||||
Bookshelf,
|
||||
FieldMapCodec<UintCodec>,
|
||||
DefaultedFieldCodec<ListCodec<BookCodec>>,
|
||||
FieldMapCodec<UintCodec>,
|
||||
>,
|
||||
>;
|
||||
pub static BOOKSHELF_CODEC: BookshelfCodec = validate(
|
||||
&struct_codec!(
|
||||
for_getter(field(&UINT_CODEC, "id"), |b: &Bookshelf| &b.id),
|
||||
for_getter(
|
||||
optional_field_with_default(&unbounded_list(&BOOK_CODEC), "books", Vec::new),
|
||||
|b: &Bookshelf| &b.books
|
||||
),
|
||||
for_getter(field(&UINT_CODEC, "capacity"), |b: &Bookshelf| &b.capacity),
|
||||
|id, books, capacity| Bookshelf {
|
||||
id,
|
||||
books,
|
||||
capacity
|
||||
}
|
||||
),
|
||||
|b| {
|
||||
// The number of books on the bookshelf must be less than or equal to its capacity.
|
||||
if b.books.len() <= b.capacity as usize {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Bookshelf cannot have {} books because its capacity is {}",
|
||||
b.books.len(),
|
||||
b.capacity
|
||||
))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let example = Bookshelf {
|
||||
id: 1234,
|
||||
books: vec![
|
||||
Book {
|
||||
name: "Charlie and the Chocolate Factory".to_string(),
|
||||
author: "Roald Dahl".to_string(),
|
||||
pages: 192,
|
||||
},
|
||||
Book {
|
||||
name: "Infinibook".to_string(),
|
||||
author: "Infiniauthor".to_string(),
|
||||
pages: 1_000_000,
|
||||
},
|
||||
],
|
||||
capacity: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
BOOKSHELF_CODEC
|
||||
.encode_start(&example, &json_ops::INSTANCE)
|
||||
.expect("Could not encode bookshelf"),
|
||||
json![{
|
||||
"id": 1234,
|
||||
"capacity": 2,
|
||||
"books": [
|
||||
{
|
||||
"name": "Charlie and the Chocolate Factory",
|
||||
"author": "Roald Dahl",
|
||||
"pages": 192,
|
||||
},
|
||||
{
|
||||
"name": "Infinibook",
|
||||
"author": "Infiniauthor",
|
||||
"pages": 1_000_000,
|
||||
}
|
||||
]
|
||||
}]
|
||||
);
|
||||
|
||||
let example = Bookshelf {
|
||||
id: 5678,
|
||||
books: vec![
|
||||
Book {
|
||||
name: "The Lord of the Rings".to_string(),
|
||||
author: "J.R.R. Tolkien".to_string(),
|
||||
pages: 1150,
|
||||
},
|
||||
Book {
|
||||
name: "Sherlock Holmes".to_string(),
|
||||
author: "Arthur Conan Doyle".to_string(),
|
||||
pages: 1320,
|
||||
},
|
||||
Book {
|
||||
name: "Empty Book".to_string(),
|
||||
author: String::new(),
|
||||
pages: 0,
|
||||
},
|
||||
],
|
||||
capacity: 2,
|
||||
};
|
||||
|
||||
assert!(
|
||||
BOOKSHELF_CODEC
|
||||
.encode_start(&example, &json_ops::INSTANCE)
|
||||
// We should get an error because the bookshelf cannot handle
|
||||
// more than 2 books.
|
||||
.get_message()
|
||||
.expect("Encoding bookshelf here should be an error")
|
||||
.starts_with("Bookshelf cannot have")
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOKSHELF_CODEC,
|
||||
json!({"id": 36, "capacity": 6, "books": [
|
||||
{"name": "Book A", "author": "Author A", "pages": 10},
|
||||
{"name": "Book B", "author": "Author B", "pages": 20},
|
||||
{"name": "Book C", "author": "Author C", "pages": 30},
|
||||
{"name": "Book D", "author": "Author D", "pages": 40},
|
||||
{"name": "Book E", "author": "Author E", "pages": 50}
|
||||
]}),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOKSHELF_CODEC,
|
||||
json!({"id": 93273, "capacity": 4, "books": [
|
||||
{"name": "Book 1", "author": "Author 1", "pages": 100},
|
||||
{"name": "Book 2", "author": "Author 2", "pages": 200},
|
||||
{"name": "Book 3", "author": "Author 3", "pages": 300},
|
||||
{"name": "Book 4", "author": "Author 4", "pages": 400},
|
||||
// This should fail because 5 > 4.
|
||||
{"name": "Book 5", "author": "Author 5", "pages": 500}
|
||||
]}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOKSHELF_CODEC,
|
||||
// This will work because "books" is an optional field.
|
||||
json!({"id": 254, "capacity": 10}),
|
||||
&json_ops::INSTANCE,
|
||||
is_success
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOKSHELF_CODEC,
|
||||
// This will not work because "books" expects an array.
|
||||
json!({"id": 6252, "capacity": 1, "books": {"name": "A Tale of Two Cities", "author": "Charles Dickens", "pages": 480}}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
|
||||
assert_decode!(
|
||||
BOOKSHELF_CODEC,
|
||||
json!({"id": 6253, "capacity": 1, "books": [{"name": "A Tale of Two Cities", "author": "Charles Dickens"}]}),
|
||||
&json_ops::INSTANCE,
|
||||
is_error
|
||||
);
|
||||
}
|
||||
}
|
||||
64
pumpkin-codegen/Cargo.lock
generated
64
pumpkin-codegen/Cargo.lock
generated
@@ -157,20 +157,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -325,12 +311,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -416,15 +396,6 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -501,19 +472,6 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -614,7 +572,6 @@ dependencies = [
|
||||
name = "pumpkin-codecs"
|
||||
version = "0.1.0-dev+26.1"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"either",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -720,15 +677,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
@@ -745,12 +693,6 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -852,12 +794,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::compound::NbtCompound;
|
||||
use crate::tag::NbtTag;
|
||||
use pumpkin_codecs::DataResult;
|
||||
use pumpkin_codecs::DynamicOps;
|
||||
use pumpkin_codecs::Lifecycle;
|
||||
use pumpkin_codecs::MapLike;
|
||||
use pumpkin_codecs::Number;
|
||||
use pumpkin_codecs::data_result::DataResult;
|
||||
use pumpkin_codecs::dynamic_ops::DynamicOps;
|
||||
use pumpkin_codecs::lifecycle::Lifecycle;
|
||||
use pumpkin_codecs::map_like::MapLike;
|
||||
use pumpkin_codecs::struct_builder::{ResultStructBuilder, StringStructBuilder, StructBuilder};
|
||||
use pumpkin_codecs::{impl_get_list, impl_string_struct_builder, impl_struct_builder};
|
||||
use std::iter::Map;
|
||||
@@ -14,9 +14,6 @@ use tracing::warn;
|
||||
/// A [`DynamicOps`] to serialize to/deserialize from NBT data.
|
||||
pub struct NbtOps;
|
||||
|
||||
/// An instance of [`NbtOps`], which serializes/deserializes NBT data.
|
||||
pub static INSTANCE: NbtOps = NbtOps;
|
||||
|
||||
impl DynamicOps for NbtOps {
|
||||
type Value = NbtTag;
|
||||
type StructBuilder = NbtStructBuilder;
|
||||
@@ -428,7 +425,7 @@ impl StructBuilder for NbtStructBuilder {
|
||||
type Value = NbtTag;
|
||||
|
||||
impl_struct_builder!(builder);
|
||||
impl_string_struct_builder!(builder, INSTANCE);
|
||||
impl_string_struct_builder!(builder, NbtOps);
|
||||
}
|
||||
|
||||
impl StringStructBuilder for NbtStructBuilder {
|
||||
@@ -667,168 +664,8 @@ add_inner_specific_array_collector_impl!(InnerLongListCollector, Long, LongArray
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::compound::NbtCompound;
|
||||
use crate::nbt_ops::{INSTANCE, ListCollector};
|
||||
use crate::nbt_ops::ListCollector;
|
||||
use crate::tag::NbtTag;
|
||||
use pumpkin_codecs::codec::{
|
||||
BOOL_CODEC, BYTE_BUFFER_CODEC, BYTE_CODEC, ComapFlatMapCodec, DOUBLE_CODEC,
|
||||
DefaultedFieldCodec, FieldMapCodec, INT_CODEC, INT_STREAM_CODEC, LONG_CODEC,
|
||||
LONG_STREAM_CODEC, SHORT_CODEC, STRING_CODEC, UBYTE_CODEC, UINT_CODEC, UbyteCodec,
|
||||
UintCodec, comap_flat_map, field, optional_field_with_default, unbounded_list,
|
||||
unbounded_map, validate,
|
||||
};
|
||||
use pumpkin_codecs::codecs::list::ListCodec;
|
||||
use pumpkin_codecs::codecs::primitive::{ByteBufferCodec, StringCodec};
|
||||
use pumpkin_codecs::codecs::unbounded_map::UnboundedMapCodec;
|
||||
use pumpkin_codecs::codecs::validated::ValidatedCodec;
|
||||
use pumpkin_codecs::coders::{Decoder, Encoder};
|
||||
use pumpkin_codecs::data_result::DataResult;
|
||||
use pumpkin_codecs::map_codec::for_getter;
|
||||
use pumpkin_codecs::struct_codec;
|
||||
use pumpkin_codecs::struct_codecs::{StructCodec2, StructCodec3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Convenience function to easily create an [`NbtTag::Compound`].
|
||||
macro_rules! nbt_compound_tag {
|
||||
|
||||
( { $($key:literal : $tag:expr),+ $(,)* } ) => {
|
||||
{
|
||||
let mut compound = NbtCompound::new();
|
||||
$( compound.put($key, $tag); )+
|
||||
NbtTag::Compound(compound)
|
||||
}
|
||||
};
|
||||
// For empty compounds
|
||||
( {} ) => {
|
||||
NbtTag::Compound(NbtCompound::new())
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn primitives() {
|
||||
// Simple types
|
||||
assert_eq!(
|
||||
INT_CODEC
|
||||
.encode_start(&45, &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::Int(45)
|
||||
);
|
||||
assert_eq!(
|
||||
BOOL_CODEC
|
||||
.encode_start(&true, &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::Byte(1)
|
||||
);
|
||||
assert_eq!(
|
||||
BYTE_CODEC
|
||||
.encode_start(&-89, &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::Byte(-89)
|
||||
);
|
||||
assert_eq!(
|
||||
DOUBLE_CODEC
|
||||
.encode_start(&1.0, &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::Double(1.0)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
STRING_CODEC
|
||||
.encode_start(&"Sample Text".to_string(), &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::String("Sample Text".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
INT_CODEC
|
||||
.parse(NbtTag::Int(50), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
50
|
||||
);
|
||||
assert_eq!(
|
||||
SHORT_CODEC
|
||||
.parse(NbtTag::Short(-1235), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
-1235
|
||||
);
|
||||
assert_eq!(
|
||||
LONG_CODEC
|
||||
.parse(NbtTag::Long(53234), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
53234
|
||||
);
|
||||
|
||||
// Packed array types
|
||||
let byte_vec = vec![
|
||||
1u8, 45u8, 100u8, 170u8, 203u8, 98u8, 245u8, 255u8, 0u8, 13u8,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
BYTE_BUFFER_CODEC
|
||||
.encode_start(&Box::from(&byte_vec[0..3]), &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::ByteArray(Box::from(vec![1, 45, 100]))
|
||||
);
|
||||
assert_eq!(
|
||||
BYTE_BUFFER_CODEC
|
||||
.encode_start(&Box::from(&byte_vec[2..7]), &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::ByteArray(Box::from(vec![100, 170, 203, 98, 245]))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
INT_STREAM_CODEC
|
||||
.encode_start(&vec![-100, 1234, 23948], &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::IntArray(vec![-100, 1234, 23948])
|
||||
);
|
||||
assert_eq!(
|
||||
INT_STREAM_CODEC
|
||||
.encode_start(&vec![1, 120938, 1231909999], &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::IntArray(vec![1, 120938, 1231909999])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
LONG_STREAM_CODEC
|
||||
.encode_start(&vec![10_000_000_000, -99_999_999_999], &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::LongArray(vec![10_000_000_000, -99_999_999_999])
|
||||
);
|
||||
assert_eq!(
|
||||
LONG_STREAM_CODEC
|
||||
.encode_start(&vec![123_456_789_012_345, 66], &INSTANCE)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::LongArray(vec![123_456_789_012_345, 66])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
BYTE_BUFFER_CODEC
|
||||
.parse(NbtTag::ByteArray(Box::new([1, 4])), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
vec![1, 4].into_boxed_slice()
|
||||
);
|
||||
// All `get_...` packed array functions allow any arbitrary number array.
|
||||
assert_eq!(
|
||||
BYTE_BUFFER_CODEC
|
||||
.parse(NbtTag::IntArray(vec![120]), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
vec![120].into_boxed_slice()
|
||||
);
|
||||
assert_eq!(
|
||||
INT_STREAM_CODEC
|
||||
.parse(NbtTag::LongArray(vec![1, 2, 3]), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
vec![1, 2, 3]
|
||||
);
|
||||
assert_eq!(
|
||||
LONG_STREAM_CODEC
|
||||
.parse(NbtTag::IntArray(vec![0, 0]), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
vec![0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_collecting() {
|
||||
@@ -890,586 +727,4 @@ mod test {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Specific codec tests
|
||||
|
||||
#[test]
|
||||
fn employee() {
|
||||
/// A struct to store a single employee.
|
||||
/// The `name` and `department` of the employee should not be empty.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Employee {
|
||||
name: String,
|
||||
department: String,
|
||||
salary: u32,
|
||||
}
|
||||
|
||||
pub type NonEmptyStringCodec = ValidatedCodec<StringCodec>;
|
||||
/// Convenience codec for only encoding/decoding non-empty strings.
|
||||
pub static NON_EMPTY_STRING_CODEC: NonEmptyStringCodec = validate(&STRING_CODEC, |s| {
|
||||
if s.is_empty() {
|
||||
Err("String should not be empty".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
pub type EmployeeCodec = StructCodec3<
|
||||
Employee,
|
||||
FieldMapCodec<NonEmptyStringCodec>,
|
||||
FieldMapCodec<NonEmptyStringCodec>,
|
||||
FieldMapCodec<UintCodec>,
|
||||
>;
|
||||
pub static EMPLOYEE_CODEC: EmployeeCodec = struct_codec!(
|
||||
for_getter(field(&NON_EMPTY_STRING_CODEC, "name"), |s: &Employee| &s
|
||||
.name),
|
||||
for_getter(
|
||||
field(&NON_EMPTY_STRING_CODEC, "department"),
|
||||
|s: &Employee| &s.department
|
||||
),
|
||||
for_getter(field(&UINT_CODEC, "salary"), |s: &Employee| &s.salary),
|
||||
|name, department, salary| Employee {
|
||||
name,
|
||||
department,
|
||||
salary
|
||||
}
|
||||
);
|
||||
|
||||
// Encoding
|
||||
|
||||
assert_eq!(
|
||||
EMPLOYEE_CODEC
|
||||
.encode_start(
|
||||
&Employee {
|
||||
name: "John Doe".to_string(),
|
||||
department: "Marketing".to_string(),
|
||||
salary: 82_000
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
nbt_compound_tag!({
|
||||
"name": NbtTag::String("John Doe".to_string()),
|
||||
"department": NbtTag::String("Marketing".to_string()),
|
||||
"salary": NbtTag::Int(82_000)
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
EMPLOYEE_CODEC
|
||||
.encode_start(
|
||||
&Employee {
|
||||
name: "Linna Hall".to_string(),
|
||||
// Department is empty.
|
||||
department: String::new(),
|
||||
salary: 90_000
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.get_message()
|
||||
.expect("Encoding should fail"),
|
||||
"String should not be empty"
|
||||
);
|
||||
|
||||
// Decoding
|
||||
|
||||
assert_eq!(
|
||||
EMPLOYEE_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"name": NbtTag::String("Kelly Peak".to_string()),
|
||||
"department": NbtTag::String("Sales".to_string()),
|
||||
"salary": NbtTag::Int(72_000)
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed"),
|
||||
Employee {
|
||||
name: "Kelly Peak".to_string(),
|
||||
department: "Sales".to_string(),
|
||||
salary: 72_000
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
EMPLOYEE_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"name": NbtTag::String(String::new()),
|
||||
"department": NbtTag::String("Information Technology".to_string()),
|
||||
"salary": NbtTag::Int(100_000)
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.get_message()
|
||||
.expect("Decoding should fail"),
|
||||
"String should not be empty"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn text() {
|
||||
/// Alignments of a line of text.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum TextAlignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl From<&TextAlignment> for String {
|
||||
fn from(value: &TextAlignment) -> Self {
|
||||
match value {
|
||||
TextAlignment::Left => "left",
|
||||
TextAlignment::Center => "center",
|
||||
TextAlignment::Right => "right",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidTextAlignmentError;
|
||||
|
||||
impl TryFrom<String> for TextAlignment {
|
||||
type Error = InvalidTextAlignmentError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"left" => Ok(Self::Left),
|
||||
"center" => Ok(Self::Center),
|
||||
"right" => Ok(Self::Right),
|
||||
|
||||
_ => Err(InvalidTextAlignmentError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type TextAlignmentCodec = ComapFlatMapCodec<TextAlignment, StringCodec>;
|
||||
|
||||
// The transformer codec:
|
||||
// - always converts `TextAlignment` -> `String`
|
||||
// - but only converts `String` -> `TextAlignment` if the string is valid.
|
||||
pub static TEXT_ALIGNMENT_CODEC: TextAlignmentCodec = comap_flat_map(
|
||||
&STRING_CODEC,
|
||||
|string| {
|
||||
string.clone().try_into().map_or_else(
|
||||
|_| DataResult::new_error(format!("Invalid alignment: {string}")),
|
||||
DataResult::new_success,
|
||||
)
|
||||
},
|
||||
|modifier: &TextAlignment| modifier.into(),
|
||||
);
|
||||
|
||||
/// A single piece of text.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
struct Text {
|
||||
content: String,
|
||||
/// Optional field, defaults to `Left` alignment.
|
||||
alignment: TextAlignment,
|
||||
}
|
||||
|
||||
pub type TextCodec =
|
||||
StructCodec2<Text, FieldMapCodec<StringCodec>, DefaultedFieldCodec<TextAlignmentCodec>>;
|
||||
pub static TEXT_CODEC: TextCodec = struct_codec!(
|
||||
for_getter(field(&STRING_CODEC, "content"), |t: &Text| &t.content),
|
||||
for_getter(
|
||||
optional_field_with_default(&TEXT_ALIGNMENT_CODEC, "alignment", || {
|
||||
TextAlignment::Left
|
||||
}),
|
||||
|t| &t.alignment
|
||||
),
|
||||
|content, alignment| Text { content, alignment }
|
||||
);
|
||||
|
||||
// Encoding
|
||||
|
||||
assert_eq!(
|
||||
TEXT_CODEC
|
||||
.encode_start(
|
||||
&Text {
|
||||
content: "Lorem ipsum".to_string(),
|
||||
alignment: TextAlignment::Left
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
nbt_compound_tag!({
|
||||
"content": NbtTag::String("Lorem ipsum".to_string()),
|
||||
// Since "left" is the default, it will not be included.
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
TEXT_CODEC
|
||||
.encode_start(
|
||||
&Text {
|
||||
content: "An apple a day keeps the doctor away".to_string(),
|
||||
alignment: TextAlignment::Center
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
nbt_compound_tag!({
|
||||
"content": NbtTag::String("An apple a day keeps the doctor away".to_string()),
|
||||
"alignment": NbtTag::String("center".to_string())
|
||||
})
|
||||
);
|
||||
|
||||
// Decoding
|
||||
|
||||
assert_eq!(
|
||||
TEXT_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"content": NbtTag::String("Surprise Sample Text".to_string()),
|
||||
"alignment": NbtTag::String("right".to_string())
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed"),
|
||||
Text {
|
||||
content: "Surprise Sample Text".to_string(),
|
||||
alignment: TextAlignment::Right
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
TEXT_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"content": NbtTag::String("Will the test succeed?".to_string()),
|
||||
// Alignment omitted; it will default to `Left`.
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Decoding should succeed"),
|
||||
Text {
|
||||
content: "Will the test succeed?".to_string(),
|
||||
alignment: TextAlignment::Left
|
||||
}
|
||||
);
|
||||
|
||||
assert!(
|
||||
TEXT_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"content": NbtTag::String("Some random document".to_string()),
|
||||
// Unfortunately, we don't have *justify* in our possible alignments.
|
||||
"alignment": NbtTag::String("justify".to_string())
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.get_message()
|
||||
.expect("Decoding should fail")
|
||||
.starts_with("Invalid alignment")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn dog_park() {
|
||||
/// Represents an arbitrary dog.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
struct Dog {
|
||||
breed: String,
|
||||
age: u8,
|
||||
// Optional, defaults to an empty `Vec`.
|
||||
tricks: Vec<String>,
|
||||
}
|
||||
|
||||
/// A dog park representation.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct DogPark {
|
||||
name: String,
|
||||
/// Each key of this map is the dog's name.
|
||||
dogs: HashMap<String, Dog>,
|
||||
}
|
||||
|
||||
pub type DogCodec = StructCodec3<
|
||||
Dog,
|
||||
FieldMapCodec<StringCodec>,
|
||||
FieldMapCodec<UbyteCodec>,
|
||||
DefaultedFieldCodec<ListCodec<StringCodec>>,
|
||||
>;
|
||||
pub static DOG_CODEC: DogCodec = struct_codec!(
|
||||
for_getter(field(&STRING_CODEC, "breed"), |t: &Dog| &t.breed),
|
||||
for_getter(field(&UBYTE_CODEC, "age"), |t: &Dog| &t.age),
|
||||
for_getter(
|
||||
optional_field_with_default(&unbounded_list(&STRING_CODEC), "tricks", Vec::new),
|
||||
|t: &Dog| &t.tricks
|
||||
),
|
||||
|breed, age, tricks| Dog { breed, age, tricks }
|
||||
);
|
||||
|
||||
pub type DogParkCodec = StructCodec2<
|
||||
DogPark,
|
||||
FieldMapCodec<StringCodec>,
|
||||
FieldMapCodec<UnboundedMapCodec<StringCodec, DogCodec>>,
|
||||
>;
|
||||
pub static DOG_PARK_CODEC: DogParkCodec = struct_codec!(
|
||||
for_getter(field(&STRING_CODEC, "name"), |p: &DogPark| &p.name),
|
||||
for_getter(
|
||||
field(&unbounded_map(&STRING_CODEC, &DOG_CODEC), "dogs"),
|
||||
|p| &p.dogs
|
||||
),
|
||||
|name, dogs| DogPark { name, dogs }
|
||||
);
|
||||
|
||||
// Encoding
|
||||
|
||||
let mut dogs = HashMap::new();
|
||||
dogs.insert(
|
||||
"Rodrick".to_string(),
|
||||
Dog {
|
||||
breed: "German Shepherd".to_string(),
|
||||
age: 4,
|
||||
tricks: vec!["spin".to_string()],
|
||||
},
|
||||
);
|
||||
dogs.insert(
|
||||
"Lucy".to_string(),
|
||||
Dog {
|
||||
breed: "Beagle".to_string(),
|
||||
age: 6,
|
||||
tricks: vec!["fetch".to_string(), "sit".to_string()],
|
||||
},
|
||||
);
|
||||
dogs.insert(
|
||||
"Dan".to_string(),
|
||||
Dog {
|
||||
breed: "Chihuahua".to_string(),
|
||||
age: 3,
|
||||
tricks: vec![],
|
||||
},
|
||||
);
|
||||
|
||||
let serialized_park = DOG_PARK_CODEC
|
||||
.encode_start(
|
||||
&DogPark {
|
||||
name: "Sunny Side Park".to_string(),
|
||||
dogs,
|
||||
},
|
||||
&INSTANCE,
|
||||
)
|
||||
.expect("Encoding should succeed");
|
||||
|
||||
let compound = serialized_park
|
||||
.extract_compound()
|
||||
.expect("Tag should be a compound");
|
||||
|
||||
assert_eq!(
|
||||
compound
|
||||
.clone()
|
||||
.get_string("name")
|
||||
.expect("Compound tag should have a 'name' key"),
|
||||
"Sunny Side Park"
|
||||
);
|
||||
|
||||
for (k, v) in compound
|
||||
.get_compound("dogs")
|
||||
.expect("Compound tag should have a 'dogs' key")
|
||||
.clone()
|
||||
{
|
||||
match k.as_str() {
|
||||
"Rodrick" => assert_eq!(
|
||||
v,
|
||||
nbt_compound_tag!({
|
||||
"breed": NbtTag::String("German Shepherd".to_string()),
|
||||
"age": NbtTag::Byte(4),
|
||||
"tricks": NbtTag::List(vec![NbtTag::String("spin".to_string())])
|
||||
})
|
||||
),
|
||||
"Lucy" => assert_eq!(
|
||||
v,
|
||||
nbt_compound_tag!({
|
||||
"breed": NbtTag::String("Beagle".to_string()),
|
||||
"age": NbtTag::Byte(6),
|
||||
"tricks": NbtTag::List(vec![NbtTag::String("fetch".to_string()), NbtTag::String("sit".to_string())])
|
||||
})
|
||||
),
|
||||
"Dan" => assert_eq!(
|
||||
v,
|
||||
nbt_compound_tag!({
|
||||
"breed": NbtTag::String("Chihuahua".to_string()),
|
||||
"age": NbtTag::Byte(3),
|
||||
// 'tricks' will be omitted for an empty list.
|
||||
})
|
||||
),
|
||||
_ => panic!("Unexpected dog {k} found"),
|
||||
}
|
||||
}
|
||||
|
||||
// Decoding
|
||||
|
||||
let deserialized_park = DOG_PARK_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"name": NbtTag::String("Lighthouse Meadow Park".to_string()),
|
||||
"dogs": nbt_compound_tag!({
|
||||
"Adam": nbt_compound_tag!({
|
||||
"breed": NbtTag::String("Bulldog".to_string()),
|
||||
"age": NbtTag::Byte(8),
|
||||
"tricks": NbtTag::List(vec![NbtTag::String("catch".to_string())])
|
||||
})
|
||||
})
|
||||
}),
|
||||
&INSTANCE,
|
||||
)
|
||||
.expect("Decoding should succeed");
|
||||
|
||||
assert_eq!(deserialized_park.name, "Lighthouse Meadow Park");
|
||||
assert_eq!(deserialized_park.dogs.len(), 1);
|
||||
assert_eq!(
|
||||
deserialized_park
|
||||
.dogs
|
||||
.get("Adam")
|
||||
.expect("No dog 'Adam' in dogs"),
|
||||
&Dog {
|
||||
breed: "Bulldog".to_string(),
|
||||
age: 8,
|
||||
tricks: vec!["catch".to_string()]
|
||||
}
|
||||
);
|
||||
|
||||
assert!(
|
||||
DOG_PARK_CODEC
|
||||
.parse(
|
||||
nbt_compound_tag!({
|
||||
"name": NbtTag::String("Dark Park".to_string()),
|
||||
"dogs": nbt_compound_tag!({
|
||||
"Adam": nbt_compound_tag!({
|
||||
"breed": NbtTag::String("Poodle".to_string()),
|
||||
// Negative ages are not allowed.
|
||||
"age": NbtTag::Byte(-2)
|
||||
})
|
||||
})
|
||||
}),
|
||||
&INSTANCE
|
||||
)
|
||||
.get_message()
|
||||
.expect("Decoding should fail")
|
||||
.starts_with("Could not fit i8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn packed_color() {
|
||||
/// A color stored using 4 bytes, one each for red, green, blue and alpha.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
struct PackedColor {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
/// Optional field, defaults to `255` (full alpha).
|
||||
a: u8,
|
||||
}
|
||||
|
||||
pub type PackedColorCodec = ComapFlatMapCodec<PackedColor, ByteBufferCodec>;
|
||||
pub static PACKED_COLOR_CODEC: PackedColorCodec = comap_flat_map(
|
||||
&BYTE_BUFFER_CODEC,
|
||||
|v| {
|
||||
// While decoding, our codec only accepts byte buffers (arrays) with exactly 3 or 4 elements.
|
||||
if v.len() == 4 {
|
||||
DataResult::new_success(PackedColor {
|
||||
r: v[0],
|
||||
g: v[1],
|
||||
b: v[2],
|
||||
a: v[3],
|
||||
})
|
||||
} else if v.len() == 3 {
|
||||
// Alpha defaults to 255.
|
||||
DataResult::new_success(PackedColor {
|
||||
r: v[0],
|
||||
g: v[1],
|
||||
b: v[2],
|
||||
a: 255,
|
||||
})
|
||||
} else {
|
||||
DataResult::new_error(format!("Invalid byte buffer for color: {v:?}"))
|
||||
}
|
||||
},
|
||||
|c| vec![c.r, c.g, c.b, c.a].into_boxed_slice(),
|
||||
);
|
||||
|
||||
// Encoding
|
||||
|
||||
assert_eq!(
|
||||
PACKED_COLOR_CODEC
|
||||
.encode_start(
|
||||
&PackedColor {
|
||||
r: 100,
|
||||
g: 121,
|
||||
b: 89,
|
||||
a: 201
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::ByteArray(Box::new([100, 121, 89, 201]))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PACKED_COLOR_CODEC
|
||||
.encode_start(
|
||||
&PackedColor {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 255
|
||||
},
|
||||
&INSTANCE
|
||||
)
|
||||
.expect("Encoding should succeed"),
|
||||
NbtTag::ByteArray(Box::new([0, 0, 0, 255]))
|
||||
);
|
||||
|
||||
// Decoding
|
||||
|
||||
assert_eq!(
|
||||
PACKED_COLOR_CODEC
|
||||
.parse(NbtTag::ByteArray(Box::new([100, 121, 89, 201])), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
PackedColor {
|
||||
r: 100,
|
||||
g: 121,
|
||||
b: 89,
|
||||
a: 201
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PACKED_COLOR_CODEC
|
||||
.parse(NbtTag::ByteArray(Box::new([255, 255, 0])), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
PackedColor {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 0,
|
||||
a: 255
|
||||
}
|
||||
);
|
||||
|
||||
assert!(
|
||||
PACKED_COLOR_CODEC
|
||||
.parse(NbtTag::ByteArray(Box::new([120])), &INSTANCE)
|
||||
.get_message()
|
||||
.expect("Decoding should fail")
|
||||
.starts_with("Invalid byte buffer for color")
|
||||
);
|
||||
|
||||
// Even other number array types will work.
|
||||
assert_eq!(
|
||||
PACKED_COLOR_CODEC
|
||||
.parse(NbtTag::IntArray(vec![1, 2, 3, 4]), &INSTANCE)
|
||||
.expect("Decoding should succeed"),
|
||||
PackedColor {
|
||||
r: 1,
|
||||
g: 2,
|
||||
b: 3,
|
||||
a: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user