From e57c4502a5c56cc7f514daf416d2c7cb4db4e46c Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Sat, 11 Oct 2025 08:41:55 -0300 Subject: [PATCH] Improve modules --- Cargo.lock | 28 ++++ Cargo.toml | 2 +- duper/src/ast.rs | 36 +++++ duper/src/builder.rs | 140 +++++++++++++++++ duper/src/format.rs | 52 +++++++ duper/src/{duper.pest => grammar.pest} | 2 +- duper/src/lib.rs | 207 +++---------------------- duper/src/parser.rs | 19 +++ duper/src/serializer.rs | 146 +++++++++++++++++ duper/src/types.rs | 21 +++ duper/src/visitor.rs | 29 ++++ 11 files changed, 495 insertions(+), 187 deletions(-) create mode 100644 duper/src/ast.rs create mode 100644 duper/src/builder.rs create mode 100644 duper/src/format.rs rename duper/src/{duper.pest => grammar.pest} (96%) create mode 100644 duper/src/parser.rs create mode 100644 duper/src/serializer.rs create mode 100644 duper/src/types.rs create mode 100644 duper/src/visitor.rs diff --git a/Cargo.lock b/Cargo.lock index 08c8a3d..10ff4f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,34 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_duper" +version = "0.1.0" +dependencies = [ + "duper", + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/Cargo.toml b/Cargo.toml index 3237cf7..f7771b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["duper"] +members = ["duper", "serde_duper"] diff --git a/duper/src/ast.rs b/duper/src/ast.rs new file mode 100644 index 0000000..b3f902d --- /dev/null +++ b/duper/src/ast.rs @@ -0,0 +1,36 @@ +use std::{borrow::Cow, fmt::Debug}; + +use crate::visitor::DuperVisitor; + +#[derive(Debug, Clone)] +pub struct DuperValue<'a> { + pub identifier: Option<&'a str>, + pub inner: DuperInner<'a>, +} + +#[derive(Debug, Clone)] +pub enum DuperInner<'a> { + Object(Vec<(Cow<'a, str>, DuperValue<'a>)>), + Array(Vec>), + String(Cow<'a, str>), + Bytes(Cow<'a, [u8]>), + Integer(i64), + Float(f64), + Boolean(bool), + Null, +} + +impl<'a> DuperValue<'a> { + pub fn accept<'v, V: DuperVisitor>(self, visitor: &'v mut V) -> V::Value { + match self.inner { + DuperInner::Object(object) => visitor.visit_object(self.identifier, object), + DuperInner::Array(array) => visitor.visit_array(self.identifier, array), + DuperInner::String(string) => visitor.visit_string(self.identifier, string), + DuperInner::Bytes(bytes) => visitor.visit_bytes(self.identifier, bytes), + DuperInner::Integer(integer) => visitor.visit_integer(self.identifier, integer), + DuperInner::Float(float) => visitor.visit_float(self.identifier, float), + DuperInner::Boolean(boolean) => visitor.visit_boolean(self.identifier, boolean), + DuperInner::Null => visitor.visit_null(self.identifier), + } + } +} diff --git a/duper/src/builder.rs b/duper/src/builder.rs new file mode 100644 index 0000000..d574baa --- /dev/null +++ b/duper/src/builder.rs @@ -0,0 +1,140 @@ +use std::borrow::Cow; + +use json_escape::explicit::unescape; +use pest::{Parser as _, error::Error, iterators::Pair}; + +use crate::{ + ast::{DuperInner, DuperValue}, + parser::Rule, +}; + +pub(crate) struct DuperBuilder; + +impl DuperBuilder { + pub(crate) fn build_duper_stream( + pair: Pair<'_, Rule>, + ) -> Result>, Error> { + pair.into_inner() + .map(|inner_pair| Self::build_duper_trunk(inner_pair)) + .collect() + } + + pub(crate) fn build_duper(pair: Pair<'_, Rule>) -> Result, Error> { + Self::build_duper_trunk(pair) + } + + fn build_duper_trunk(pair: Pair<'_, Rule>) -> Result, Error> { + let mut duper_trunk = pair.into_inner(); + let mut next = duper_trunk.next().unwrap(); + let identifier = match next.as_rule() { + Rule::identifier => { + let identifier = next.as_str(); + next = duper_trunk.next().unwrap(); + Some(identifier) + } + _ => None, + }; + match next.as_rule() { + Rule::object => Ok(DuperValue { + identifier, + inner: DuperInner::Object(Self::build_object(next)?), + }), + Rule::array => Ok(DuperValue { + identifier, + inner: DuperInner::Array(Self::build_array(next)?), + }), + _ => unreachable!(), + } + } + + fn build_object( + pair: Pair<'_, Rule>, + ) -> Result, DuperValue<'_>)>, Error> { + debug_assert!(matches!(pair.as_rule(), Rule::object)); + pair.into_inner() + .map(|pair| { + let mut inner_pair = pair.into_inner(); + let key_pair = inner_pair.next().unwrap(); + let key = match key_pair.as_rule() { + Rule::string => { + Self::unescape_str(key_pair.into_inner().next().unwrap().as_str()) + } + Rule::raw_string => { + Cow::Borrowed(key_pair.into_inner().next().unwrap().as_str()) + } + Rule::plain_key => Cow::Borrowed(key_pair.as_str()), + _ => unreachable!(), + }; + let value = Self::build_value(inner_pair.next().unwrap()); + value.map(|v| (key, v)) + }) + .collect() + } + + fn build_array(pair: Pair<'_, Rule>) -> Result>, Error> { + debug_assert!(matches!(pair.as_rule(), Rule::array)); + pair.into_inner() + .map(|pair| Self::build_value(pair)) + .collect() + } + + fn build_value(pair: Pair<'_, Rule>) -> Result, Error> { + let mut inner_pair = pair.into_inner(); + let mut next = inner_pair.next().unwrap(); + let identifier = match next.as_rule() { + Rule::identifier => { + let identifier = next.as_str(); + next = inner_pair.next().unwrap(); + Some(identifier) + } + _ => None, + }; + Ok(DuperValue { + identifier, + inner: match next.as_rule() { + Rule::object => DuperInner::Object(Self::build_object(next)?), + Rule::array => DuperInner::Array(Self::build_array(next)?), + Rule::string => DuperInner::String(Self::unescape_str( + next.into_inner().next().unwrap().as_str(), + )), + Rule::raw_string => { + DuperInner::String(Cow::Borrowed(next.into_inner().next().unwrap().as_str())) + } + Rule::bytes => DuperInner::Bytes(Cow::Owned( + Self::unescape_str(next.into_inner().next().unwrap().as_str()) + .as_bytes() + .to_vec(), + )), + Rule::raw_bytes => DuperInner::Bytes(Cow::Borrowed( + next.into_inner().next().unwrap().as_str().as_bytes(), + )), + Rule::integer => DuperInner::Integer({ + let integer_inner = next.into_inner().next().unwrap(); + match integer_inner.as_rule() { + Rule::decimal_integer => { + i64::from_str_radix(integer_inner.as_str(), 10).unwrap() + } + Rule::hex_integer => { + i64::from_str_radix(integer_inner.as_str().split_at(2).1, 16).unwrap() + } + Rule::octal_integer => { + i64::from_str_radix(integer_inner.as_str().split_at(2).1, 8).unwrap() + } + Rule::binary_integer => { + i64::from_str_radix(integer_inner.as_str().split_at(2).1, 2).unwrap() + } + _ => unreachable!(), + } + }), + Rule::float => DuperInner::Float(next.as_str().replace('_', "").parse().unwrap()), + Rule::boolean => DuperInner::Boolean(next.as_str().parse().unwrap()), + Rule::null => DuperInner::Null, + _ => unreachable!(), + }, + }) + } + + fn unescape_str(input: &'_ str) -> Cow<'_, str> { + unescape(input).decode_utf8().unwrap() + } +} diff --git a/duper/src/format.rs b/duper/src/format.rs new file mode 100644 index 0000000..d477837 --- /dev/null +++ b/duper/src/format.rs @@ -0,0 +1,52 @@ +use json_escape::explicit::escape_str; +use std::{ascii, borrow::Cow}; + +use crate::types::DuperTypes; + +pub(crate) fn format_key<'a>(key: Cow<'a, str>) -> Cow<'a, str> { + if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + key + } else { + format_string(key) + } +} + +pub(crate) fn format_string<'a>(string: Cow<'a, str>) -> Cow<'a, str> { + if string.is_empty() { + return Cow::Borrowed(r#""""#); + } + // TO-DO raw string heuristics + let escaped_key = Cow::from(escape_str(&string)).into_owned(); + Cow::Owned(format!(r#""{escaped_key}""#)) +} + +pub(crate) fn format_bytes<'a>(bytes: Cow<'a, [u8]>) -> Cow<'a, str> { + if bytes.is_empty() { + return Cow::Borrowed(r#"b"""#); + } + // TO-DO raw bytes heuristics + let escaped_bytes: String = bytes + .into_iter() + .copied() + .flat_map(ascii::escape_default) + .map(|b| b as char) + .collect(); + Cow::Owned(format!(r#"b"{escaped_bytes}""#)) +} + +pub(crate) fn format_integer(integer: i64, typ: Option) -> String { + match typ { + Some(DuperTypes::HexInteger) => format!("0x{integer:x}"), + Some(DuperTypes::OctInteger) => format!("0o{integer:o}"), + Some(DuperTypes::BinInteger) => format!("0b{integer:b}"), + _ => integer.to_string(), + } +} + +pub(crate) fn format_float(float: f64, _typ: Option) -> String { + float.to_string() +} + +pub(crate) fn format_boolean(bool: bool) -> String { + bool.to_string() +} diff --git a/duper/src/duper.pest b/duper/src/grammar.pest similarity index 96% rename from duper/src/duper.pest rename to duper/src/grammar.pest index eaa5901..07c0840 100644 --- a/duper/src/duper.pest +++ b/duper/src/grammar.pest @@ -16,7 +16,7 @@ object = { } pair = { key ~ ":" ~ described_value } key = _{ string | raw_string | plain_key } -plain_key = @{ (LOWERCASE_LETTER | "_") ~ (LETTER | NUMBER | "_")* } +plain_key = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } array = { "[" ~ ","? ~ "]" | diff --git a/duper/src/lib.rs b/duper/src/lib.rs index 3aa3cfe..67fca5d 100644 --- a/duper/src/lib.rs +++ b/duper/src/lib.rs @@ -1,190 +1,27 @@ -use std::borrow::Cow; - -use json_escape::explicit::unescape; -use pest::{Parser, error::Error, iterators::Pair}; -use pest_derive::Parser; - -#[derive(Debug)] -struct DuperStream<'a>(Vec>); - -#[derive(Debug)] -enum DuperTrunk<'a> { - Object { - identifier: Option<&'a str>, - value: Vec<(Cow<'a, str>, DuperValue<'a>)>, - }, - Array { - identifier: Option<&'a str>, - value: Vec>, - }, -} - -#[derive(Debug)] -struct DuperValue<'a> { - identifier: Option<&'a str>, - value: DuperInner<'a>, -} - -#[derive(Debug)] -enum DuperInner<'a> { - Object(Vec<(Cow<'a, str>, DuperValue<'a>)>), - Array(Vec>), - String(Cow<'a, str>), - Bytes(Cow<'a, [u8]>), - Integer(i64), - Float(f64), - Boolean(bool), - Null, -} - -impl<'a> Into> for DuperTrunk<'a> { - fn into(self) -> DuperValue<'a> { - match self { - DuperTrunk::Object { identifier, value } => DuperValue { - identifier, - value: DuperInner::Object(value), - }, - DuperTrunk::Array { identifier, value } => DuperValue { - identifier, - value: DuperInner::Array(value), - }, - } - } -} - -#[derive(Parser)] -#[grammar = "duper.pest"] -struct DuperParser; - -fn parse_duper_stream(input: &str) -> Result, Error> { - let duper = DuperParser::parse(Rule::duper_stream, input)? - .next() - .unwrap(); - - Ok(DuperStream( - duper.into_inner().map(parse_duper_trunk).collect(), - )) -} - -fn parse_duper(input: &str) -> Result, Error> { - let duper = DuperParser::parse(Rule::duper, input)?.next().unwrap(); - - Ok(parse_duper_trunk(duper).into()) -} - -fn parse_duper_trunk(pair: Pair<'_, Rule>) -> DuperTrunk<'_> { - let mut duper_trunk = pair.into_inner(); - let mut next = duper_trunk.next().unwrap(); - let identifier = match next.as_rule() { - Rule::identifier => { - let identifier = next.as_str(); - next = duper_trunk.next().unwrap(); - Some(identifier) - } - _ => None, - }; - match next.as_rule() { - Rule::object => DuperTrunk::Object { - identifier, - value: parse_object(next), - }, - Rule::array => DuperTrunk::Array { - identifier, - value: parse_array(next), - }, - _ => unreachable!(), - } -} - -fn parse_object(pair: Pair<'_, Rule>) -> Vec<(Cow<'_, str>, DuperValue<'_>)> { - debug_assert!(matches!(pair.as_rule(), Rule::object)); - pair.into_inner() - .map(|pair| { - let mut inner_pair = pair.into_inner(); - let key_pair = inner_pair.next().unwrap(); - let key = match key_pair.as_rule() { - Rule::string => unescape_str(key_pair.into_inner().next().unwrap().as_str()), - Rule::raw_string => Cow::Borrowed(key_pair.into_inner().next().unwrap().as_str()), - Rule::plain_key => Cow::Borrowed(key_pair.as_str()), - _ => unreachable!(), - }; - let value = parse_value(inner_pair.next().unwrap()); - (key, value) - }) - .collect() -} - -fn parse_array(pair: Pair<'_, Rule>) -> Vec> { - debug_assert!(matches!(pair.as_rule(), Rule::array)); - pair.into_inner().map(|pair| parse_value(pair)).collect() -} - -fn parse_value(pair: Pair<'_, Rule>) -> DuperValue<'_> { - let mut inner_pair = pair.into_inner(); - let mut next = inner_pair.next().unwrap(); - let identifier = match next.as_rule() { - Rule::identifier => { - let identifier = next.as_str(); - next = inner_pair.next().unwrap(); - Some(identifier) - } - _ => None, - }; - DuperValue { - identifier, - value: match next.as_rule() { - Rule::object => DuperInner::Object(parse_object(next)), - Rule::array => DuperInner::Array(parse_array(next)), - Rule::string => { - DuperInner::String(unescape_str(next.into_inner().next().unwrap().as_str())) - } - Rule::raw_string => { - DuperInner::String(Cow::Borrowed(next.into_inner().next().unwrap().as_str())) - } - Rule::bytes => DuperInner::Bytes(Cow::Owned( - unescape_str(next.into_inner().next().unwrap().as_str()) - .as_bytes() - .to_vec(), - )), - Rule::raw_bytes => DuperInner::Bytes(Cow::Borrowed( - next.into_inner().next().unwrap().as_str().as_bytes(), - )), - Rule::integer => DuperInner::Integer({ - let integer_inner = next.into_inner().next().unwrap(); - match integer_inner.as_rule() { - Rule::decimal_integer => { - i64::from_str_radix(integer_inner.as_str(), 10).unwrap() - } - Rule::hex_integer => { - i64::from_str_radix(integer_inner.as_str().split_at(2).1, 16).unwrap() - } - Rule::octal_integer => { - i64::from_str_radix(integer_inner.as_str().split_at(2).1, 8).unwrap() - } - Rule::binary_integer => { - i64::from_str_radix(integer_inner.as_str().split_at(2).1, 2).unwrap() - } - _ => unreachable!(), - } - }), - Rule::float => DuperInner::Float(next.as_str().replace('_', "").parse().unwrap()), - Rule::boolean => DuperInner::Boolean(next.as_str().parse().unwrap()), - Rule::null => DuperInner::Null, - _ => unreachable!(), - }, - } -} - -fn unescape_str(input: &str) -> Cow<'_, str> { - unescape(input).decode_utf8().unwrap() -} - -#[test] -fn it_works() { - let duper: DuperTrunk = parse_duper( +mod ast; +mod builder; +mod format; +mod parser; +mod serializer; +mod types; +mod visitor; + +pub use ast::{DuperInner, DuperValue}; +pub use parser::DuperParser; +pub use serializer::DuperSerializer; + +#[cfg(test)] +mod tests { + use crate::{ast::DuperValue, parser::DuperParser, serializer::DuperSerializer}; + + #[test] + fn it_works() { + let duper: DuperValue = DuperParser::parse_duper( r##"{"hello ": Array([" world", Null(null), true, b"bytes", 42, {inner: 0x12, "\uD83D\uDE00": 1.32e+20}]),r#"raw string"#: br#"a\b"#,}"##, ) .unwrap(); - println!("{:?}", duper); + println!("{:?}", duper); + println!("{}", DuperSerializer::new().serialize(duper)); + } } diff --git a/duper/src/parser.rs b/duper/src/parser.rs new file mode 100644 index 0000000..a29e125 --- /dev/null +++ b/duper/src/parser.rs @@ -0,0 +1,19 @@ +use pest::{Parser as _, error::Error}; + +use crate::{ast::DuperValue, builder::DuperBuilder}; + +#[derive(pest_derive::Parser)] +#[grammar = "grammar.pest"] +pub struct DuperParser; + +impl DuperParser { + pub fn parse_duper_stream(input: &'_ str) -> Result>, Error> { + let mut pairs = Self::parse(Rule::duper_stream, input)?; + DuperBuilder::build_duper_stream(pairs.next().unwrap()) + } + + pub fn parse_duper(input: &'_ str) -> Result, Error> { + let mut pairs = Self::parse(Rule::duper, input)?; + DuperBuilder::build_duper(pairs.next().unwrap()) + } +} diff --git a/duper/src/serializer.rs b/duper/src/serializer.rs new file mode 100644 index 0000000..4547900 --- /dev/null +++ b/duper/src/serializer.rs @@ -0,0 +1,146 @@ +use std::borrow::Cow; + +use crate::{ + ast::DuperValue, + format::{ + format_boolean, format_bytes, format_float, format_integer, format_key, format_string, + }, + visitor::DuperVisitor, +}; + +pub struct DuperSerializer; + +impl DuperSerializer { + pub fn new() -> Self { + Self + } + + pub fn serialize<'a>(&mut self, value: DuperValue<'a>) -> String { + value.accept(self) + } +} + +impl DuperVisitor for DuperSerializer { + type Value = String; + + fn visit_object<'a>( + &mut self, + identifier: Option<&'a str>, + object: Vec<(Cow<'a, str>, DuperValue<'a>)>, + ) -> Self::Value { + let mut string = String::new(); + let len = object.len(); + + if let Some(identifier) = identifier { + string.push_str(identifier); + string.push_str("({"); + for (i, (key, value)) in object.into_iter().enumerate() { + string.push_str(&format_key(key)); + string.push_str(": "); + string.push_str(&value.accept(self)); + if i < len - 1 { + string.push_str(", "); + } + } + string.push_str("})"); + } else { + string.push('{'); + for (i, (key, value)) in object.into_iter().enumerate() { + string.push_str(&format_key(key)); + string.push_str(": "); + string.push_str(&value.accept(self)); + if i < len - 1 { + string.push_str(", "); + } + } + string.push('}'); + } + + string + } + + fn visit_array<'a>( + &mut self, + identifier: Option<&str>, + array: Vec>, + ) -> Self::Value { + let mut string = String::new(); + let len = array.len(); + + if let Some(identifier) = identifier { + string.push_str(identifier); + string.push_str("(["); + for (i, value) in array.into_iter().enumerate() { + string.push_str(&value.accept(self)); + if i < len - 1 { + string.push_str(", "); + } + } + string.push_str("])"); + } else { + string.push('['); + for (i, value) in array.into_iter().enumerate() { + string.push_str(&value.accept(self)); + if i < len - 1 { + string.push_str(", "); + } + } + string.push(']'); + } + + string + } + + fn visit_string<'a>(&mut self, identifier: Option<&str>, value: Cow<'a, str>) -> Self::Value { + if let Some(identifier) = identifier { + let value = format_string(value); + format!("{identifier}({value})") + } else { + format_string(value).into_owned() + } + } + + fn visit_bytes<'a>(&mut self, identifier: Option<&str>, bytes: Cow<'a, [u8]>) -> Self::Value { + if let Some(identifier) = identifier { + let bytes = format_bytes(bytes); + format!("{identifier}({bytes})") + } else { + format_bytes(bytes).into_owned() + } + } + + fn visit_integer(&mut self, identifier: Option<&str>, integer: i64) -> Self::Value { + if let Some(identifier) = identifier { + let value = format_integer(integer, identifier.try_into().ok()); + format!("{identifier}({value})") + } else { + format_integer(integer, None) + } + } + + fn visit_float(&mut self, identifier: Option<&str>, float: f64) -> Self::Value { + if let Some(identifier) = identifier { + let value = format_float(float, identifier.try_into().ok()); + format!("{identifier}({value})") + } else { + format_float(float, None) + } + } + + fn visit_boolean(&mut self, identifier: Option<&str>, boolean: bool) -> Self::Value { + if let Some(identifier) = identifier { + let value = format_boolean(boolean); + format!("{identifier}({value})") + } else { + format_boolean(boolean) + } + } + + fn visit_null(&mut self, identifier: Option<&str>) -> Self::Value { + if let Some(identifier) = identifier { + format!("{identifier}(null)") + } else { + "null".into() + } + } +} diff --git a/duper/src/types.rs b/duper/src/types.rs new file mode 100644 index 0000000..810f368 --- /dev/null +++ b/duper/src/types.rs @@ -0,0 +1,21 @@ +#[non_exhaustive] +pub enum DuperTypes { + DecInteger, + HexInteger, + OctInteger, + BinInteger, +} + +impl TryFrom<&str> for DuperTypes { + type Error = (); + + fn try_from(value: &str) -> Result { + match value { + "Integer" | "DecInteger" => Ok(DuperTypes::DecInteger), + "HexInteger" => Ok(DuperTypes::HexInteger), + "OctInteger" => Ok(DuperTypes::OctInteger), + "BinInteger" => Ok(DuperTypes::BinInteger), + _ => Err(()), + } + } +} diff --git a/duper/src/visitor.rs b/duper/src/visitor.rs new file mode 100644 index 0000000..429dcfa --- /dev/null +++ b/duper/src/visitor.rs @@ -0,0 +1,29 @@ +use std::borrow::Cow; + +use crate::ast::DuperValue; + +pub trait DuperVisitor { + type Value; + + fn visit_object<'a>( + &mut self, + identifier: Option<&'a str>, + object: Vec<(Cow<'a, str>, DuperValue<'a>)>, + ) -> Self::Value; + fn visit_array<'a>( + &mut self, + identifier: Option<&'a str>, + array: Vec>, + ) -> Self::Value; + fn visit_string<'a>( + &mut self, + identifier: Option<&'a str>, + string: Cow<'a, str>, + ) -> Self::Value; + fn visit_bytes<'a>(&mut self, identifier: Option<&'a str>, bytes: Cow<'a, [u8]>) + -> Self::Value; + fn visit_integer<'a>(&mut self, identifier: Option<&'a str>, integer: i64) -> Self::Value; + fn visit_float<'a>(&mut self, identifier: Option<&'a str>, float: f64) -> Self::Value; + fn visit_boolean<'a>(&mut self, identifier: Option<&'a str>, boolean: bool) -> Self::Value; + fn visit_null<'a>(&mut self, identifier: Option<&'a str>) -> Self::Value; +} -- 2.51.2