feat: Implement basic codecs and DynamicOps implementations from DataFixerUpper (#1483)

* initial commit

# Conflicts:
#	Cargo.toml

* implemented more `DataResult` methods

* finished data result struct

* fixed clippy errors

* implemented `map_like` and more `dynamic_ops` methods

* finished most other `DynamicOps` methods

* Fixed doc in get_bytes

* added encoder and decoder files

* fixed some stuff and added primitive codecs

* added some primitive codecs (has errors for now)

* changed the "stream" ops methods

* reworked types of the coders and codecs, added list codecs

* added JSON ops

* fixed list codecs, added some doc test examples in `JsonOps`

* added lazy codecs

* added map encoders, decoders and struct builders

* actually added mapdecoder

* added encoder and decoder transformations

* fixed encoder and decoder transformations and added transformation functions and range codecs

* added field encoders and decoders and separated range codecs

* added field encoders and decoders and separated range codecs

* added BaseMapCodec and SimpleMapCodec

* added struct codecs and moved tests

* added `NbtOps`, unsigned number codec types, range codec tests, a struct test, detailed `Codec` documentation

* added optional field map codecs, the validator function and reworked struct builder functions

* added unbounded map and struct codec tests and reworked validated codecs

* did some visibility refactoring

* fixed some stuff and renamed unsigned number codecs

* fixed formatting

* fixed docs to reflect changes on renaming unsigned number codecs

* fixed validated test

* fixed doctest

* moved base_map_codec and fixed docs about codecs

* fixed some mistakes in the code

* fixed some more mistakes in the code and converted some `DataResult::error` calls to `DataResult::partial_error`

* fixed formatting

* fixed some incorrect docs

* removed `OnceLock`s in `MapCodec`s so they can be used more easily

* fixed transformer codec descriptions, added more `NbtOps` tests and fixed some `NbtOps` list methods

* fixed doctest

* added a fourth specific `NbtOps` test for byte buffers (`ByteArray`s in NBT)

* fixed grammar mistake

* log messages for discarded keys in `create_map`

* use the `tracing` crate for logging

* added a method to allow creating a field using a `MapCodec` reference

* bifurcated `MapCodecCodec`s as well

* removed imports only for documentation and replaced them with intra-doc links

* added additional docs to `optional_field_with_default` and `lenient_optional_field_with_default` Codec methods for additional details on encoding

* renamed crate to `pumpkin-codecs` so that data fixing can be placed somewhere else not frequently used

* fixed some more errors and reworked `nbt_ops`'s `ListCollector` to be like 1.21.11, also allows &str in `DataResult`'s creation methods

* fixed invalid nbt tag for test

* reverted wrapping the tags for heterogeneous elements

* renamed function parameter in `DataResult::apply_2`

* removed unnecessary comment

* fixed a few more issues

* fixed formatting

* fixed another mistake for `DataResult::add_message`

* renamed `DataResult` factory methods to be more idiomatic and removed unnecessary `dyn` method

* removed unnecessary `clone()`

* updated Cargo.lock
This commit is contained in:
SomeYellowGuy
2026-03-24 13:37:51 +05:30
committed by GitHub
parent c6bf417773
commit 24f6e2732d
35 changed files with 7458 additions and 36 deletions

76
Cargo.lock generated
View File

@@ -924,9 +924,9 @@ dependencies = [
[[package]]
name = "crypto-bigint"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f438b626cb7c9dd48a613a9826e6bad9db71097f9d628f7237af2f6bc13c0ec"
checksum = "9fde2467e74147f492aebb834985186b2c74761927b8b9b3bd303bcb2e72199d"
dependencies = [
"cpubits",
"ctutils",
@@ -961,7 +961,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a"
dependencies = [
"crypto-bigint 0.7.0",
"crypto-bigint 0.7.1",
"libm",
"rand_core 0.10.0",
]
@@ -1415,19 +1415,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"r-efi",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"r-efi",
"rand_core 0.10.0",
"wasip2",
"wasip3",
@@ -1898,9 +1898,9 @@ checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
[[package]]
name = "ipnet"
version = "2.11.0"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "itertools"
@@ -1958,9 +1958,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.91"
version = "0.3.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -2008,11 +2008,10 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.12"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
"bitflags",
"libc",
]
@@ -2471,9 +2470,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
@@ -2737,6 +2736,15 @@ dependencies = [
"syn",
]
[[package]]
name = "pumpkin-codecs"
version = "0.1.0-dev+1.21.11"
dependencies = [
"dashmap",
"serde_json",
"tracing",
]
[[package]]
name = "pumpkin-config"
version = "0.1.0-dev+1.21.11"
@@ -2791,9 +2799,11 @@ dependencies = [
"bytes",
"cesu8",
"flate2",
"pumpkin-codecs",
"serde",
"tempfile",
"thiserror 2.0.18",
"tracing",
]
[[package]]
@@ -2902,12 +2912,6 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -2936,7 +2940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"getrandom 0.4.1",
"rand_core 0.10.0",
]
@@ -3088,7 +3092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ed3e93fc7e473e464b9726f4759659e72bc8665e4b8ea227547024f416d905"
dependencies = [
"const-oid 0.10.2",
"crypto-bigint 0.7.0",
"crypto-bigint 0.7.1",
"crypto-primes",
"digest 0.11.0",
"pkcs1",
@@ -3615,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom 0.4.1",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
@@ -3749,9 +3753,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
@@ -4130,7 +4134,7 @@ version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"getrandom 0.4.1",
"js-sys",
"md-5",
"serde_core",
@@ -4194,9 +4198,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
version = "0.2.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866"
dependencies = [
"cfg-if",
"once_cell",
@@ -4207,9 +4211,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
version = "0.2.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4217,9 +4221,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
version = "0.2.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -4230,9 +4234,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
version = "0.2.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53"
dependencies = [
"unicode-ident",
]

View File

@@ -11,6 +11,7 @@ members = [
"pumpkin/",
"pumpkin-data",
"pumpkin-plugin-api",
"pumpkin-codecs"
]
exclude = ["pumpkin-codegen"]
@@ -139,6 +140,7 @@ p384 = "0.13.1"
phf = "0.13.1"
pkcs8 = "=0.11.0-rc.11"
proc-macro2 = "1.0"
pumpkin-codecs = { path = "pumpkin-codecs" }
pumpkin-config = { path = "pumpkin-config" }
pumpkin-data = { path = "pumpkin-data" }
pumpkin-inventory = { path = "pumpkin-inventory" }

13
pumpkin-codecs/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "pumpkin-codecs"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
serde_json.workspace = true
dashmap.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,85 @@
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})"))
}
}

579
pumpkin-codecs/src/codec.rs Normal file
View File

@@ -0,0 +1,579 @@
use crate::HasValue;
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.**
///
/// # 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
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)
}
// 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());
}};
}

View File

@@ -0,0 +1,47 @@
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),
}
}

View File

@@ -0,0 +1,269 @@
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);
}
}
}

View File

@@ -0,0 +1,53 @@
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))
}
}

View File

@@ -0,0 +1,7 @@
pub mod lazy;
pub mod list;
pub mod map_codec;
pub mod primitive;
pub mod range;
pub mod unbounded_map;
pub mod validated;

View File

@@ -0,0 +1,208 @@
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);
}
}

View File

@@ -0,0 +1,176 @@
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
);
}
}

View File

@@ -0,0 +1,216 @@
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
);
}
}

View File

@@ -0,0 +1,164 @@
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
);
}
}

View File

@@ -0,0 +1,192 @@
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)
}

View File

@@ -0,0 +1,635 @@
use crate::lifecycle::Lifecycle;
/// Collects the partial value and message from a `DataResult` if it is an error.
/// Returns an [`Option`] of the provided `DataResult`.
/// - The partial value is stored into `$partial_name`.
/// - If a message is found, it is pushed to `$messages_vec`.
macro_rules! collect_partial_and_message {
($partial_name:ident, $result:ident, $messages_vec:ident) => {
let $partial_name = match $result {
DataResult::Success { result, .. } => Some(result),
DataResult::Error {
message,
partial_result,
..
} => {
$messages_vec.push(message);
partial_result
}
};
};
}
/// A macro to generate a function to apply a function to each result of `n` `DataResult`s.
macro_rules! impl_apply {
(@internal_method $self:ident $f:ident $($result:ident),+) => {
let result_1 = $self;
if !(result_1.is_error() $(|| $result.is_error())+) {
// All n results are successful.
return DataResult::new_success($f(
result_1.into_result().unwrap()
$( , $result.into_result().unwrap() )+
));
}
let mut messages: Vec<String> = vec![];
// Collect any found errors.
collect_partial_and_message!(partial_1, result_1, messages);
$( collect_partial_and_message!($result, $result, messages); )+
return DataResult::new_option_error_with_lifecycle(
messages.join("; "),
match (partial_1, $($result, )+) {
(Some(result_1), $(Some($result), )+) => Some($f(result_1 $(, $result )+)),
_ => None,
},
Lifecycle::Experimental,
);
};
($name:ident, $n:literal, $($ty:ident, $result:ident),+) => {
#[doc = concat!("Applies a function to each result of ", stringify!($n), " `DataResult`s of different types.")]
///
/// - If any of the given results is a non-result, the returned result will also be a non-result.
/// - Any errors found in error results (non-result or partial result) will be added to the returned result.
/// - If all results are at least partial, `f` is called, which should return the final item to be wrapped in the returned result.
///
/// The returned result is a *success* **if and only if** all provided results are successes as well.
pub fn $name<$($ty,)+ T>(
self,
f: impl FnOnce(R $(,$ty)+) -> T
$(, $result: DataResult<$ty>)+
) -> DataResult<T> {
impl_apply!(@internal_method self f $($result),+);
}
};
(expect $name:ident, $n:literal, $($ty:ident, $result:ident),+) => {
#[doc = concat!("Applies a function to each result of ", stringify!($n), " `DataResult`s of different types.")]
///
/// - If any of the given results is a non-result, the returned result will also be a non-result.
/// - Any errors found in error results (non-result or partial result) will be added to the returned result.
/// - If all results are at least partial, `f` is called, which should return the final item to be wrapped in the returned result.
///
/// The returned result is a *success* **if and only if** all provided results are successes as well.
#[expect(clippy::too_many_arguments)]
pub fn $name<$($ty,)+ T>(
self,
f: impl FnOnce(R $(,$ty)+) -> T
$(, $result: DataResult<$ty>)+
) -> DataResult<T> {
impl_apply!(@internal_method self f $($result),+);
}
};
}
/// A result that can either represent a successful result, or a
/// *partial* or non-result with an error.
///
/// `R` is the type of result stored.
#[derive(Clone, Debug)]
#[must_use]
pub enum DataResult<R> {
/// Contains a complete result and has no error.
Success { result: R, lifecycle: Lifecycle },
/// Contains no or a partial result and has an error.
/// The error is a *format string*.
Error {
partial_result: Option<R>,
lifecycle: Lifecycle,
message: String,
},
}
impl<R> DataResult<R> {
/// Returns this `DataResult`'s lifecycle.
pub const fn lifecycle(&self) -> Lifecycle {
match self {
Self::Success { lifecycle, .. } | Self::Error { lifecycle, .. } => *lifecycle,
}
}
/// Sets this `DataResult`'s lifecycle and returns a new result.
pub fn with_lifecycle(self, new_lifecycle: Lifecycle) -> Self {
match self {
Self::Success { result, .. } => Self::Success {
result,
lifecycle: new_lifecycle,
},
Self::Error {
partial_result,
message,
..
} => Self::Error {
partial_result,
message,
lifecycle: new_lifecycle,
},
}
}
/// Adds another `Lifecycle` to this `DataResult`'s lifecycle and returns the new result.
pub fn add_lifecycle(self, added_lifecycle: Lifecycle) -> Self {
let new_lifecycle = self.lifecycle().add(added_lifecycle);
self.with_lifecycle(new_lifecycle)
}
/// Returns a *successful* `DataResult` with an experimental lifecycle.
#[inline]
pub const fn new_success(result: R) -> Self {
Self::new_success_with_lifecycle(result, Lifecycle::Experimental)
}
/// Returns a *successful* `DataResult` with a given lifecycle.
#[inline]
pub const fn new_success_with_lifecycle(result: R, lifecycle: Lifecycle) -> Self {
Self::Success { result, lifecycle }
}
/// Returns an *errored* `DataResult` with no result and an experimental lifecycle.
#[inline]
pub fn new_error(error: impl Into<String>) -> Self {
Self::new_error_with_lifecycle(error.into(), Lifecycle::Experimental)
}
/// Returns an *errored* `DataResult` with a partial result and an experimental lifecycle.
#[inline]
pub fn new_partial_error(error: impl Into<String>, partial_result: R) -> Self {
Self::new_partial_error_with_lifecycle(
error.into(),
partial_result,
Lifecycle::Experimental,
)
}
/// Returns an *errored* `DataResult` with no result and a given lifecycle.
#[inline]
pub fn new_error_with_lifecycle<T>(
message: impl Into<String>,
lifecycle: Lifecycle,
) -> DataResult<T> {
DataResult::Error {
partial_result: None,
lifecycle,
message: message.into(),
}
}
/// Returns an *errored* `DataResult` with a partial result and a given lifecycle.
#[inline]
pub fn new_partial_error_with_lifecycle(
message: impl Into<String>,
partial_result: R,
lifecycle: Lifecycle,
) -> Self {
Self::Error {
partial_result: Some(partial_result),
lifecycle,
message: message.into(),
}
}
/// Returns an *errored* `DataResult` with result [`Option<R>`] and a given lifecycle.
#[inline]
const fn new_option_error_with_lifecycle(
message: String,
partial_result: Option<R>,
lifecycle: Lifecycle,
) -> Self {
Self::Error {
partial_result,
lifecycle,
message,
}
}
/// Tries to get a complete result from this `DataResult`. If no such result exists, this returns [`None`] (even for partial results).
///
/// To allow partial results, use [`DataResult::into_result_or_partial`].
#[inline]
pub fn into_result(self) -> Option<R> {
if let Self::Success { result, .. } = self {
Some(result)
} else {
None
}
}
/// Tries to get a complete or partial result. If no such result exists, this returns [`None`].
pub fn into_result_or_partial(self) -> Option<R> {
match self {
Self::Success { result, .. } => Some(result),
Self::Error { partial_result, .. } => partial_result,
}
}
/// Tries to get a complete or partial result as a reference. If no such result exists, this returns [`None`].
pub const fn result_or_partial_as_ref(&self) -> Option<&R> {
match self {
Self::Success { result, .. } => Some(result),
Self::Error { partial_result, .. } => partial_result.as_ref(),
}
}
/// Tries to get a complete result from this `DataResult`. If no such result exists, this function panics.
pub fn unwrap(self) -> R {
self.expect("No complete result found for DataResult")
}
/// Tries to get a complete or partial result from this `DataResult`. If no such result exists, this function panics.
pub fn unwrap_or_partial(self) -> R {
self.expect_or_partial("No complete or partial result found for DataResult")
}
/// Tries to get a complete result from this `DataResult`. If no such result exists, this function panics with a custom message.
pub fn expect(self, message: &str) -> R {
self.into_result().unwrap_or_else(|| panic!("{}", message))
}
/// Tries to get a complete or partial result from this `DataResult`. If no such result exists, this function panics with a custom message.
pub fn expect_or_partial(self, message: &str) -> R {
self.into_result_or_partial()
.unwrap_or_else(|| panic!("{}", message))
}
/// Returns whether this `DataResult` has a complete or partial result.
pub const fn has_result_or_partial(&self) -> bool {
!matches!(
self,
Self::Error {
partial_result: None,
..
}
)
}
/// Appends two messages to form a bigger one.
/// This is useful for stacking message for data results with more than 1 error.
#[must_use]
pub fn append_messages(first: &str, second: &str) -> String {
format!("{first}; {second}")
}
/// Maps a `DataResult` of a type `R` to a `DataResult` of a type `T` by applying a function, leaving non-results untouched.
///
/// `f` is applied to complete results and partial ones. For partial results, `f` is applied to their partial value.
pub fn map<T>(self, op: impl FnOnce(R) -> T) -> DataResult<T> {
match self {
Self::Success { result, lifecycle } => {
DataResult::new_success_with_lifecycle(op(result), lifecycle)
}
Self::Error {
partial_result,
lifecycle,
message,
} => DataResult::new_option_error_with_lifecycle(
message,
partial_result.map(op),
lifecycle,
),
}
}
/// Maps a `DataResult` of a type `R` to a type `T`.
/// - If there is a complete result, `f` (the result function) is called with that result.
/// - Otherwise, if there is an error, `default` (the error function) is called with the error as the parameter.
pub fn map_or_else<T>(self, default: impl FnOnce(Self) -> T, f: impl Fn(R) -> T) -> T {
match self {
Self::Success { result, .. } => f(result),
Self::Error { .. } => default(self),
}
}
/// Chains a `DataResult` with another function taking a `DataResult`.
/// - If there is a complete or partial result, `f` is called with that result, and the value returned by `f` is returned.
/// For a partial result, new messages are propagated via concatenation.
/// - Otherwise, if there is an error with no result, this propagates this error `DataResult`.
///
/// In other words, `f` will process the complete or partial result of this `DataResult` (if any), appending errors if necessary.
///
/// The name of this function is equivalent to `and_then`.
pub fn flat_map<T>(self, f: impl FnOnce(R) -> DataResult<T>) -> DataResult<T> {
match self {
Self::Success { result, lifecycle } => {
// Add this DataResult's lifecycle to the new DataResult.
f(result).add_lifecycle(lifecycle)
}
Self::Error {
partial_result,
lifecycle,
message,
} => {
if let Some(result) = partial_result {
// Try mapping the internal partial value.
let second_result = f(result);
let new_lifecycle = second_result.lifecycle().add(lifecycle);
match second_result {
DataResult::Success { result, .. } => {
DataResult::new_partial_error_with_lifecycle(
message,
result,
new_lifecycle,
)
}
DataResult::Error {
partial_result,
message: second_message,
..
} => DataResult::new_option_error_with_lifecycle(
Self::append_messages(&message, &second_message),
partial_result,
new_lifecycle,
),
}
} else {
// Return this same Error.
DataResult::Error {
partial_result: None,
lifecycle,
message,
}
}
}
}
}
/// Applies a function wrapped in a `DataResult` to the value wrapped in this `DataResult`.
pub fn apply<T>(self, function_result: DataResult<impl FnOnce(R) -> T>) -> DataResult<T> {
let lifecycle = self.lifecycle().add(function_result.lifecycle());
match (self, function_result) {
(Self::Success { result, .. }, DataResult::Success { result: f, .. }) => {
DataResult::new_success_with_lifecycle(f(result), lifecycle)
}
(
Self::Success { result, .. },
DataResult::Error {
partial_result,
message: func_message,
..
},
) => DataResult::new_option_error_with_lifecycle(
func_message,
partial_result.map(|f| f(result)),
lifecycle,
),
(
Self::Error {
partial_result,
message,
..
},
DataResult::Success { result: f, .. },
) => DataResult::new_option_error_with_lifecycle(
message,
partial_result.map(f),
lifecycle,
),
(
Self::Error {
partial_result,
message,
..
},
DataResult::Error {
partial_result: partial_func_result,
message: func_message,
..
},
) => DataResult::new_option_error_with_lifecycle(
Self::append_messages(&message, &func_message),
partial_result.and_then(|r| partial_func_result.map(|f| f(r))),
lifecycle,
),
}
}
/// Similar to [`Self::apply_2`], but this also marks the returned `DataResult` as [`Lifecycle::Stable`].
pub fn apply_2_and_make_stable<R2, T>(
self,
f: impl FnOnce(R, R2) -> T,
second_result: DataResult<R2>,
) -> DataResult<T> {
self.apply_2(f, second_result)
.with_lifecycle(Lifecycle::Stable)
}
impl_apply!(apply_2, 2, R2, second_result);
impl_apply!(apply_3, 3, R2, result_2, R3, result_3);
impl_apply!(apply_4, 4, R2, result_2, R3, result_3, R4, result_4);
impl_apply!(
apply_5, 5, R2, result_2, R3, result_3, R4, result_4, R5, result_5
);
impl_apply!(
apply_6, 6, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6
);
impl_apply!(expect apply_7, 7, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7);
impl_apply!(expect apply_8, 8, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8);
impl_apply!(expect apply_9, 9, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9);
impl_apply!(expect apply_10, 10, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10);
impl_apply!(expect apply_11, 11, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11);
impl_apply!(expect apply_12, 12, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11, R12, result_12);
impl_apply!(expect apply_13, 13, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11, R12, result_12, R13, result_13);
impl_apply!(expect apply_14, 14, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11, R12, result_12, R13, result_13, R14, result_14);
impl_apply!(expect apply_15, 15, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11, R12, result_12, R13, result_13, R14, result_14, R15, result_15);
impl_apply!(expect apply_16, 16, R2, result_2, R3, result_3, R4, result_4, R5, result_5, R6, result_6, R7, result_7, R8, result_8, R9, result_9, R10, result_10, R11, result_11, R12, result_12, R13, result_13, R14, result_14, R15, result_15, R16, result_16);
/// Applies a function to `DataResult` errors, leaving successes untouched.
/// This can be used to provide additional context to an error.
pub fn map_error(self, f: impl FnOnce(String) -> String) -> Self {
match self {
Self::Success { .. } => self,
Self::Error {
message,
lifecycle,
partial_result,
} => Self::new_option_error_with_lifecycle(f(message), partial_result, lifecycle),
}
}
/// Promotes a `DataResult` containing a partial result to a success `DataResult`, providing
/// the error message to a function `f` (consumer function) and removing it from the new `DataResult`.
/// `DataResult`s with no result or a complete result are left untouched.
pub fn promote_partial(self, f: impl FnOnce(String)) -> Self {
match self {
Self::Success { .. } => self,
Self::Error {
message,
lifecycle,
partial_result,
} => {
f(message.clone());
partial_result.map_or_else(
|| Self::new_error_with_lifecycle(message, lifecycle),
|result| Self::new_success_with_lifecycle(result, lifecycle),
)
}
}
}
/// Returns a `DataResult` with a new partial value (always partial), leaving `DataResult`s with a complete result untouched.
pub fn with_partial(self, partial_value: R) -> Self {
match self {
Self::Success { .. } => self,
Self::Error {
message, lifecycle, ..
} => Self::new_partial_error_with_lifecycle(message, partial_value, lifecycle),
}
}
/// Returns a `DataResult` with a new result/partial result, depending on the type of `DataResult` this is.
/// - For a complete result, this returns another `DataResult` whose complete result is `value`.
/// - For a partial result, this returns another `DataResult` whose partial result is `value`.
/// - For a non-result, this returns itself.
pub fn with_complete_or_partial<T>(self, value: T) -> DataResult<T> {
match self {
Self::Success { lifecycle, .. } => {
DataResult::new_success_with_lifecycle(value, lifecycle)
}
Self::Error {
message,
lifecycle,
partial_result: Some(_),
} => DataResult::new_partial_error_with_lifecycle(message, value, lifecycle),
Self::Error {
message, lifecycle, ..
} => Self::new_error_with_lifecycle(message, lifecycle),
}
}
/// Returns whether this `DataResult` was a success.
pub const fn is_success(&self) -> bool {
matches!(self, &Self::Success { .. })
}
/// Returns whether this `DataResult` was an error (including partial result errors).
pub const fn is_error(&self) -> bool {
!self.is_success()
}
/// Add a message of another `DataResult` (`other_result`) to this `DataResult`.
///
/// This is useful for *unit tuple* `DataResult`s used simply for final results of complex objects.
/// - If `other_result` is a complete result, nothing happens.
/// - If both results are partial, the returned result is also partial. Otherwise, it is a non-result.
/// - Messages found in any `DataResult` error are concatenated and used in the returned result.
///
/// This always returns a *stable* result.
pub fn add_message<T>(self, other_result: &DataResult<T>) -> Self {
match (self, other_result) {
// Both results are successful.
(Self::Success { result: r, .. }, DataResult::Success { .. }) => {
Self::new_success_with_lifecycle(r, Lifecycle::Stable)
}
// Both results are errors.
(
Self::Error {
partial_result: p1,
message: m1,
..
},
DataResult::Error {
partial_result: p2,
message: m2,
..
},
) => Self::new_option_error_with_lifecycle(
Self::append_messages(&m1, m2),
if p1.is_some() && p2.is_some() {
p1
} else {
None
},
Lifecycle::Stable,
),
// Exactly one of both results is an error.
(
Self::Error {
message: m1,
partial_result,
..
},
_,
) => Self::new_option_error_with_lifecycle(m1, partial_result, Lifecycle::Stable),
(
Self::Success { result, .. },
DataResult::Error {
message: m2,
partial_result,
..
},
) => Self::new_option_error_with_lifecycle(
m2.clone(),
partial_result.is_some().then_some(result),
Lifecycle::Stable,
),
}
}
/// Tries to add errors from the given `result` and adds them to `self`
/// if `self` is not already an error result.
///
/// The [`Lifecycle`] of the returned `DataResult` is the addition of both results.
pub fn with_errors_from<T>(self, result: &DataResult<T>) -> Self {
match (&self, result) {
// If both values are successes, do nothing.
// If `self` is already an error result, do nothing.
(Self::Success { .. }, DataResult::Success { .. }) | (Self::Error { .. }, _) => self,
(
Self::Success { .. },
DataResult::Error {
message,
lifecycle: other_lifecycle,
..
},
) => {
let self_lifecycle = self.lifecycle();
if let Self::Success { result, .. } = self {
Self::new_partial_error_with_lifecycle(
message.clone(),
result,
self_lifecycle.add(*other_lifecycle),
)
} else {
unreachable!()
}
}
}
}
/// Returns the message of this `DataResult`, if any. Only error results have messages.
pub fn get_message(self) -> Option<String> {
match self {
Self::Success { .. } => None,
Self::Error { message, .. } => Some(message),
}
}
}
// Assertion functions
/// Asserts that the `$left` `DataResult` is a complete result (success) whose stored result is `$right`.
#[macro_export]
macro_rules! assert_success {
($left:expr, $right:expr $(,)?) => {{
let result = $left;
assert!(
result.is_success(),
"Expected a `DataResult` success, got: {:?}",
result
);
assert_eq!(
result.unwrap(),
$right,
"`DataResult` was successful but the value doesn't match"
);
}};
}
impl<T> Default for DataResult<T> {
fn default() -> Self {
Self::new_error("Default DataResult")
}
}

View File

@@ -0,0 +1,357 @@
use crate::Number;
use crate::data_result::DataResult;
use crate::list_builder::{ListBuilder, new_list_builder_impl};
use crate::map_like::MapLike;
use crate::struct_builder::ResultStructBuilder;
use std::{collections::HashMap, fmt::Display};
/// Generates a default implementation for a create_*number* function.
macro_rules! create_number_impl {
($name:ident, $ty:ty, $number_ty:ident, $func:ident) => {
/// Returns how a generic
#[doc = concat!("`", stringify!($name), "`")]
/// in Java (equivalent to
#[doc = concat!("[`", stringify!($ty), "`])")]
/// is represented by this `DynamicOps`.
fn $func(&self, data: $ty) -> Self::Value {
self.create_number(Number::$number_ty(data))
}
};
}
/// A macro providing a default implementation of a get_... function in `DynamicOps`.
///
/// Those functions include:
/// - [`DynamicOps::get_byte_buffer`] (put `box` before the target expression (usually self))
/// - [`DynamicOps::get_int_list`]
/// - [`DynamicOps::get_long_list`]
#[macro_export]
macro_rules! impl_get_list {
(box $target:expr, $input:expr, $ty:literal) => {
$target.get_iter($input).flat_map(|iter| {
// We want all elements in the iterator to be numbers.
iter.map(|e| $target.get_number(&e).into_result().map(Into::into))
.collect::<Option<Vec<_>>>()
.map_or_else(
|| DataResult::new_error(concat!("Some elements are not ", $ty)),
|v| DataResult::new_success(v.into_boxed_slice()),
)
})
};
($target:expr, $input:expr, $ty:literal) => {
$target.get_iter($input).flat_map(|iter| {
// We want all elements in the iterator to be numbers.
iter.map(|e| $target.get_number(&e).into_result().map(Into::into))
.collect::<Option<Vec<_>>>()
.map_or_else(
|| DataResult::new_error(concat!("Some elements are not ", $ty)),
DataResult::new_success,
)
})
};
}
/// A trait describing methods to read and write a specific format (like NBT or JSON).
/// The `Value` of this trait is the type that can be used to represent anything in this format.
pub trait DynamicOps {
type Value: PartialEq + Display + Clone;
type StructBuilder: ResultStructBuilder<Value = Self::Value>;
/// Returns how an empty value is represented by this `DynamicOps`.
fn empty(&self) -> Self::Value;
/// Returns how an empty list is represented by this `DynamicOps`.
fn empty_list(&self) -> Self::Value {
self.create_list(vec![])
}
/// Returns how an empty map is represented by this `DynamicOps`.
fn empty_map(&self) -> Self::Value {
self.create_map(HashMap::new())
}
/// Returns how a generic number is represented by this `DynamicOps`.
fn create_number(&self, n: Number) -> Self::Value;
create_number_impl!(byte, i8, Byte, create_byte);
create_number_impl!(short, i16, Short, create_short);
create_number_impl!(int, i32, Int, create_int);
create_number_impl!(long, i64, Long, create_long);
create_number_impl!(float, f32, Float, create_float);
create_number_impl!(double, f64, Double, create_double);
/// Returns how a boolean is represented by this `DynamicOps`.
fn create_bool(&self, data: bool) -> Self::Value {
self.create_byte(i8::from(data))
}
/// Returns how a string is represented by this `DynamicOps`.
fn create_string(&self, data: &str) -> Self::Value;
/// Returns how a list is represented by this `DynamicOps`.
fn create_list<I>(&self, values: I) -> Self::Value
where
I: IntoIterator<Item = Self::Value>;
/// Returns how a map is represented by this `DynamicOps`.
fn create_map<I>(&self, entries: I) -> Self::Value
where
I: IntoIterator<Item = (Self::Value, Self::Value)>;
/// Tries to get a `bool` represented by this `DynamicOps`.
fn get_bool(&self, input: &Self::Value) -> DataResult<bool> {
self.get_number(input).map(|n| i8::from(n) != 0)
}
/// Tries to get a number represented by this `DynamicOps`.
fn get_number(&self, input: &Self::Value) -> DataResult<Number>;
/// Tries to get a string represented by this `DynamicOps`.
fn get_string(&self, input: &Self::Value) -> DataResult<String>;
/// Gets an [`Iterator`] of key-value pairs from a map represented by this `DynamicOps`.
/// This is only applicable to map-like values.
fn get_map_iter<'a>(
&'a self,
input: &'a Self::Value,
) -> DataResult<impl Iterator<Item = (Self::Value, &'a Self::Value)> + 'a>;
/// Tries to get a [`MapLike`] for a map represented by this `DynamicOps`.
fn get_map<'a>(
&self,
input: &'a Self::Value,
) -> DataResult<impl MapLike<Value = Self::Value> + 'a>;
/// Gets an [`Iterator`] from a generic value represented by this `DynamicOps`.
/// This is the equivalent of DFU's `getStream()` function, and is only applicable to list-like values.
fn get_iter(&self, input: Self::Value) -> DataResult<impl Iterator<Item = Self::Value>>;
/// Gets a `Box<[u8]>` (byte buffer) from a generic value represented by this `DynamicOps`.
/// This is the equivalent of DFU's `getByteBuffer()` function.
fn get_byte_buffer(&self, input: Self::Value) -> DataResult<Box<[u8]>> {
impl_get_list!(box self, input, "bytes")
}
/// Creates a byte buffer that can be represented by this `DynamicOps` using a [`Vec<u8>`].
fn create_byte_buffer(&self, buffer: Vec<u8>) -> Self::Value {
self.create_list(buffer.iter().map(|b| self.create_byte(*b as i8)))
}
/// Gets a [`Vec<i32>`] (`int` list) from a generic value represented by this `DynamicOps`.
/// This is the equivalent of DFU's `getIntStream()` function.
fn get_int_list(&self, input: Self::Value) -> DataResult<Vec<i32>> {
impl_get_list!(self, input, "ints")
}
/// Creates an `int` list ([`Vec<i32>`]) that can be represented by this `DynamicOps`.
fn create_int_list(&self, vec: Vec<i32>) -> Self::Value {
self.create_list(vec.into_iter().map(|i| self.create_int(i)))
}
/// Gets a [`Vec<i64>`] (`long` list) from a generic value represented by this `DynamicOps`.
/// This is the equivalent of DFU's `getLongStream()` function.
fn get_long_list(&self, input: Self::Value) -> DataResult<Vec<i64>> {
impl_get_list!(self, input, "longs")
}
/// Creates a `long` list ([`Vec<i64>`]) that can be represented by this `DynamicOps`.
fn create_long_list(&self, vec: Vec<i64>) -> Self::Value {
self.create_list(vec.into_iter().map(|l| self.create_long(l)))
}
/// Merges a value represented by this `DynamicOps` to a list represented by this `DynamicOps`.
/// This is only valid if `list` is an actual list.
fn merge_into_list(&self, list: Self::Value, value: Self::Value) -> DataResult<Self::Value>;
/// Merges a list of values represented by this `DynamicOps` into another such list.
/// This is only valid if `list` is an actual list.
fn merge_values_into_list<I>(&self, list: Self::Value, values: I) -> DataResult<Self::Value>
where
I: IntoIterator<Item = Self::Value>,
{
let mut result = DataResult::new_success(list);
for value in values {
result = result.flat_map(|list_value| self.merge_into_list(list_value, value));
}
result
}
/// Adds a key-value pair, both represented by this `DynamicOps`, to a map also represented by this `DynamicOps`,
/// returning the new map. This is only valid if `map` is an actual map or is empty.
fn merge_into_map(
&self,
map: Self::Value,
key: Self::Value,
value: Self::Value,
) -> DataResult<Self::Value>
where
Self::Value: Clone;
/// Merges a map represented by this `DynamicOps` into another such map, returning the new map.
/// This is only valid if `map` is an actual map or is empty.
fn merge_entries_into_map<I>(&self, map: Self::Value, entries: I) -> DataResult<Self::Value>
where
I: IntoIterator<Item = (Self::Value, Self::Value)>,
Self::Value: Clone,
{
let mut result = DataResult::new_success(map);
for (key, value) in entries {
result = result.flat_map(|list_value| self.merge_into_map(list_value, key, value));
}
result
}
/// Merges a [`MapLike`] represented by this `DynamicOps` into another such map, returning the new map.
/// This is only valid if `map` is an actual map or is empty.
fn merge_map_like_into_map<M>(
&self,
map: Self::Value,
other_map_like: M,
) -> DataResult<Self::Value>
where
M: MapLike<Value = Self::Value>,
Self::Value: Clone,
{
let mut result = DataResult::new_success(map);
for (key, value) in other_map_like.iter() {
result =
result.flat_map(|list_value| self.merge_into_map(list_value, key, value.clone()));
}
result
}
/// Merges a value represented by this `DynamicOps` into a primitive type.
fn merge_into_primitive(
&self,
prefix: Self::Value,
value: Self::Value,
) -> DataResult<Self::Value>
where
<Self as DynamicOps>::Value: PartialEq,
{
if prefix == self.empty() {
DataResult::new_success(value)
} else {
DataResult::new_error(format!(
"Do not know how to append a primitive value {value} to {prefix}"
))
}
}
/// Tries to remove something from a value represented by this `DynamicOps` using a key.
/// 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> {
self.get_element_generic(input, &self.create_string(key))
}
/// Tries to get a value from a value represented by this `DynamicOps` using a key also represented by this `DynamicOps`.
fn get_element_generic<'a>(
&'a self,
input: &'a Self::Value,
key: &Self::Value,
) -> DataResult<&'a Self::Value>
where {
self.get_map_iter(input).flat_map(|mut iter| {
iter.find(|(k, _)| k == key).map_or_else(
|| DataResult::new_error(format!("No element {key} in the map")),
|(_, v)| DataResult::new_success(v),
)
})
}
/// Tries to set a value represented by this `DynamicOps` to a key to a map also represented by this `DynamicOps`.
/// - It this was successful, this returns the new map value.
/// - Otherwise, this simply returns `input`.
fn set_element(&self, input: &Self::Value, key: &str, value: Self::Value) -> Self::Value
where
Self::Value: Clone,
{
self.merge_into_map(input.clone(), self.create_string(key), value)
.into_result()
.unwrap_or(input.clone())
}
/// Tries to update a value represented by this `DynamicOps` of a map also represented by this `DynamicOps`, with
/// a key and a mapper function (`f`) whose return value will be the new value of the specified key.
/// - It this was successful, this returns the newly manipulated map.
/// - Otherwise, this simply returns `input`.
fn update_element<F>(&self, input: &Self::Value, key: &str, f: F) -> Self::Value
where
F: FnOnce(&Self::Value) -> Self::Value,
{
self.get_element(input, key)
.map(|v| self.set_element(input, key, f(v)))
.into_result()
.unwrap_or(input.clone())
}
/// Tries to update a value represented by this `DynamicOps` of a map also represented by this `DynamicOps`, with
/// a key also represented by this `DynamicOps` and a mapper function (`f`) whose return value will be the new key's value.
/// - It this was successful, this returns the newly manipulated map.
/// - Otherwise, this simply returns `input`.
fn update_element_generic<F>(&self, input: &Self::Value, key: &Self::Value, f: F) -> Self::Value
where
F: FnOnce(&Self::Value) -> Self::Value,
{
self.get_element_generic(input, key)
.flat_map(|v| self.merge_into_map(input.clone(), key.clone(), f(v)))
.into_result()
.unwrap_or(input.clone())
}
/// Converts a value represented by this `DynamicOps` to another value represented by another `DynamicOps`.
fn convert_to<U>(&self, out_ops: &impl DynamicOps<Value = U>, input: Self::Value) -> U;
/// Converts a list represented by this `DynamicOps` to another list represented by another `DynamicOps`.
fn convert_list<U>(&self, out_ops: &impl DynamicOps<Value = U>, input: Self::Value) -> U {
out_ops.create_list(
self.get_iter(input)
.into_result()
.into_iter()
.flatten()
.map(|v| self.convert_to(out_ops, v)),
)
}
/// Converts a map represented by this `DynamicOps` to another map represented by another `DynamicOps`.
fn convert_map<U>(&self, out_ops: &impl DynamicOps<Value = U>, input: Self::Value) -> U {
out_ops.create_map(
self.get_map_iter(&input)
.into_result()
.into_iter()
.flatten()
.map(|(k, v)| {
(
self.convert_to(out_ops, k),
self.convert_to(out_ops, v.clone()),
)
}),
)
}
/// Returns a [`ListBuilder`] for this `DynamicOps`.
fn list_builder(&'static self) -> impl ListBuilder<Value = Self::Value>
where
Self: Sized,
{
new_list_builder_impl(self)
}
/// Returns a [`MapBuilder`] for this `DynamicOps`.
fn map_builder(&'static self) -> Self::StructBuilder;
}

View File

@@ -0,0 +1,423 @@
use std::fmt::{Debug, Display};
use crate::{Number, data_result::DataResult, dynamic_ops::DynamicOps, map_like::MapLike};
use crate::lifecycle::Lifecycle;
use crate::struct_builder::{ResultStructBuilder, StringStructBuilder, StructBuilder};
use crate::{impl_string_struct_builder, impl_struct_builder};
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 };
impl JsonOps {
/// A function to get a JSON value as a string, similar to Google's GSON's `getAsString()` method for `JsonElement`.
/// This is to keep parity with the `JsonOps` methods that check for `compressed`.
///
/// In particular, this method may return `Some` for *ONLY* the following:
/// - Booleans (always)
/// - Numbers (always)
/// - Strings (always)
/// - Arrays with exactly 1 element (in this case, this is called for that element).
///
/// Any other case returns `None`.
fn get_as_string(input: &Value) -> Option<String> {
match input {
Value::Array(elements) => {
// If we have an array, it must only have 1 element.
if elements.len() == 1 {
Self::get_as_string(&elements[0])
} else {
None
}
}
Value::Bool(b) => Some(b.to_string()),
Value::Number(n) => Some(n.to_string()),
Value::String(s) => Some(s.clone()),
_ => None,
}
}
/// 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(_))
}
}
}
impl DynamicOps for JsonOps {
type Value = Value;
type StructBuilder = JsonStructBuilder;
fn empty(&self) -> Self::Value {
Value::Null
}
fn create_number(&self, n: Number) -> Self::Value {
n.into()
}
fn create_bool(&self, data: bool) -> Self::Value {
Value::Bool(data)
}
fn create_string(&self, data: &str) -> Self::Value {
Value::String(data.to_owned())
}
fn create_list<I>(&self, values: I) -> Self::Value
where
I: IntoIterator<Item = Self::Value>,
{
Value::Array(values.into_iter().collect())
}
fn create_map<I>(&self, entries: I) -> Self::Value
where
I: IntoIterator<Item = (Self::Value, Self::Value)>,
{
Value::Object(
entries
.into_iter()
.filter_map(|(k, v)| Self::get_as_string(&k).map(|k| (k, v)))
.collect(),
)
}
fn get_bool(&self, input: &Self::Value) -> DataResult<bool> {
if let Value::Bool(b) = input {
DataResult::new_success(*b)
} else {
DataResult::new_error(format!("Not a boolean: {input}"))
}
}
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}"));
}
}
_ => {}
}
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.
DataResult::new_success(Self::get_as_string(input).unwrap())
} else {
DataResult::new_error(format!("Not a string: {input}"))
}
}
fn get_map_iter<'a>(
&self,
input: &'a Self::Value,
) -> DataResult<impl Iterator<Item = (Self::Value, &'a Self::Value)> + 'a> {
if let Value::Object(map) = input {
DataResult::new_success(map.iter().map(|(k, v)| (Value::String(k.clone()), v)))
} else {
DataResult::new_error(format!("Not a JSON object: {input}"))
}
}
fn get_map<'a>(
&self,
input: &'a Self::Value,
) -> DataResult<impl MapLike<Value = Self::Value> + 'a> {
if let Value::Object(map) = input {
DataResult::new_success(JsonMapLike { map })
} else {
DataResult::new_error(format!("Not a JSON object: {input}"))
}
}
fn get_iter(&self, input: Self::Value) -> DataResult<impl Iterator<Item = Self::Value>> {
// This only works for JSON arrays.
if let Value::Array(list) = input {
DataResult::new_success(list.into_iter())
} else {
DataResult::new_error(format!("Not a JSON array: {input}"))
}
}
fn merge_into_list(&self, list: Self::Value, value: Self::Value) -> DataResult<Self::Value> {
if matches!(list, Value::Array(_)) || list == self.empty() {
let mut result_vec = vec![];
if let Value::Array(a) = list {
result_vec.extend(a);
}
result_vec.push(value);
DataResult::new_success(Value::Array(result_vec))
} else {
DataResult::new_partial_error(format!("Not a list: {list}"), list)
}
}
fn merge_values_into_list<I>(&self, list: Self::Value, values: I) -> DataResult<Self::Value>
where
I: IntoIterator<Item = Self::Value>,
{
if matches!(list, Value::Array(_)) || list == self.empty() {
let mut result_vec = vec![];
if let Value::Array(a) = list {
result_vec.extend(a);
}
result_vec.extend(values);
DataResult::new_success(Value::Array(result_vec))
} else {
DataResult::new_partial_error(format!("Not a list: {list}"), list)
}
}
fn merge_into_map(
&self,
map: Self::Value,
key: Self::Value,
value: Self::Value,
) -> DataResult<Self::Value>
where
Self::Value: Clone,
{
if !matches!(map, Value::Object(_)) && map != self.empty() {
return DataResult::new_partial_error(format!("Not a map: {map}"), map);
}
if !self.is_valid_key(&key) {
return DataResult::new_partial_error(format!("Key is not a string: {key}"), map);
}
let mut output_map = Map::new();
if let Value::Object(mut m) = map {
output_map.append(&mut m);
}
output_map.insert(Self::get_as_string(&key).unwrap(), value);
DataResult::new_success(Value::Object(output_map))
}
fn merge_map_like_into_map<M>(
&self,
map: Self::Value,
other_map_like: M,
) -> DataResult<Self::Value>
where
M: MapLike<Value = Self::Value>,
Self::Value: Clone,
{
if matches!(map, Value::Object(_)) || map == self.empty() {
let mut output_map = Map::new();
if let Value::Object(mut m) = map {
output_map.append(&mut m);
}
// Store the missed entries.
let mut missed = vec![];
for entry in other_map_like.iter() {
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);
}
}
let object = Value::Object(output_map);
let pretty_missed = serde_json::to_string_pretty(&missed);
if missed.is_empty() {
DataResult::new_success(object)
} else {
DataResult::new_partial_error(
format!(
"Some keys are not strings{}",
pretty_missed.map_or_else(|_| String::new(), |r| format!(": {r}"))
),
object,
)
}
} else {
DataResult::new_partial_error(format!("Not a map: {map}"), map)
}
}
fn remove(&self, input: Self::Value, key: &str) -> Value {
if let Value::Object(m) = input {
Value::Object(m.into_iter().filter(|(k, _)| k != key).collect())
} else {
input
}
}
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(),
Value::Bool(b) => out_ops.create_bool(b),
Value::String(s) => out_ops.create_string(&s),
Value::Array(_) => self.convert_list(out_ops, input),
Value::Object(_) => self.convert_map(out_ops, input),
Value::Number(n) => {
// First, check for possible integers.
if let Some(l) = n.as_i64() {
if (l as i8) as i64 == l {
return out_ops.create_byte(l as i8);
} else if (l as i16) as i64 == l {
return out_ops.create_short(l as i16);
} else if (l as i32) as i64 == l {
return out_ops.create_int(l as i32);
}
out_ops.create_long(l)
// If no integer is possible, check for possible floating-point values.
} else if let Some(f) = n.as_f64() {
if (f as f32) as f64 == f {
return out_ops.create_float(f as f32);
}
out_ops.create_double(f)
} else {
// Just in case.
warn!("Number could not be placed in JSON while converting: {n}");
out_ops.create_double(0.0)
}
}
}
}
fn map_builder(&'static self) -> Self::StructBuilder {
JsonStructBuilder {
builder: DataResult::new_success_with_lifecycle(
Value::Object(Map::new()),
Lifecycle::Stable,
),
}
}
}
/// An implementation of [`MapLike`] for JSON objects.
/// The lifetime is that of the referenced map.
struct JsonMapLike<'a> {
map: &'a Map<String, Value>,
}
impl MapLike for JsonMapLike<'_> {
type Value = Value;
fn get(&self, key: &Self::Value) -> Option<&Self::Value> {
JsonOps::get_as_string(key).and_then(|s| self.get_str(&s))
}
fn get_str(&self, key: &str) -> Option<&Self::Value> {
self.map.get(key)
}
fn iter(&self) -> impl Iterator<Item = (Self::Value, &Self::Value)> + '_ {
self.map.iter().map(|(k, v)| (Value::String(k.clone()), v))
}
}
impl Display for JsonMapLike<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.map.fmt(f)
}
}
/// An implementation of [`StructBuilder`] for JSON objects.
pub struct JsonStructBuilder {
builder: DataResult<Value>,
}
impl ResultStructBuilder for JsonStructBuilder {
type Result = Value;
fn build_with_builder(
self,
builder: Self::Result,
prefix: Self::Value,
) -> DataResult<Self::Value> {
match prefix {
Value::Null => DataResult::new_success(builder),
Value::Object(mut map) => {
match builder {
Value::Object(builder_map) => {
for (k, v) in builder_map {
map.insert(k, v);
}
}
// This shouldn't happen, but just in case.
_ => {
return DataResult::new_error(format!(
"Expected object in builder, found {builder}"
));
}
}
DataResult::new_success(Value::Object(map))
}
_ => DataResult::new_partial_error(format!("Prefix is not a map: {prefix}"), prefix),
}
}
}
impl StructBuilder for JsonStructBuilder {
type Value = Value;
impl_struct_builder!(builder);
impl_string_struct_builder!(builder, INSTANCE);
}
impl StringStructBuilder for JsonStructBuilder {
fn append(&self, key: &str, value: Self::Value, mut builder: Self::Result) -> Self::Result {
builder
.as_object_mut()
.unwrap()
.insert(key.to_string(), value);
builder
}
}

View File

@@ -0,0 +1,78 @@
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
}
}

View File

@@ -0,0 +1,6 @@
/// 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>;
}

175
pumpkin-codecs/src/lib.rs Normal file
View File

@@ -0,0 +1,175 @@
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;
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;
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;
}
/// Represents a generic number in Java.
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)),
)
}
}

View File

@@ -0,0 +1,61 @@
/// A marker to convey the lifecycle of some object.
/// It could be stable, experimental or deprecated.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lifecycle {
/// Represents something stable.
Stable,
/// Represents something experimental.
Experimental,
/// Represents something deprecated. The `u32` with this lifecycle represents the *date* from which it was marked to be deprecated.
///
/// If the number is smaller, it was deprecated earlier, whereas if it is bigger, it was deprecated later.
Deprecated(u32),
}
impl Lifecycle {
/// Adds a lifecycle to another, returning a resultant lifecycle which matches the more restrictive of the two.
///
/// This function follows this order:
/// - If at least one of the lifecycles is *experimental*, [`Lifecycle::Experimental`] is returned.
/// - If both lifecycles are *deprecated*, this returns the earlier-deprecated one (where the *date* is lower).
/// - If exactly one lifecycle is *deprecated*, that lifecycle is returned.
/// - If nothing above matches, [`Lifecycle::Stable`] is returned.
#[must_use]
pub const fn add(self, other: Self) -> Self {
match (self, other) {
(Self::Experimental, _) | (_, Self::Experimental) => Self::Experimental,
(d1 @ Self::Deprecated(s1), d2 @ Self::Deprecated(s2)) => {
if s1 < s2 {
d1
} else {
d2
}
}
(d @ Self::Deprecated(_), _) | (_, d @ Self::Deprecated(_)) => d,
_ => Self::Stable,
}
}
}
#[cfg(test)]
mod test {
use crate::lifecycle::Lifecycle;
#[test]
fn add_lifecycles() {
assert_eq!(Lifecycle::Stable.add(Lifecycle::Stable), Lifecycle::Stable);
assert_eq!(
Lifecycle::Experimental.add(Lifecycle::Deprecated(10)),
Lifecycle::Experimental
);
assert_eq!(
Lifecycle::Deprecated(10).add(Lifecycle::Deprecated(15)),
Lifecycle::Deprecated(10)
);
}
}

View File

@@ -0,0 +1,65 @@
use crate::data_result::DataResult;
use crate::dynamic_ops::DynamicOps;
/// A trait for building lists.
#[must_use]
pub trait ListBuilder {
type Value;
/// Builds the final list and returns the result.
fn build(self, prefix: Self::Value) -> DataResult<Self::Value>;
/// Adds a direct value to this [`ListBuilder`].
#[must_use]
fn add(self, value: Self::Value) -> Self;
/// Adds a [`DataResult`] to this [`ListBuilder`].
#[must_use]
fn add_data_result(self, value: DataResult<Self::Value>) -> Self;
}
/// An implementation of [`ListBuilder`].
pub struct ListBuilderImpl<T, O: 'static> {
elements: DataResult<Vec<T>>,
ops: &'static O,
}
pub fn new_list_builder_impl<T>(
ops: &'static impl DynamicOps<Value = T>,
) -> impl ListBuilder<Value = T> {
ListBuilderImpl {
elements: DataResult::new_success(vec![]),
ops,
}
}
impl<T, O> ListBuilder for ListBuilderImpl<T, O>
where
O: DynamicOps<Value = T>,
{
type Value = T;
fn build(self, prefix: Self::Value) -> DataResult<Self::Value> {
self.elements
.flat_map(|e| self.ops.merge_values_into_list(prefix, e))
}
fn add(mut self, value: Self::Value) -> Self {
self.elements = self.elements.map(|mut e: Vec<T>| {
e.push(value);
e
});
self
}
fn add_data_result(mut self, value: DataResult<T>) -> Self {
self.elements = self.elements.apply_2_and_make_stable(
|mut e, v| {
e.push(v);
e
},
value,
);
self
}
}

View File

@@ -0,0 +1,241 @@
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::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.
///
/// # 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
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)
}

View File

@@ -0,0 +1,103 @@
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,
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod field_coders;
pub mod optional_field;
pub mod simple;
pub mod validated;

View File

@@ -0,0 +1,168 @@
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,
}
}

View File

@@ -0,0 +1,71 @@
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,
}
}

View File

@@ -0,0 +1,71 @@
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 }
}

View File

@@ -0,0 +1,416 @@
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,
}
}

View File

@@ -0,0 +1,14 @@
/// A trait that can be used to treat something as a map view of a dynamic type.
/// The [`Value`] of this trait is the *dynamic type* of this map-like.
pub trait MapLike {
type Value;
/// Gets the value of this map view with a key of the *dynamic type* of this map-like.
fn get(&self, key: &Self::Value) -> Option<&Self::Value>;
/// Gets the value of this map view with a `&str` key of the *dynamic type* of this map-like with the provided [`DynamicOps`] of this map-like's *dynamic type*.
fn get_str(&self, key: &str) -> Option<&Self::Value>;
/// Returns an `Iterator` to each key-value pair in this map-like, both of its *dynamic type*.
fn iter(&self) -> impl Iterator<Item = (Self::Value, &Self::Value)> + '_;
}

View File

@@ -0,0 +1,301 @@
use crate::data_result::DataResult;
use crate::dynamic_ops::DynamicOps;
use crate::lifecycle::Lifecycle;
/// A trait specifying a builder to add key-value pairs in order to create a composite type.
///
/// `Value` is the dynamic type for this builder.
/// For a struct, some methods here can be implemented via the `impl_struct_builder` macro.
pub trait StructBuilder {
type Value;
/// Adds a single key-value pair to this builder and returns the builder.
#[must_use]
fn add_key_value(self, key: Self::Value, value: Self::Value) -> Self;
/// Adds a single key-'value result' pair to this builder and returns the builder.
#[must_use]
fn add_key_value_result(self, key: Self::Value, value: DataResult<Self::Value>) -> Self;
/// Adds a single 'key result'-'value result' pair to this builder and returns the builder.
#[must_use]
fn add_key_result_value_result(
self,
key: DataResult<Self::Value>,
value: DataResult<Self::Value>,
) -> Self;
/// Adds the error message from a provided `DataResult` (if any) to this builder and returns the builder.
#[must_use]
fn with_errors_from<T>(self, result: &DataResult<T>) -> Self;
/// Adds a string key-value pair to this builder and returns the builder.
#[must_use]
fn add_string_key_value(self, key: &str, value: Self::Value) -> Self;
/// Adds a string key-'value result' pair to this builder and returns the builder.
#[must_use]
fn add_string_key_value_result(self, key: &str, value: DataResult<Self::Value>) -> Self;
/// Sets the lifecycle of this builder and returns the builder.
#[must_use]
fn set_lifecycle(self, lifecycle: Lifecycle) -> Self;
/// Maps the error from the internal builder to the function `f` and returns the builder.
#[must_use]
fn map_error(self, f: impl FnOnce(String) -> String) -> Self;
/// Builds the map stored in this builder along with a prefix and returns the result.
fn build(self, prefix: Self::Value) -> DataResult<Self::Value>;
/// Builds the map stored in this builder along with a `DataResult` prefix and returns the result.
fn build_with_result_prefix(self, prefix: DataResult<Self::Value>) -> DataResult<Self::Value>
where
Self: Sized,
{
prefix.flat_map(|p| self.build(p))
}
}
/// A [`StructBuilder`] with a specified `Result` type for builders.
pub trait ResultStructBuilder: StructBuilder {
type Result;
/// Builds the map stored in `builder` along with a prefix and returns the result.
fn build_with_builder(
self,
builder: Self::Result,
prefix: Self::Value,
) -> DataResult<Self::Value>;
}
/// A subtrait of [`StructBuilder`] for appending string keys instead of dynamic type keys.
/// The methods in `StructBuilder` can also be implemented via the `impl_string_struct_builder` macro.
pub trait StringStructBuilder: ResultStructBuilder {
/// Appends a string key-value pair to `builder`, mutating it.
fn append(&self, key: &str, value: Self::Value, builder: Self::Result) -> Self::Result;
}
/// A subtrait of [`StructBuilder`] for appending dynamic keys. The methods in `StructBuilder`
/// can also be implemented via the `impl_universal_struct_builder` macro.
pub trait UniversalStructBuilder: ResultStructBuilder {
/// Appends a key-value pair to `builder`, mutating it.
fn append(&self, key: Self::Value, value: Self::Value, builder: Self::Result) -> Self::Result;
}
/// A macro to be placed inside an `impl` block of a struct implementing [`StructBuilder`].
///
/// Place this in a `impl StructBuilder for ...` block.
/// This automatically implements the methods to add key-value pairs to the builder.
/// Make sure to have a struct field of type [`DataResult<Self::Value>`] of name `$builder`.
#[macro_export]
macro_rules! impl_struct_builder {
($builder:ident) => {
fn set_lifecycle(mut self, lifecycle: Lifecycle) -> Self {
self.$builder = self.$builder.with_lifecycle(lifecycle);
self
}
fn map_error(mut self, f: impl FnOnce(String) -> String) -> Self {
self.$builder = self.$builder.map_error(f);
self
}
fn with_errors_from<U>(mut self, result: &DataResult<U>) -> Self {
self.$builder = self.$builder.with_errors_from(result);
self
}
fn build(self, prefix: Self::Value) -> DataResult<Self::Value> {
self.$builder
.clone()
.flat_map(|b| self.build_with_builder(b, prefix))
}
};
}
/// A macro to be placed inside an `impl` block of a struct implementing [`StringStructBuilder`].
///
/// Place this in a `impl StructBuilder for ...` block.
/// This automatically implements the methods to add key-value pairs to the builder.
#[macro_export]
macro_rules! impl_string_struct_builder {
(@internal $builder:ident) => {
fn add_string_key_value(mut self, key: &str, value: Self::Value) -> Self {
self.$builder = std::mem::take(&mut self.$builder).map(|r| self.append(key, value, r));
self
}
fn add_string_key_value_result(mut self, key: &str, value: DataResult<Self::Value>) -> Self {
self.$builder = std::mem::take(&mut self.$builder).apply_2_and_make_stable(|r, v| self.append(key, v, r), value);
self
}
};
// For constant ops
($builder:ident, $ops:ident) => {
impl_string_struct_builder!(@internal $builder);
fn add_key_value(mut self, key: Self::Value, value: Self::Value) -> Self {
self.$builder = $ops.get_string(&key).flat_map(
|s| std::mem::take(&mut self.$builder).map(|r| self.append(&s, value, r))
);
self
}
fn add_key_value_result(mut self, key: Self::Value, value: DataResult<Self::Value>) -> Self {
self.$builder = $ops.get_string(&key).flat_map(
|s| std::mem::take(&mut self.$builder).apply_2_and_make_stable(|r, v| self.append(&s, v, r), value)
);
self
}
fn add_key_result_value_result(
mut self,
key: DataResult<Self::Value>,
value: DataResult<Self::Value>,
) -> Self {
self.$builder = key.flat_map(|v| $ops.get_string(&v)).flat_map(|s| {
std::mem::take(&mut self.$builder).apply_2_and_make_stable(|r, v| self.append(&s, v, r), value)
});
self
}
};
// For stored ops
($builder:ident, self. $ops:ident) => {
impl_string_struct_builder!(@internal $builder);
fn add_key_value(mut self, key: Self::Value, value: Self::Value) -> Self {
self.$builder = self.$ops.get_string(&key).flat_map(
|s| std::mem::take(&mut self.$builder).map(|r| self.append(&s, value, r))
);
self
}
fn add_key_value_result(mut self, key: Self::Value, value: DataResult<Self::Value>) -> Self {
self.$builder = self.$ops.get_string(&key).flat_map(
|s| std::mem::take(&mut self.$builder).apply_2_and_make_stable(|r, v| self.append(&s, v, r), value)
);
self
}
fn add_key_result_value_result(
mut self,
key: DataResult<Self::Value>,
value: DataResult<Self::Value>,
) -> Self {
self.$builder = key.flat_map(|v| self.$ops.get_string(&v)).flat_map(|s| {
std::mem::take(&mut self.$builder).apply_2_and_make_stable(|r, v| self.append(&s, v, r), value)
});
self
}
};
}
/// A macro to be placed inside an `impl` block of a struct implementing `UniversalStructBuilder`.
///
/// Place this in a `impl StructBuilder for ...` block.
/// This automatically implements the methods to add key-value pairs to the builder.
#[macro_export]
macro_rules! impl_universal_struct_builder {
(@internal $builder:ident) => {
fn add_key_value(mut self, key: Self::Value, value: Self::Value) -> Self {
self.$builder = std::mem::take(&mut self.$builder).map(|b| self.append(key, value, b));
self
}
fn add_key_value_result(mut self, key: Self::Value, value: DataResult<Self::Value>) -> Self {
self.$builder = std::mem::take(&mut self.$builder)
.apply_2_and_make_stable(|b, v| self.append(key, v, b), value);
self
}
fn add_key_result_value_result(
mut self,
key: DataResult<Self::Value>,
value: DataResult<Self::Value>,
) -> Self {
self.$builder = self
.$builder
.clone()
.apply(key.apply_2_and_make_stable(|k, v| (|b| self.append(k, v, b)), value));
self
}
};
// For constant ops
($builder:ident, $ops:ident) => {
impl_universal_struct_builder!(@internal $builder);
fn add_string_key_value(self, key: &str, value: Self::Value) -> Self {
self.add_key_value($ops.create_string(key), value)
}
fn add_string_key_value_result(self, key: &str, value: DataResult<Self::Value>) -> Self {
self.add_key_value_result($ops.create_string(key), value)
}
};
// For stored ops
($builder:ident, self. $ops:ident) => {
impl_universal_struct_builder!(@internal $builder);
fn add_string_key_value(self, key: &str, value: Self::Value) -> Self {
let string = self.$ops.create_string(key);
self.add_key_value(string, value)
}
fn add_string_key_value_result(self, key: &str, value: DataResult<Self::Value>) -> Self {
let string = self.$ops.create_string(key);
self.add_key_value_result(string, value)
}
};
}
pub struct MapBuilder<T, O: DynamicOps<Value = T> + 'static> {
builder: DataResult<Vec<(T, T)>>,
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;
impl_struct_builder!(builder);
impl_universal_struct_builder!(builder, self.ops);
}
impl<T: Clone, O: DynamicOps<Value = T>> ResultStructBuilder for MapBuilder<T, O> {
type Result = Vec<(T, T)>;
fn build_with_builder(
self,
builder: Self::Result,
prefix: Self::Value,
) -> DataResult<Self::Value> {
self.ops.merge_entries_into_map(prefix, builder)
}
}
impl<T: Clone, O: DynamicOps<Value = T>> UniversalStructBuilder for MapBuilder<T, O> {
fn append(
&self,
key: Self::Value,
value: Self::Value,
mut builder: Self::Result,
) -> Self::Result {
builder.push((key, value));
builder
}
}

View File

@@ -0,0 +1,740 @@
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
);
}
}

View File

@@ -5,12 +5,14 @@ edition.workspace = true
rust-version.workspace = true
[dependencies]
pumpkin-codecs.workspace = true
serde.workspace = true
thiserror.workspace = true
bytes.workspace = true
cesu8.workspace = true
flate2.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true

View File

@@ -15,6 +15,7 @@ use thiserror::Error;
pub mod compound;
pub mod deserializer;
pub mod nbt_compress;
pub mod nbt_ops;
pub mod serializer;
pub mod tag;

1475
pumpkin-nbt/src/nbt_ops.rs Normal file

File diff suppressed because it is too large Load Diff