Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
da9168c
Add new error relating to ranges
Ztry8 Apr 21, 2026
362900a
Add basic deserialization for ranges
Ztry8 Apr 25, 2026
df87102
Add basic serialization for ranges
Ztry8 Apr 25, 2026
1892f20
Add support for inclusive ranges serialization
Ztry8 Apr 25, 2026
930f462
Make difference for inclusives in deserialization
Ztry8 Apr 25, 2026
da5b656
Add support for `core::range`
Ztry8 Apr 25, 2026
fc89a9a
Fix serialization for `core::range` support
Ztry8 Apr 25, 2026
b1e9404
Allow byte literals
Ztry8 Apr 26, 2026
d278210
Add test for integer bases, binary, octal, hex, and byte literals
Ztry8 Apr 26, 2026
7f41a78
Add unclosed ranges & new test for them
Ztry8 Apr 26, 2026
348d834
Add pretty config for ranges & new test for them
Ztry8 Apr 26, 2026
873c42f
Use pattern matching for field in `deserialize_struct`
Ztry8 Apr 26, 2026
481377c
`RangeMapAccess`: change magic numbers to enum
Ztry8 Apr 26, 2026
91de9b5
Move `NumberDeserializer` into `value` module
Ztry8 Apr 26, 2026
66c891f
Add additional name check in `deserialize_struct`
Ztry8 Apr 26, 2026
d423eef
Add ranges to `grammar.md` & `README.md`
Ztry8 Apr 26, 2026
51a1ad0
Fix punctuation
Ztry8 Apr 28, 2026
b2b6d41
Fix `docs/grammar.md`: restrict range bounds to numbers
Ztry8 Apr 28, 2026
1400780
Update `docs/grammar.md`: add number type
Ztry8 Apr 28, 2026
bb80be8
Add additional check for field name in ranges deserialization
Ztry8 Apr 28, 2026
8a62f7b
Remove visitor inspection (no longer needed)
Ztry8 Apr 28, 2026
3c04f28
Remove code duplication in deserialization
Ztry8 Apr 28, 2026
c5272a5
Add RangeFull & new tests for unclosed ranges
Ztry8 Apr 28, 2026
9beb51b
Guard against excess value reads in Range{From,To}MapAccess
Ztry8 May 2, 2026
4b2765e
Validate numeric bounds before emitting compact range syntax
Ztry8 May 2, 2026
30e425e
Split RangeTo and RangeToInclusive into separate deserialize branches
Ztry8 May 2, 2026
6e7a558
Fix formatting deserialization error
Ztry8 May 2, 2026
93fb3a1
Fix formatting deserialization error
Ztry8 May 2, 2026
a528961
Add check for structure name in range deserealization
Ztry8 May 2, 2026
c2be8f8
Refactor deserialization: add helper function
Ztry8 May 2, 2026
4788a8a
Add new tests for unclosed ranges of byte literals
Ztry8 May 2, 2026
9e94138
Handle inf/NaN as range bounds in deserialization
Ztry8 May 2, 2026
b7ea12d
Fix error formatting in range deserialization
Ztry8 May 2, 2026
270f27b
Improve untagged test
Ztry8 May 2, 2026
76c4422
Expand ranges description in `README.md`
Ztry8 May 4, 2026
fbb1068
Move helper function into `Parser` `impl` & remove old comment
Ztry8 May 8, 2026
bf844d0
Refactor deserialization: factor repeated code to common function
Ztry8 May 8, 2026
50175a6
Refactor deserialization: change condition
Ztry8 May 8, 2026
c6fada3
Refactor parsing & deserialization of NaN/inf float ranges
Ztry8 May 8, 2026
9b8f2b0
Replace bool flags with enum in serialization
Ztry8 May 8, 2026
b5c8e16
Replace `BTreeMap` with fields in ranges serialization
Ztry8 May 8, 2026
7e32f30
Use buffered fields in ranges serialization fallback
Ztry8 May 8, 2026
eee1757
Replace `String` with `Number` in range serialization
Ztry8 May 8, 2026
b7d06f1
Remove unused import
Ztry8 May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

RON is a simple readable data serialization format that looks similar to Rust syntax.
It's designed to support all of [Serde's data model](https://serde.rs/data-model.html), so
structs, enums, tuples, arrays, generic maps, and primitive values.
structs, enums, tuples, arrays, generic maps, ranges and primitive values.
Comment thread
Ztry8 marked this conversation as resolved.
Outdated

## Example

Expand Down Expand Up @@ -57,6 +57,7 @@ GameConfig( // optional struct name
* Lists: `["abc", "def"]`
* Structs: `( foo: 1.0, bar: ( baz: "I'm nested" ) )`
* Maps: `{ "arbitrary": "keys", "are": "allowed" }`
* Ranges: `3..5`, `-2..=7`, `..10`, `-15..`
Comment thread
Ztry8 marked this conversation as resolved.
Outdated

> **Note:** Serde's data model represents fixed-size Rust arrays as tuple (instead of as list)

Expand Down
22 changes: 20 additions & 2 deletions docs/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ For the extension names see the [`extensions.md`][exts] document.
## Value

```ebnf
value = integer | byte | float | string | byte_string | char | bool | option | list | map | tuple | struct | enum_variant;
value = integer | byte | float | string | byte_string | char | bool | option | list | map | tuple | struct | enum_variant | range;
```

## Numbers
Expand Down Expand Up @@ -139,6 +139,24 @@ option = "None" | option_some;
option_some = "Some", ws, "(", ws, value, ws, ")";
```

## Range

```ebnf
range = range_from_to_inclusive | range_from_to_exclusive | range_from | range_to_inclusive | range_to_exclusive | range_full;
range_from_to_inclusive = value, ws, "..=", ws, value;
range_from_to_exclusive = value, ws, "..", ws, value;
range_from = value, ws, "..";
range_to_inclusive = "..=", ws, value;
range_to_exclusive = "..", ws, value;
range_full = "..";
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
```

> Note: Alternatives are ordered by specificity — `..=` must be matched before `..`
to avoid ambiguity. A parser should attempt the longer token (`..=`) first.
The six range forms correspond directly to Rust's range types:
`x..y` → `Range`, `x..=y` → `RangeInclusive`, `x..` → `RangeFrom`,
`..y` → `RangeTo`, `..=y` → `RangeToInclusive`, `..` → `RangeFull`.

## List

```ebnf
Expand Down Expand Up @@ -188,4 +206,4 @@ ident_raw = "r", "#", ident_raw_rest, { ident_raw_rest };
ident_raw_rest = ident_std_rest | "." | "+" | "-";
```

> Note: [XID_Start](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Start%3A%5D&abb=on&g=&i=) and [XID_Continue](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Continue%3A%5D&abb=on&g=&i=) refer to Unicode character sets.
> Note: [XID_Start](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Start%3A%5D&abb=on&g=&i=) and [XID_Continue](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Continue%3A%5D&abb=on&g=&i=) refer to Unicode character sets.
257 changes: 254 additions & 3 deletions src/de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
extensions::Extensions,
options::Options,
parse::{NewtypeMode, ParsedByteStr, ParsedStr, Parser, StructType, TupleMode},
value::NumberDeserializer,
};

#[cfg(feature = "std")]
Expand Down Expand Up @@ -152,6 +153,14 @@ macro_rules! guard_recursion {
}};
}

struct VisitorExpecting<V>(V);

impl<'de, V: Visitor<'de>> core::fmt::Display for VisitorExpecting<&'_ V> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
self.0.expecting(fmt)
}
}

Comment thread
Ztry8 marked this conversation as resolved.
Outdated
impl<'de> Deserializer<'de> {
/// Check if the remaining bytes are whitespace only,
/// otherwise return an error.
Expand Down Expand Up @@ -349,9 +358,45 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
'(' => self.handle_any_struct(visitor, None),
'[' => self.deserialize_seq(visitor),
'{' => self.deserialize_map(visitor),
'0'..='9' | '+' | '-' | '.' => self.parser.any_number()?.visit(visitor),
'0'..='9' | '+' | '-' | '.' => {
let start = self.parser.any_number()?;

if self.parser.consume_str("..=") {
Comment thread
Ztry8 marked this conversation as resolved.
let end = self.parser.any_number()?;
return visitor.visit_map(RangeMapAccess::new(start, end, "end"));
} else if self.parser.consume_str("..") {
if self.parser.peek_char().map_or(true, |c| {
!matches!(c, '0'..='9' | '+' | '-' | 'b')
|| (c == 'b' && !self.parser.src().starts_with("b'"))
}) {
return visitor.visit_map(RangeFromMapAccess::new(start));
}
let end = self.parser.any_number()?;
return visitor.visit_map(RangeMapAccess::new(start, end, "end"));
}

start.visit(visitor)
}
'"' | 'r' => self.deserialize_string(visitor),
'b' if self.parser.src().starts_with("b'") => self.parser.any_number()?.visit(visitor),
'b' if self.parser.src().starts_with("b'") => {
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
let start = self.parser.any_number()?;

if self.parser.consume_str("..=") {
let end = self.parser.any_number()?;
return visitor.visit_map(RangeMapAccess::new(start, end, "end"));
} else if self.parser.consume_str("..") {
if self.parser.peek_char().map_or(true, |c| {
!matches!(c, '0'..='9' | '+' | '-' | 'b')
|| (c == 'b' && !self.parser.src().starts_with("b'"))
}) {
return visitor.visit_map(RangeFromMapAccess::new(start));
}
let end = self.parser.any_number()?;
return visitor.visit_map(RangeMapAccess::new(start, end, "end"));
}

start.visit(visitor)
}
'b' => self.deserialize_byte_buf(visitor),
'\'' => self.deserialize_char(visitor),
other => Err(Error::UnexpectedChar(other)),
Comment thread
Ztry8 marked this conversation as resolved.
Expand Down Expand Up @@ -715,12 +760,94 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
fn deserialize_struct<V>(
self,
name: &'static str,
_fields: &'static [&'static str],
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
let visitor_name = VisitorExpecting(&visitor).to_string();

if let ["start", end_field] = fields {
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
if let Some(c) = self.parser.peek_char() {
if matches!(c, '0'..='9' | '+' | '-' | '.' | 'b')
&& (c != 'b' || self.parser.src().starts_with("b'"))
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
{
let start = self.parser.any_number()?;

let inclusive = if self.parser.consume_str("..=") {
true
} else if self.parser.consume_str("..") {
false
} else {
return Err(Error::ExpectedRangeSyntax);
};

if inclusive && visitor_name == "struct Range" {
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
return Err(Error::Message(String::from(
"expected `..` for Range, found `..=`",
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
)));
}
if !inclusive && visitor_name == "struct RangeInclusive" {
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
return Err(Error::Message(String::from(
"expected `..=` for RangeInclusive, found `..`",
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
)));
}
Comment thread
Ztry8 marked this conversation as resolved.
Outdated

let end = self.parser.any_number()?;
return visitor.visit_map(RangeMapAccess::new(start, end, end_field));
}
}
}

if fields == ["start"] && (name == "RangeFrom" || visitor_name == "struct RangeFrom") {
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
if let Some(c) = self.parser.peek_char() {
if matches!(c, '0'..='9' | '+' | '-' | 'b')
&& (c != 'b' || self.parser.src().starts_with("b'"))
{
let start = self.parser.any_number()?;
if self.parser.consume_str("..=") {
return Err(Error::Message(String::from(
"expected `..` for RangeFrom, found `..=`",
)));
} else if !self.parser.consume_str("..") {
return Err(Error::ExpectedRangeSyntax);
}
return visitor.visit_map(RangeFromMapAccess::new(start));
}
}
}

if fields == ["end"]
Comment thread
Ztry8 marked this conversation as resolved.
Outdated
&& (name == "RangeTo"
|| name == "RangeToInclusive"
|| visitor_name == "struct RangeTo"
|| visitor_name == "struct RangeToInclusive")
{
if self.parser.check_str("..=") || self.parser.check_str("..") {
let inclusive = if self.parser.consume_str("..=") {
true
} else {
self.parser.consume_str("..");
false
};
if inclusive && (name == "RangeTo" || visitor_name == "struct RangeTo") {
return Err(Error::Message(String::from(
"expected `..` for RangeTo, found `..=`",
)));
}
if !inclusive
&& (name == "RangeToInclusive" || visitor_name == "struct RangeToInclusive")
{
return Err(Error::Message(String::from(
"expected `..=` for RangeToInclusive, found `..`",
)));
}
let end = self.parser.any_number()?;
return visitor.visit_map(RangeToMapAccess::new(end, fields[0]));
}
}

if !self.newtype_variant {
self.parser.consume_struct_name(name)?;
}
Expand Down Expand Up @@ -827,6 +954,130 @@ impl<'a, 'de> CommaSeparated<'a, 'de> {
}
}

enum RangeMapState {
StartKey,
StartValue,
EndKey,
EndValue,
Done,
}

struct RangeMapAccess {
start: crate::value::Number,
end: crate::value::Number,
state: RangeMapState,
end_key: &'static str,
}

impl RangeMapAccess {
fn new(start: crate::value::Number, end: crate::value::Number, end_key: &'static str) -> Self {
RangeMapAccess {
start,
end,
state: RangeMapState::StartKey,
end_key,
}
}
}

impl<'de> de::MapAccess<'de> for RangeMapAccess {
type Error = Error;

fn next_key_seed<K: de::DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
match self.state {
RangeMapState::StartKey => {
self.state = RangeMapState::StartValue;
seed.deserialize(de::value::StrDeserializer::<Error>::new("start"))
.map(Some)
}
RangeMapState::EndKey => {
self.state = RangeMapState::EndValue;
seed.deserialize(de::value::StrDeserializer::<Error>::new(self.end_key))
.map(Some)
}
_ => Ok(None),
}
}

fn next_value_seed<V: de::DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
match self.state {
RangeMapState::StartValue => {
self.state = RangeMapState::EndKey;
seed.deserialize(NumberDeserializer(self.start))
}
RangeMapState::EndValue => {
self.state = RangeMapState::Done;
seed.deserialize(NumberDeserializer(self.end))
}
_ => Err(Error::ExpectedDifferentLength {
expected: String::from("map of length 2"),
found: 3,
}),
}
}
}

struct RangeFromMapAccess {
start: crate::value::Number,
done: bool,
}

impl RangeFromMapAccess {
fn new(start: crate::value::Number) -> Self {
RangeFromMapAccess { start, done: false }
}
}

impl<'de> de::MapAccess<'de> for RangeFromMapAccess {
type Error = Error;

fn next_key_seed<K: de::DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
if self.done {
return Ok(None);
}
seed.deserialize(de::value::StrDeserializer::<Error>::new("start"))
.map(Some)
}

fn next_value_seed<V: de::DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
self.done = true;
Comment thread
Ztry8 marked this conversation as resolved.
seed.deserialize(NumberDeserializer(self.start))
}
}

struct RangeToMapAccess {
end: crate::value::Number,
end_key: &'static str,
done: bool,
}

impl RangeToMapAccess {
fn new(end: crate::value::Number, end_key: &'static str) -> Self {
RangeToMapAccess {
end,
end_key,
done: false,
}
}
}

impl<'de> de::MapAccess<'de> for RangeToMapAccess {
type Error = Error;

fn next_key_seed<K: de::DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
if self.done {
return Ok(None);
}
seed.deserialize(de::value::StrDeserializer::<Error>::new(self.end_key))
.map(Some)
}

fn next_value_seed<V: de::DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
self.done = true;
seed.deserialize(NumberDeserializer(self.end))
}
}

impl<'de, 'a> de::SeqAccess<'de> for CommaSeparated<'a, 'de> {
type Error = Error;

Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ pub enum Error {
ExpectedRawValue,
ExceededRecursionLimit,
ExpectedStructName(String),
ExpectedRangeSyntax,
}

impl fmt::Display for SpannedError {
Expand Down Expand Up @@ -286,6 +287,7 @@ impl fmt::Display for Error {
"Expected the explicit struct name {}, but none was found",
Identifier(name)
),
Error::ExpectedRangeSyntax => f.write_str("Expected `..` or `..=` for range syntax"),
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,9 @@ impl<'a> Parser<'a> {
}
}

let num_bytes = self.next_chars_while_len(is_float_char);
let raw_bytes = self.next_chars_while_len(is_float_char);
let src = &self.src()[..raw_bytes];
let num_bytes = src.find("..").unwrap_or(raw_bytes);

if num_bytes == 0 {
return Err(Error::ExpectedFloat);
Expand Down Expand Up @@ -1019,7 +1021,12 @@ impl<'a> Parser<'a> {
'+' | '-' => 1,
_ => 0,
};
let valid_float_len = self.next_chars_while_from_len(skip, is_float_char);
let raw_float_len = self.next_chars_while_from_len(skip, is_float_char);
// Trim at ".." to avoid treating range operators as float chars
let valid_float_len = self.src()[skip..]
.find("..")
.map(|i| i.min(raw_float_len))
.unwrap_or(raw_float_len);
let valid_int_len = self.next_chars_while_from_len(skip, is_int_char);
valid_float_len > valid_int_len
} else {
Expand Down
Loading
Loading