diff --git a/Cargo.lock b/Cargo.lock index 885d96e..855b1df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.6" @@ -124,6 +130,18 @@ dependencies = [ "json-escape", "pest", "pest_derive", + "serde_core", +] + +[[package]] +name = "duper_py" +version = "0.1.0" +dependencies = [ + "duper", + "pyo3", + "serde", + "serde-pyobject", + "serde_duper", ] [[package]] @@ -172,6 +190,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -206,6 +230,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "insta" version = "1.43.2" @@ -250,6 +280,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -323,6 +362,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -332,6 +377,69 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.41" @@ -351,6 +459,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-pyobject" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c485853a65e1a5f2db72e818ec4c7548a39614fabdd988f5e3504071453b7d7" +dependencies = [ + "pyo3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -415,6 +533,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "tower" version = "0.5.2" @@ -459,6 +583,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index a421908..a9170af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["axum_duper","duper", "serde_duper"] +members = ["axum_duper", "duper", "duper_py", "serde_duper"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c2d6f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2025 Eric Rodrigues Pires + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..190461f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Duper + +The format that's super. diff --git a/axum_duper/Cargo.toml b/axum_duper/Cargo.toml index a14f946..832d050 100644 --- a/axum_duper/Cargo.toml +++ b/axum_duper/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "axum_duper" +description = "Axum extractor/response for Duper." version = "0.1.0" edition = "2024" +license = "MIT" +authors = ["Eric Rodrigues Pires "] [dependencies] axum = { version = "0.8.6", default-features = false } diff --git a/axum_duper/LICENSE b/axum_duper/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/axum_duper/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/duper/Cargo.toml b/duper/Cargo.toml index 664e19c..3be8366 100644 --- a/duper/Cargo.toml +++ b/duper/Cargo.toml @@ -1,12 +1,20 @@ [package] name = "duper" +description = "The format that's super." version = "0.1.0" edition = "2024" +license = "MIT" +authors = ["Eric Rodrigues Pires "] + +[features] +default = ["serde"] +serde = ["dep:serde_core"] [dependencies] json-escape = "0.3.0" pest = "2.8.3" pest_derive = "2.8.3" +serde_core = { version = "1.0.228", optional = true } [dev-dependencies] insta = "1.43.2" diff --git a/duper/LICENSE b/duper/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/duper/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/duper/src/grammar.pest b/duper/src/grammar.pest index 3bc14db..94eebc9 100644 --- a/duper/src/grammar.pest +++ b/duper/src/grammar.pest @@ -1,73 +1,66 @@ WHITESPACE = _{ " " | "\t" | "\r" | "\n" } -COMMENT = _{ "//" ~ (!"\n" ~ ANY)* | "/*" ~ (!"*/" ~ ANY)* ~ "*/" } +COMMENT = _{ "//" ~ (!"\n" ~ ANY)* | "/*" ~ (!"*/" ~ ANY)* ~ "*/" } -duper = _{ SOI ~ duper_trunk ~ EOI } +duper = _{ SOI ~ duper_trunk ~ EOI } duper_stream = _{ SOI ~ duper_trunk? ~ ("---"+ ~ duper_trunk?)* ~ EOI } -duper_trunk = { identifier ~ "(" ~ (object | array) ~ ")" | object | array } -identifier = @{ "X-" ~ LETTER ~ ("-"? ~ LETTER | NUMBER)* | UPPERCASE_LETTER ~ (LETTER | NUMBER)* } +duper_trunk = { identifier ~ "(" ~ (object | array) ~ ")" | object | array } +identifier = @{ "X-" ~ LETTER ~ ("-"? ~ LETTER | NUMBER)* | UPPERCASE_LETTER ~ (LETTER | NUMBER)* } -described_value = { identifier ~ "(" ~ value ~ ")" | value } -value = _{ object | array | tuple | string | raw_string | bytes | raw_bytes | float | integer | boolean | null } +described_value = { identifier ~ "(" ~ value ~ ")" | value } +value = _{ object | array | tuple | string | raw_string | bytes | raw_bytes | float | integer | boolean | null } -object = { - "{" ~ ","? ~ "}" | - "{" ~ pair ~ ("," ~ pair)* ~ ","? ~ "}" +object = { + "{" ~ ","? ~ "}" + | "{" ~ pair ~ ("," ~ pair)* ~ ","? ~ "}" } -pair = { key ~ ":" ~ described_value } -key = _{ string | raw_string | plain_key } +pair = { key ~ ":" ~ described_value } +key = _{ string | raw_string | plain_key } plain_key = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } tuple = { - "(" ~ described_value? ~ "," ~ ")" | - "(" ~ described_value ~ ("," ~ described_value)+ ~ ","? ~ ")" + "(" ~ described_value? ~ "," ~ ")" + | "(" ~ described_value ~ ("," ~ described_value)+ ~ ","? ~ ")" } array = { - "[" ~ ","? ~ "]" | - "[" ~ described_value ~ ("," ~ described_value)* ~ ","? ~ "]" + "[" ~ ","? ~ "]" + | "[" ~ described_value ~ ("," ~ described_value)* ~ ","? ~ "]" } inner = @{ char* } -char = { +char = { !("\"" | "\\") ~ ANY - | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") - | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) + | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") + | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) } string = ${ "\"" ~ inner ~ "\"" } bytes = ${ "b\"" ~ inner ~ "\"" } -raw_string = ${ - "r" ~ PUSH("#"*) ~ "\"" - ~ raw_string_interior - ~ "\"" ~ POP +raw_string = ${ + "r" ~ PUSH("#"*) ~ "\"" ~ raw_string_interior ~ "\"" ~ POP } -raw_string_interior = @{ ( !("\"" ~ PEEK) ~ ANY )* } +raw_string_interior = @{ (!("\"" ~ PEEK) ~ ANY)* } -raw_bytes = ${ - "br" ~ PUSH("#"*) ~ "\"" - ~ raw_bytes_interior - ~ "\"" ~ POP +raw_bytes = ${ + "br" ~ PUSH("#"*) ~ "\"" ~ raw_bytes_interior ~ "\"" ~ POP } -raw_bytes_interior = @{ ( !("\"" ~ PEEK) ~ ANY )* } +raw_bytes_interior = @{ (!("\"" ~ PEEK) ~ ANY)* } -integer = { hex_integer | octal_integer | binary_integer | decimal_integer } +integer = { hex_integer | octal_integer | binary_integer | decimal_integer } decimal_integer = @{ ("+" | "-")? ~ decimal_digits } -decimal_digits = { "0" | ASCII_NONZERO_DIGIT ~ ("_"? ~ ASCII_DIGIT)* } -hex_integer = @{ "0x" ~ hex_digits } -hex_digits = { ASCII_HEX_DIGIT ~ ("_"? ~ ASCII_HEX_DIGIT)* } -octal_integer = @{ "0o" ~ octal_digits } -octal_digits = { ASCII_OCT_DIGIT ~ ("_"? ~ ASCII_OCT_DIGIT)* } -binary_integer = @{ "0b" ~ binary_digits } -binary_digits = { ASCII_BIN_DIGIT ~ ("_"? ~ ASCII_BIN_DIGIT)* } +decimal_digits = { "0" | ASCII_NONZERO_DIGIT ~ ("_"? ~ ASCII_DIGIT)* } +hex_integer = @{ "0x" ~ hex_digits } +hex_digits = { ASCII_HEX_DIGIT ~ ("_"? ~ ASCII_HEX_DIGIT)* } +octal_integer = @{ "0o" ~ octal_digits } +octal_digits = { ASCII_OCT_DIGIT ~ ("_"? ~ ASCII_OCT_DIGIT)* } +binary_integer = @{ "0b" ~ binary_digits } +binary_digits = { ASCII_BIN_DIGIT ~ ("_"? ~ ASCII_BIN_DIGIT)* } float = @{ - ("+" | "-")? - ~ ("0" | ASCII_NONZERO_DIGIT ~ ("_"? ~ ASCII_DIGIT)*) - ~ "." ~ ASCII_DIGIT ~ ("_"? ~ ASCII_DIGIT)* - ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT ~ ("_"? ~ ASCII_DIGIT)*)? + ("+" | "-")? ~ ("0" | ASCII_NONZERO_DIGIT ~ ("_"? ~ ASCII_DIGIT)*) ~ "." ~ ASCII_DIGIT ~ ("_"? ~ ASCII_DIGIT)* ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT ~ ("_"? ~ ASCII_DIGIT)*)? } boolean = { "true" | "false" } diff --git a/duper/src/lib.rs b/duper/src/lib.rs index 7c70511..83c39cc 100644 --- a/duper/src/lib.rs +++ b/duper/src/lib.rs @@ -1,8 +1,9 @@ mod ast; mod builder; mod format; -pub mod parser; +mod parser; mod pretty_printer; +mod serde; mod serializer; pub mod types; pub mod visitor; @@ -11,6 +12,7 @@ pub use ast::{ DuperArray, DuperBytes, DuperIdentifier, DuperInner, DuperKey, DuperObject, DuperString, DuperTuple, DuperValue, }; +pub use parser::{DuperParser, Rule as DuperRule}; pub use pretty_printer::PrettyPrinter; pub use serializer::Serializer; diff --git a/duper/src/pretty_printer.rs b/duper/src/pretty_printer.rs index ac5c8cb..b923400 100644 --- a/duper/src/pretty_printer.rs +++ b/duper/src/pretty_printer.rs @@ -10,18 +10,22 @@ use crate::{ }; pub struct PrettyPrinter { + curr_indent: usize, indent: usize, } impl Default for PrettyPrinter { fn default() -> Self { - Self::new() + Self::new(2) } } impl PrettyPrinter { - pub fn new() -> Self { - Self { indent: 0 } + pub fn new(indent: usize) -> Self { + Self { + curr_indent: 0, + indent, + } } pub fn pretty_print<'a>(&mut self, value: DuperValue<'a>) -> String { @@ -29,15 +33,15 @@ impl PrettyPrinter { } fn increase_indentation(&mut self) { - self.indent += 2 + self.curr_indent += self.indent; } fn decrease_indentation(&mut self) { - self.indent -= 2 + self.curr_indent -= self.indent; } fn indentation(&self) -> String { - (0..self.indent).map(|_| ' ').collect() + (0..self.curr_indent).map(|_| ' ').collect() } } @@ -186,7 +190,7 @@ impl DuperVisitor for PrettyPrinter { ) -> Self::Value { if let Some(identifier) = identifier { let value = format_duper_string(value); - if value.len() + self.indent > 60 { + if value.len() + self.curr_indent > 60 { let mut string = String::new(); string.push_str(identifier.as_ref()); string.push_str("(\n"); @@ -213,7 +217,7 @@ impl DuperVisitor for PrettyPrinter { ) -> Self::Value { if let Some(identifier) = identifier { let bytes = format_duper_bytes(bytes); - if bytes.len() + self.indent > 60 { + if bytes.len() + self.curr_indent > 60 { let mut string = String::new(); string.push_str(identifier.as_ref()); string.push_str("(\n"); @@ -293,7 +297,7 @@ mod pretty_printer_tests { identifier: None, inner: DuperInner::Object(DuperObject(vec![])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -304,7 +308,7 @@ mod pretty_printer_tests { identifier: None, inner: DuperInner::Array(DuperArray(vec![])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -321,7 +325,7 @@ mod pretty_printer_tests { }, )])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -335,7 +339,7 @@ mod pretty_printer_tests { inner: DuperInner::Integer(42), }])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -380,7 +384,7 @@ mod pretty_printer_tests { ), ])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -404,7 +408,7 @@ mod pretty_printer_tests { }, ])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -448,7 +452,7 @@ mod pretty_printer_tests { }, )])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } @@ -478,7 +482,7 @@ mod pretty_printer_tests { }, ])), }; - let pp = PrettyPrinter::new().pretty_print(value); + let pp = PrettyPrinter::new(2).pretty_print(value); assert_snapshot!(pp); let _ = DuperParser::parse_duper(&pp).unwrap(); } diff --git a/duper/src/serde.rs b/duper/src/serde.rs new file mode 100644 index 0000000..6e9801a --- /dev/null +++ b/duper/src/serde.rs @@ -0,0 +1,285 @@ +use crate::{DuperArray, DuperBytes, DuperInner, DuperKey, DuperObject, DuperString, DuperValue}; +#[cfg(feature = "serde")] +use serde_core::de::Visitor; +use std::borrow::Cow; + +#[cfg(feature = "serde")] +impl<'a> serde_core::Serialize for DuperValue<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde_core::Serializer, + { + match &self.inner { + crate::DuperInner::Object(object) => { + use serde_core::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(object.0.len()))?; + for (key, value) in object.0.iter() { + use serde_core::ser::SerializeMap; + map.serialize_entry(key.as_ref(), value)?; + } + map.end() + } + crate::DuperInner::Array(array) => { + use serde_core::ser::SerializeSeq; + + let mut seq = serializer.serialize_seq(Some(array.0.len()))?; + for value in array.0.iter() { + seq.serialize_element(value)?; + } + seq.end() + } + crate::DuperInner::Tuple(tuple) => { + use serde_core::ser::SerializeTuple; + + let mut seq = serializer.serialize_tuple(tuple.0.len())?; + for value in tuple.0.iter() { + seq.serialize_element(value)?; + } + seq.end() + } + crate::DuperInner::String(string) => serializer.serialize_str(string.as_ref()), + crate::DuperInner::Bytes(bytes) => serializer.serialize_bytes(bytes.as_ref()), + crate::DuperInner::Integer(integer) => serializer.serialize_i64(*integer), + crate::DuperInner::Float(float) => serializer.serialize_f64(*float), + crate::DuperInner::Boolean(boolean) => serializer.serialize_bool(*boolean), + crate::DuperInner::Null => serializer.serialize_none(), + } + } +} + +#[cfg(feature = "serde")] +struct DuperValueDeserializerVisitor; + +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for DuperValueDeserializerVisitor { + type Value = DuperValue<'de>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid Duper value") + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Boolean(v), + }) + } + + fn visit_i8(self, v: i8) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_i16(self, v: i16) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_i32(self, v: i32) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Integer(v), + }) + } + + fn visit_u8(self, v: u8) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_u16(self, v: u16) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde_core::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde_core::de::Error, + { + if let Ok(i) = i64::try_from(v) { + self.visit_i64(i) + } else { + self.visit_f64(v as f64) + } + } + + fn visit_f32(self, v: f32) -> Result + where + E: serde_core::de::Error, + { + self.visit_f64(v as f64) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Float(v), + }) + } + + fn visit_char(self, v: char) -> Result + where + E: serde_core::de::Error, + { + self.visit_str(&v.to_string()) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde_core::de::Error, + { + self.visit_string(v.to_string()) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::String(DuperString::from(Cow::Borrowed(v))), + }) + } + + fn visit_string(self, v: String) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::String(DuperString::from(Cow::Owned(v))), + }) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde_core::de::Error, + { + self.visit_byte_buf(v.to_vec()) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Bytes(DuperBytes::from(Cow::Borrowed(v))), + }) + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Bytes(DuperBytes::from(Cow::Owned(v))), + }) + } + + fn visit_none(self) -> Result + where + E: serde_core::de::Error, + { + Ok(DuperValue { + identifier: None, + inner: DuperInner::Null, + }) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: serde_core::Deserializer<'de>, + { + serde_core::Deserialize::deserialize(deserializer) + } + + fn visit_unit(self) -> Result + where + E: serde_core::de::Error, + { + self.visit_none() + } + + fn visit_newtype_struct(self, deserializer: D) -> Result + where + D: serde_core::Deserializer<'de>, + { + serde_core::Deserialize::deserialize(deserializer) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde_core::de::SeqAccess<'de>, + { + let mut values = Vec::new(); + + while let Some(value) = seq.next_element()? { + values.push(value); + } + + Ok(DuperValue { + identifier: None, + inner: DuperInner::Array(DuperArray::from(values)), + }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde_core::de::MapAccess<'de>, + { + let mut entries = Vec::new(); + + while let Some((key, value)) = map.next_entry()? { + entries.push((DuperKey::from(Cow::Owned(key)), value)); + } + + Ok(DuperValue { + identifier: None, + inner: DuperInner::Object(DuperObject::from(entries)), + }) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde_core::Deserialize<'de> for DuperValue<'de> { + fn deserialize(deserializer: D) -> Result + where + D: serde_core::Deserializer<'de>, + { + deserializer.deserialize_any(DuperValueDeserializerVisitor) + } +} diff --git a/duper_py/.gitignore b/duper_py/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/duper_py/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/duper_py/Cargo.toml b/duper_py/Cargo.toml new file mode 100644 index 0000000..51f0a13 --- /dev/null +++ b/duper_py/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "duper_py" +description = "The format that's super." +version = "0.1.0" +edition = "2024" +license = "MIT" +authors = ["Eric Rodrigues Pires "] + +[lib] +name = "duper" +crate-type = ["cdylib"] + +[dependencies] +duper = { path = "../duper" } +pyo3 = "0.24.0" +serde = "1.0.228" +serde-pyobject = "0.6.2" +serde_duper = { path = "../serde_duper" } diff --git a/duper_py/LICENSE b/duper_py/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/duper_py/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/duper_py/pyproject.toml b/duper_py/pyproject.toml new file mode 100644 index 0000000..2dee2bb --- /dev/null +++ b/duper_py/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "duper-py" +description = "The format that's super." +license = "MIT" +authors = [{ name = "Eric Rodrigues Pires", email = "eric@eric.dev.br" }] +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] + +[build-system] +requires = ["maturin>=1.9,<2.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["pyo3/extension-module"] + +[dependency-groups] +dev = ["maturin>=1.9.6"] diff --git a/duper_py/src/lib.rs b/duper_py/src/lib.rs new file mode 100644 index 0000000..98d0420 --- /dev/null +++ b/duper_py/src/lib.rs @@ -0,0 +1,33 @@ +use duper::{DuperParser, DuperValue, PrettyPrinter, Serializer}; +use pyo3::{exceptions::PyValueError, prelude::*}; +use serde_pyobject::from_pyobject; + +use crate::visitor::Visitor; + +mod visitor; + +/// Utilities for converting to and from Python types into Duper. +#[pymodule(name = "duper")] +fn duper_py(m: &Bound<'_, PyModule>) -> PyResult<()> { + /// Serialize obj as a Duper value formatted str. + #[pyfn(m)] + #[pyo3(signature = (obj, *, indent=None))] + fn dumps<'py>(obj: Bound<'py, PyAny>, indent: Option) -> PyResult { + let value: DuperValue = from_pyobject(obj)?; + if let Some(indent) = indent { + Ok(PrettyPrinter::new(indent).pretty_print(value)) + } else { + Ok(Serializer::new().serialize(value)) + } + } + + /// Deserialize s (a str instance containing a Duper object or array) to a Python object. + #[pyfn(m)] + fn loads<'py>(py: Python<'py>, s: &str) -> PyResult> { + let value = DuperParser::parse_duper(s) + .map_err(|err| PyErr::new::(err.to_string()))?; + value.accept(&mut Visitor { py }) + } + + Ok(()) +} diff --git a/duper_py/src/visitor.rs b/duper_py/src/visitor.rs new file mode 100644 index 0000000..478fd11 --- /dev/null +++ b/duper_py/src/visitor.rs @@ -0,0 +1,88 @@ +use duper::visitor::DuperVisitor; +use pyo3::{ + Bound, PyAny, PyResult, Python, + types::{IntoPyDict, PyBool, PyBytes, PyFloat, PyInt, PyList, PyString, PyTuple}, +}; + +#[derive(Clone)] +pub(crate) struct Visitor<'py> { + pub(crate) py: Python<'py>, +} + +impl<'py> DuperVisitor for Visitor<'py> { + type Value = PyResult>; + + fn visit_object<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + object: &duper::DuperObject<'a>, + ) -> Self::Value { + let seq: PyResult> = object + .iter() + .map(|(key, value)| value.accept(self).map(|value| (key.as_ref(), value))) + .collect(); + Ok(seq?.into_py_dict(self.py).unwrap().into_any()) + } + + fn visit_array<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + array: &duper::DuperArray<'a>, + ) -> Self::Value { + let vec: PyResult> = array.iter().map(|value| value.accept(self)).collect(); + PyList::new(self.py, vec?).map(|value| value.into_any()) + } + + fn visit_tuple<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + tuple: &duper::DuperTuple<'a>, + ) -> Self::Value { + let vec: PyResult> = tuple.iter().map(|value| value.accept(self)).collect(); + PyTuple::new(self.py, vec?).map(|value| value.into_any()) + } + + fn visit_string<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + string: &duper::DuperString<'a>, + ) -> Self::Value { + Ok(PyString::new(self.py, &string.clone().into_inner()).into_any()) + } + + fn visit_bytes<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + bytes: &duper::DuperBytes<'a>, + ) -> Self::Value { + Ok(PyBytes::new(self.py, &bytes.clone().into_inner()).into_any()) + } + + fn visit_integer<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + integer: i64, + ) -> Self::Value { + Ok(PyInt::new(self.py, integer).into_any()) + } + + fn visit_float<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + float: f64, + ) -> Self::Value { + Ok(PyFloat::new(self.py, float).into_any()) + } + + fn visit_boolean<'a>( + &mut self, + _identifier: Option<&duper::DuperIdentifier<'a>>, + boolean: bool, + ) -> Self::Value { + Ok(PyBool::new(self.py, boolean).to_owned().into_any()) + } + + fn visit_null<'a>(&mut self, _identifier: Option<&duper::DuperIdentifier<'a>>) -> Self::Value { + Ok(self.py.None().into_bound(self.py).into_any()) + } +} diff --git a/duper_py/uv.lock b/duper_py/uv.lock new file mode 100644 index 0000000..ee9bdb6 --- /dev/null +++ b/duper_py/uv.lock @@ -0,0 +1,90 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "duper-py" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "maturin" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "maturin", specifier = ">=1.9.6" }] + +[[package]] +name = "maturin" +version = "1.9.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/35/c3370188492f4c139c7a318f438d01b8185c216303c49c4bc885c98b6afb/maturin-1.9.6.tar.gz", hash = "sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a", size = 214846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/5c/b435418ba4ba2647a1f7a95d53314991b1e556e656ae276dea993c3bce1d/maturin-1.9.6-py3-none-linux_armv6l.whl", hash = "sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f", size = 8134483 }, + { url = "https://files.pythonhosted.org/packages/4d/1c/8e58eda6601f328b412cdeeaa88a9b6a10e591e2a73f313e8c0154d68385/maturin-1.9.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369", size = 15776470 }, + { url = "https://files.pythonhosted.org/packages/6c/33/8c967cce6848cdd87a2e442c86120ac644b80c5ed4c32e3291bde6a17df8/maturin-1.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba", size = 8226557 }, + { url = "https://files.pythonhosted.org/packages/58/bd/3e2675cdc8b7270700ba30c663c852a35694441732a107ac30ebd6878bd8/maturin-1.9.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8", size = 8166544 }, + { url = "https://files.pythonhosted.org/packages/58/1f/a2047ddf2230e700d5f8a13dd4b9af5ce806ad380c32e58105888205926e/maturin-1.9.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238", size = 8641010 }, + { url = "https://files.pythonhosted.org/packages/be/1f/265d63c7aa6faf363d4a3f23396f51bc6b4d5c7680a4190ae68dba25dea2/maturin-1.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f", size = 7965945 }, + { url = "https://files.pythonhosted.org/packages/4c/ca/a8e61979ccfe080948bcc1bddd79356157aee687134df7fb013050cec783/maturin-1.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3", size = 7978820 }, + { url = "https://files.pythonhosted.org/packages/bf/4a/81b412f8ad02a99801ef19ec059fba0822d1d28fb44cb6a92e722f05f278/maturin-1.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e", size = 10452064 }, + { url = "https://files.pythonhosted.org/packages/5b/12/cc96c7a8cb51d8dcc9badd886c361caa1526fba7fa69d1e7892e613b71d4/maturin-1.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de", size = 8852401 }, + { url = "https://files.pythonhosted.org/packages/51/8e/653ac3c9f2c25cdd81aefb0a2d17ff140ca5a14504f5e3c7f94dcfe4dbb7/maturin-1.9.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc", size = 8425057 }, + { url = "https://files.pythonhosted.org/packages/db/29/f13490328764ae9bfc1da55afc5b707cebe4fa75ad7a1573bfa82cfae0c6/maturin-1.9.6-py3-none-win32.whl", hash = "sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309", size = 7165956 }, + { url = "https://files.pythonhosted.org/packages/db/9f/dd51e5ac1fce47581b8efa03d77a03f928c0ef85b6e48a61dfa37b6b85a2/maturin-1.9.6-py3-none-win_amd64.whl", hash = "sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a", size = 8145722 }, + { url = "https://files.pythonhosted.org/packages/65/f2/e97aaba6d0d78c5871771bf9dd71d4eb8dac15df9109cf452748d2207412/maturin-1.9.6-py3-none-win_arm64.whl", hash = "sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c", size = 6857006 }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +] diff --git a/serde_duper/Cargo.toml b/serde_duper/Cargo.toml index 9ea07f7..98b4c40 100644 --- a/serde_duper/Cargo.toml +++ b/serde_duper/Cargo.toml @@ -1,11 +1,14 @@ [package] name = "serde_duper" +description = "Serde support for Duper." version = "0.1.0" edition = "2024" +license = "MIT" +authors = ["Eric Rodrigues Pires "] [dependencies] serde_core = "1.0.228" -duper = { path = "../duper" } +duper = { path = "../duper", features = ["serde"] } pest = "2.8.3" [dev-dependencies] diff --git a/serde_duper/LICENSE b/serde_duper/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/serde_duper/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/serde_duper/src/de.rs b/serde_duper/src/de.rs index 6028dfd..836d958 100644 --- a/serde_duper/src/de.rs +++ b/serde_duper/src/de.rs @@ -1,5 +1,5 @@ use duper::DuperKey; -use duper::{DuperInner, DuperValue, parser::DuperParser}; +use duper::{DuperInner, DuperParser, DuperValue}; use serde_core::Deserialize; use serde_core::de::{self, DeserializeSeed, IntoDeserializer, Visitor}; @@ -66,7 +66,7 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { inner: DuperInner::Tuple(tuple), .. }) => { - let seq = SequenceDeserializer::new(tuple.into_inner()); + let seq = TupleDeserializer::new(tuple.into_inner()); visitor.visit_seq(seq) } Some(DuperValue { @@ -138,16 +138,13 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { where V: Visitor<'de>, { - match self.value.take() { + match &self.value { Some(DuperValue { inner: DuperInner::Null, .. }) | None => visitor.visit_none(), - value => { - self.value = value; - visitor.visit_some(self) - } + _ => visitor.visit_some(self), } } @@ -169,23 +166,41 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { self.deserialize_any(visitor) } - fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + fn deserialize_tuple(self, len: usize, visitor: V) -> Result where V: Visitor<'de>, { - self.deserialize_seq(visitor) + match self.value.take() { + Some(DuperValue { + inner: DuperInner::Array(array), + .. + }) if array.len() == len => { + let seq = TupleDeserializer::new(array.into_inner()); + visitor.visit_seq(seq) + } + Some(DuperValue { + inner: DuperInner::Tuple(tuple), + .. + }) if tuple.len() == len => { + let seq = TupleDeserializer::new(tuple.into_inner()); + visitor.visit_seq(seq) + } + value => Err(de::Error::custom(format!( + "expected tuple of len {len}, found {value:?}" + ))), + } } fn deserialize_tuple_struct( self, _name: &'static str, - _len: usize, + len: usize, visitor: V, ) -> Result where V: Visitor<'de>, { - self.deserialize_seq(visitor) + self.deserialize_tuple(len, visitor) } fn deserialize_map(self, visitor: V) -> Result @@ -294,7 +309,7 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { where V: Visitor<'de>, { - self.deserialize_i64(visitor) + self.deserialize_f64(visitor) } fn deserialize_f32(self, visitor: V) -> Result @@ -379,6 +394,41 @@ impl<'de> de::SeqAccess<'de> for SequenceDeserializer<'de> { } } +struct TupleDeserializer<'de> { + iter: std::vec::IntoIter>, + len: usize, +} + +impl<'de> TupleDeserializer<'de> { + fn new(vec: Vec>) -> Self { + let len = vec.len(); + Self { + iter: vec.into_iter(), + len, + } + } +} + +impl<'de> de::SeqAccess<'de> for TupleDeserializer<'de> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: DeserializeSeed<'de>, + { + match self.iter.next() { + Some(value) => seed + .deserialize(&mut Deserializer::from_value(value)) + .map(Some), + None => Ok(None), + } + } + + fn size_hint(&self) -> Option { + Some(self.len) + } +} + struct MapDeserializer<'de> { iter: std::vec::IntoIter<(DuperKey<'de>, DuperValue<'de>)>, value: Option>, diff --git a/serde_duper/src/error.rs b/serde_duper/src/error.rs index 98b4290..66778db 100644 --- a/serde_duper/src/error.rs +++ b/serde_duper/src/error.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Display}; #[derive(Debug, Clone)] pub enum ErrorKind { - ParseError(Box>), + ParseError(Box>), SerializationError, DeserializationError(serde_core::de::value::Error), InvalidValue, @@ -78,8 +78,8 @@ impl From for Error { } } -impl From>> for Error { - fn from(value: Box>) -> Self { +impl From>> for Error { + fn from(value: Box>) -> Self { let message = value.variant.message().into_owned(); Self::new(ErrorKind::ParseError(value), message) } diff --git a/serde_duper/src/ser.rs b/serde_duper/src/ser.rs index 1d2f323..ffe2548 100644 --- a/serde_duper/src/ser.rs +++ b/serde_duper/src/ser.rs @@ -21,29 +21,26 @@ impl<'a> Serializer<'a> { } } -pub fn to_duper<'a, T>(value: &'a T) -> Result, String> +pub fn to_duper<'a, T>(value: &'a T) -> Result, Error> where T: Serialize, { let mut serializer = Serializer::new(); - let value = value - .serialize(&mut serializer) - .map_err(|e| format!("Serialization error: {e:?}"))?; - Ok(value) + value.serialize(&mut serializer) } -pub fn to_string(value: &T) -> Result +pub fn to_string(value: &T) -> Result where T: Serialize, { Ok(DuperSerializer::new().serialize(to_duper(value)?)) } -pub fn to_string_pretty(value: &T) -> Result +pub fn to_string_pretty(value: &T, indent: usize) -> Result where T: Serialize, { - Ok(DuperPrettyPrinter::new().pretty_print(to_duper(value)?)) + Ok(DuperPrettyPrinter::new(indent).pretty_print(to_duper(value)?)) } pub struct SerializeSeq<'a, 'b> { @@ -158,12 +155,15 @@ impl<'a, 'b> ser::Serializer for &'a mut Serializer<'b> { } fn serialize_u64(self, v: u64) -> Result { + let Ok(integer) = v.try_into() else { + return Ok(DuperValue { + identifier: None, + inner: DuperInner::Float(v as f64), + }); + }; Ok(DuperValue { identifier: None, - inner: DuperInner::Integer( - v.try_into() - .map_err(|_| Error::invalid_value("integer must fit in i64"))?, - ), + inner: DuperInner::Integer(integer), }) } @@ -464,7 +464,7 @@ impl<'a, 'b> ser::SerializeMap for SerializeMap<'a, 'b> { self.next_key = Some(DuperKey::from(s.into_inner())); Ok(()) } - _ => Err(Error::invalid_value("map key must be a string")), + _ => Err(Error::serialization("map key must be a string")), } }