Skip to content

Commit 0abafd6

Browse files
authored
Doc updates and version bump (#45)
* Small doc cleanups
1 parent 2dd2687 commit 0abafd6

4 files changed

Lines changed: 95 additions & 59 deletions

File tree

CONFIGURATION.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Choose what happens when floats are encountered:
2222
- **Default**: Return `FloatDisabled` with raw string preserved for manual parsing
2323
- **`float-error`**: Fail parsing when floats are encountered (embedded fail-fast)
2424
- **`float-truncate`**: Truncate simple decimals to integers (1.7 → 1, errors on scientific notation)
25-
- **`float-skip`**: Skip float values during parsing (continue with next token) [TODO]
25+
- **`float-skip`**: Skip float values during parsing (continue with next token)
2626

2727
## Configuration Examples
2828

@@ -63,37 +63,58 @@ picojson = { path = "../picojson", features = ["int64"] }
6363

6464
## API Usage
6565

66-
All configurations preserve the exact raw string while providing different parsed representations:
66+
All configurations preserve the exact raw string of a number, while providing different parsed representations through the `JsonNumber` enum. The `parsed()` method on `JsonNumber` returns a `NumberResult` which can be matched to handle all possible outcomes.
6767

6868
```rust
69+
use picojson::{Event, JsonNumber, NumberResult};
70+
71+
// In your parsing loop:
6972
match event {
7073
Event::Number(num) => {
71-
// Raw string always available (exact precision)
72-
println!("Raw: {}", num.as_str());
73-
74-
// Parsed value depends on configuration
75-
match num.parsed {
76-
NumberResult::Integer(i) => println!("Integer: {}", i),
77-
NumberResult::IntegerOverflow => println!("Overflow: {}", num.as_str()),
78-
79-
#[cfg(feature = "float")]
80-
NumberResult::Float(f) => println!("Float: {}", f),
81-
82-
#[cfg(all(not(feature = "float"), feature = "float-truncate"))]
83-
NumberResult::FloatTruncated(i) => println!("Truncated: {}", i),
84-
85-
#[cfg(not(feature = "float"))]
74+
// The raw string is always available for full precision.
75+
println!("Raw number: {}", num.as_str());
76+
77+
// Match on the result of `num.parsed()` to handle different outcomes.
78+
match num.parsed() {
79+
NumberResult::Integer(i) => {
80+
// This variant is used for integers that fit within the configured size (i32/i64).
81+
println!("Parsed as integer: {}", i);
82+
}
83+
NumberResult::Float(f) => {
84+
// This variant is only available if the "float" feature is enabled.
85+
println!("Parsed as float: {}", f);
86+
}
87+
NumberResult::IntegerOverflow => {
88+
// Used when an integer exceeds the configured size (e.g., > i32::MAX on an i32 build).
89+
println!("Integer overflow! Raw value: {}", num.as_str());
90+
}
8691
NumberResult::FloatDisabled => {
87-
// Manual parsing still available
88-
let manual: f64 = num.parse().unwrap();
92+
// Used when the "float" feature is disabled and no other float-handling
93+
// feature (like truncate or error) is active.
94+
println!("Float parsing is disabled. Raw value: {}", num.as_str());
95+
}
96+
NumberResult::FloatTruncated(i) => {
97+
// Used with the "float-truncate" feature for simple decimals.
98+
println!("Float was truncated to integer: {}", i);
99+
}
100+
NumberResult::FloatSkipped => {
101+
// This variant is used with the "float-skip" feature.
102+
println!("Float value was skipped.");
89103
}
90104
}
91105

92-
// Convenience methods adapt to configuration
106+
// Convenience methods are also available.
93107
if let Some(int_val) = num.as_int() {
94-
println!("As configured int: {}", int_val);
108+
// This will only return Some if the number was successfully parsed as an integer
109+
// within the configured size.
110+
println!("Successfully read as integer: {}", int_val);
111+
}
112+
if let Some(float_val) = num.as_f64() {
113+
// This will only return Some if the "float" feature is enabled.
114+
println!("Successfully read as float: {}", float_val);
95115
}
96116
}
117+
_ => {}
97118
}
98119
```
99120

DESIGN.md

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
This document outlines the design for the `picojson` crate, a high-level, allocation-free JSON pull-parser.
99

10-
The primary philosophy is to build upon the lean, compact, and low-level `ujson` tokenizer to provide an ergonomic and highly efficient API for consumers.
10+
The primary philosophy is to build upon the lean, compact, and low-level `ujson` tokenizer to provide an ergonomic API for consumers.
1111

1212
The core design goals are:
1313
- **Zero Heap Allocations**: The parser must not perform any heap allocations during its operation. All memory will be provided by the caller.
@@ -21,26 +21,31 @@ Standard `Iterator` trait cannot be implemented because of borrowed return value
2121

2222
## 3. Memory Management: External Scratch Buffer
2323

24-
To achieve the zero-allocation goal while still handling complex cases like string un-escaping, the parser will not manage its own memory. Instead, the caller must provide a temporary "scratch" buffer during instantiation.
24+
To achieve the zero-allocation goal while still handling complex cases like string un-escaping, the parser will not manage its own memory. Instead, the caller can provide a temporary "scratch" buffer during instantiation for operations that require it.
2525

2626
This design was chosen over an internal, fixed-size buffer to avoid complex lifetime issues with the borrow checker and to give the user full control over the memory's size and location (stack, static arena, etc.).
2727

28-
The parser's constructor will have the following signature:
28+
The parser offers constructors that support both zero-copy parsing (when no escapes are present) and parsing with a scratch buffer for handling escaped strings:
2929

3030
```rust
31+
// Conceptual representation of constructors
3132
impl<'a, 'b> SliceParser<'a, 'b> {
32-
/// Creates a new parser for the given JSON input.
33-
///
34-
/// - `input`: A string slice containing the JSON data to be parsed.
35-
/// - `scratch_buffer`: A mutable byte slice for temporary operations,
36-
/// like string un-escaping.
37-
pub fn new(input: &'a str, scratch_buffer: &'b mut [u8]) -> Self {
33+
/// Creates a new parser for inputs with no string escapes.
34+
/// This is a zero-copy, zero-allocation operation.
35+
pub fn new(input: &'a str) -> Self {
36+
// ...
37+
}
38+
39+
/// Creates a new parser with a scratch buffer for handling escapes.
40+
/// - `input`: A string slice containing the JSON data.
41+
/// - `scratch_buffer`: A mutable byte slice for temporary operations.
42+
pub fn with_buffer(input: &'a str, scratch_buffer: &'b mut [u8]) -> Self {
3843
// ...
3944
}
4045
}
4146
```
4247

43-
The `'a` lifetime is tied to the input data, while `'b` is tied to the scratch buffer.
48+
The 'a lifetime is tied to the input data, while 'b is tied to the scratch buffer.
4449

4550
## 4. Handling String Values: The `String` Enum
4651

@@ -77,21 +82,30 @@ The algorithm is as follows:
7782

7883
This ensures that work is only done when absolutely necessary.
7984

80-
## 6. Final Data Structures
85+
## 6. Core Data Structures
8186

8287
Here is a summary of the core public-facing data structures.
8388

8489
```rust
85-
// The main parser struct
90+
// The main parser for slices
8691
pub struct SliceParser<'a, 'b> { /* ... private fields ... */ }
8792

93+
// The main parser for streams
94+
pub struct StreamParser<'b, R: Reader> { /* ... private fields ... */ }
95+
8896
// The custom "Cow-like" string type
8997
#[derive(Debug, PartialEq, Eq)]
9098
pub enum String<'a, 'b> {
9199
Borrowed(&'a str),
92100
Unescaped(&'b str),
93101
}
94102

103+
// The custom number type for flexible parsing
104+
pub enum JsonNumber<'a, 'b> {
105+
Borrowed { raw: &'a str, parsed: NumberResult },
106+
Copied { raw: &'b str, parsed: NumberResult },
107+
}
108+
95109
// The events yielded by the iterator
96110
#[derive(Debug, PartialEq)]
97111
pub enum Event<'a, 'b> {
@@ -101,47 +115,47 @@ pub enum Event<'a, 'b> {
101115
EndArray,
102116
Key(String<'a, 'b>),
103117
String(String<'a, 'b>),
104-
Number(f64), // Assuming f64 for now
118+
Number(JsonNumber<'a, 'b>),
105119
Bool(bool),
106120
Null,
107121
}
108122

109123
// The comprehensive error type
110124
#[derive(Debug, PartialEq)]
111125
pub enum ParseError {
112-
/// An error bubbled up from the underlying tokenizer.
113-
Tokenizer(ujson::Error),
114-
/// The provided scratch buffer was not large enough for an operation.
126+
/// An error from the underlying tokenizer (e.g., invalid syntax).
127+
TokenizerError,
128+
/// The provided scratch buffer was not large enough.
115129
ScratchBufferFull,
116130
/// A string slice was not valid UTF-8.
117-
InvalidUtf8(core::str::Utf8Error),
118-
/// A number string could not be parsed.
119-
InvalidNumber(core::num::ParseFloatError),
120-
/// The parser entered an unexpected internal state.
121-
UnexpectedState(&'static str),
131+
InvalidUtf8,
132+
/// A number string could not be parsed as the configured type.
133+
InvalidNumber,
134+
// ... other specific error conditions
122135
}
123136
```
124137

125138
## 6. Dealing with non-slice input
126139

127-
IMPORTANT!!!
140+
To support parsing from sources other than in-memory slices (like files, network sockets, or serial ports), the library provides a `StreamParser`. This parser is built upon a custom `Reader` trait, which is analogous to `std::io::Read` but is available in `no_std` environments.
128141

129-
More: In addition of taking just slice [u8] as input, we should accept an `impl Reader` of some sort.
130-
So that the input can come no-copy from any source with low buffering
142+
```rust
143+
pub trait Reader {
144+
type Error;
145+
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
146+
}
147+
```
131148

132-
Note std::io has Read trait, but unfortunately that's not available in core::, so probably have to
133-
make our own, and auto-implement it for arrays and slices or for anything that looks like AsRef<[u8]>
149+
The `StreamParser` takes an implementation of this `Reader` and an internal buffer, processing the input as data becomes available. This allows for efficient parsing of large JSON documents with a small, fixed-size memory footprint.
134150

135-
## 7. TODO: Working with returned values
151+
For convenience, the crate provides `picojson::ChunkReader`, a panic-free `Reader` implementation for parsing from byte slices, which is mostly useful for testing and examples.
136152

137-
String values in picojson now have Deref, AsRef and Format support, so using them in default examples
138-
with things like println! is convenient and easy.
153+
## 7. Ergonomics and Returned Values
139154

140-
Same should be done with Number, but it's a little more tricky to design, given the configuration
141-
variability
155+
String and number values returned by the parser are designed for ease of use.
142156

143-
## 8. TODO: Add direct defmt support for user API
157+
- `picojson::String` implements `Deref<Target=str>`, so it can be used just like a regular `&str`.
158+
- `picojson::JsonNumber` provides `as_int()` and `as_f64()` methods for easy conversion, along with `Deref<Target=str>` to access the raw number string. This design supports both zero-copy access to the raw number and convenient conversion to standard numeric types.
144159

145-
For any user of the pull parser with defmt:: enabled, all the formatting should do sensible
146-
default things. Most tricky is number formatting. The objective is to have clean, ergonomic, readable
147-
examples
160+
161+
```

TODO.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## TODO list
2-
- [ ] Constify what's possible
2+
- [?] Constify what's possible ( most is probably done )
33
- [x] Remove `.unwrap()` calls
44
- [x] Remove copy_from_slice - can panic
55
- [x] Remove all bounds checked slice indexing that could panic!
@@ -13,7 +13,8 @@
1313
- [ ] Provide user guide docs
1414
- [ ] Direct defmt support
1515
- [x] Stack size benchmarks
16-
- [x] Code size benchmarks
16+
- [x] Code size benchmarks
1717
- [ ] Sax-style push parser
1818
- [x] See if we can bring an Iterator impl back
1919
- Can't practically be done with borrowing return values
20+
- [ ] Conformance tests at parser level, not just tokenizer

picojson/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "picojson"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
edition = "2021"
55
license = "Apache-2.0"
66
exclude = ["/.github/**"]

0 commit comments

Comments
 (0)