mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
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:
76
Cargo.lock
generated
76
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
13
pumpkin-codecs/Cargo.toml
Normal 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
|
||||
85
pumpkin-codecs/src/base_map_codec.rs
Normal file
85
pumpkin-codecs/src/base_map_codec.rs
Normal 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
579
pumpkin-codecs/src/codec.rs
Normal 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());
|
||||
}};
|
||||
}
|
||||
47
pumpkin-codecs/src/codecs/lazy.rs
Normal file
47
pumpkin-codecs/src/codecs/lazy.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
269
pumpkin-codecs/src/codecs/list.rs
Normal file
269
pumpkin-codecs/src/codecs/list.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
pumpkin-codecs/src/codecs/map_codec.rs
Normal file
53
pumpkin-codecs/src/codecs/map_codec.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
7
pumpkin-codecs/src/codecs/mod.rs
Normal file
7
pumpkin-codecs/src/codecs/mod.rs
Normal 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;
|
||||
208
pumpkin-codecs/src/codecs/primitive.rs
Normal file
208
pumpkin-codecs/src/codecs/primitive.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
176
pumpkin-codecs/src/codecs/range.rs
Normal file
176
pumpkin-codecs/src/codecs/range.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
216
pumpkin-codecs/src/codecs/unbounded_map.rs
Normal file
216
pumpkin-codecs/src/codecs/unbounded_map.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
164
pumpkin-codecs/src/codecs/validated.rs
Normal file
164
pumpkin-codecs/src/codecs/validated.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
192
pumpkin-codecs/src/coders.rs
Normal file
192
pumpkin-codecs/src/coders.rs
Normal 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)
|
||||
}
|
||||
635
pumpkin-codecs/src/data_result.rs
Normal file
635
pumpkin-codecs/src/data_result.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
357
pumpkin-codecs/src/dynamic_ops.rs
Normal file
357
pumpkin-codecs/src/dynamic_ops.rs
Normal 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;
|
||||
}
|
||||
423
pumpkin-codecs/src/json_ops.rs
Normal file
423
pumpkin-codecs/src/json_ops.rs
Normal 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
|
||||
}
|
||||
}
|
||||
78
pumpkin-codecs/src/key_compressor.rs
Normal file
78
pumpkin-codecs/src/key_compressor.rs
Normal 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
|
||||
}
|
||||
}
|
||||
6
pumpkin-codecs/src/keyable.rs
Normal file
6
pumpkin-codecs/src/keyable.rs
Normal 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
175
pumpkin-codecs/src/lib.rs
Normal 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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
61
pumpkin-codecs/src/lifecycle.rs
Normal file
61
pumpkin-codecs/src/lifecycle.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
65
pumpkin-codecs/src/list_builder.rs
Normal file
65
pumpkin-codecs/src/list_builder.rs
Normal 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
|
||||
}
|
||||
}
|
||||
241
pumpkin-codecs/src/map_codec.rs
Normal file
241
pumpkin-codecs/src/map_codec.rs
Normal 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)
|
||||
}
|
||||
103
pumpkin-codecs/src/map_codecs/field_coders.rs
Normal file
103
pumpkin-codecs/src/map_codecs/field_coders.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
4
pumpkin-codecs/src/map_codecs/mod.rs
Normal file
4
pumpkin-codecs/src/map_codecs/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod field_coders;
|
||||
pub mod optional_field;
|
||||
pub mod simple;
|
||||
pub mod validated;
|
||||
168
pumpkin-codecs/src/map_codecs/optional_field.rs
Normal file
168
pumpkin-codecs/src/map_codecs/optional_field.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
71
pumpkin-codecs/src/map_codecs/simple.rs
Normal file
71
pumpkin-codecs/src/map_codecs/simple.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
71
pumpkin-codecs/src/map_codecs/validated.rs
Normal file
71
pumpkin-codecs/src/map_codecs/validated.rs
Normal 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 }
|
||||
}
|
||||
416
pumpkin-codecs/src/map_coders.rs
Normal file
416
pumpkin-codecs/src/map_coders.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
14
pumpkin-codecs/src/map_like.rs
Normal file
14
pumpkin-codecs/src/map_like.rs
Normal 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)> + '_;
|
||||
}
|
||||
301
pumpkin-codecs/src/struct_builder.rs
Normal file
301
pumpkin-codecs/src/struct_builder.rs
Normal 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
|
||||
}
|
||||
}
|
||||
740
pumpkin-codecs/src/struct_codecs.rs
Normal file
740
pumpkin-codecs/src/struct_codecs.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
1475
pumpkin-nbt/src/nbt_ops.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user