diff --git a/Cargo.lock b/Cargo.lock index 154c79583..69c820879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 6ece634e3..b9686d906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/pumpkin-codecs/Cargo.toml b/pumpkin-codecs/Cargo.toml new file mode 100644 index 000000000..02451a1be --- /dev/null +++ b/pumpkin-codecs/Cargo.toml @@ -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 diff --git a/pumpkin-codecs/src/base_map_codec.rs b/pumpkin-codecs/src/base_map_codec.rs new file mode 100644 index 000000000..50e19845f --- /dev/null +++ b/pumpkin-codecs/src/base_map_codec.rs @@ -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 + 'static; + + /// The value (element) type of this map codec. + type Element; + type ElementCodec: Codec + 'static; + + fn key_codec(&self) -> &'static Self::KeyCodec; + fn element_codec(&self) -> &'static Self::ElementCodec; + + fn encode( + &self, + input: &HashMap, + ops: &'static impl DynamicOps, + mut prefix: impl StructBuilder, + ) -> impl StructBuilder { + 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( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult> { + let mut read_map: HashMap = 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})")) + } +} diff --git a/pumpkin-codecs/src/codec.rs b/pumpkin-codecs/src/codec.rs new file mode 100644 index 000000000..221511364 --- /dev/null +++ b/pumpkin-codecs/src/codec.rs @@ -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` and `Vec`). +/// +/// # 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 and Decoder will also implement Codec. +impl Codec for T where T: Encoder + Decoder {} + +/// A codec allowing an arbitrary encoder and decoder. +pub struct ComposedCodec + 'static> { + encoder: E, + decoder: D, +} + +impl> HasValue for ComposedCodec { + type Value = E::Value; +} + +impl> Encoder for ComposedCodec { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + self.encoder.encode(input, ops, prefix) + } +} + +impl> Decoder for ComposedCodec { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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(f: fn() -> C) -> LazyCodec { + new_lazy_codec(f) +} + +/// Creates a [`ListCodec`] of another [`Codec`] with the provided minimum and maximum size. +pub const fn list(codec: &'static C, min_size: usize, max_size: usize) -> ListCodec { + new_list_codec(codec, min_size, max_size) +} + +/// Creates a [`ListCodec`] of another [`Codec`] with the provided maximum size. +pub const fn limited_list(codec: &'static C, max_size: usize) -> ListCodec { + new_list_codec(codec, 0, max_size) +} + +/// Creates a [`ListCodec`] of another [`Codec`], which allows any size. +pub const fn unbounded_list(codec: &'static C) -> ListCodec { + 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 = ComposedCodec<$encoder_type, $decoder_type>; + + #[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, S>(codec: &'static C, to: fn(A) -> $to_func_result, from: fn(&S) -> $from_func_result) -> $short_type { + 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, + A, + "partially equivalent", + "equivalent" +); + +make_codec_transformation_function!( + flat_map_comap, + FlatMapComapCodec, + FlatComappedEncoderImpl, + MappedDecoderImpl, + flat_comap, + map, + S, + DataResult, + "equivalent", + "partially equivalent" +); + +make_codec_transformation_function!( + flat_xmap, + FlatXmapCodec, + FlatComappedEncoderImpl, + FlatMappedDecoderImpl, + flat_comap, + flat_map, + DataResult, + DataResult, + "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( + codec: &'static C, + validator: fn(&C::Value) -> Result<(), String>, +) -> ValidatedCodec { + 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( + key_codec: &'static K, + element_codec: &'static V, + keyable: Key, +) -> SimpleMapCodec +where + ::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( + key_codec: &'static K, + element_codec: &'static V, +) -> UnboundedMapCodec +where + ::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, FieldMapCodec>; +/// +/// // 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 = ComposedMapCodec< + FieldEncoder<::Value, C>, + FieldDecoder<::Value, C>, +>; + +/// Creates a [`MapCodec`] for a field which relies on the provided [`Codec`] for serialization/deserialization. +pub const fn field(codec: &'static C, name: &'static str) -> FieldMapCodec { + 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( + codec: &'static C, + name: &'static str, +) -> OptionalFieldMapCodec { + 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( + codec: &'static C, + name: &'static str, +) -> OptionalFieldMapCodec { + new_optional_field_map_codec(codec, name, true) +} + +pub type DefaultedFieldCodec = + DefaultValueProviderMapCodec<::Value, OptionalFieldMapCodec>; + +/// 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( + codec: &'static C, + name: &'static str, + factory: fn() -> C::Value, +) -> DefaultedFieldCodec +where + ::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( + codec: &'static C, + name: &'static str, + factory: fn() -> C::Value, +) -> DefaultedFieldCodec +where + ::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()); + }}; +} diff --git a/pumpkin-codecs/src/codecs/lazy.rs b/pumpkin-codecs/src/codecs/lazy.rs new file mode 100644 index 000000000..5224f3838 --- /dev/null +++ b/pumpkin-codecs/src/codecs/lazy.rs @@ -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 +where + C: Codec, +{ + codec: LazyLock, +} + +impl HasValue for LazyCodec { + type Value = C::Value; +} + +impl Encoder for LazyCodec { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + self.codec.encode(input, ops, prefix) + } +} + +impl Decoder for LazyCodec { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> DataResult<(Self::Value, T)> { + self.codec.decode(input, ops) + } +} + +/// Creates a new [`LazyCodec`]. +pub(crate) const fn new_lazy_codec(f: fn() -> C) -> LazyCodec { + LazyCodec { + codec: LazyLock::new(f), + } +} diff --git a/pumpkin-codecs/src/codecs/list.rs b/pumpkin-codecs/src/codecs/list.rs new file mode 100644 index 000000000..ddbdaecbc --- /dev/null +++ b/pumpkin-codecs/src/codecs/list.rs @@ -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`]. +/// `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 +where + C: Codec + ?Sized + 'static, +{ + element_codec: &'static C, + min_size: usize, + max_size: usize, +} + +impl ListCodec { + fn create_too_short_error(&self, size: usize) -> DataResult { + DataResult::new_error(format!( + "List is too short: {size}, expected range [{}-{}]", + self.min_size, self.max_size + )) + } + + fn create_too_long_error(&self, size: usize) -> DataResult { + DataResult::new_error(format!( + "List is too long: {size}, expected range [{}-{}]", + self.min_size, self.max_size + )) + } +} + +impl HasValue for ListCodec { + type Value = Vec; +} + +impl Encoder for ListCodec { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + 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 Decoder for ListCodec +where + C: Codec, +{ + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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 = 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( + codec: &'static C, + min_size: usize, + max_size: usize, +) -> ListCodec { + 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 = 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 = 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> = + 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 = 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> = + 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); + } + } +} diff --git a/pumpkin-codecs/src/codecs/map_codec.rs b/pumpkin-codecs/src/codecs/map_codec.rs new file mode 100644 index 000000000..2a1cab42e --- /dev/null +++ b/pumpkin-codecs/src/codecs/map_codec.rs @@ -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 { + Owned(C), + Borrowed(&'static C), +} + +impl MapCodecCodec { + const fn codec(&self) -> &C { + match self { + Self::Owned(c) => c, + Self::Borrowed(c) => c, + } + } +} + +impl HasValue for MapCodecCodec { + type Value = C::Value; +} + +impl Encoder for MapCodecCodec { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + self.codec() + .encode(input, ops, self.codec().builder(ops)) + .build(prefix) + } +} + +impl Decoder for MapCodecCodec { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> DataResult<(Self::Value, T)> { + self.codec() + .compressed_decode(input.clone(), ops) + .map(|a| (a, input)) + } +} diff --git a/pumpkin-codecs/src/codecs/mod.rs b/pumpkin-codecs/src/codecs/mod.rs new file mode 100644 index 000000000..bf0777888 --- /dev/null +++ b/pumpkin-codecs/src/codecs/mod.rs @@ -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; diff --git a/pumpkin-codecs/src/codecs/primitive.rs b/pumpkin-codecs/src/codecs/primitive.rs new file mode 100644 index 000000000..e4b61a49f --- /dev/null +++ b/pumpkin-codecs/src/codecs/primitive.rs @@ -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( + &self, + ops: &'static impl DynamicOps, + input: T, + ) -> DataResult<$prim> { + ops.get_number(&input).map(|n| <$prim>::from(n)) + } + + fn write(&self, ops: &'static impl DynamicOps, 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( + &self, + ops: &'static impl DynamicOps, + input: T, + ) -> DataResult> { + ops.$get_func(input) + } + + fn write(&self, ops: &'static impl DynamicOps, value: &Vec<$elem>) -> T { + ops.$create_func(value.to_vec()) + } + } + }; +} + +/// A generic primitive codec. +trait PrimitiveCodec: Codec { + fn read( + &self, + ops: &'static impl DynamicOps, + input: T, + ) -> DataResult; + + fn write(&self, ops: &'static impl DynamicOps, value: &Self::Value) -> T; +} + +impl Encoder for C { + fn encode( + &self, + input: &::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + ops.merge_into_primitive(prefix, self.write(ops, input)) + } +} + +impl Decoder for C { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> DataResult<(::Value, T)> { + self.read(ops, input).map(|r| (r, ops.empty())) + } +} + +// Implementations + +impl_primitive_codec_start!(BoolCodec, bool); +impl PrimitiveCodec for BoolCodec { + fn read(&self, ops: &'static impl DynamicOps, input: T) -> DataResult { + ops.get_bool(&input) + } + + fn write(&self, ops: &'static impl DynamicOps, 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(&self, ops: &'static impl DynamicOps, input: T) -> DataResult { + ops.get_string(&input) + } + + fn write(&self, ops: &'static impl DynamicOps, value: &String) -> T { + ops.create_string(value) + } +} + +impl_primitive_codec_start!(ByteBufferCodec, Box<[u8]>); +impl PrimitiveCodec for ByteBufferCodec { + fn read(&self, ops: &'static impl DynamicOps, input: T) -> DataResult> { + ops.get_byte_buffer(input) + } + + fn write(&self, ops: &'static impl DynamicOps, 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); + } +} diff --git a/pumpkin-codecs/src/codecs/range.rs b/pumpkin-codecs/src/codecs/range.rs new file mode 100644 index 000000000..5b9fccb0c --- /dev/null +++ b/pumpkin-codecs/src/codecs/range.rs @@ -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 +where + C::Value: PartialOrd + Display + Clone, +{ + codec: &'static C, + min: C::Value, + max: C::Value, +} + +impl HasValue for RangeCodec +where + ::Value: PartialOrd + Display + Clone, +{ + type Value = C::Value; +} + +impl Encoder for RangeCodec +where + ::Value: PartialOrd + Display + Clone, +{ + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + check_range(input, &self.min, &self.max).flat_map(|t| self.codec.encode(&t, ops, prefix)) + } +} + +impl Decoder for RangeCodec +where + ::Value: PartialOrd + Display + Clone, +{ + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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(input: &T, min: &T, max: &T) -> DataResult { + 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>( + codec: &'static C, + min: A, + max: A, +) -> RangeCodec { + 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 + ); + } +} diff --git a/pumpkin-codecs/src/codecs/unbounded_map.rs b/pumpkin-codecs/src/codecs/unbounded_map.rs new file mode 100644 index 000000000..7f2d0b110 --- /dev/null +++ b/pumpkin-codecs/src/codecs/unbounded_map.rs @@ -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 +where + K::Value: Display + Eq + Hash, +{ + key_codec: &'static K, + element_codec: &'static V, +} + +impl BaseMapCodec for UnboundedMapCodec +where + ::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 HasValue for UnboundedMapCodec +where + ::Value: Display + Eq + Hash, +{ + type Value = HashMap; +} + +impl Encoder for UnboundedMapCodec +where + ::Value: Display + Eq + Hash, +{ + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + BaseMapCodec::encode(self, input, ops, ops.map_builder()).build(prefix) + } +} + +impl Decoder for UnboundedMapCodec +where + ::Value: Display + Eq + Hash, +{ + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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( + key_codec: &'static K, + element_codec: &'static V, +) -> UnboundedMapCodec +where + ::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 = + unbounded_map(&STRING_CODEC, &INT_CODEC); + + let mut map = HashMap::::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, 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::::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, 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 + ); + } +} diff --git a/pumpkin-codecs/src/codecs/validated.rs b/pumpkin-codecs/src/codecs/validated.rs new file mode 100644 index 000000000..02f79e9ff --- /dev/null +++ b/pumpkin-codecs/src/codecs/validated.rs @@ -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 { + codec: &'static C, + /// The validator function used. + validator: fn(&C::Value) -> Result<(), String>, +} + +impl HasValue for ValidatedCodec { + type Value = C::Value; +} + +impl Encoder for ValidatedCodec { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + (self.validator)(input).map_or_else( + |error| DataResult::new_error(error), + |()| self.codec.encode(input, ops, prefix), + ) + } +} + +impl Decoder for ValidatedCodec { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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( + codec: &'static C, + validator: fn(&C::Value) -> Result<(), String>, +) -> ValidatedCodec { + 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 = 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 = 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 + ); + } +} diff --git a/pumpkin-codecs/src/coders.rs b/pumpkin-codecs/src/coders.rs new file mode 100644 index 000000000..0c299419d --- /dev/null +++ b/pumpkin-codecs/src/coders.rs @@ -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( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult; + + /// 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( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + ) -> DataResult { + self.encode(input, ops, ops.empty()) + } +} + +pub struct ComappedEncoderImpl { + encoder: &'static E, + function: fn(&B) -> E::Value, +} + +impl HasValue for ComappedEncoderImpl { + type Value = B; +} + +impl Encoder for ComappedEncoderImpl { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + 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( + encoder: &'static E, + f: fn(&B) -> E::Value, +) -> ComappedEncoderImpl { + ComappedEncoderImpl { + encoder, + function: f, + } +} + +pub struct FlatComappedEncoderImpl { + encoder: &'static E, + function: fn(&B) -> DataResult, +} + +impl HasValue for FlatComappedEncoderImpl { + type Value = B; +} + +impl Encoder for FlatComappedEncoderImpl { + fn encode( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: T, + ) -> DataResult { + (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( + encoder: &'static E, + f: fn(&B) -> DataResult, +) -> FlatComappedEncoderImpl { + FlatComappedEncoderImpl { + encoder, + function: f, + } +} + +pub(crate) const fn encoder_field>( + name: &'static str, + encoder: &'static E, +) -> FieldEncoder { + 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( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> DataResult { + self.decode(input, ops).map(|r| r.0) + } +} + +pub struct MappedDecoderImpl { + decoder: &'static D, + function: fn(D::Value) -> B, +} + +impl HasValue for MappedDecoderImpl { + type Value = B; +} + +impl Decoder for MappedDecoderImpl { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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( + decoder: &'static D, + f: fn(D::Value) -> B, +) -> MappedDecoderImpl { + MappedDecoderImpl { + decoder, + function: f, + } +} + +pub struct FlatMappedDecoderImpl { + decoder: &'static D, + function: fn(D::Value) -> DataResult, +} + +impl HasValue for FlatMappedDecoderImpl { + type Value = B; +} + +impl Decoder for FlatMappedDecoderImpl { + fn decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> 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( + decoder: &'static D, + f: fn(D::Value) -> DataResult, +) -> FlatMappedDecoderImpl { + FlatMappedDecoderImpl { + decoder, + function: f, + } +} + +pub(crate) const fn decoder_field>( + name: &'static str, + decoder: &'static D, +) -> FieldDecoder { + FieldDecoder::new(name, decoder) +} diff --git a/pumpkin-codecs/src/data_result.rs b/pumpkin-codecs/src/data_result.rs new file mode 100644 index 000000000..267d5c876 --- /dev/null +++ b/pumpkin-codecs/src/data_result.rs @@ -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 = 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 { + 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 { + 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 { + /// 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, + lifecycle: Lifecycle, + message: String, + }, +} + +impl DataResult { + /// 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) -> 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, 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( + message: impl Into, + lifecycle: Lifecycle, + ) -> DataResult { + 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, + partial_result: R, + lifecycle: Lifecycle, + ) -> Self { + Self::Error { + partial_result: Some(partial_result), + lifecycle, + message: message.into(), + } + } + + /// Returns an *errored* `DataResult` with result [`Option`] and a given lifecycle. + #[inline] + const fn new_option_error_with_lifecycle( + message: String, + partial_result: Option, + 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 { + 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 { + 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(self, op: impl FnOnce(R) -> T) -> DataResult { + 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(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(self, f: impl FnOnce(R) -> DataResult) -> DataResult { + 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(self, function_result: DataResult T>) -> DataResult { + 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( + self, + f: impl FnOnce(R, R2) -> T, + second_result: DataResult, + ) -> DataResult { + 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(self, value: T) -> DataResult { + 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(self, other_result: &DataResult) -> 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(self, result: &DataResult) -> 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 { + 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 Default for DataResult { + fn default() -> Self { + Self::new_error("Default DataResult") + } +} diff --git a/pumpkin-codecs/src/dynamic_ops.rs b/pumpkin-codecs/src/dynamic_ops.rs new file mode 100644 index 000000000..187ca7dcc --- /dev/null +++ b/pumpkin-codecs/src/dynamic_ops.rs @@ -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::>>() + .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::>>() + .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; + + /// 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(&self, values: I) -> Self::Value + where + I: IntoIterator; + + /// Returns how a map is represented by this `DynamicOps`. + fn create_map(&self, entries: I) -> Self::Value + where + I: IntoIterator; + + /// Tries to get a `bool` represented by this `DynamicOps`. + fn get_bool(&self, input: &Self::Value) -> DataResult { + 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; + + /// Tries to get a string represented by this `DynamicOps`. + fn get_string(&self, input: &Self::Value) -> DataResult; + + /// 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 + 'a>; + + /// Tries to get a [`MapLike`] for a map represented by this `DynamicOps`. + fn get_map<'a>( + &self, + input: &'a Self::Value, + ) -> DataResult + '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>; + + /// 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> { + impl_get_list!(box self, input, "bytes") + } + + /// Creates a byte buffer that can be represented by this `DynamicOps` using a [`Vec`]. + fn create_byte_buffer(&self, buffer: Vec) -> Self::Value { + self.create_list(buffer.iter().map(|b| self.create_byte(*b as i8))) + } + + /// Gets a [`Vec`] (`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> { + impl_get_list!(self, input, "ints") + } + + /// Creates an `int` list ([`Vec`]) that can be represented by this `DynamicOps`. + fn create_int_list(&self, vec: Vec) -> Self::Value { + self.create_list(vec.into_iter().map(|i| self.create_int(i))) + } + + /// Gets a [`Vec`] (`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> { + impl_get_list!(self, input, "longs") + } + + /// Creates a `long` list ([`Vec`]) that can be represented by this `DynamicOps`. + fn create_long_list(&self, vec: Vec) -> 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; + + /// 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(&self, list: Self::Value, values: I) -> DataResult + where + I: IntoIterator, + { + 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 + 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(&self, map: Self::Value, entries: I) -> DataResult + where + I: IntoIterator, + 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( + &self, + map: Self::Value, + other_map_like: M, + ) -> DataResult + where + M: MapLike, + 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 + where + ::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(&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(&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(&self, out_ops: &impl DynamicOps, input: Self::Value) -> U; + + /// Converts a list represented by this `DynamicOps` to another list represented by another `DynamicOps`. + fn convert_list(&self, out_ops: &impl DynamicOps, 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(&self, out_ops: &impl DynamicOps, 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 + where + Self: Sized, + { + new_list_builder_impl(self) + } + + /// Returns a [`MapBuilder`] for this `DynamicOps`. + fn map_builder(&'static self) -> Self::StructBuilder; +} diff --git a/pumpkin-codecs/src/json_ops.rs b/pumpkin-codecs/src/json_ops.rs new file mode 100644 index 000000000..11c595e9f --- /dev/null +++ b/pumpkin-codecs/src/json_ops.rs @@ -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 { + 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(&self, values: I) -> Self::Value + where + I: IntoIterator, + { + Value::Array(values.into_iter().collect()) + } + + fn create_map(&self, entries: I) -> Self::Value + where + I: IntoIterator, + { + 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 { + 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 { + 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::() { + return DataResult::new_success(Number::Int(i)); + } + if let Ok(l) = string.parse::() { + return DataResult::new_success(Number::Long(l)); + } + if let Ok(d) = string.parse::() { + 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 { + 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 + '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 + '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> { + // 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 { + 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(&self, list: Self::Value, values: I) -> DataResult + where + I: IntoIterator, + { + 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 + 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( + &self, + map: Self::Value, + other_map_like: M, + ) -> DataResult + where + M: MapLike, + 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(&self, out_ops: &impl DynamicOps, 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, +} + +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 + '_ { + 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, +} + +impl ResultStructBuilder for JsonStructBuilder { + type Result = Value; + + fn build_with_builder( + self, + builder: Self::Result, + prefix: Self::Value, + ) -> DataResult { + 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 + } +} diff --git a/pumpkin-codecs/src/key_compressor.rs b/pumpkin-codecs/src/key_compressor.rs new file mode 100644 index 000000000..ea9bbd937 --- /dev/null +++ b/pumpkin-codecs/src/key_compressor.rs @@ -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>> = + 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, + decompress_map: HashMap, + 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) { + // 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( + &self, + key: usize, + ops: &'static impl DynamicOps, + ) -> Option { + self.decompress_map.get(&key).map(|s| ops.create_string(s)) + } + + /// Gets the compressed key of the provided dynamic type. + pub fn compress_key( + &self, + key: &T, + ops: &'static impl DynamicOps, + ) -> Option { + 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 { + self.compress_map.get(key).copied() + } + + /// Returns the size of the compressed/decompressed maps. + #[must_use] + pub const fn size(&self) -> usize { + self.size + } +} diff --git a/pumpkin-codecs/src/keyable.rs b/pumpkin-codecs/src/keyable.rs new file mode 100644 index 000000000..a12df7be6 --- /dev/null +++ b/pumpkin-codecs/src/keyable.rs @@ -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; +} diff --git a/pumpkin-codecs/src/lib.rs b/pumpkin-codecs/src/lib.rs new file mode 100644 index 000000000..f712c6fdc --- /dev/null +++ b/pumpkin-codecs/src/lib.rs @@ -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 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 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 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 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 for u8 { + fn from(num: Number) -> Self { + i32::from(num) as Self + } +} + +impl From 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 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 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 { + num.clone().try_into() + } +} + +impl TryFrom for Number { + type Error = FromJsonValueError; + + fn try_from(num: serde_json::Value) -> Result { + match num { + serde_json::Value::Number(n) => n.try_into(), + _ => Err(FromJsonValueError), + } + } +} + +impl TryFrom for Number { + type Error = FromJsonValueError; + + fn try_from(num: serde_json::Number) -> Result { + // 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)), + ) + } +} diff --git a/pumpkin-codecs/src/lifecycle.rs b/pumpkin-codecs/src/lifecycle.rs new file mode 100644 index 000000000..7bb36e2f1 --- /dev/null +++ b/pumpkin-codecs/src/lifecycle.rs @@ -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) + ); + } +} diff --git a/pumpkin-codecs/src/list_builder.rs b/pumpkin-codecs/src/list_builder.rs new file mode 100644 index 000000000..1519746fd --- /dev/null +++ b/pumpkin-codecs/src/list_builder.rs @@ -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; + + /// 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; +} + +/// An implementation of [`ListBuilder`]. +pub struct ListBuilderImpl { + elements: DataResult>, + ops: &'static O, +} + +pub fn new_list_builder_impl( + ops: &'static impl DynamicOps, +) -> impl ListBuilder { + ListBuilderImpl { + elements: DataResult::new_success(vec![]), + ops, + } +} + +impl ListBuilder for ListBuilderImpl +where + O: DynamicOps, +{ + type Value = T; + + fn build(self, prefix: Self::Value) -> DataResult { + 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| { + e.push(value); + e + }); + self + } + + fn add_data_result(mut self, value: DataResult) -> Self { + self.elements = self.elements.apply_2_and_make_stable( + |mut e, v| { + e.push(v); + e + }, + value, + ); + self + } +} diff --git a/pumpkin-codecs/src/map_codec.rs b/pumpkin-codecs/src/map_codec.rs new file mode 100644 index 000000000..1a4b69e37 --- /dev/null +++ b/pumpkin-codecs/src/map_codec.rs @@ -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 and MapDecoder will also implement MapCodec. +impl MapCodec for T where T: MapEncoder + MapDecoder {} + +/// A map codec allowing an arbitrary encoder and decoder. +pub struct ComposedMapCodec + 'static> { + pub(crate) encoder: E, + pub(crate) decoder: D, +} + +impl> HasValue for ComposedMapCodec { + type Value = E::Value; +} + +impl> Keyable for ComposedMapCodec { + fn keys(&self) -> Vec { + let mut vec = self.encoder.keys(); + vec.extend(self.decoder.keys()); + vec + } +} + +impl> CompressorHolder for ComposedMapCodec { + fn compressor(&self) -> Arc { + // This could return either the encoder or decoder's compressor, but we'll stick with the encoder's. + self.encoder.compressor() + } +} + +impl> MapEncoder for ComposedMapCodec { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: B, + ) -> B { + self.encoder.encode(input, ops, prefix) + } +} + +impl> MapDecoder for ComposedMapCodec { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + self.decoder.decode(input, ops) + } +} + +/// Wraps a [`MapCodec`] to make its [`DataResult`]s stable. +pub struct StableMapCodec { + map_codec: C, +} + +impl HasValue for StableMapCodec { + type Value = C::Value; +} + +impl Keyable for StableMapCodec { + fn keys(&self) -> Vec { + self.map_codec.keys() + } +} + +impl CompressorHolder for StableMapCodec { + fn compressor(&self) -> Arc { + self.map_codec.compressor() + } +} + +impl MapEncoder for StableMapCodec { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: B, + ) -> B { + self.map_codec + .encode(input, ops, prefix) + .set_lifecycle(Lifecycle::Stable) + } +} + +impl MapDecoder for StableMapCodec { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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( + map_codec: C, + getter: fn(&T) -> &C::Value, +) -> Field { + 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( + map_codec: &'static C, + getter: fn(&T) -> &C::Value, +) -> Field { + 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(map_codec: C) -> StableMapCodec { + 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 = ComposedMapCodec<$encoder_type, $decoder_type>; + + #[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, S>(map_codec: &'static C, to: fn(A) -> $to_func_result, from: fn(&S) -> $from_func_result) -> $short_type { + 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, + DataResult, + "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( + codec: &'static C, + validator: fn(&C::Value) -> Result<(), String>, +) -> ValidatedMapCodec { + new_validated_map_codec(codec, validator) +} diff --git a/pumpkin-codecs/src/map_codecs/field_coders.rs b/pumpkin-codecs/src/map_codecs/field_coders.rs new file mode 100644 index 000000000..842180745 --- /dev/null +++ b/pumpkin-codecs/src/map_codecs/field_coders.rs @@ -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 + 'static> { + /// The name of the key. + name: &'static str, + /// The [`Encoder`] for encoding the value. + element_encoder: &'static E, +} + +impl> HasValue for FieldEncoder { + type Value = A; +} + +impl> Keyable for FieldEncoder { + fn keys(&self) -> Vec { + vec![self.name.to_string()] + } +} + +impl> CompressorHolder for FieldEncoder { + impl_compressor!(); +} + +impl> MapEncoder for FieldEncoder { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: B, + ) -> B { + prefix.add_string_key_value_result(self.name, self.element_encoder.encode_start(input, ops)) + } +} + +impl> FieldEncoder { + /// 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 + 'static> { + /// The name of the key. + name: &'static str, + /// The [`Decoder`] for encoding the value. + element_decoder: &'static D, +} + +impl> HasValue for FieldDecoder { + type Value = A; +} + +impl> Keyable for FieldDecoder { + fn keys(&self) -> Vec { + vec![self.name.to_string()] + } +} + +impl> CompressorHolder for FieldDecoder { + impl_compressor!(); +} + +impl> MapDecoder for FieldDecoder { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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> FieldDecoder { + /// 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, + } + } +} diff --git a/pumpkin-codecs/src/map_codecs/mod.rs b/pumpkin-codecs/src/map_codecs/mod.rs new file mode 100644 index 000000000..daa8fbbd8 --- /dev/null +++ b/pumpkin-codecs/src/map_codecs/mod.rs @@ -0,0 +1,4 @@ +pub mod field_coders; +pub mod optional_field; +pub mod simple; +pub mod validated; diff --git a/pumpkin-codecs/src/map_codecs/optional_field.rs b/pumpkin-codecs/src/map_codecs/optional_field.rs new file mode 100644 index 000000000..1242a2e01 --- /dev/null +++ b/pumpkin-codecs/src/map_codecs/optional_field.rs @@ -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 { + 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 HasValue for OptionalFieldMapCodec { + // The type of this `MapCodec` should be an `Option`. + type Value = Option; +} + +impl Keyable for OptionalFieldMapCodec { + fn keys(&self) -> Vec { + vec![self.name.to_string()] + } +} + +impl CompressorHolder for OptionalFieldMapCodec { + impl_compressor!(); +} + +impl MapEncoder for OptionalFieldMapCodec { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + 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 MapDecoder for OptionalFieldMapCodec { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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> + 'static, +> { + codec: C, + default: fn() -> T, +} + +impl>> HasValue + for DefaultValueProviderMapCodec +{ + type Value = T; +} + +impl>> Keyable + for DefaultValueProviderMapCodec +{ + fn keys(&self) -> Vec { + self.codec.keys() + } +} + +impl>> CompressorHolder + for DefaultValueProviderMapCodec +{ + fn compressor(&self) -> Arc { + self.codec.compressor() + } +} + +impl>> MapEncoder + for DefaultValueProviderMapCodec +{ + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: B, + ) -> B { + let clone = Some(input.clone()); + self.codec.encode( + if *input == (self.default)() { + &None + } else { + &clone + }, + ops, + prefix, + ) + } +} + +impl>> MapDecoder + for DefaultValueProviderMapCodec +{ + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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>, +>( + map_codec: C, + default: fn() -> T, +) -> DefaultValueProviderMapCodec { + DefaultValueProviderMapCodec { + codec: map_codec, + default, + } +} + +/// Returns a new [`OptionalFieldMapCodec`]. +pub(crate) const fn new_optional_field_map_codec( + element_codec: &'static C, + name: &'static str, + lenient: bool, +) -> OptionalFieldMapCodec { + OptionalFieldMapCodec { + element_codec, + name, + lenient, + } +} diff --git a/pumpkin-codecs/src/map_codecs/simple.rs b/pumpkin-codecs/src/map_codecs/simple.rs new file mode 100644 index 000000000..92146c0b9 --- /dev/null +++ b/pumpkin-codecs/src/map_codecs/simple.rs @@ -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 +where + K::Value: Display + Eq + Hash, +{ + key_codec: &'static K, + element_codec: &'static V, + + keyable: Key, +} +impl Keyable for SimpleMapCodec +where + K::Value: Display + Eq + Hash, +{ + fn keys(&self) -> Vec { + self.keyable.keys() + } +} + +impl CompressorHolder for SimpleMapCodec +where + K::Value: Display + Eq + Hash, +{ + impl_compressor!(); +} + +impl BaseMapCodec for SimpleMapCodec +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( + key_codec: &'static K, + element_codec: &'static V, + keyable: Key, +) -> SimpleMapCodec +where + ::Value: Display + Eq + Hash, +{ + SimpleMapCodec { + key_codec, + element_codec, + keyable, + } +} diff --git a/pumpkin-codecs/src/map_codecs/validated.rs b/pumpkin-codecs/src/map_codecs/validated.rs new file mode 100644 index 000000000..53cb1c776 --- /dev/null +++ b/pumpkin-codecs/src/map_codecs/validated.rs @@ -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 { + codec: &'static C, + /// The validator function used. + validator: fn(&C::Value) -> Result<(), String>, +} + +impl HasValue for ValidatedMapCodec { + type Value = C::Value; +} + +impl Keyable for ValidatedMapCodec { + fn keys(&self) -> Vec { + self.codec.keys() + } +} + +impl CompressorHolder for ValidatedMapCodec { + fn compressor(&self) -> Arc { + self.codec.compressor() + } +} + +impl MapEncoder for ValidatedMapCodec { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + 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 MapDecoder for ValidatedMapCodec { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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( + codec: &'static C, + validator: fn(&C::Value) -> Result<(), String>, +) -> ValidatedMapCodec { + ValidatedMapCodec { codec, validator } +} diff --git a/pumpkin-codecs/src/map_coders.rs b/pumpkin-codecs/src/map_coders.rs new file mode 100644 index 000000000..6e1e82847 --- /dev/null +++ b/pumpkin-codecs/src/map_coders.rs @@ -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 + 'static> { + builder: DataResult>, + ops: &'static O, + compressor: &'a KeyCompressor, +} + +impl<'a, T: Clone, O: DynamicOps + '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> StructBuilder for CompressedStructBuilder<'_, T, O> { + type Value = T; + + impl_struct_builder!(builder); + impl_universal_struct_builder!(builder, self.ops); +} + +impl> ResultStructBuilder for CompressedStructBuilder<'_, T, O> { + type Result = Vec; + + fn build_with_builder( + self, + builder: Self::Result, + prefix: Self::Value, + ) -> DataResult { + self.ops.merge_values_into_list(prefix, builder) + } +} + +impl> 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 + 'static> { + Normal(O::StructBuilder), + Compressed(MapBuilder), +} + +/// 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> StructBuilder for EncoderStructBuilder { + 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 { + delegate_encoder_struct_builder_method!(self, add_key_value_result, key, value) + } + + fn add_key_result_value_result( + self, + key: DataResult, + value: DataResult, + ) -> Self { + delegate_encoder_struct_builder_method!(self, add_key_result_value_result, key, value) + } + + fn with_errors_from(self, result: &DataResult) -> 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 { + 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 { + 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; +} + +/// 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>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + prefix: B, + ) -> B; + + /// Returns a [`StructBuilder`] of this `MapEncoder` with the provided [`DynamicOps`]. + fn builder<'a, T: Display + Clone + 'a, O: DynamicOps + 'static>( + &'a self, + ops: &'static O, + ) -> EncoderStructBuilder { + 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( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult; + + fn compressed_decode( + &self, + input: T, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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 + 'static> { + list: Vec, + compressor: Arc, + ops: &'static O, + } + + impl> MapLike for CompressorMapLikeImpl { + 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 + '_ { + 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 { + // We get the unique pointer of this holder. + let key = std::ptr::from_ref::(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 { + encoder: &'static E, + function: fn(&B) -> $function_return, + } + + impl HasValue for $name { + type Value = B; + } + + impl Keyable for $name { + fn keys(&self) -> Vec { + self.encoder.keys() + } + } + + impl CompressorHolder for $name { + fn compressor(&self) -> Arc { + self.encoder.compressor() + } + } + }; +} + +impl_map_encoder_transformer!(ComappedMapEncoderImpl, E::Value); + +impl MapEncoder for ComappedMapEncoderImpl { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + 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( + encoder: &'static E, + f: fn(&B) -> E::Value, +) -> ComappedMapEncoderImpl { + ComappedMapEncoderImpl { + encoder, + function: f, + } +} + +impl_map_encoder_transformer!(FlatComappedMapEncoderImpl, DataResult); + +impl MapEncoder for FlatComappedMapEncoderImpl { + fn encode>( + &self, + input: &Self::Value, + ops: &'static impl DynamicOps, + 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( + encoder: &'static E, + f: fn(&B) -> DataResult, +) -> FlatComappedMapEncoderImpl { + FlatComappedMapEncoderImpl { + encoder, + function: f, + } +} + +macro_rules! impl_map_decoder_transformer { + ($name:ident, $function_return:ty) => { + pub struct $name { + decoder: &'static D, + function: fn(D::Value) -> $function_return, + } + + impl HasValue for $name { + type Value = B; + } + + impl Keyable for $name { + fn keys(&self) -> Vec { + self.decoder.keys() + } + } + + impl CompressorHolder for $name { + fn compressor(&self) -> Arc { + self.decoder.compressor() + } + } + }; +} + +impl_map_decoder_transformer!(MappedMapDecoderImpl, B); + +impl MapDecoder for MappedMapDecoderImpl { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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( + decoder: &'static D, + f: fn(D::Value) -> B, +) -> MappedMapDecoderImpl { + MappedMapDecoderImpl { + decoder, + function: f, + } +} + +impl_map_decoder_transformer!(FlatMappedMapDecoderImpl, DataResult); + +impl MapDecoder for FlatMappedMapDecoderImpl { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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( + decoder: &'static D, + f: fn(D::Value) -> DataResult, +) -> FlatMappedMapDecoderImpl { + FlatMappedMapDecoderImpl { + decoder, + function: f, + } +} diff --git a/pumpkin-codecs/src/map_like.rs b/pumpkin-codecs/src/map_like.rs new file mode 100644 index 000000000..1bf498704 --- /dev/null +++ b/pumpkin-codecs/src/map_like.rs @@ -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 + '_; +} diff --git a/pumpkin-codecs/src/struct_builder.rs b/pumpkin-codecs/src/struct_builder.rs new file mode 100644 index 000000000..8c9218a91 --- /dev/null +++ b/pumpkin-codecs/src/struct_builder.rs @@ -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; + + /// 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, + value: DataResult, + ) -> Self; + + /// Adds the error message from a provided `DataResult` (if any) to this builder and returns the builder. + #[must_use] + fn with_errors_from(self, result: &DataResult) -> 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; + + /// 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; + + /// Builds the map stored in this builder along with a `DataResult` prefix and returns the result. + fn build_with_result_prefix(self, prefix: DataResult) -> DataResult + 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; +} + +/// 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`] 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(mut self, result: &DataResult) -> Self { + self.$builder = self.$builder.with_errors_from(result); + self + } + + fn build(self, prefix: Self::Value) -> DataResult { + 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 { + 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 { + 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, + value: DataResult, + ) -> 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 { + 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, + value: DataResult, + ) -> 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 { + 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, + value: DataResult, + ) -> 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 { + 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 { + let string = self.$ops.create_string(key); + self.add_key_value_result(string, value) + } + }; +} + +pub struct MapBuilder + 'static> { + builder: DataResult>, + ops: &'static O, +} + +impl> MapBuilder { + pub(crate) const fn new(ops: &'static O) -> Self { + Self { + builder: DataResult::new_success_with_lifecycle(vec![], Lifecycle::Stable), + ops, + } + } +} + +impl> StructBuilder for MapBuilder { + type Value = T; + + impl_struct_builder!(builder); + impl_universal_struct_builder!(builder, self.ops); +} + +impl> ResultStructBuilder for MapBuilder { + type Result = Vec<(T, T)>; + + fn build_with_builder( + self, + builder: Self::Result, + prefix: Self::Value, + ) -> DataResult { + self.ops.merge_entries_into_map(prefix, builder) + } +} + +impl> UniversalStructBuilder for MapBuilder { + fn append( + &self, + key: Self::Value, + value: Self::Value, + mut builder: Self::Result, + ) -> Self::Result { + builder.push((key, value)); + builder + } +} diff --git a/pumpkin-codecs/src/struct_codecs.rs b/pumpkin-codecs/src/struct_codecs.rs new file mode 100644 index 000000000..7144c3907 --- /dev/null +++ b/pumpkin-codecs/src/struct_codecs.rs @@ -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 { + Owned(C, fn(&T) -> &C::Value), + Borrowed(&'static C, fn(&T) -> &C::Value), +} + +impl Field { + 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 { + field_1: Field, + $( $field: Field ,)* + apply_function: fn(C1::Value $(, $codec_type::Value)*) -> T + } + + impl HasValue for $name { + type Value = T; + } + + impl Keyable for $name { + #[allow(unused_mut)] + fn keys(&self) -> Vec { + let mut keys = self.field_1.map_codec().keys(); + $( keys.extend(self.$field.map_codec().keys()); )* + keys + } + } + + impl CompressorHolder for $name { + impl_compressor!(); + } + + impl MapEncoder for $name { + #[allow(clippy::let_and_return)] + fn encode>(&self, input: &Self::Value, ops: &'static impl DynamicOps, 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 MapDecoder for $name { + fn decode( + &self, + input: &impl MapLike, + ops: &'static impl DynamicOps, + ) -> DataResult { + 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 = MapCodecCodec<$name>; + }; + + ($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( + field_1: Field, + $($field: Field,)* + f: fn(C1::Value $(, $codec_type::Value)*) -> T, + ) -> $alias { + 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( + field_1: Field, + $($field: Field,)* + f: fn(C1::Value $(, $codec_type::Value)*) -> T, + ) -> $alias { + 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, + FieldMapCodec, + FieldMapCodec, + >; + + 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, + capacity: u32, + } + + pub type BookshelfCodec = ValidatedCodec< + StructCodec3< + Bookshelf, + FieldMapCodec, + DefaultedFieldCodec>, + FieldMapCodec, + >, + >; + 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 + ); + } +} diff --git a/pumpkin-nbt/Cargo.toml b/pumpkin-nbt/Cargo.toml index bf9db3b70..fb61a79d8 100644 --- a/pumpkin-nbt/Cargo.toml +++ b/pumpkin-nbt/Cargo.toml @@ -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 diff --git a/pumpkin-nbt/src/lib.rs b/pumpkin-nbt/src/lib.rs index 1be4dcce3..42d16a5a7 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -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; diff --git a/pumpkin-nbt/src/nbt_ops.rs b/pumpkin-nbt/src/nbt_ops.rs new file mode 100644 index 000000000..497343531 --- /dev/null +++ b/pumpkin-nbt/src/nbt_ops.rs @@ -0,0 +1,1475 @@ +use crate::compound::NbtCompound; +use crate::tag::NbtTag; +use pumpkin_codecs::Number; +use pumpkin_codecs::data_result::DataResult; +use pumpkin_codecs::dynamic_ops::DynamicOps; +use pumpkin_codecs::lifecycle::Lifecycle; +use pumpkin_codecs::map_like::MapLike; +use pumpkin_codecs::struct_builder::{ResultStructBuilder, StringStructBuilder, StructBuilder}; +use pumpkin_codecs::{impl_get_list, impl_string_struct_builder, impl_struct_builder}; +use std::iter::Map; +use std::vec::IntoIter; +use tracing::warn; + +/// A [`DynamicOps`] to serialize to/deserialize from NBT data. +pub struct NbtOps; + +/// An instance of [`NbtOps`], which serializes/deserializes NBT data. +pub static INSTANCE: NbtOps = NbtOps; + +impl DynamicOps for NbtOps { + type Value = NbtTag; + type StructBuilder = NbtStructBuilder; + + fn empty(&self) -> Self::Value { + NbtTag::End + } + + fn create_number(&self, n: Number) -> Self::Value { + NbtTag::Double(n.into()) + } + + fn create_byte(&self, data: i8) -> Self::Value { + NbtTag::Byte(data) + } + + fn create_short(&self, data: i16) -> Self::Value { + NbtTag::Short(data) + } + + fn create_int(&self, data: i32) -> Self::Value { + NbtTag::Int(data) + } + + fn create_long(&self, data: i64) -> Self::Value { + NbtTag::Long(data) + } + + fn create_float(&self, data: f32) -> Self::Value { + NbtTag::Float(data) + } + + fn create_double(&self, data: f64) -> Self::Value { + NbtTag::Double(data) + } + + fn create_bool(&self, data: bool) -> Self::Value { + NbtTag::Byte(data.into()) + } + + fn create_string(&self, data: &str) -> Self::Value { + NbtTag::String(data.to_string()) + } + + fn create_list(&self, values: I) -> Self::Value + where + I: IntoIterator, + { + ListCollector::new_collector().accept_all(values).result() + } + + fn create_map(&self, entries: I) -> Self::Value + where + I: IntoIterator, + { + let mut compound = NbtCompound::new(); + for (k, v) in entries { + if let Some(key) = k.extract_string() { + compound.put(key, v); + } else { + // The Minecraft implementation just uses the string representation of the key tag, + // but that probably isn't meant to be intentionally used, so we will just + // log a warning. + warn!("Invalid key tag for creating NBT compound: {k}"); + } + } + compound.into() + } + + fn get_number(&self, input: &Self::Value) -> DataResult { + match input { + NbtTag::Byte(b) => DataResult::new_success(Number::Byte(*b)), + NbtTag::Short(s) => DataResult::new_success(Number::Short(*s)), + NbtTag::Int(i) => DataResult::new_success(Number::Int(*i)), + NbtTag::Long(l) => DataResult::new_success(Number::Long(*l)), + NbtTag::Float(f) => DataResult::new_success(Number::Float(*f)), + NbtTag::Double(d) => DataResult::new_success(Number::Double(*d)), + + _ => DataResult::new_error("Not a number".to_string()), + } + } + + fn get_string(&self, input: &Self::Value) -> DataResult { + input.extract_string().map_or_else( + || DataResult::new_error("Not a string".to_string()), + |s| DataResult::new_success(s.to_string()), + ) + } + + fn get_map_iter<'a>( + &'a self, + input: &'a Self::Value, + ) -> DataResult + 'a> { + if let NbtTag::Compound(compound) = input { + DataResult::new_success( + compound + .child_tags + .iter() + .map(|(k, v)| (self.create_string(k), v)), + ) + } else { + DataResult::new_error(format!("Not a map: {input}")) + } + } + + fn get_map<'a>( + &self, + input: &'a Self::Value, + ) -> DataResult + 'a> { + if let NbtTag::Compound(compound) = input { + DataResult::new_success(NbtMapLike { compound }) + } else { + DataResult::new_error(format!("Not a map: {input}")) + } + } + + fn get_iter(&self, input: Self::Value) -> DataResult> { + match input { + NbtTag::List(l) => { + // Check the type of this list. + // If the list contains compounds, we try unwrapping them. + if !l.is_empty() + && let NbtTag::Compound(_) = l.first().unwrap() + { + DataResult::new_success(NbtIter::CompoundList(l.into_iter().map(|c| { + if let NbtTag::Compound(compound) = c { + Self::try_unwrap(compound) + } else { + c.clone() + } + }))) + } else { + DataResult::new_success(NbtIter::List(l.into_iter())) + } + } + + NbtTag::ByteArray(b) => DataResult::new_success(NbtIter::ByteArray( + b.into_iter().map(|b| Self.create_byte(b as i8)), + )), + NbtTag::IntArray(i) => DataResult::new_success(NbtIter::IntArray( + i.into_iter().map(|i| Self.create_int(i)), + )), + NbtTag::LongArray(l) => DataResult::new_success(NbtIter::LongArray( + l.into_iter().map(|l| Self.create_long(l)), + )), + + _ => DataResult::new_error(format!("Not a list: {input}")), + } + } + + fn get_byte_buffer(&self, input: Self::Value) -> DataResult> { + if let NbtTag::ByteArray(b) = input { + DataResult::new_success(b) + } else { + impl_get_list!(box self, input, "bytes") + } + } + + fn create_byte_buffer(&self, buffer: Vec) -> Self::Value { + NbtTag::ByteArray(buffer.into_boxed_slice()) + } + + fn get_int_list(&self, input: Self::Value) -> DataResult> { + if let NbtTag::IntArray(i) = input { + DataResult::new_success(i) + } else { + impl_get_list!(self, input, "ints") + } + } + + fn create_int_list(&self, list: Vec) -> Self::Value { + NbtTag::IntArray(list) + } + + fn get_long_list(&self, input: Self::Value) -> DataResult> { + if let NbtTag::LongArray(i) = input { + DataResult::new_success(i) + } else { + impl_get_list!(self, input, "longs") + } + } + + fn create_long_list(&self, list: Vec) -> Self::Value { + NbtTag::LongArray(list) + } + + fn merge_into_list(&self, list: Self::Value, value: Self::Value) -> DataResult { + ListCollector::new(list.clone()).map_or_else( + || DataResult::new_partial_error("Not a list".to_string(), list), + |c| DataResult::new_success(c.accept(value).result()), + ) + } + + fn merge_values_into_list(&self, list: Self::Value, values: I) -> DataResult + where + I: IntoIterator, + { + ListCollector::new(list.clone()).map_or_else( + || DataResult::new_partial_error("Not a list".to_string(), list), + |c| DataResult::new_success(c.accept_all(values).result()), + ) + } + + fn merge_into_map( + &self, + map: Self::Value, + key: Self::Value, + value: Self::Value, + ) -> DataResult + where + Self::Value: Clone, + { + if !matches!(map, NbtTag::Compound(_) | NbtTag::End) { + DataResult::new_partial_error(format!("Not a map: {map}"), map) + } else if !matches!(key, NbtTag::String(_)) { + DataResult::new_partial_error(format!("Key is not a string: {key}"), map) + } else { + let mut compound = if let NbtTag::Compound(c) = map { + c + } else { + NbtCompound::new() + }; + compound.put(key.extract_string().unwrap(), value); + DataResult::new_success(compound.into()) + } + } + + fn merge_map_like_into_map( + &self, + map: Self::Value, + other_map_like: M, + ) -> DataResult + where + M: MapLike, + Self::Value: Clone, + { + if matches!(map, NbtTag::Compound(_) | NbtTag::End) { + let mut compound = if let NbtTag::Compound(c) = map { + c + } else { + NbtCompound::default() + }; + let mut failed = vec![]; + other_map_like.iter().for_each(|(k, v)| { + if let NbtTag::String(key) = k { + compound.put(&key, v.clone()); + } else { + failed.push((k, v)); + } + }); + if failed.is_empty() { + DataResult::new_success(compound.into()) + } else { + DataResult::new_partial_error( + format!("Some keys are not strings: {failed:?}"), + NbtTag::Compound(compound), + ) + } + } else { + DataResult::new_partial_error(format!("Not a map: {map}"), map) + } + } + + fn remove(&self, input: Self::Value, key: &str) -> Self::Value { + if let NbtTag::Compound(compound) = input { + // Try to remove any entries whose key matches with `key`. + NbtTag::Compound( + compound + .child_tags + .into_iter() + .filter(|s| s.0 != key) + .collect(), + ) + } else { + input + } + } + + fn convert_to(&self, out_ops: &impl DynamicOps, input: Self::Value) -> U { + match input { + NbtTag::End => out_ops.empty(), + NbtTag::Byte(b) => out_ops.create_byte(b), + NbtTag::Short(s) => out_ops.create_short(s), + NbtTag::Int(i) => out_ops.create_int(i), + NbtTag::Long(l) => out_ops.create_long(l), + NbtTag::Float(f) => out_ops.create_float(f), + NbtTag::Double(d) => out_ops.create_double(d), + NbtTag::ByteArray(b) => out_ops.create_byte_buffer(b.to_vec()), + NbtTag::String(s) => out_ops.create_string(&s), + NbtTag::List(_) => self.convert_list(out_ops, input), + NbtTag::Compound(_) => self.convert_map(out_ops, input), + NbtTag::IntArray(i) => out_ops.create_int_list(i), + NbtTag::LongArray(l) => out_ops.create_long_list(l), + } + } + + fn map_builder(&'static self) -> Self::StructBuilder { + NbtStructBuilder { + builder: DataResult::new_success_with_lifecycle( + NbtTag::Compound(NbtCompound::new()), + Lifecycle::Stable, + ), + } + } +} + +impl NbtOps { + /// Tries to unwrap an [`NbtCompound`]. + /// + /// If `compound` only has one element with an empty key (`""`), it returns that element. + /// Otherwise, this simply returns a new [`NbtTag::Compound`] with `compound`. + fn try_unwrap(mut compound: NbtCompound) -> NbtTag { + if compound.child_tags.len() == 1 + && let Some(_) = compound.get("") + { + // Remove the element to own the contained tag. + compound.child_tags.remove(0).1 + } else { + NbtTag::from(compound) + } + } +} + +/// A single concrete type for an iterator of an NBT element. +enum NbtIter { + List(IntoIter), + CompoundList(Map, fn(NbtTag) -> NbtTag>), + ByteArray(Map, fn(u8) -> NbtTag>), + IntArray(Map, fn(i32) -> NbtTag>), + LongArray(Map, fn(i64) -> NbtTag>), +} + +impl Iterator for NbtIter { + type Item = NbtTag; + + fn next(&mut self) -> Option { + match self { + Self::List(iter) => iter.next(), + Self::CompoundList(iter) => iter.next(), + Self::ByteArray(iter) => iter.next(), + Self::IntArray(iter) => iter.next(), + Self::LongArray(iter) => iter.next(), + } + } +} + +/// An implementation of [`MapLike`] for NBT objects. +/// The lifetime is that of the referenced map. +struct NbtMapLike<'a> { + compound: &'a NbtCompound, +} + +impl MapLike for NbtMapLike<'_> { + type Value = NbtTag; + + fn get(&self, key: &Self::Value) -> Option<&Self::Value> { + key.extract_string().and_then(|s| self.get_str(s)) + } + + fn get_str(&self, key: &str) -> Option<&Self::Value> { + self.compound.get(key) + } + + fn iter(&self) -> impl Iterator + '_ { + self.compound + .child_tags + .iter() + .map(|(k, v)| (NbtTag::String(k.clone()), v)) + } +} + +/// An implementation of [`StructBuilder`] for NBT objects. +pub struct NbtStructBuilder { + builder: DataResult, +} + +impl ResultStructBuilder for NbtStructBuilder { + type Result = NbtTag; + + fn build_with_builder( + self, + builder: Self::Result, + prefix: Self::Value, + ) -> DataResult { + match prefix { + NbtTag::End => DataResult::new_success(builder), + NbtTag::Compound(mut compound) => { + match builder { + NbtTag::Compound(builder_compound) => { + for (k, v) in builder_compound { + compound.put(&k, v); + } + } + // This shouldn't happen, but just in case. + _ => { + return DataResult::new_error(format!( + "Expected compound in builder, found {builder}" + )); + } + } + DataResult::new_success(compound.into()) + } + _ => DataResult::new_partial_error(format!("Prefix is not a map: {prefix}"), prefix), + } + } +} + +impl StructBuilder for NbtStructBuilder { + type Value = NbtTag; + + impl_struct_builder!(builder); + impl_string_struct_builder!(builder, INSTANCE); +} + +impl StringStructBuilder for NbtStructBuilder { + fn append(&self, key: &str, value: Self::Value, builder: Self::Result) -> Self::Result { + if let NbtTag::Compound(mut compound) = builder { + compound.put(key, value); + compound.into() + } else { + builder + } + } +} + +// List collectors + +/// A collector object for NBT lists. +/// +/// The variants of this object should not be used as that is an implementation detail. +enum ListCollector { + Generic(InnerGenericListCollector), + + Byte(InnerByteListCollector), + Int(InnerIntListCollector), + Long(InnerLongListCollector), +} + +impl ListCollector { + /// Creates a new [`ListCollector`]. + /// + /// This only returns an actual collector for [`NbtTag::End`] and all list [`NbtTag`]s. + fn new(tag: NbtTag) -> Option { + match tag { + NbtTag::End => Some(Self::new_collector()), + + NbtTag::List(_) | NbtTag::ByteArray(_) | NbtTag::IntArray(_) | NbtTag::LongArray(_) => { + // Try to get the length of the tag. + let len = match &tag { + NbtTag::List(list) => list.len(), + + NbtTag::ByteArray(list) => list.len(), + NbtTag::IntArray(list) => list.len(), + NbtTag::LongArray(list) => list.len(), + + _ => unreachable!(), + }; + + if len == 0 { + return Some(Self::new_collector()); + } + + // From this point onwards, we know that the list is not empty. + match tag { + NbtTag::List(list) => Some(Self::Generic(InnerGenericListCollector::new(list))), + NbtTag::ByteArray(list) => Some(Self::Byte(InnerByteListCollector::new(list))), + NbtTag::IntArray(list) => Some(Self::Int(InnerIntListCollector::new(list))), + NbtTag::LongArray(list) => Some(Self::Long(InnerLongListCollector::new(list))), + + _ => unreachable!(), + } + } + + _ => None, + } + } + + /// Creates a new initial collector. + /// [`NbtTag`]s can directly be added to this collector without any type worries. + const fn new_collector() -> Self { + Self::Generic(InnerGenericListCollector { + result: NbtTag::List(vec![]), + }) + } + + /// Accepts an [`NbtTag`]. + fn accept(self, tag: NbtTag) -> Self { + match self { + Self::Generic(c) => c.accept(tag), + Self::Byte(c) => c.accept(tag), + Self::Int(c) => c.accept(tag), + Self::Long(c) => c.accept(tag), + } + } + + /// Accepts all [`NbtTag`]s of the provided list. + fn accept_all(self, tags: impl IntoIterator) -> Self { + let mut collector = self; + for tag in tags { + collector = collector.accept(tag); + } + collector + } + + /// Provides the final result. + fn result(self) -> NbtTag { + match self { + Self::Generic(c) => c.result(), + Self::Byte(c) => c.result(), + Self::Int(c) => c.result(), + Self::Long(c) => c.result(), + } + } +} + +/// An 'inner' list collector stored in one of the corresponding [`ListCollector`] enums. +trait InnerListCollector { + fn accept(self, tag: NbtTag) -> ListCollector + where + Self: Sized; + + fn result(self) -> NbtTag; +} + +/// An implementation of [`InnerListCollector`] for a generic list (of any type). +struct InnerGenericListCollector { + result: NbtTag, +} + +impl InnerListCollector for InnerGenericListCollector { + fn accept(mut self, tag: NbtTag) -> ListCollector + where + Self: Sized, + { + match &mut self.result { + NbtTag::List(list) => list.push(tag), + _ => unreachable!(), + } + ListCollector::Generic(self) + } + + fn result(self) -> NbtTag { + self.result + } +} + +impl From for InnerGenericListCollector { + fn from(value: InnerByteListCollector) -> Self { + Self { + result: NbtTag::List(value.list.into_iter().map(NbtTag::Byte).collect()), + } + } +} + +impl InnerGenericListCollector { + const fn new(list: Vec) -> Self { + Self { + result: NbtTag::List(list), + } + } +} + +/// An implementation of [`InnerListCollector`] specifically for [`NbtTag::ByteArray`]s. +struct InnerByteListCollector { + list: Vec, +} + +impl InnerListCollector for InnerByteListCollector { + fn accept(mut self, tag: NbtTag) -> ListCollector + where + Self: Sized, + { + if let NbtTag::Byte(byte) = tag { + self.list.push(byte); + ListCollector::Byte(self) + } else { + >::into(self).accept(tag) + } + } + + fn result(self) -> NbtTag { + NbtTag::ByteArray( + self.list + .into_iter() + .map(|i| i as u8) + .collect::>() + .into_boxed_slice(), + ) + } +} + +impl InnerByteListCollector { + fn new(list: Box<[u8]>) -> Self { + Self { + list: list.into_iter().map(|i| i as i8).collect(), + } + } +} + +macro_rules! add_inner_specific_array_collector_impl { + ($name:ident, $single_type:ident, $array_type:ident, $ty:ty) => { + #[doc = concat!("An implementation of [`InnerListCollector`] specifically for [`NbtTag::", stringify!($array_type), "`]s.")] + struct $name { + list: Vec<$ty> + } + + impl InnerListCollector for $name { + fn accept(mut self, tag: NbtTag) -> ListCollector + where + Self: Sized + { + if let NbtTag::$single_type(v) = tag { + self.list.push(v); + ListCollector::$single_type(self) + } else { + >::into(self) + .accept(tag) + } + } + + fn result(self) -> NbtTag { + NbtTag::$array_type(self.list) + } + } + + impl $name { + const fn new(list: Vec<$ty>) -> Self { + Self { + list + } + } + } + + impl From<$name> for InnerGenericListCollector { + fn from(value: $name) -> Self { + InnerGenericListCollector { + result: NbtTag::List( + value.list.into_iter().map(|b| NbtTag::$single_type(b)).collect() + ) + } + } + } + }; +} + +add_inner_specific_array_collector_impl!(InnerIntListCollector, Int, IntArray, i32); +add_inner_specific_array_collector_impl!(InnerLongListCollector, Long, LongArray, i64); + +#[cfg(test)] +mod test { + use crate::compound::NbtCompound; + use crate::nbt_ops::{INSTANCE, ListCollector}; + use crate::tag::NbtTag; + use pumpkin_codecs::codec::{ + BOOL_CODEC, BYTE_BUFFER_CODEC, BYTE_CODEC, ComapFlatMapCodec, DOUBLE_CODEC, + DefaultedFieldCodec, FieldMapCodec, INT_CODEC, INT_STREAM_CODEC, LONG_CODEC, + LONG_STREAM_CODEC, SHORT_CODEC, STRING_CODEC, UBYTE_CODEC, UINT_CODEC, UbyteCodec, + UintCodec, comap_flat_map, field, optional_field_with_default, unbounded_list, + unbounded_map, validate, + }; + use pumpkin_codecs::codecs::list::ListCodec; + use pumpkin_codecs::codecs::primitive::{ByteBufferCodec, StringCodec}; + use pumpkin_codecs::codecs::unbounded_map::UnboundedMapCodec; + use pumpkin_codecs::codecs::validated::ValidatedCodec; + use pumpkin_codecs::coders::{Decoder, Encoder}; + use pumpkin_codecs::data_result::DataResult; + use pumpkin_codecs::map_codec::for_getter; + use pumpkin_codecs::struct_codec; + use pumpkin_codecs::struct_codecs::{StructCodec2, StructCodec3}; + use std::collections::HashMap; + + /// Convenience function to easily create an [`NbtTag::Compound`]. + macro_rules! nbt_compound_tag { + + ( { $($key:literal : $tag:expr),+ $(,)* } ) => { + { + let mut compound = NbtCompound::new(); + $( compound.put($key, $tag); )+ + NbtTag::Compound(compound) + } + }; + // For empty compounds + ( {} ) => { + NbtTag::Compound(NbtCompound::new()) + }; + } + + #[test] + #[allow(clippy::too_many_lines)] + fn primitives() { + // Simple types + assert_eq!( + INT_CODEC + .encode_start(&45, &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::Int(45) + ); + assert_eq!( + BOOL_CODEC + .encode_start(&true, &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::Byte(1) + ); + assert_eq!( + BYTE_CODEC + .encode_start(&-89, &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::Byte(-89) + ); + assert_eq!( + DOUBLE_CODEC + .encode_start(&1.0, &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::Double(1.0) + ); + + assert_eq!( + STRING_CODEC + .encode_start(&"Sample Text".to_string(), &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::String("Sample Text".to_string()) + ); + + assert_eq!( + INT_CODEC + .parse(NbtTag::Int(50), &INSTANCE) + .expect("Decoding should succeed"), + 50 + ); + assert_eq!( + SHORT_CODEC + .parse(NbtTag::Short(-1235), &INSTANCE) + .expect("Decoding should succeed"), + -1235 + ); + assert_eq!( + LONG_CODEC + .parse(NbtTag::Long(53234), &INSTANCE) + .expect("Decoding should succeed"), + 53234 + ); + + // Packed array types + let byte_vec = vec![ + 1u8, 45u8, 100u8, 170u8, 203u8, 98u8, 245u8, 255u8, 0u8, 13u8, + ]; + + assert_eq!( + BYTE_BUFFER_CODEC + .encode_start(&Box::from(&byte_vec[0..3]), &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::ByteArray(Box::from(vec![1, 45, 100])) + ); + assert_eq!( + BYTE_BUFFER_CODEC + .encode_start(&Box::from(&byte_vec[2..7]), &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::ByteArray(Box::from(vec![100, 170, 203, 98, 245])) + ); + + assert_eq!( + INT_STREAM_CODEC + .encode_start(&vec![-100, 1234, 23948], &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::IntArray(vec![-100, 1234, 23948]) + ); + assert_eq!( + INT_STREAM_CODEC + .encode_start(&vec![1, 120938, 1231909999], &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::IntArray(vec![1, 120938, 1231909999]) + ); + + assert_eq!( + LONG_STREAM_CODEC + .encode_start(&vec![10_000_000_000, -99_999_999_999], &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::LongArray(vec![10_000_000_000, -99_999_999_999]) + ); + assert_eq!( + LONG_STREAM_CODEC + .encode_start(&vec![123_456_789_012_345, 66], &INSTANCE) + .expect("Encoding should succeed"), + NbtTag::LongArray(vec![123_456_789_012_345, 66]) + ); + + assert_eq!( + BYTE_BUFFER_CODEC + .parse(NbtTag::ByteArray(Box::new([1, 4])), &INSTANCE) + .expect("Decoding should succeed"), + vec![1, 4].into_boxed_slice() + ); + // All `get_...` packed array functions allow any arbitrary number array. + assert_eq!( + BYTE_BUFFER_CODEC + .parse(NbtTag::IntArray(vec![120]), &INSTANCE) + .expect("Decoding should succeed"), + vec![120].into_boxed_slice() + ); + assert_eq!( + INT_STREAM_CODEC + .parse(NbtTag::LongArray(vec![1, 2, 3]), &INSTANCE) + .expect("Decoding should succeed"), + vec![1, 2, 3] + ); + assert_eq!( + LONG_STREAM_CODEC + .parse(NbtTag::IntArray(vec![0, 0]), &INSTANCE) + .expect("Decoding should succeed"), + vec![0, 0] + ); + } + + #[test] + fn list_collecting() { + // Int list collector + let tag = NbtTag::IntArray(vec![10, 15, 20]); + + assert_eq!( + ListCollector::new(tag) + .expect("List collector should exist") + .result(), + NbtTag::IntArray(vec![10, 15, 20]) + ); + + // Byte list collector + let tag = NbtTag::ByteArray(Box::new([255, 45, 100])); + + assert_eq!( + ListCollector::new(tag) + .expect("List collector should exist") + .result(), + NbtTag::ByteArray(Box::new([255, 45, 100])) + ); + + // Long list + let tag = NbtTag::LongArray(vec![11_234_567_890, -986, 1, -937_238_122]); + + assert_eq!( + ListCollector::new(tag) + .expect("List collector should exist") + .result(), + NbtTag::LongArray(vec![11_234_567_890, -986, 1, -937_238_122]) + ); + + // Generic list collector + // Homogeneous elements + let mut collector = ListCollector::new_collector(); + + collector = collector.accept(NbtTag::Float(-123.4)); + collector = collector.accept(NbtTag::Float(12.5)); + + assert_eq!( + collector.result(), + NbtTag::List(vec![NbtTag::Float(-123.4), NbtTag::Float(12.5)]) + ); + + // Heterogeneous elements + let mut collector = ListCollector::new_collector(); + + collector = collector.accept(NbtTag::Byte(99)); + collector = collector.accept(NbtTag::String("99".to_string())); + collector = collector.accept(NbtTag::LongArray(vec![1, 2, 3])); + + assert_eq!( + collector.result(), + NbtTag::List(vec![ + NbtTag::Byte(99), + NbtTag::String("99".to_string()), + NbtTag::LongArray(vec![1, 2, 3]) + ]) + ); + } + + // Specific codec tests + + #[test] + fn employee() { + /// A struct to store a single employee. + /// The `name` and `department` of the employee should not be empty. + #[derive(Debug, PartialEq)] + struct Employee { + name: String, + department: String, + salary: u32, + } + + pub type NonEmptyStringCodec = ValidatedCodec; + /// Convenience codec for only encoding/decoding non-empty strings. + pub static NON_EMPTY_STRING_CODEC: NonEmptyStringCodec = validate(&STRING_CODEC, |s| { + if s.is_empty() { + Err("String should not be empty".to_string()) + } else { + Ok(()) + } + }); + + pub type EmployeeCodec = StructCodec3< + Employee, + FieldMapCodec, + FieldMapCodec, + FieldMapCodec, + >; + pub static EMPLOYEE_CODEC: EmployeeCodec = struct_codec!( + for_getter(field(&NON_EMPTY_STRING_CODEC, "name"), |s: &Employee| &s + .name), + for_getter( + field(&NON_EMPTY_STRING_CODEC, "department"), + |s: &Employee| &s.department + ), + for_getter(field(&UINT_CODEC, "salary"), |s: &Employee| &s.salary), + |name, department, salary| Employee { + name, + department, + salary + } + ); + + // Encoding + + assert_eq!( + EMPLOYEE_CODEC + .encode_start( + &Employee { + name: "John Doe".to_string(), + department: "Marketing".to_string(), + salary: 82_000 + }, + &INSTANCE + ) + .expect("Encoding should succeed"), + nbt_compound_tag!({ + "name": NbtTag::String("John Doe".to_string()), + "department": NbtTag::String("Marketing".to_string()), + "salary": NbtTag::Int(82_000) + }) + ); + + assert_eq!( + EMPLOYEE_CODEC + .encode_start( + &Employee { + name: "Linna Hall".to_string(), + // Department is empty. + department: String::new(), + salary: 90_000 + }, + &INSTANCE + ) + .get_message() + .expect("Encoding should fail"), + "String should not be empty" + ); + + // Decoding + + assert_eq!( + EMPLOYEE_CODEC + .parse( + nbt_compound_tag!({ + "name": NbtTag::String("Kelly Peak".to_string()), + "department": NbtTag::String("Sales".to_string()), + "salary": NbtTag::Int(72_000) + }), + &INSTANCE + ) + .expect("Decoding should succeed"), + Employee { + name: "Kelly Peak".to_string(), + department: "Sales".to_string(), + salary: 72_000 + } + ); + + assert_eq!( + EMPLOYEE_CODEC + .parse( + nbt_compound_tag!({ + "name": NbtTag::String(String::new()), + "department": NbtTag::String("Information Technology".to_string()), + "salary": NbtTag::Int(100_000) + }), + &INSTANCE + ) + .get_message() + .expect("Decoding should fail"), + "String should not be empty" + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn text() { + /// Alignments of a line of text. + #[derive(Debug, PartialEq, Clone)] + enum TextAlignment { + Left, + Center, + Right, + } + + impl From<&TextAlignment> for String { + fn from(value: &TextAlignment) -> Self { + match value { + TextAlignment::Left => "left", + TextAlignment::Center => "center", + TextAlignment::Right => "right", + } + .to_string() + } + } + + struct InvalidTextAlignmentError; + + impl TryFrom for TextAlignment { + type Error = InvalidTextAlignmentError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "left" => Ok(Self::Left), + "center" => Ok(Self::Center), + "right" => Ok(Self::Right), + + _ => Err(InvalidTextAlignmentError), + } + } + } + + pub type TextAlignmentCodec = ComapFlatMapCodec; + + // The transformer codec: + // - always converts `TextAlignment` -> `String` + // - but only converts `String` -> `TextAlignment` if the string is valid. + pub static TEXT_ALIGNMENT_CODEC: TextAlignmentCodec = comap_flat_map( + &STRING_CODEC, + |string| { + string.clone().try_into().map_or_else( + |_| DataResult::new_error(format!("Invalid alignment: {string}")), + DataResult::new_success, + ) + }, + |modifier: &TextAlignment| modifier.into(), + ); + + /// A single piece of text. + #[derive(Debug, PartialEq, Clone)] + struct Text { + content: String, + /// Optional field, defaults to `Left` alignment. + alignment: TextAlignment, + } + + pub type TextCodec = + StructCodec2, DefaultedFieldCodec>; + pub static TEXT_CODEC: TextCodec = struct_codec!( + for_getter(field(&STRING_CODEC, "content"), |t: &Text| &t.content), + for_getter( + optional_field_with_default(&TEXT_ALIGNMENT_CODEC, "alignment", || { + TextAlignment::Left + }), + |t| &t.alignment + ), + |content, alignment| Text { content, alignment } + ); + + // Encoding + + assert_eq!( + TEXT_CODEC + .encode_start( + &Text { + content: "Lorem ipsum".to_string(), + alignment: TextAlignment::Left + }, + &INSTANCE + ) + .expect("Encoding should succeed"), + nbt_compound_tag!({ + "content": NbtTag::String("Lorem ipsum".to_string()), + // Since "left" is the default, it will not be included. + }) + ); + + assert_eq!( + TEXT_CODEC + .encode_start( + &Text { + content: "An apple a day keeps the doctor away".to_string(), + alignment: TextAlignment::Center + }, + &INSTANCE + ) + .expect("Encoding should succeed"), + nbt_compound_tag!({ + "content": NbtTag::String("An apple a day keeps the doctor away".to_string()), + "alignment": NbtTag::String("center".to_string()) + }) + ); + + // Decoding + + assert_eq!( + TEXT_CODEC + .parse( + nbt_compound_tag!({ + "content": NbtTag::String("Surprise Sample Text".to_string()), + "alignment": NbtTag::String("right".to_string()) + }), + &INSTANCE + ) + .expect("Decoding should succeed"), + Text { + content: "Surprise Sample Text".to_string(), + alignment: TextAlignment::Right + } + ); + + assert_eq!( + TEXT_CODEC + .parse( + nbt_compound_tag!({ + "content": NbtTag::String("Will the test succeed?".to_string()), + // Alignment omitted; it will default to `Left`. + }), + &INSTANCE + ) + .expect("Decoding should succeed"), + Text { + content: "Will the test succeed?".to_string(), + alignment: TextAlignment::Left + } + ); + + assert!( + TEXT_CODEC + .parse( + nbt_compound_tag!({ + "content": NbtTag::String("Some random document".to_string()), + // Unfortunately, we don't have *justify* in our possible alignments. + "alignment": NbtTag::String("justify".to_string()) + }), + &INSTANCE + ) + .get_message() + .expect("Decoding should fail") + .starts_with("Invalid alignment") + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn dog_park() { + /// Represents an arbitrary dog. + #[derive(Debug, PartialEq, Clone)] + struct Dog { + breed: String, + age: u8, + // Optional, defaults to an empty `Vec`. + tricks: Vec, + } + + /// A dog park representation. + #[derive(Debug, PartialEq)] + struct DogPark { + name: String, + /// Each key of this map is the dog's name. + dogs: HashMap, + } + + pub type DogCodec = StructCodec3< + Dog, + FieldMapCodec, + FieldMapCodec, + DefaultedFieldCodec>, + >; + pub static DOG_CODEC: DogCodec = struct_codec!( + for_getter(field(&STRING_CODEC, "breed"), |t: &Dog| &t.breed), + for_getter(field(&UBYTE_CODEC, "age"), |t: &Dog| &t.age), + for_getter( + optional_field_with_default(&unbounded_list(&STRING_CODEC), "tricks", Vec::new), + |t: &Dog| &t.tricks + ), + |breed, age, tricks| Dog { breed, age, tricks } + ); + + pub type DogParkCodec = StructCodec2< + DogPark, + FieldMapCodec, + FieldMapCodec>, + >; + pub static DOG_PARK_CODEC: DogParkCodec = struct_codec!( + for_getter(field(&STRING_CODEC, "name"), |p: &DogPark| &p.name), + for_getter( + field(&unbounded_map(&STRING_CODEC, &DOG_CODEC), "dogs"), + |p| &p.dogs + ), + |name, dogs| DogPark { name, dogs } + ); + + // Encoding + + let mut dogs = HashMap::new(); + dogs.insert( + "Rodrick".to_string(), + Dog { + breed: "German Shepherd".to_string(), + age: 4, + tricks: vec!["spin".to_string()], + }, + ); + dogs.insert( + "Lucy".to_string(), + Dog { + breed: "Beagle".to_string(), + age: 6, + tricks: vec!["fetch".to_string(), "sit".to_string()], + }, + ); + dogs.insert( + "Dan".to_string(), + Dog { + breed: "Chihuahua".to_string(), + age: 3, + tricks: vec![], + }, + ); + + let serialized_park = DOG_PARK_CODEC + .encode_start( + &DogPark { + name: "Sunny Side Park".to_string(), + dogs, + }, + &INSTANCE, + ) + .expect("Encoding should succeed"); + + let compound = serialized_park + .extract_compound() + .expect("Tag should be a compound"); + + assert_eq!( + compound + .clone() + .get_string("name") + .expect("Compound tag should have a 'name' key"), + "Sunny Side Park" + ); + + for (k, v) in compound + .get_compound("dogs") + .expect("Compound tag should have a 'dogs' key") + .clone() + { + match k.as_str() { + "Rodrick" => assert_eq!( + v, + nbt_compound_tag!({ + "breed": NbtTag::String("German Shepherd".to_string()), + "age": NbtTag::Byte(4), + "tricks": NbtTag::List(vec![NbtTag::String("spin".to_string())]) + }) + ), + "Lucy" => assert_eq!( + v, + nbt_compound_tag!({ + "breed": NbtTag::String("Beagle".to_string()), + "age": NbtTag::Byte(6), + "tricks": NbtTag::List(vec![NbtTag::String("fetch".to_string()), NbtTag::String("sit".to_string())]) + }) + ), + "Dan" => assert_eq!( + v, + nbt_compound_tag!({ + "breed": NbtTag::String("Chihuahua".to_string()), + "age": NbtTag::Byte(3), + // 'tricks' will be omitted for an empty list. + }) + ), + _ => panic!("Unexpected dog {k} found"), + } + } + + // Decoding + + let deserialized_park = DOG_PARK_CODEC + .parse( + nbt_compound_tag!({ + "name": NbtTag::String("Lighthouse Meadow Park".to_string()), + "dogs": nbt_compound_tag!({ + "Adam": nbt_compound_tag!({ + "breed": NbtTag::String("Bulldog".to_string()), + "age": NbtTag::Byte(8), + "tricks": NbtTag::List(vec![NbtTag::String("catch".to_string())]) + }) + }) + }), + &INSTANCE, + ) + .expect("Decoding should succeed"); + + assert_eq!(deserialized_park.name, "Lighthouse Meadow Park"); + assert_eq!(deserialized_park.dogs.len(), 1); + assert_eq!( + deserialized_park + .dogs + .get("Adam") + .expect("No dog 'Adam' in dogs"), + &Dog { + breed: "Bulldog".to_string(), + age: 8, + tricks: vec!["catch".to_string()] + } + ); + + assert!( + DOG_PARK_CODEC + .parse( + nbt_compound_tag!({ + "name": NbtTag::String("Dark Park".to_string()), + "dogs": nbt_compound_tag!({ + "Adam": nbt_compound_tag!({ + "breed": NbtTag::String("Poodle".to_string()), + // Negative ages are not allowed. + "age": NbtTag::Byte(-2) + }) + }) + }), + &INSTANCE + ) + .get_message() + .expect("Decoding should fail") + .starts_with("Could not fit i8") + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn packed_color() { + /// A color stored using 4 bytes, one each for red, green, blue and alpha. + #[derive(Debug, PartialEq, Clone)] + struct PackedColor { + r: u8, + g: u8, + b: u8, + /// Optional field, defaults to `255` (full alpha). + a: u8, + } + + pub type PackedColorCodec = ComapFlatMapCodec; + pub static PACKED_COLOR_CODEC: PackedColorCodec = comap_flat_map( + &BYTE_BUFFER_CODEC, + |v| { + // While decoding, our codec only accepts byte buffers (arrays) with exactly 3 or 4 elements. + if v.len() == 4 { + DataResult::new_success(PackedColor { + r: v[0], + g: v[1], + b: v[2], + a: v[3], + }) + } else if v.len() == 3 { + // Alpha defaults to 255. + DataResult::new_success(PackedColor { + r: v[0], + g: v[1], + b: v[2], + a: 255, + }) + } else { + DataResult::new_error(format!("Invalid byte buffer for color: {v:?}")) + } + }, + |c| vec![c.r, c.g, c.b, c.a].into_boxed_slice(), + ); + + // Encoding + + assert_eq!( + PACKED_COLOR_CODEC + .encode_start( + &PackedColor { + r: 100, + g: 121, + b: 89, + a: 201 + }, + &INSTANCE + ) + .expect("Encoding should succeed"), + NbtTag::ByteArray(Box::new([100, 121, 89, 201])) + ); + + assert_eq!( + PACKED_COLOR_CODEC + .encode_start( + &PackedColor { + r: 0, + g: 0, + b: 0, + a: 255 + }, + &INSTANCE + ) + .expect("Encoding should succeed"), + NbtTag::ByteArray(Box::new([0, 0, 0, 255])) + ); + + // Decoding + + assert_eq!( + PACKED_COLOR_CODEC + .parse(NbtTag::ByteArray(Box::new([100, 121, 89, 201])), &INSTANCE) + .expect("Decoding should succeed"), + PackedColor { + r: 100, + g: 121, + b: 89, + a: 201 + } + ); + + assert_eq!( + PACKED_COLOR_CODEC + .parse(NbtTag::ByteArray(Box::new([255, 255, 0])), &INSTANCE) + .expect("Decoding should succeed"), + PackedColor { + r: 255, + g: 255, + b: 0, + a: 255 + } + ); + + assert!( + PACKED_COLOR_CODEC + .parse(NbtTag::ByteArray(Box::new([120])), &INSTANCE) + .get_message() + .expect("Decoding should fail") + .starts_with("Invalid byte buffer for color") + ); + + // Even other number array types will work. + assert_eq!( + PACKED_COLOR_CODEC + .parse(NbtTag::IntArray(vec![1, 2, 3, 4]), &INSTANCE) + .expect("Decoding should succeed"), + PackedColor { + r: 1, + g: 2, + b: 3, + a: 4 + } + ); + } +}