diff --git a/Cargo.lock b/Cargo.lock index 36ecec4..885d96e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,58 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum_duper" +version = "0.1.0" +dependencies = [ + "axum", + "serde_core", + "serde_duper", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -11,6 +63,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cfg-if" version = "1.0.3" @@ -74,6 +132,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -84,6 +172,40 @@ dependencies = [ "version_check", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "insta" version = "1.43.2" @@ -95,6 +217,12 @@ dependencies = [ "similar", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "json-escape" version = "0.3.0" @@ -110,18 +238,36 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.3" @@ -165,6 +311,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -251,6 +409,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "typenum" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index f7771b5..a421908 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["duper", "serde_duper"] +members = ["axum_duper","duper", "serde_duper"] diff --git a/axum_duper/Cargo.toml b/axum_duper/Cargo.toml new file mode 100644 index 0000000..a14f946 --- /dev/null +++ b/axum_duper/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "axum_duper" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8.6", default-features = false } +serde_core = "1.0.228" +serde_duper = { path = "../serde_duper" } diff --git a/axum_duper/src/lib.rs b/axum_duper/src/lib.rs new file mode 100644 index 0000000..5f6f082 --- /dev/null +++ b/axum_duper/src/lib.rs @@ -0,0 +1,138 @@ +use std::ops::Deref; + +use axum::{ + extract::{FromRequest, OptionalFromRequest, Request}, + http::{HeaderValue, StatusCode, header::CONTENT_TYPE}, + response::{IntoResponse, Response}, +}; +use serde_core::{Serialize, de::DeserializeOwned}; +use serde_duper::ErrorKind; + +#[derive(Debug)] +#[non_exhaustive] +pub enum DuperRejection { + DuperDataError, + DuperSyntaxError, + MissingDuperContentType, + InternalDuperError, +} + +static DUPER_CONTENT_TYPE: &str = "application/duper"; + +impl IntoResponse for DuperRejection { + fn into_response(self) -> Response { + match self { + DuperRejection::DuperDataError | DuperRejection::DuperSyntaxError => { + (StatusCode::BAD_REQUEST, "Failed to parse duper").into_response() + } + DuperRejection::MissingDuperContentType => ( + StatusCode::BAD_REQUEST, + format!("Content-Type header must be {DUPER_CONTENT_TYPE}"), + ) + .into_response(), + DuperRejection::InternalDuperError => { + (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() + } + } + } +} + +pub struct Duper(pub T); + +impl Duper +where + T: DeserializeOwned, +{ + pub fn from_bytes(bytes: &[u8]) -> Result { + let string = str::from_utf8(bytes).map_err(|_| DuperRejection::DuperDataError)?; + Self::from_str(string) + } + + pub fn from_str(string: &str) -> Result { + match serde_duper::from_str(string) { + Ok(value) => Ok(Self(value)), + Err(err) => match err.inner.kind { + ErrorKind::ParseError(_) => Err(DuperRejection::DuperSyntaxError), + ErrorKind::SerializationError + | ErrorKind::DeserializationError(_) + | ErrorKind::InvalidValue + | ErrorKind::Custom => Err(DuperRejection::InternalDuperError), + }, + } + } +} + +impl FromRequest for Duper +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = DuperRejection; + + async fn from_request(req: Request, state: &S) -> Result { + if req + .headers() + .get(CONTENT_TYPE) + .and_then(|content_type| content_type.to_str().ok()) + != Some(DUPER_CONTENT_TYPE) + { + return Err(DuperRejection::MissingDuperContentType); + } + let string = String::from_request(req, state) + .await + .map_err(|_| DuperRejection::DuperDataError)?; + Self::from_str(&string) + } +} + +impl OptionalFromRequest for Duper +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = DuperRejection; + + async fn from_request(req: Request, state: &S) -> Result, Self::Rejection> { + let Some(content_type) = req.headers().get(CONTENT_TYPE) else { + return Ok(None); + }; + if content_type.to_str().ok() != Some(DUPER_CONTENT_TYPE) { + return Err(DuperRejection::MissingDuperContentType); + } + let string = String::from_request(req, state) + .await + .map_err(|_| DuperRejection::DuperDataError)?; + Ok(Some(Self::from_str(&string)?)) + } +} + +impl IntoResponse for Duper +where + T: Serialize, +{ + fn into_response(self) -> Response { + serde_duper::to_string(&self.0) + .map(|string| { + ( + [(CONTENT_TYPE, HeaderValue::from_static(DUPER_CONTENT_TYPE))], + string, + ) + }) + .map_err(|_| DuperRejection::InternalDuperError) + .into_response() + } +} + +impl Deref for Duper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Duper { + fn from(value: T) -> Self { + Self(value) + } +} diff --git a/duper/src/format.rs b/duper/src/format.rs index 06fa49a..292fef0 100644 --- a/duper/src/format.rs +++ b/duper/src/format.rs @@ -9,38 +9,118 @@ use crate::{ pub(crate) fn format_key<'a>(key: &'a DuperKey<'a>) -> Cow<'a, str> { if key.0.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { Cow::Borrowed(key.0.as_ref()) - } else if key.0.is_empty() { - Cow::Borrowed(r#""""#) } else { - // TO-DO raw string heuristics - let escaped_key = Cow::from(escape_str(&key.0)).into_owned(); - Cow::Owned(format!(r#""{escaped_key}""#)) + format_cow_str(&key.0) } } -pub(crate) fn format_string<'a>(string: &'a DuperString<'a>) -> Cow<'a, str> { - if string.0.is_empty() { +pub(crate) fn format_duper_string<'a>(string: &'a DuperString<'a>) -> Cow<'a, str> { + format_cow_str(&string.0) +} + +fn format_cow_str<'a>(string: &Cow<'a, str>) -> Cow<'a, str> { + if string.is_empty() { + // Empty string Cow::Borrowed(r#""""#) } else { - // TO-DO raw string heuristics - let escaped_key = Cow::from(escape_str(&string.0)).into_owned(); - Cow::Owned(format!(r#""{escaped_key}""#)) + // Check if it's benefic to turn into a raw string + let mut quotes = 0usize; + let mut was_hashtag = false; + let mut curr_hashtags = 0usize; + let mut max_hashtags = 0usize; + let mut has_char_that_should_be_escaped = false; + for char in string.chars() { + match char { + '"' => { + was_hashtag = false; + quotes += 1 + } + '#' if was_hashtag => { + curr_hashtags += 1; + max_hashtags = max_hashtags.max(curr_hashtags); + } + '#' => { + was_hashtag = true; + curr_hashtags = 1; + max_hashtags = max_hashtags.max(curr_hashtags); + } + ' ' => was_hashtag = false, + '\r' | '\n' | '\t' | _ if char.is_control() || char.is_whitespace() => { + has_char_that_should_be_escaped = true; + break; + } + _ => was_hashtag = false, + } + } + if quotes > max_hashtags && !has_char_that_should_be_escaped { + // Raw string + let hashtags: String = (0..=max_hashtags).map(|_| '#').collect(); + Cow::Owned(format!(r#"r{}"{}"{}"#, hashtags, string, hashtags)) + } else { + // Regular string with escaping + let escaped_key = Cow::from(escape_str(&string)).into_owned(); + Cow::Owned(format!(r#""{escaped_key}""#)) + } } } -pub(crate) fn format_bytes<'a>(bytes: &'a DuperBytes<'a>) -> Cow<'a, str> { +pub(crate) fn format_duper_bytes<'a>(bytes: &'a DuperBytes<'a>) -> Cow<'a, str> { if bytes.0.is_empty() { - return Cow::Borrowed(r#"b"""#); + // Empty bytes + Cow::Borrowed(r#"b"""#) + } else { + // Check if it's benefic to turn into raw bytes + let mut quotes = 0usize; + let mut was_hashtag = false; + let mut curr_hashtags = 0usize; + let mut max_hashtags = 0usize; + let mut has_char_that_should_be_escaped = false; + for byte in bytes.0.iter() { + match byte { + b'"' => { + was_hashtag = false; + quotes += 1 + } + b'#' if was_hashtag => { + curr_hashtags += 1; + max_hashtags = max_hashtags.max(curr_hashtags); + } + b'#' => { + was_hashtag = true; + curr_hashtags = 1; + max_hashtags = max_hashtags.max(curr_hashtags); + } + b' ' => was_hashtag = false, + b'\r' | b'\n' | b'\t' | _ + if byte.is_ascii_control() || byte.is_ascii_whitespace() => + { + has_char_that_should_be_escaped = true; + break; + } + _ => was_hashtag = false, + } + } + if quotes > max_hashtags && !has_char_that_should_be_escaped { + // Raw bytes + let hashtags: String = (0..=max_hashtags).map(|_| '#').collect(); + let unesecaped_bytes: String = + bytes.0.into_iter().copied().map(|b| b as char).collect(); + return Cow::Owned(format!( + r#"r{}"{}"{}"#, + hashtags, unesecaped_bytes, hashtags + )); + } else { + // Regular bytes with escaping + let escaped_bytes: String = bytes + .0 + .into_iter() + .copied() + .flat_map(ascii::escape_default) + .map(|b| b as char) + .collect(); + Cow::Owned(format!(r#"b"{escaped_bytes}""#)) + } } - // TO-DO raw bytes heuristics - let escaped_bytes: String = bytes - .0 - .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 { diff --git a/duper/src/pretty_printer.rs b/duper/src/pretty_printer.rs index ae045aa..6aeb977 100644 --- a/duper/src/pretty_printer.rs +++ b/duper/src/pretty_printer.rs @@ -3,7 +3,8 @@ use crate::{ DuperArray, DuperBytes, DuperIdentifier, DuperObject, DuperString, DuperTuple, DuperValue, }, format::{ - format_boolean, format_bytes, format_float, format_integer, format_key, format_string, + format_boolean, format_duper_bytes, format_duper_string, format_float, format_integer, + format_key, }, visitor::DuperVisitor, }; @@ -184,7 +185,7 @@ impl DuperVisitor for PrettyPrinter { value: &DuperString<'a>, ) -> Self::Value { if let Some(identifier) = identifier { - let value = format_string(value); + let value = format_duper_string(value); if value.len() + self.indent > 60 { let mut string = String::new(); string.push_str(identifier.as_ref()); @@ -201,7 +202,7 @@ impl DuperVisitor for PrettyPrinter { format!("{identifier}({value})") } } else { - format_string(value).into_owned() + format_duper_string(value).into_owned() } } @@ -211,7 +212,7 @@ impl DuperVisitor for PrettyPrinter { bytes: &DuperBytes<'a>, ) -> Self::Value { if let Some(identifier) = identifier { - let bytes = format_bytes(bytes); + let bytes = format_duper_bytes(bytes); if bytes.len() + self.indent > 60 { let mut string = String::new(); string.push_str(identifier.as_ref()); @@ -227,7 +228,7 @@ impl DuperVisitor for PrettyPrinter { format!("{identifier}({bytes})") } } else { - format_bytes(bytes).into_owned() + format_duper_bytes(bytes).into_owned() } } diff --git a/duper/src/serializer.rs b/duper/src/serializer.rs index 9f6232b..5767437 100644 --- a/duper/src/serializer.rs +++ b/duper/src/serializer.rs @@ -3,7 +3,8 @@ use crate::{ DuperArray, DuperBytes, DuperIdentifier, DuperObject, DuperString, DuperTuple, DuperValue, }, format::{ - format_boolean, format_bytes, format_float, format_integer, format_key, format_string, + format_boolean, format_duper_bytes, format_duper_string, format_float, format_integer, + format_key, }, visitor::DuperVisitor, }; @@ -135,10 +136,10 @@ impl DuperVisitor for Serializer { value: &DuperString<'a>, ) -> Self::Value { if let Some(identifier) = identifier { - let value = format_string(value); + let value = format_duper_string(value); format!("{identifier}({value})") } else { - format_string(value).into_owned() + format_duper_string(value).into_owned() } } @@ -148,10 +149,10 @@ impl DuperVisitor for Serializer { bytes: &DuperBytes<'a>, ) -> Self::Value { if let Some(identifier) = identifier { - let bytes = format_bytes(bytes); + let bytes = format_duper_bytes(bytes); format!("{identifier}({bytes})") } else { - format_bytes(bytes).into_owned() + format_duper_bytes(bytes).into_owned() } } diff --git a/duper/src/snapshots/duper__pretty_printer__pretty_printer_tests__complex_array.snap b/duper/src/snapshots/duper__pretty_printer__pretty_printer_tests__complex_array.snap index b8f10b6..154531c 100644 --- a/duper/src/snapshots/duper__pretty_printer__pretty_printer_tests__complex_array.snap +++ b/duper/src/snapshots/duper__pretty_printer__pretty_printer_tests__complex_array.snap @@ -8,5 +8,5 @@ expression: pp "So many arrays!", ], ], - "\"Hello world!\"", + r#""Hello world!""#, ] diff --git a/duper/src/types.rs b/duper/src/types.rs index 213628b..394a5ae 100644 --- a/duper/src/types.rs +++ b/duper/src/types.rs @@ -28,8 +28,8 @@ pub enum DuperTypes { impl TryFrom<&DuperIdentifier<'_>> for DuperTypes { type Error = String; - fn try_from(value: &DuperIdentifier<'_>) -> Result { - match value.0.as_ref() { + fn try_from(identifier: &DuperIdentifier<'_>) -> Result { + match identifier.0.as_ref() { "Integer" | "DecInteger" => Ok(DuperTypes::DecInteger), "HexInteger" => Ok(DuperTypes::HexInteger), "OctInteger" => Ok(DuperTypes::OctInteger), @@ -49,7 +49,7 @@ impl TryFrom<&DuperIdentifier<'_>> for DuperTypes { "Regex" => Ok(DuperTypes::Regex), "Base64" => Ok(DuperTypes::Base64), "Jwt" => Ok(DuperTypes::Jwt), - _ => Err(format!("Unknown type {}", value.0.as_ref())), + _ => Err(format!("Unsupported type {}", identifier.0.as_ref())), } } } diff --git a/serde_duper/src/deserializer.rs b/serde_duper/src/de.rs similarity index 100% rename from serde_duper/src/deserializer.rs rename to serde_duper/src/de.rs diff --git a/serde_duper/src/error.rs b/serde_duper/src/error.rs index 12315c4..f567efb 100644 --- a/serde_duper/src/error.rs +++ b/serde_duper/src/error.rs @@ -10,14 +10,14 @@ pub enum ErrorKind { } #[derive(Debug, Clone)] -struct ErrorImpl { - kind: ErrorKind, - message: String, +pub struct ErrorImpl { + pub kind: ErrorKind, + pub message: String, } #[derive(Debug, Clone)] pub struct Error { - inner: Box, + pub inner: Box, } impl Error { @@ -84,3 +84,5 @@ impl From> for Error { Self::new(ErrorKind::ParseError(value), message) } } + +pub type Result = std::result::Result; diff --git a/serde_duper/src/lib.rs b/serde_duper/src/lib.rs index 7e2233e..52c445a 100644 --- a/serde_duper/src/lib.rs +++ b/serde_duper/src/lib.rs @@ -1,8 +1,8 @@ -mod deserializer; +mod de; mod error; -mod serializer; +mod ser; -pub use deserializer::{Deserializer, from_str, from_value}; +pub use de::{Deserializer, from_str, from_value}; pub use duper::{DuperInner, DuperValue}; -pub use error::Error; -pub use serializer::{to_duper, to_string}; +pub use error::{Error, ErrorImpl, ErrorKind, Result}; +pub use ser::{Serializer, to_duper, to_string, to_string_pretty}; diff --git a/serde_duper/src/serializer.rs b/serde_duper/src/ser.rs similarity index 97% rename from serde_duper/src/serializer.rs rename to serde_duper/src/ser.rs index 43148e1..a14f926 100644 --- a/serde_duper/src/serializer.rs +++ b/serde_duper/src/ser.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, marker::PhantomData}; use duper::{ DuperArray, DuperBytes, DuperIdentifier, DuperInner, DuperKey, DuperObject, DuperString, - DuperTuple, DuperValue, Serializer as DuperSerializer, + DuperTuple, DuperValue, PrettyPrinter as DuperPrettyPrinter, Serializer as DuperSerializer, }; use serde_core::{Serialize, ser}; @@ -21,7 +21,7 @@ impl<'a> Serializer<'a> { } } -pub fn to_string(value: &T) -> Result +pub fn to_duper<'a, T>(value: &'a T) -> Result, String> where T: Serialize, { @@ -29,18 +29,21 @@ where let value = value .serialize(&mut serializer) .map_err(|e| format!("Serialization error: {e:?}"))?; - Ok(DuperSerializer::new().serialize(value)) + Ok(value) } -pub fn to_duper<'a, T>(value: &'a T) -> Result, String> +pub fn to_string(value: &T) -> Result where T: Serialize, { - let mut serializer = Serializer::new(); - let value = value - .serialize(&mut serializer) - .map_err(|e| format!("Serialization error: {e:?}"))?; - Ok(value) + Ok(DuperSerializer::new().serialize(to_duper(value)?)) +} + +pub fn to_string_pretty(value: &T) -> Result +where + T: Serialize, +{ + Ok(DuperPrettyPrinter::new().pretty_print(to_duper(value)?)) } pub struct SerializeSeq<'a, 'b> {