From 7c883610678027491d6ef36046f6ee0c16b09140 Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Sat, 6 Dec 2025 20:11:58 -0300 Subject: [PATCH] Improve docstrings and handle more types in LSP --- Cargo.lock | 2 + axum_duper/src/lib.rs | 2 + duper/src/ast.rs | 33 +++++--- duper/src/escape.rs | 71 +++++++++++----- duper/src/format.rs | 12 ++- duper/src/parser/mod.rs | 56 +++++++++++-- duper/src/parser/temporal.rs | 23 +++++ duper/src/serde/de.rs | 2 + duper/src/serde/error.rs | 2 + duper/src/serde/meta.rs | 4 + duper/src/serde/mod.rs | 9 ++ duper/src/serde/ser.rs | 2 + duper/src/serde/temporal.rs | 10 ++- duper/src/validate.rs | 13 +++ duper_lsp/CHANGELOG.md | 4 + duper_lsp/Cargo.toml | 2 + duper_lsp/src/diagnostics.rs | 139 +++++++++++++++++++++++++++---- duperq/src/formatter.rs | 2 +- duperq/src/processor.rs | 1 + serde_duper/CHANGELOG.md | 2 +- serde_duper/src/types/mod.rs | 3 + tracing_duper/examples/simple.rs | 2 +- tracing_duper/src/lib.rs | 12 +-- 23 files changed, 339 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7717ef7..6cae91a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1025,9 +1025,11 @@ dependencies = [ "duper", "duperfmt", "futures", + "ipnet", "line-index", "lsp-types", "regex", + "rust_decimal", "tokio", "tokio-util", "tower", diff --git a/axum_duper/src/lib.rs b/axum_duper/src/lib.rs index e866c87..2faafff 100644 --- a/axum_duper/src/lib.rs +++ b/axum_duper/src/lib.rs @@ -16,7 +16,9 @@ use axum::{ use duper::serde::error::DuperSerdeErrorKind; use serde_core::{Serialize, de::DeserializeOwned}; +/// Default MIME type for Duper files. pub static DUPER_CONTENT_TYPE: &str = "application/duper"; +/// Alternative MIME type for Duper files. pub static DUPER_ALT_CONTENT_TYPE: &str = "application/x-duper"; /// Rejection used for [`Duper`]. diff --git a/duper/src/ast.rs b/duper/src/ast.rs index 006ec97..5e7c6f7 100644 --- a/duper/src/ast.rs +++ b/duper/src/ast.rs @@ -261,7 +261,7 @@ impl<'a> DuperIdentifier<'a> { )) } - /// Create a clone of this DuperIdentifier with a static lifetime. + /// Create a clone of this `DuperIdentifier` with a static lifetime. pub fn static_clone(&self) -> DuperIdentifier<'static> { DuperIdentifier(Cow::Owned(self.0.clone().into_owned())) } @@ -348,7 +348,7 @@ impl<'a> DuperTemporalIdentifier<'a> { Self::try_from(identifier) } - /// Create a clone of this DuperTemporalIdentifier with a static lifetime. + /// Create a clone of this `DuperTemporalIdentifier` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalIdentifier<'static> { DuperTemporalIdentifier(DuperIdentifier(Cow::Owned(self.0.0.clone().into_owned()))) } @@ -458,6 +458,7 @@ impl<'a> DuperKey<'a> { self.0 } + /// Create a clone of this `DuperKey` with a static lifetime. pub fn static_clone(&self) -> DuperKey<'static> { DuperKey(Cow::Owned(self.0.clone().into_owned())) } @@ -591,6 +592,7 @@ impl<'a> DuperValue<'a> { } } + /// Returns a clone of the identifier associated with this Duper value. pub fn identifier(&self) -> Option> { match self { DuperValue::Temporal(inner) => inner.identifier(), @@ -606,6 +608,8 @@ impl<'a> DuperValue<'a> { } } + /// Replaces the identifier of the value, returning an error if the + /// identifier is invalid. pub fn with_identifier( self, identifier: Option>, @@ -779,6 +783,7 @@ impl<'a> DuperObject<'a> { self.0.iter() } + /// Returns the [`DuperValue`] with the given key. pub fn get<'b>(&'b self, key: &'b DuperKey<'_>) -> Option<&'b DuperValue<'a>> { self.0.get(key) } @@ -860,6 +865,10 @@ impl Display for DuperObjectTryFromError<'_> { impl std::error::Error for DuperObjectTryFromError<'_> {} impl<'a> DuperTemporal<'a> { + /// Replaces the identifier of the value, returning an error if the + /// identifier is invalid. + /// + /// Only unspecified values can have their identifier updated. pub fn with_identifier( self, identifier: Option>, @@ -892,6 +901,7 @@ impl<'a> DuperTemporal<'a> { } } + /// Create a clone of this `DuperTemporal` with a static lifetime. pub fn static_clone(&self) -> DuperTemporal<'static> { match self { DuperTemporal::Instant { inner } => DuperTemporal::Instant { @@ -927,6 +937,7 @@ impl<'a> DuperTemporal<'a> { } } + /// Returns a clone of identifier associated with this Duper Temporal value. pub fn identifier(&self) -> Option> { match self { DuperTemporal::Instant { .. } => Some(DuperIdentifier(Cow::Borrowed("Instant"))), @@ -1060,7 +1071,7 @@ impl<'a> DuperTemporalInstant<'a> { self.0 } - /// Create a clone of this DuperTemporalInstant with a static lifetime. + /// Create a clone of this `DuperTemporalInstant` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalInstant<'static> { DuperTemporalInstant(Cow::Owned(self.0.clone().into_owned())) } @@ -1103,7 +1114,7 @@ impl<'a> DuperTemporalZonedDateTime<'a> { self.0 } - /// Create a clone of this DuperTemporalZonedDateTime with a static lifetime. + /// Create a clone of this `DuperTemporalZonedDateTime` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalZonedDateTime<'static> { DuperTemporalZonedDateTime(Cow::Owned(self.0.clone().into_owned())) } @@ -1146,7 +1157,7 @@ impl<'a> DuperTemporalPlainDate<'a> { self.0 } - /// Create a clone of this DuperTemporalPlainDate with a static lifetime. + /// Create a clone of this `DuperTemporalPlainDate` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalPlainDate<'static> { DuperTemporalPlainDate(Cow::Owned(self.0.clone().into_owned())) } @@ -1189,7 +1200,7 @@ impl<'a> DuperTemporalPlainTime<'a> { self.0 } - /// Create a clone of this DuperTemporalPlainTime with a static lifetime. + /// Create a clone of this `DuperTemporalPlainTime` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalPlainTime<'static> { DuperTemporalPlainTime(Cow::Owned(self.0.clone().into_owned())) } @@ -1232,7 +1243,7 @@ impl<'a> DuperTemporalPlainDateTime<'a> { self.0 } - /// Create a clone of this DuperTemporalPlainDateTime with a static lifetime. + /// Create a clone of this `DuperTemporalPlainDateTime` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalPlainDateTime<'static> { DuperTemporalPlainDateTime(Cow::Owned(self.0.clone().into_owned())) } @@ -1275,7 +1286,7 @@ impl<'a> DuperTemporalPlainYearMonth<'a> { self.0 } - /// Create a clone of this DuperTemporalPlainYearMonth with a static lifetime. + /// Create a clone of this `DuperTemporalPlainYearMonth` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalPlainYearMonth<'static> { DuperTemporalPlainYearMonth(Cow::Owned(self.0.clone().into_owned())) } @@ -1318,7 +1329,7 @@ impl<'a> DuperTemporalPlainMonthDay<'a> { self.0 } - /// Create a clone of this DuperTemporalPlainMonthDay with a static lifetime. + /// Create a clone of this `DuperTemporalPlainMonthDay` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalPlainMonthDay<'static> { DuperTemporalPlainMonthDay(Cow::Owned(self.0.clone().into_owned())) } @@ -1361,7 +1372,7 @@ impl<'a> DuperTemporalDuration<'a> { self.0 } - /// Create a clone of this DuperTemporalDuration with a static lifetime. + /// Create a clone of this `DuperTemporalDuration` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalDuration<'static> { DuperTemporalDuration(Cow::Owned(self.0.clone().into_owned())) } @@ -1404,7 +1415,7 @@ impl<'a> DuperTemporalUnspecified<'a> { self.0 } - /// Create a clone of this DuperTemporal with a static lifetime. + /// Create a clone of this `DuperTemporal` with a static lifetime. pub fn static_clone(&self) -> DuperTemporalUnspecified<'static> { DuperTemporalUnspecified(Cow::Owned(self.0.clone().into_owned())) } diff --git a/duper/src/escape.rs b/duper/src/escape.rs index 496e7e4..7be56ec 100644 --- a/duper/src/escape.rs +++ b/duper/src/escape.rs @@ -1,41 +1,56 @@ +//! Functions to handle escaping and unescaping of strings and byte strings. + use std::{ascii, borrow::Cow, fmt::Display}; use unicode_general_category::{GeneralCategory, get_general_category}; #[derive(Debug)] +/// Possible errors when unescaping Duper strings/byte strings. pub enum UnescapeError { - UnescapedTab, + /// An unescaped control character was found. + UnescapedControlCharacter, + /// An invalid byte sequence was present. InvalidByteSequence(String), - InvalidUnicode(String), + /// A malformed 4-digit unicode sequence was present. + Invalid4DigitUnicode(String), + /// A malformed 8-digit unicode sequence was present. + Invalid8DigitUnicode(String), } impl Display for UnescapeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - UnescapeError::UnescapedTab => f.write_str("unescaped tab not allowed"), + UnescapeError::UnescapedControlCharacter => { + f.write_str("unescaped control character not allowed") + } UnescapeError::InvalidByteSequence(hex_str) => f.write_fmt(format_args!( "invalid escape sequence for bytes: \\x{hex_str}" )), - UnescapeError::InvalidUnicode(hex_str) => f.write_fmt(format_args!( + UnescapeError::Invalid4DigitUnicode(hex_str) => f.write_fmt(format_args!( "invalid escape sequence for unicode: \\u{hex_str}" )), + UnescapeError::Invalid8DigitUnicode(hex_str) => f.write_fmt(format_args!( + "invalid escape sequence for unicode: \\U{hex_str}" + )), } } } impl std::error::Error for UnescapeError {} +/// Unescape the inner contents of a quoted string. pub fn unescape_str<'a>(input: &'a str) -> Result, UnescapeError> { - if !input.contains('\\') { - return Ok(Cow::Borrowed(input)); - } - + let mut is_escaped = false; let mut result = String::with_capacity(input.len()); let mut chars = input.chars(); while let Some(c) = chars.next() { - if c == '\t' { - return Err(UnescapeError::UnescapedTab); + if ('\u{0000}'..='\u{0009}').contains(&c) + || ('\u{000b}'..='\u{001f}').contains(&c) + || c == '\u{007f}' + { + return Err(UnescapeError::UnescapedControlCharacter); } else if c == '\\' { + is_escaped = true; match chars.next() { Some('"') => result.push('"'), Some('\\') => result.push('\\'), @@ -96,7 +111,7 @@ pub fn unescape_str<'a>(input: &'a str) -> Result, UnescapeError> { { result.push(unicode_char); } else { - return Err(UnescapeError::InvalidUnicode(hex_str)); + return Err(UnescapeError::Invalid4DigitUnicode(hex_str)); } } Some('U') => { @@ -107,7 +122,7 @@ pub fn unescape_str<'a>(input: &'a str) -> Result, UnescapeError> { { result.push(unicode_char); } else { - return Err(UnescapeError::InvalidUnicode(hex_str)); + return Err(UnescapeError::Invalid8DigitUnicode(hex_str)); } } Some(other) => { @@ -121,9 +136,14 @@ pub fn unescape_str<'a>(input: &'a str) -> Result, UnescapeError> { } } - Ok(Cow::Owned(result)) + Ok(if is_escaped { + Cow::Owned(result) + } else { + Cow::Borrowed(input) + }) } +/// Escape a string into a Duper quoted string. pub fn escape_str<'a>(input: &'a Cow<'a, str>) -> Cow<'a, str> { let mut result = None; @@ -218,19 +238,21 @@ pub fn escape_str<'a>(input: &'a Cow<'a, str>) -> Cow<'a, str> { } } +/// Unescape the inner contents of a quoted byte string. pub fn unescape_bytes<'a>(input: &'a str) -> Result, UnescapeError> { - if !input.contains('\\') { - return Ok(Cow::Borrowed(input.as_bytes())); - } - + let mut is_escaped = false; let mut result = Vec::with_capacity(input.len()); let mut chars = input.chars(); let mut buf = [0u8; 4]; while let Some(c) = chars.next() { - if c == '\t' { - return Err(UnescapeError::UnescapedTab); + if ('\u{0000}'..='\u{0009}').contains(&c) + || ('\u{000b}'..='\u{001f}').contains(&c) + || c == '\u{007f}' + { + return Err(UnescapeError::UnescapedControlCharacter); } else if c == '\\' { + is_escaped = true; match chars.next() { Some('"') => result.push(b'"'), Some('\\') => result.push(b'\\'), @@ -259,7 +281,7 @@ pub fn unescape_bytes<'a>(input: &'a str) -> Result, UnescapeError { result.extend_from_slice(unicode_char.encode_utf8(&mut buf).as_bytes()); } else { - return Err(UnescapeError::InvalidUnicode(hex_str)); + return Err(UnescapeError::Invalid4DigitUnicode(hex_str)); } } Some('U') => { @@ -270,7 +292,7 @@ pub fn unescape_bytes<'a>(input: &'a str) -> Result, UnescapeError { result.extend_from_slice(unicode_char.encode_utf8(&mut buf).as_bytes()); } else { - return Err(UnescapeError::InvalidUnicode(hex_str)); + return Err(UnescapeError::Invalid8DigitUnicode(hex_str)); } } Some(other) => { @@ -284,9 +306,14 @@ pub fn unescape_bytes<'a>(input: &'a str) -> Result, UnescapeError } } - Ok(Cow::Owned(result)) + Ok(if is_escaped { + Cow::Owned(result) + } else { + Cow::Borrowed(input.as_bytes()) + }) } +/// Escape a byte slice into a Duper quoted byte string. pub fn escape_bytes<'a>(input: &'a Cow<'a, [u8]>) -> Cow<'a, str> { if input.iter().all(|&b| { b.is_ascii() diff --git a/duper/src/format.rs b/duper/src/format.rs index dd82dfa..50511bd 100644 --- a/duper/src/format.rs +++ b/duper/src/format.rs @@ -1,3 +1,5 @@ +//! Functions to handle formatting of Duper values. + use std::borrow::Cow; use base64::{Engine, prelude::BASE64_STANDARD}; @@ -7,6 +9,7 @@ use crate::{ escape::{escape_bytes, escape_str, is_invisible_unicode}, }; +/// Format a key as a Duper plain key or string. pub fn format_key<'a>(key: &'a DuperKey<'a>) -> Cow<'a, str> { if key.0.is_empty() { return Cow::Borrowed(r#""""#); @@ -37,6 +40,7 @@ pub fn format_key<'a>(key: &'a DuperKey<'a>) -> Cow<'a, str> { } } +/// Format a string as a Duper quoted or raw string. pub fn format_duper_string(string: &str) -> String { if string.is_empty() { // Empty string @@ -98,6 +102,7 @@ pub fn format_duper_string(string: &str) -> String { } } +/// Format a byte slice as a Duper quoted, raw, or Base64 byte string. pub fn format_duper_bytes(bytes: &[u8]) -> String { if bytes.is_empty() { // Empty bytes @@ -176,22 +181,27 @@ pub fn format_duper_bytes(bytes: &[u8]) -> String { } } +// Format a Temporal value for Duper. pub fn format_temporal(temporal: impl AsRef) -> String { - format!("'{}'", temporal.as_ref()) + format!("'{}'", temporal.as_ref().trim()) } +// Format an integer for Duper. pub fn format_integer(integer: i64) -> String { integer.to_string() } +// Format a float for Duper. pub fn format_float(float: f64) -> String { ryu::Buffer::new().format(float).into() } +// Format a boolean for Duper. pub fn format_boolean(bool: bool) -> &'static str { if bool { "true" } else { "false" } } +// Format a null value for Duper. pub fn format_null() -> &'static str { "null" } diff --git a/duper/src/parser/mod.rs b/duper/src/parser/mod.rs index 94bbe9b..21e7373 100644 --- a/duper/src/parser/mod.rs +++ b/duper/src/parser/mod.rs @@ -1,3 +1,5 @@ +//! Parsing and AST-building utilities for Duper. + use std::borrow::Cow; use base64::Engine; @@ -16,20 +18,41 @@ pub struct DuperParser; impl DuperParser { /// Parse a Duper trunk, i.e. only an array, tuple, or object at the top level. /// - /// A pretty-printed version of the error can be obtained from the `prettify_error` method. + /// A pretty-printed version of the error can be obtained from the + /// [`DuperParser::prettify_error`] method. + /// + /// ## Example + /// + /// ``` + /// use duper::DuperParser; + /// + /// DuperParser::parse_duper_trunk(r#" + /// ("key", {}) + /// "#).unwrap(); + /// ``` pub fn parse_duper_trunk<'a>(input: &'a str) -> Result, Vec>> { let value = duper_trunk().parse(input).into_result()?; - match &value { - &DuperValue::Object { .. } | &DuperValue::Array { .. } | &DuperValue::Tuple { .. } => { - Ok(value) - } - _ => unreachable!(), - } + debug_assert!(matches!( + &value, + &DuperValue::Object { .. } | &DuperValue::Array { .. } | &DuperValue::Tuple { .. } + )); + Ok(value) } /// Parse a Duper value at the top level. /// - /// A pretty-printed version of the error can be obtained from the `prettify_error` method. + /// A pretty-printed version of the error can be obtained from the + /// [`DuperParser::prettify_error`] method. + /// + /// ## Example + /// + /// ``` + /// use duper::DuperParser; + /// + /// DuperParser::parse_duper_value(r#" + /// "Hello, Duper!" + /// "#).unwrap(); + /// ``` pub fn parse_duper_value<'a>(input: &'a str) -> Result, Vec>> { duper_value().parse(input).into_result() } @@ -116,6 +139,7 @@ pub(crate) fn identifier_lossy<'a>() .map(|string| DuperIdentifier(Cow::Owned(string))) } +/// Parse an identifier. pub fn identifier<'a>() -> impl Parser<'a, &'a str, DuperIdentifier<'a>, extra::Err>> + Clone { one_of('A'..='Z') @@ -125,6 +149,7 @@ pub fn identifier<'a>() .map(|identifier| DuperIdentifier(Cow::Borrowed(identifier))) } +/// Parse a trunk value with an optional identifier. pub fn identified_trunk<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { let inner_trunk = choice(( @@ -145,6 +170,7 @@ pub fn identified_trunk<'a>() .padded_by(whitespace_and_comments()) } +/// Parse a Duper value with an optional identifier. pub fn identified_value<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { recursive(move |identified_value| { @@ -212,6 +238,7 @@ pub fn identified_value<'a>() }) } +/// Parse a Duper object. pub fn object<'a>( identified_value: impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone, ) -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { @@ -237,6 +264,7 @@ pub fn object<'a>( ) } +/// Parse a Duper object key. pub fn object_key<'a>() -> impl Parser<'a, &'a str, DuperKey<'a>, extra::Err>> + Clone { let plain_key = ascii_alphabetic() @@ -252,6 +280,7 @@ pub fn object_key<'a>() -> impl Parser<'a, &'a str, DuperKey<'a>, extra::Err( identified_value: impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone, ) -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { @@ -281,6 +310,7 @@ pub fn array<'a>( })) } +/// Parse a Duper tuple. pub fn tuple<'a>( identified_value: impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone, ) -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { @@ -310,6 +340,7 @@ pub fn tuple<'a>( })) } +/// Parse a quote-delimited Duper string. pub fn quoted_string<'a>() -> impl Parser<'a, &'a str, Cow<'a, str>, extra::Err>> + Clone { quoted_inner() @@ -317,6 +348,7 @@ pub fn quoted_string<'a>() .delimited_by(just('"'), just('"')) } +/// Parse a Base64 Duper byte string. pub fn base64_bytes<'a>() -> impl Parser<'a, &'a str, Vec, extra::Err>> + Clone { base64_digit() .padded() @@ -335,6 +367,7 @@ pub fn base64_bytes<'a>() -> impl Parser<'a, &'a str, Vec, extra::Err() -> impl Parser<'a, &'a str, Cow<'a, [u8]>, extra::Err>> + Clone { quoted_inner() @@ -342,6 +375,7 @@ pub fn quoted_bytes<'a>() .delimited_by(just("b\""), just('"')) } +/// Parse the inner contents of a Duper quoted string or quoted byte string. pub fn quoted_inner<'a>() -> impl Parser<'a, &'a str, &'a str, extra::Err>> + Clone { let escaped_characters = just('\\') .then(choice(( @@ -360,6 +394,7 @@ pub fn quoted_inner<'a>() -> impl Parser<'a, &'a str, &'a str, extra::Err() -> impl Parser<'a, &'a str, &'a str, extra::Err>> + Clone { let hashtags = just('#') .repeated() @@ -398,6 +433,7 @@ pub fn raw_string<'a>() -> impl Parser<'a, &'a str, &'a str, extra::Err() -> impl Parser<'a, &'a str, &'a [u8], extra::Err>> + Clone { let hashtags = just('#') .repeated() @@ -438,6 +474,7 @@ pub fn raw_bytes<'a>() -> impl Parser<'a, &'a str, &'a [u8], extra::Err() -> impl Parser<'a, &'a str, f64, extra::Err>> + Clone { let decimal = one_of("+-").or_not().then(integer_digits()).to_slice(); @@ -473,6 +510,7 @@ pub fn float<'a>() -> impl Parser<'a, &'a str, f64, extra::Err>> }) } +/// Parse a Duper integer. pub fn integer<'a>() -> impl Parser<'a, &'a str, i64, extra::Err>> + Clone { let decimal_integer = one_of("+-") .or_not() @@ -519,10 +557,12 @@ pub fn integer<'a>() -> impl Parser<'a, &'a str, i64, extra::Err> choice((hex_integer, octal_integer, binary_integer, decimal_integer)) } +/// Parse a Duper boolean. pub fn boolean<'a>() -> impl Parser<'a, &'a str, bool, extra::Err>> + Clone { choice((just("true").to(true), just("false").to(false))) } +/// Parse a Duper null value. pub fn null<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { just("null").to(()) } diff --git a/duper/src/parser/temporal.rs b/duper/src/parser/temporal.rs index d0a775c..4c4b50c 100644 --- a/duper/src/parser/temporal.rs +++ b/duper/src/parser/temporal.rs @@ -1,3 +1,5 @@ +//! Temporal-specific parsing and AST-building utilities for Duper. + use std::borrow::Cow; use chumsky::prelude::*; @@ -14,6 +16,7 @@ use crate::{ // Duper Temporal values +/// Parse a known Temporal value. pub fn temporal_specified<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { choice(( @@ -29,6 +32,7 @@ pub fn temporal_specified<'a>() .padded_by(whitespace_and_comments()) } +/// Parse a Temporal Instant, including the identifier and single quotes. pub fn temporal_instant<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("Instant") @@ -51,6 +55,7 @@ pub fn temporal_instant<'a>() }) } +/// Parse a Temporal ZonedDateTime, including the identifier and single quotes. pub fn temporal_zoned_date_time<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("ZonedDateTime") @@ -73,6 +78,7 @@ pub fn temporal_zoned_date_time<'a>() }) } +/// Parse a Temporal PlainDate, including the identifier and single quotes. pub fn temporal_plain_date<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("PlainDate") @@ -95,6 +101,7 @@ pub fn temporal_plain_date<'a>() }) } +/// Parse a Temporal PlainTime, including the identifier and single quotes. pub fn temporal_plain_time<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("PlainTime") @@ -117,6 +124,7 @@ pub fn temporal_plain_time<'a>() }) } +/// Parse a Temporal PlainDateTime, including the identifier and single quotes. pub fn temporal_plain_date_time<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("PlainDateTime") @@ -139,6 +147,7 @@ pub fn temporal_plain_date_time<'a>() }) } +/// Parse a Temporal PlainYearMonth, including the identifier and single quotes. pub fn temporal_plain_year_month<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("PlainYearMonth") @@ -161,6 +170,7 @@ pub fn temporal_plain_year_month<'a>() }) } +/// Parse a Temporal PlainMonthDay, including the identifier and single quotes. pub fn temporal_plain_month_day<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("PlainMonthDay") @@ -183,6 +193,7 @@ pub fn temporal_plain_month_day<'a>() }) } +/// Parse a Temporal Duration, including the identifier and single quotes. pub fn temporal_duration<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { just("Duration") @@ -205,6 +216,7 @@ pub fn temporal_duration<'a>() }) } +/// Parse an unspecified Temporal value, delimited by single quotes. pub fn temporal_unspecified<'a>() -> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { unspecified() @@ -221,6 +233,7 @@ pub fn temporal_unspecified<'a>() // Inner values +/// Parse a ZonedDateTime. pub fn zoned_date_time<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { date_time() .then(time_offset()) @@ -230,6 +243,7 @@ pub fn zoned_date_time<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { date_time() @@ -240,6 +254,7 @@ pub fn non_z_zoned_date_time<'a>() .ignored() } +/// Parse an Instant. pub fn instant<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( zoned_date_time(), @@ -247,6 +262,7 @@ pub fn instant<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> )) } +/// Parse an instant with a non-Z offset. pub fn non_z_instant<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( non_z_zoned_date_time(), @@ -254,6 +270,7 @@ pub fn non_z_instant<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( plain_date_time(), @@ -261,6 +278,7 @@ pub fn plain_date<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( plain_date_time(), @@ -268,6 +286,7 @@ pub fn plain_time<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( non_z_instant(), @@ -275,6 +294,7 @@ pub fn plain_date_time<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( plain_date(), @@ -285,6 +305,7 @@ pub fn plain_year_month<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( plain_date(), @@ -292,6 +313,7 @@ pub fn plain_month_day<'a>() -> impl Parser<'a, &'a str, (), extra::Err() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { let fractional = text::int(10).then(just('.').then(text::digits(10).at_most(9)).or_not()); @@ -355,6 +377,7 @@ pub fn duration<'a>() -> impl Parser<'a, &'a str, (), extra::Err> .ignored() } +/// Parse an unspecified Temporal value. pub fn unspecified<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> + Clone { choice(( instant(), diff --git a/duper/src/serde/de.rs b/duper/src/serde/de.rs index 814bce4..1cdb546 100644 --- a/duper/src/serde/de.rs +++ b/duper/src/serde/de.rs @@ -1,3 +1,5 @@ +//! Serde [`serde_core::Deserializer`] implementation for Duper. + use std::borrow::Cow; use crate::{DuperKey, DuperParser, DuperValue, serde::temporal::TemporalString}; diff --git a/duper/src/serde/error.rs b/duper/src/serde/error.rs index 578503f..1bfcc6f 100644 --- a/duper/src/serde/error.rs +++ b/duper/src/serde/error.rs @@ -1,3 +1,5 @@ +//! Module for the [`DuperSerdeError`] value. + use std::fmt::{self, Display}; use crate::{ diff --git a/duper/src/serde/meta.rs b/duper/src/serde/meta.rs index 2ec1243..d630f90 100644 --- a/duper/src/serde/meta.rs +++ b/duper/src/serde/meta.rs @@ -1,3 +1,7 @@ +//! A module containing additional methods for [`DuperValue`], which allow serialization +//! and deserialization into values containing all metadata required to reconstruct +//! a Duper value losslessly. + use std::{borrow::Cow, fmt::Display}; use indexmap::IndexMap; diff --git a/duper/src/serde/mod.rs b/duper/src/serde/mod.rs index 95e17b0..2e2d913 100644 --- a/duper/src/serde/mod.rs +++ b/duper/src/serde/mod.rs @@ -1,3 +1,12 @@ +//! Serde support for Duper. This requires the `serde` feature flag to be enabled. +//! +//! Included in this module are: +//! - (De)serialize implementations to work with Duper values directly; +//! - [`ser`] / [`de`]: (De)serializer implementations for native types; +//! - [`meta`]: Meta-(de)serialization, for wider support; +//! - [`temporal`]: Custom Serde support for Temporal values; +//! - [`error`]: A Duper-specific Serde error. + pub mod de; pub mod error; pub mod meta; diff --git a/duper/src/serde/ser.rs b/duper/src/serde/ser.rs index ad9a2d8..6934e7a 100644 --- a/duper/src/serde/ser.rs +++ b/duper/src/serde/ser.rs @@ -1,3 +1,5 @@ +//! Serde [`serde_core::Serializer`] implementation for Duper. + use std::{borrow::Cow, marker::PhantomData}; use crate::{ diff --git a/duper/src/serde/temporal.rs b/duper/src/serde/temporal.rs index 2cfee5b..74e52f6 100644 --- a/duper/src/serde/temporal.rs +++ b/duper/src/serde/temporal.rs @@ -1,3 +1,5 @@ +//! A utility for better serialization/deserialization support of Temporal values. + use std::{borrow::Cow, marker::PhantomData}; use serde_core::{de::IntoDeserializer, ser::SerializeStruct}; @@ -75,7 +77,7 @@ impl TemporalString<'_> { TemporalString::PlainYearMonth(_) => "PlainYearMonth", TemporalString::PlainMonthDay(_) => "PlainMonthDay", TemporalString::Duration(_) => "Duration", - TemporalString::Unspecified(_) => "Unspecified", + TemporalString::Unspecified(_) => "Temporal", } } } @@ -164,7 +166,7 @@ impl<'a> serde_core::Serialize for TemporalString<'a> { TemporalString::PlainYearMonth(inner) => ("PlainYearMonth", inner.as_ref()), TemporalString::PlainMonthDay(inner) => ("PlainMonthDay", inner.as_ref()), TemporalString::Duration(inner) => ("Duration", inner.as_ref()), - TemporalString::Unspecified(inner) => ("Unspecified", inner.as_ref()), + TemporalString::Unspecified(inner) => ("Temporal", inner.as_ref()), }; let mut s = serializer.serialize_struct(STRUCT, 2)?; s.serialize_field(FIELD_TYPE, typ)?; @@ -245,13 +247,13 @@ impl<'a, 'de> serde_core::Deserialize<'de> for TemporalString<'a> { DuperTemporalDuration::try_from(Cow::Owned(value)) .map_err(serde_core::de::Error::custom)?, )), - "Unspecified" => Ok(TemporalString::Unspecified( + "Temporal" => Ok(TemporalString::Unspecified( DuperTemporalUnspecified::try_from(Cow::Owned(value)) .map_err(serde_core::de::Error::custom)?, )), typ => Err(serde_core::de::Error::invalid_value( serde_core::de::Unexpected::Str(typ), - &"one of: Instant, ZonedDateTime, PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay, Duration, Unspecified", + &"one of: Instant, ZonedDateTime, PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay, Duration, Temporal", )), } } diff --git a/duper/src/validate.rs b/duper/src/validate.rs index 71da912..6f2e467 100644 --- a/duper/src/validate.rs +++ b/duper/src/validate.rs @@ -1,55 +1,68 @@ +//! Functions to help with parse-based validation of Duper-encoded values. + use chumsky::Parser; use crate::parser; +/// Check if a string parses into a valid integer. pub fn is_valid_integer(input: &str) -> bool { !parser::integer().check(input).has_errors() } +/// Check if a string parses into a valid float. pub fn is_valid_float(input: &str) -> bool { !parser::float().check(input).has_errors() } +/// Check if a string parses into a valid Temporal Instant. pub fn is_valid_instant(input: &str) -> bool { !parser::temporal::instant().check(input).has_errors() } +/// Check if a string parses into a valid Temporal ZonedDateTime. pub fn is_valid_zoned_date_time(input: &str) -> bool { !parser::temporal::zoned_date_time() .check(input) .has_errors() } +/// Check if a string parses into a valid Temporal PlainDate. pub fn is_valid_plain_date(input: &str) -> bool { !parser::temporal::plain_date().check(input).has_errors() } +/// Check if a string parses into a valid Temporal PlainTime. pub fn is_valid_plain_time(input: &str) -> bool { !parser::temporal::plain_time().check(input).has_errors() } +/// Check if a string parses into a valid Temporal PlainDateTime. pub fn is_valid_plain_date_time(input: &str) -> bool { !parser::temporal::plain_date_time() .check(input) .has_errors() } +/// Check if a string parses into a valid Temporal PlainYearMonth. pub fn is_valid_plain_year_month(input: &str) -> bool { !parser::temporal::plain_year_month() .check(input) .has_errors() } +/// Check if a string parses into a valid Temporal PlainMonthDay. pub fn is_valid_plain_month_day(input: &str) -> bool { !parser::temporal::plain_month_day() .check(input) .has_errors() } +/// Check if a string parses into a valid Temporal Duration. pub fn is_valid_duration(input: &str) -> bool { !parser::temporal::duration().check(input).has_errors() } +/// Check if a string parses into a valid unspecified Temporal value. pub fn is_valid_unspecified_temporal(input: &str) -> bool { !parser::temporal::unspecified().check(input).has_errors() } diff --git a/duper_lsp/CHANGELOG.md b/duper_lsp/CHANGELOG.md index d3096c1..2429a4f 100644 --- a/duper_lsp/CHANGELOG.md +++ b/duper_lsp/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Validation of `Decimal` strings. + ### Changed - Bump `duper` to 0.5.0. diff --git a/duper_lsp/Cargo.toml b/duper_lsp/Cargo.toml index d20883f..7c5675e 100644 --- a/duper_lsp/Cargo.toml +++ b/duper_lsp/Cargo.toml @@ -19,9 +19,11 @@ clap = { version = "4.5.53", features = ["derive"] } duper = { version = "0.5", path = "../duper" } duperfmt = { version = "0.1", path = "../duperfmt" } futures = "0.3.31" +ipnet = "2.11.0" line-index = "0.1.2" lsp-types = "0.95.0" regex = "1.12.2" +rust_decimal = "1.39.0" tokio = { version = "1.48.0", features = ["full"] } tokio-util = { version = "0.7.17", features = ["compat"] } tower = "0.5.2" diff --git a/duper_lsp/src/diagnostics.rs b/duper_lsp/src/diagnostics.rs index bfa2a09..7520ba4 100644 --- a/duper_lsp/src/diagnostics.rs +++ b/duper_lsp/src/diagnostics.rs @@ -1,6 +1,13 @@ -use std::{borrow::Cow, collections::HashSet, sync::LazyLock}; +use std::{ + borrow::Cow, + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, + sync::LazyLock, +}; use base64::Engine; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use line_index::{LineCol, LineIndex, WideEncoding}; use lsp_types::{Diagnostic, DiagnosticSeverity}; use regex::Regex; @@ -129,6 +136,27 @@ static QUERY_FLOATS: LazyLock = LazyLock::new(|| { static REGEX_UUID: LazyLock = LazyLock::new(|| Regex::new(r"(?i)^UUID(v?\d)?$").expect("valid UUID regex")); +static REGEX_DECIMAL: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^Decimal$").expect("valid Decimal regex")); + +static REGEX_IP4ADDR: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IPv?4(Addr(ess)?)?$").expect("valid IPv4 address regex")); + +static REGEX_IP6ADDR: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IPv?6(Addr(ess)?)?$").expect("valid IPv6 address regex")); + +static REGEX_IPADDR: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IP(Addr(ess)?)?$").expect("valid IP address regex")); + +static REGEX_IP4NET: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IPv?4Net(work)?$").expect("valid IPv4 network regex")); + +static REGEX_IP6NET: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IPv?6Net(work)?$").expect("valid IPv6 network regex")); + +static REGEX_IPNET: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)^IPNet(work)?$").expect("valid IP network regex")); + pub(crate) fn get_diagnostics(source: &str, tree: &Tree, is_utf8: bool) -> Vec { let mut diagnostics = vec![]; let index = LineIndex::new(source); @@ -878,20 +906,103 @@ pub(crate) fn get_diagnostics(source: &str, tree: &Tree, is_utf8: bool) -> Vec(&mut self, value: &'a DuperValue<'a>) -> String { + pub(crate) fn visit<'a>(&mut self, value: &'a DuperValue<'a>) -> String { self.buf.clear(); value.accept(self); std::mem::take(&mut self.buf) diff --git a/duperq/src/processor.rs b/duperq/src/processor.rs index 9a211be..317c365 100644 --- a/duperq/src/processor.rs +++ b/duperq/src/processor.rs @@ -5,6 +5,7 @@ use smol::{Unblock, channel, io::AsyncWriteExt}; use crate::filter::DuperFilter; #[async_trait(?Send)] +/// An opaque layer that processes a [`DuperValue`] asynchronously. pub trait Processor { async fn process(&mut self, value: DuperValue<'static>); diff --git a/serde_duper/CHANGELOG.md b/serde_duper/CHANGELOG.md index d41e6bf..41c6c08 100644 --- a/serde_duper/CHANGELOG.md +++ b/serde_duper/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- `temporal` feature to enable Temporal support for `Duration`. +- `temporal` feature to enable Temporal support for `Duration` and support `temporal_rs` types. ### Changed diff --git a/serde_duper/src/types/mod.rs b/serde_duper/src/types/mod.rs index b7d3447..ddbfef0 100644 --- a/serde_duper/src/types/mod.rs +++ b/serde_duper/src/types/mod.rs @@ -1,4 +1,7 @@ +//! Duper-customized (de)serialization support for native and third-party types. + #![allow(non_snake_case)] + use serde_core::{Deserialize, Deserializer, Serialize, Serializer, de}; /// A helper macro to generate boilerplate for most serializable/deserializable diff --git a/tracing_duper/examples/simple.rs b/tracing_duper/examples/simple.rs index 2759952..9da0cb4 100644 --- a/tracing_duper/examples/simple.rs +++ b/tracing_duper/examples/simple.rs @@ -3,7 +3,7 @@ use tracing_duper::DuperLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tracing::instrument] -pub fn send_gifts(count: &mut usize) { +fn send_gifts(count: &mut usize) { if *count < 12 { warn!("too few gifts... try again later"); } else { diff --git a/tracing_duper/src/lib.rs b/tracing_duper/src/lib.rs index af43a29..a3061cd 100644 --- a/tracing_duper/src/lib.rs +++ b/tracing_duper/src/lib.rs @@ -1,7 +1,7 @@ #![doc(html_logo_url = "https://duper.dev.br/logos/duper-100-100.png")] //! A composable [`tracing_subscriber`] layer to emit Duper events. //! -//! You can install it to a [`tracing_subscriber::registry`] just like any other +//! You can install it to a [`tracing_subscriber::registry()`] just like any other //! filter or formatter: //! //! ``` @@ -16,15 +16,15 @@ //! //! See [`DuperLayer`] for all the available configuration. //! -//! Now, you can emit [`tracing`] spans and events as usual. If you'd like to +//! Now, you can emit `tracing` spans and events as usual. If you'd like to //! emit Duper values, use `$duper.` as the prefix of the field, and set the //! value to a Duper-formatted string. //! -//! ```ignore +//! ``` //! use tracing::{debug, warn}; //! //! #[tracing::instrument] -//! pub fn send_gifts(count: &mut usize) { +//! fn send_gifts(count: &mut usize) { //! if *count < 12 { //! warn!("too few gifts... try again later"); //! } else { @@ -49,7 +49,7 @@ //! ``` //! //! To create Duper values programmatically, look into [`duper`] and [`duper::Serializer`], or -//! the [`serde_duper`] crate. +//! the `serde_duper` crate. //! //! ## Feature flags //! @@ -607,7 +607,7 @@ where struct DuperFields<'a>(DuperObject<'a>); -pub struct DuperVisitor<'a> { +struct DuperVisitor<'a> { values: BTreeMap, DuperValue<'a>>, } -- 2.51.2