diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2cb20e3..11fc367 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,3 +21,60 @@ repos: hooks: - id: fmt name: Fmt + +- repo: local + hooks: + # Feature matrix testing to prevent breaking different configurations + - id: feature-matrix-check + name: Feature Matrix Check - Default + entry: cargo check --manifest-path=picojson/Cargo.toml + language: system + pass_filenames: false + + - id: feature-matrix-no-defaults + name: Feature Matrix Check - No Defaults + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features + language: system + pass_filenames: false + + - id: feature-matrix-int32-float + name: Feature Matrix Check - int32 + float + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int32,float" + language: system + pass_filenames: false + + - id: feature-matrix-int32-float-skip + name: Feature Matrix Check - int32 + float-skip + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int32,float-skip" + language: system + pass_filenames: false + + - id: feature-matrix-int32-float-error + name: Feature Matrix Check - int32 + float-error + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int32,float-error" + language: system + pass_filenames: false + + - id: feature-matrix-int32-float-truncate + name: Feature Matrix Check - int32 + float-truncate + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int32,float-truncate" + language: system + pass_filenames: false + + - id: feature-matrix-int64-float-skip + name: Feature Matrix Check - int64 + float-skip + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int64,float-skip" + language: system + pass_filenames: false + + - id: feature-matrix-int64-float-error + name: Feature Matrix Check - int64 + float-error + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int64,float-error" + language: system + pass_filenames: false + + - id: feature-matrix-int64-float-truncate + name: Feature Matrix Check - int64 + float-truncate + entry: cargo check --manifest-path=picojson/Cargo.toml --no-default-features --features "int64,float-truncate" + language: system + pass_filenames: false diff --git a/README.md b/README.md index b6221a8..cf7128c 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,4 @@ For production code please use [serde-json-core](https://crates.io/crates/serde- ## License -Apache 2.0; see [`LICENSE`](LICENSE) for details. \ No newline at end of file +Apache 2.0; see [`LICENSE`](LICENSE) for details. diff --git a/TODO.md b/TODO.md index 1c8bdd3..b7c05e4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,15 @@ ## TODO list -- API cleanup, rename things - Constify what's possible - Remove `.unwrap()` calls - Remove unnecessary `'static` lifetimes - Dependency cleanup - Clippy cleanup +- Unify code in PullParser and StreamParser - Address and remove `// TODO` comments -- Put all shippable features in one crate ( tokenizer, pull + push parsers ) - Clean up reference docs - Provide user guide docs - Direct defmt support - Stack size benchmarks - Code size benchmarks - Sax-style push parser +- See if we can bring an Iterator impl back diff --git a/picojson/examples/no_float_demo.rs b/picojson/examples/no_float_demo.rs index d809e26..ffb1f8c 100644 --- a/picojson/examples/no_float_demo.rs +++ b/picojson/examples/no_float_demo.rs @@ -111,6 +111,13 @@ fn parse_and_display(parser: &mut PullParser) { println!(" → Manual parse as f64: {}", f); } } + // Handle variants that shouldn't be reachable in current configuration + _ => { + println!( + " → Unexpected variant for current configuration: {:?}", + num.parsed() + ); + } } } Ok(Event::Key(String::Borrowed(key))) => { diff --git a/picojson/src/direct_buffer.rs b/picojson/src/direct_buffer.rs index d28575e..d0d0ad9 100644 --- a/picojson/src/direct_buffer.rs +++ b/picojson/src/direct_buffer.rs @@ -459,7 +459,7 @@ mod tests { } } -impl<'b> crate::number_parser::NumberExtractor for DirectBuffer<'b> { +impl crate::number_parser::NumberExtractor for DirectBuffer<'_> { fn get_number_slice( &self, start: usize, diff --git a/picojson/src/json_number.rs b/picojson/src/json_number.rs index 4cf5af1..232ba23 100644 --- a/picojson/src/json_number.rs +++ b/picojson/src/json_number.rs @@ -23,16 +23,12 @@ pub enum NumberResult { /// Integer too large for configured type (use raw string for exact representation) IntegerOverflow, /// Float value (only available with float feature) - #[cfg(feature = "float")] Float(f64), /// Float parsing disabled - behavior depends on configuration - #[cfg(not(feature = "float"))] FloatDisabled, /// Float encountered but skipped due to float-skip configuration - #[cfg(all(not(feature = "float"), feature = "float-skip"))] FloatSkipped, /// Float truncated to integer due to float-truncate configuration - #[cfg(all(not(feature = "float"), feature = "float-truncate"))] FloatTruncated(ConfiguredInt), } @@ -50,7 +46,7 @@ pub enum JsonNumber<'a, 'b> { Copied { raw: &'b str, parsed: NumberResult }, } -impl<'a, 'b> JsonNumber<'a, 'b> { +impl JsonNumber<'_, '_> { /// Get the parsed NumberResult. pub fn parsed(&self) -> &NumberResult { match self { @@ -115,7 +111,7 @@ impl<'a, 'b> JsonNumber<'a, 'b> { } } -impl<'a, 'b> AsRef for JsonNumber<'a, 'b> { +impl AsRef for JsonNumber<'_, '_> { fn as_ref(&self) -> &str { self.as_str() } @@ -129,7 +125,7 @@ impl Deref for JsonNumber<'_, '_> { } } -impl<'a, 'b> core::fmt::Display for JsonNumber<'a, 'b> { +impl core::fmt::Display for JsonNumber<'_, '_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { // Display strategy: Show parsed value when available, fall back to raw string // This provides the most meaningful representation across all configurations @@ -177,10 +173,12 @@ pub(super) fn parse_float(s: &str) -> NumberResult { pub(super) fn parse_float(s: &str) -> Result { #[cfg(feature = "float-error")] { + let _ = s; // Acknowledge parameter usage Err(ParseError::FloatNotAllowed) } #[cfg(feature = "float-skip")] { + let _ = s; // Acknowledge parameter usage Ok(NumberResult::FloatSkipped) } #[cfg(feature = "float-truncate")] @@ -209,6 +207,7 @@ pub(super) fn parse_float(s: &str) -> Result { feature = "float-truncate" )))] { + let _ = s; // Acknowledge parameter usage Ok(NumberResult::FloatDisabled) } } @@ -282,12 +281,12 @@ mod tests { #[cfg(feature = "float")] fn test_json_number_float() { let number = JsonNumber::Borrowed { - raw: "3.14159", - parsed: NumberResult::Float(3.14159), + raw: "3.25", + parsed: NumberResult::Float(3.25), }; - assert_eq!(number.as_str(), "3.14159"); + assert_eq!(number.as_str(), "3.25"); assert_eq!(number.as_int(), None); - assert_eq!(number.as_f64(), Some(3.14159)); + assert_eq!(number.as_f64(), Some(3.25)); assert!(!number.is_integer()); assert!(number.is_float()); } diff --git a/picojson/src/json_string.rs b/picojson/src/json_string.rs index 0527e7c..0a73e06 100644 --- a/picojson/src/json_string.rs +++ b/picojson/src/json_string.rs @@ -13,7 +13,7 @@ pub enum String<'a, 'b> { Unescaped(&'b str), } -impl<'a, 'b> String<'a, 'b> { +impl String<'_, '_> { /// Returns the string as a `&str`, whether borrowed or unescaped. pub fn as_str(&self) -> &str { match self { @@ -23,7 +23,7 @@ impl<'a, 'b> String<'a, 'b> { } } -impl<'a, 'b> AsRef for String<'a, 'b> { +impl AsRef for String<'_, '_> { fn as_ref(&self) -> &str { self.as_str() } @@ -40,7 +40,7 @@ impl Deref for String<'_, '_> { } } -impl<'a, 'b> core::fmt::Display for String<'a, 'b> { +impl core::fmt::Display for String<'_, '_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.as_str()) } diff --git a/picojson/src/lib.rs b/picojson/src/lib.rs index 7a9db62..dd62615 100644 --- a/picojson/src/lib.rs +++ b/picojson/src/lib.rs @@ -53,13 +53,11 @@ // Compile-time configuration validation mod config_check; -mod tokenizer; -// Temporary internal alias, not exported -use tokenizer as ujson; -pub use tokenizer::ArrayBitStack; +mod ujson; +pub use ujson::ArrayBitStack; -pub use tokenizer::ArrayBitBucket; -pub use tokenizer::{BitBucket, BitStackConfig, BitStackStruct, DefaultConfig, DepthCounter}; +pub use ujson::ArrayBitBucket; +pub use ujson::{BitBucket, BitStackConfig, BitStackStruct, DefaultConfig, DepthCounter}; mod copy_on_escape; diff --git a/picojson/src/pull_parser.rs b/picojson/src/pull_parser.rs index c73f20a..04718e1 100644 --- a/picojson/src/pull_parser.rs +++ b/picojson/src/pull_parser.rs @@ -84,8 +84,8 @@ impl<'a, 'b> PullParser<'a, 'b, DefaultConfig> { /// # Arguments /// * `input` - A string slice containing the JSON data to be parsed. /// * `scratch_buffer` - A mutable byte slice for temporary string unescaping operations. - /// This buffer needs to be at least as long as the longest - /// contiguous token (string, key, number) in the input. + /// This buffer needs to be at least as long as the longest + /// contiguous token (string, key, number) in the input. /// /// # Example /// ``` @@ -138,8 +138,8 @@ impl<'a, 'b, C: BitStackConfig> PullParser<'a, 'b, C> { /// # Arguments /// * `input` - A string slice containing the JSON data to be parsed. /// * `scratch_buffer` - A mutable byte slice for temporary string unescaping operations. - /// This buffer needs to be at least as long as the longest - /// contiguous token (string, key, number) in the input. + /// This buffer needs to be at least as long as the longest + /// contiguous token (string, key, number) in the input. pub fn with_config_and_buffer(input: &'a str, scratch_buffer: &'b mut [u8]) -> Self { Self::with_config_and_buffer_from_slice(input.as_bytes(), scratch_buffer) } @@ -439,7 +439,7 @@ impl<'a, 'b, C: BitStackConfig> PullParser<'a, 'b, C> { } else { // No event available - this shouldn't happen since we ensured have_events() above break Err(ParseError::UnexpectedState( - "No events available after ensuring events exist".into(), + "No events available after ensuring events exist", )); } } @@ -449,7 +449,7 @@ impl<'a, 'b, C: BitStackConfig> PullParser<'a, 'b, C> { #[cfg(test)] mod tests { use super::*; - use crate::String; + use crate::{ArrayBitStack, BitStackStruct, String}; use test_log::test; #[test] @@ -815,4 +815,70 @@ mod tests { assert_eq!(parser.next_event(), Ok(Event::EndDocument)); assert_eq!(parser.next_event(), Ok(Event::EndDocument)); } + + #[test] + fn test_with_config_constructors() { + // Test with_config constructor (no escapes) + let json = r#"{"simple": "no_escapes"}"#; + let mut parser = PullParser::>::with_config(json); + + assert_eq!(parser.next_event(), Ok(Event::StartObject)); + assert_eq!( + parser.next_event(), + Ok(Event::Key(String::Borrowed("simple"))) + ); + assert_eq!( + parser.next_event(), + Ok(Event::String(String::Borrowed("no_escapes"))) + ); + assert_eq!(parser.next_event(), Ok(Event::EndObject)); + assert_eq!(parser.next_event(), Ok(Event::EndDocument)); + } + + #[test] + fn test_with_config_and_buffer_constructors() { + // Test with_config_and_buffer constructor (with escapes) + let json = r#"{"escaped": "hello\nworld"}"#; + let mut scratch = [0u8; 64]; + let mut parser = + PullParser::>::with_config_and_buffer(json, &mut scratch); + + assert_eq!(parser.next_event(), Ok(Event::StartObject)); + assert_eq!( + parser.next_event(), + Ok(Event::Key(String::Borrowed("escaped"))) + ); + + if let Ok(Event::String(s)) = parser.next_event() { + assert_eq!(s.as_ref(), "hello\nworld"); // Escape should be processed + } else { + panic!("Expected String event"); + } + + assert_eq!(parser.next_event(), Ok(Event::EndObject)); + assert_eq!(parser.next_event(), Ok(Event::EndDocument)); + } + + #[test] + fn test_alternative_config_deep_nesting() { + // Test that custom BitStack configs can handle deeper nesting + let json = r#"{"a":{"b":{"c":{"d":{"e":"deep"}}}}}"#; + let mut scratch = [0u8; 64]; + let mut parser = + PullParser::>::with_config_and_buffer(json, &mut scratch); + + // Parse the deep structure + let mut depth = 0; + while let Ok(event) = parser.next_event() { + match event { + Event::StartObject => depth += 1, + Event::EndObject => depth -= 1, + Event::EndDocument => break, + _ => {} + } + } + + // Should have successfully parsed a 5-level deep structure + assert_eq!(depth, 0); // All objects should be closed + } } diff --git a/picojson/src/shared.rs b/picojson/src/shared.rs index abffdd1..a39de8a 100644 --- a/picojson/src/shared.rs +++ b/picojson/src/shared.rs @@ -50,7 +50,6 @@ pub enum ParseError { /// Invalid escape sequence character. InvalidEscapeSequence, /// Float encountered but float support is disabled and float-error is configured - #[cfg(all(not(feature = "float"), feature = "float-error"))] FloatNotAllowed, /// A JSON token was too large to fit in the available buffer space TokenTooLarge { @@ -244,3 +243,103 @@ impl ParserErrorHandler { ParseError::UnexpectedState("Incomplete Unicode escape sequence") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unicode_escape_bounds() { + // Test unicode_escape_bounds with typical position after \u1234 + let current_pos = 10; // Position after reading \u1234 + let (hex_start, hex_end, escape_start) = ContentRange::unicode_escape_bounds(current_pos); + + assert_eq!(hex_start, 6); // Start of XXXX (10 - 4) + assert_eq!(hex_end, 10); // End of XXXX + assert_eq!(escape_start, 4); // Start of \uXXXX (10 - 6) + } + + #[test] + fn test_unicode_escape_bounds_edge_cases() { + // Test with position that would underflow + let (hex_start, hex_end, escape_start) = ContentRange::unicode_escape_bounds(3); + assert_eq!(hex_start, 0); // saturating_sub prevents underflow + assert_eq!(hex_end, 3); + assert_eq!(escape_start, 0); // saturating_sub prevents underflow + } + + #[test] + fn test_string_content_bounds_before_escape() { + // Test string content bounds calculation + let quote_start = 5; // Position of opening quote + let current_pos = 15; // Position at backslash + + let (content_start, content_end) = + ContentRange::string_content_bounds_before_escape(quote_start, current_pos); + + assert_eq!(content_start, 6); // After opening quote (5 + 1) + assert_eq!(content_end, 14); // Before backslash (15 - 1) + } + + #[test] + fn test_string_content_bounds_edge_cases() { + // Test with positions that could underflow + let (content_start, content_end) = ContentRange::string_content_bounds_before_escape(0, 0); + assert_eq!(content_start, 1); + assert_eq!(content_end, 0); // This will result in empty range, which is handled elsewhere + } + + #[test] + fn test_error_constructors() { + // Test state_mismatch error constructor + let error = ParserErrorHandler::state_mismatch("string", "end"); + match error { + ParseError::UnexpectedState(msg) => { + assert_eq!(msg, "String end without String start"); + } + _ => panic!("Expected UnexpectedState error"), + } + + // Test invalid_unicode_length error constructor + let error = ParserErrorHandler::invalid_unicode_length(); + match error { + ParseError::UnexpectedState(msg) => { + assert_eq!(msg, "Invalid Unicode escape length"); + } + _ => panic!("Expected UnexpectedState error"), + } + + // Test incomplete_unicode_escape error constructor + let error = ParserErrorHandler::incomplete_unicode_escape(); + match error { + ParseError::UnexpectedState(msg) => { + assert_eq!(msg, "Incomplete Unicode escape sequence"); + } + _ => panic!("Expected UnexpectedState error"), + } + } + + #[test] + fn test_utf8_error_conversion() { + // Test From trait implementation + use core::str; + // Create a proper invalid UTF-8 sequence (lone continuation byte) dynamically + // to avoid compile-time warning about static invalid UTF-8 literals + let mut invalid_utf8_array = [0u8; 1]; + invalid_utf8_array[0] = 0b10000000u8; // Invalid UTF-8 - continuation byte without start + let invalid_utf8 = &invalid_utf8_array; + + match str::from_utf8(invalid_utf8) { + Err(utf8_error) => { + let parse_error: ParseError = utf8_error.into(); + match parse_error { + ParseError::InvalidUtf8(_) => { + // Expected - conversion works correctly + } + _ => panic!("Expected InvalidUtf8 error"), + } + } + Ok(_) => panic!("Expected UTF-8 validation to fail"), + } + } +} diff --git a/picojson/src/slice_input_buffer.rs b/picojson/src/slice_input_buffer.rs index e1ffaab..d11f049 100644 --- a/picojson/src/slice_input_buffer.rs +++ b/picojson/src/slice_input_buffer.rs @@ -22,7 +22,7 @@ pub trait InputBuffer { fn consume_byte(&mut self) -> Result; } -impl<'a> InputBuffer for SliceInputBuffer<'a> { +impl InputBuffer for SliceInputBuffer<'_> { fn is_past_end(&self) -> bool { self.pos > self.data.len() } @@ -54,7 +54,7 @@ impl<'a> SliceInputBuffer<'a> { } } -impl<'a> crate::number_parser::NumberExtractor for SliceInputBuffer<'a> { +impl crate::number_parser::NumberExtractor for SliceInputBuffer<'_> { fn get_number_slice( &self, start: usize, diff --git a/picojson/src/stream_parser.rs b/picojson/src/stream_parser.rs index ff9fa2e..b68ae8f 100644 --- a/picojson/src/stream_parser.rs +++ b/picojson/src/stream_parser.rs @@ -118,7 +118,7 @@ impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { } /// Shared methods for StreamParser with any BitStackConfig -impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { +impl StreamParser<'_, R, C> { /// Iterator-compatible method that returns None when parsing is complete. /// This method returns None when EndDocument is reached, Some(Ok(event)) for successful events, /// and Some(Err(error)) for parsing errors. @@ -157,7 +157,7 @@ impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { } }; - if let Err(_) = self.tokenizer.finish(&mut callback) { + if self.tokenizer.finish(&mut callback).is_err() { return Err(ParseError::TokenizerError); } } @@ -183,7 +183,7 @@ impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { } }; - if let Err(_) = self.tokenizer.parse_chunk(&[byte], &mut callback) { + if self.tokenizer.parse_chunk(&[byte], &mut callback).is_err() { return Err(ParseError::TokenizerError); } @@ -461,7 +461,6 @@ impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { // Normal byte accumulation - all escape processing now goes through event system if !in_escape && self.direct_buffer.has_unescaped_content() { self.append_byte_to_escape_buffer(byte)?; - } else { } } @@ -499,7 +498,6 @@ impl<'b, R: Reader, C: BitStackConfig> StreamParser<'b, R, C> { content_end, )?; } - } else { } Ok(()) @@ -620,7 +618,7 @@ mod tests { } } - impl<'a> Reader for SliceReader<'a> { + impl Reader for SliceReader<'_> { type Error = (); fn read(&mut self, buf: &mut [u8]) -> Result { @@ -1247,7 +1245,7 @@ mod tests { #[cfg(feature = "float")] crate::NumberResult::Float(f) => { // This is expected in float-enabled build - assert!((f - 3.14).abs() < f64::EPSILON); + assert!((f - 3.14).abs() < 0.01); } #[cfg(feature = "float-skip")] crate::NumberResult::FloatSkipped => { @@ -1483,4 +1481,63 @@ mod tests { assert!(matches!(parser7.next_event().unwrap(), Event::EndArray)); } } + + #[test] + fn test_escape_buffer_functions() { + // Test the uncovered escape processing functions + let json_stream = br#"{"escaped": "test\nstring"}"#; + let mut buffer = [0u8; 1024]; + let mut parser = StreamParser::new(SliceReader::new(json_stream), &mut buffer); + + // These functions are private but we can test them through the public API + // The escape processing should trigger the uncovered functions + assert_eq!(parser.next_event().unwrap(), Event::StartObject); + assert_eq!( + parser.next_event().unwrap(), + Event::Key(crate::String::Borrowed("escaped")) + ); + + // This should trigger append_byte_to_escape_buffer and queue_unescaped_reset + if let Event::String(s) = parser.next_event().unwrap() { + assert_eq!(s.as_ref(), "test\nstring"); // Escape sequence should be processed + } else { + panic!("Expected String event with escape sequence"); + } + + assert_eq!(parser.next_event().unwrap(), Event::EndObject); + assert_eq!(parser.next_event().unwrap(), Event::EndDocument); + } + + #[test] + fn test_slice_reader_constructor() { + // Test the uncovered SliceReader::new function + let data = b"test data"; + let reader = SliceReader::new(data); + assert_eq!(reader.data, data); + assert_eq!(reader.position, 0); + } + + #[test] + fn test_complex_escape_sequences() { + // Test more complex escape processing to cover the escape buffer functions + let json_stream = br#"{"multi": "line1\nline2\ttab\r\n"}"#; + let mut buffer = [0u8; 1024]; + let mut parser = StreamParser::new(SliceReader::new(json_stream), &mut buffer); + + assert_eq!(parser.next_event().unwrap(), Event::StartObject); + assert_eq!( + parser.next_event().unwrap(), + Event::Key(crate::String::Borrowed("multi")) + ); + + // This should exercise the escape buffer processing extensively + if let Event::String(s) = parser.next_event().unwrap() { + assert_eq!(s.as_ref(), "line1\nline2\ttab\r\n"); + } else { + panic!("Expected String event"); + } + + assert_eq!(parser.next_event().unwrap(), Event::EndObject); + assert_eq!(parser.next_event().unwrap(), Event::EndDocument); + } } diff --git a/picojson/src/tokenizer/README.md b/picojson/src/ujson/README.md similarity index 100% rename from picojson/src/tokenizer/README.md rename to picojson/src/ujson/README.md diff --git a/picojson/src/tokenizer/bitstack/mod.rs b/picojson/src/ujson/bitstack/mod.rs similarity index 94% rename from picojson/src/tokenizer/bitstack/mod.rs rename to picojson/src/ujson/bitstack/mod.rs index e5866d7..9e8375e 100644 --- a/picojson/src/tokenizer/bitstack/mod.rs +++ b/picojson/src/ujson/bitstack/mod.rs @@ -135,8 +135,8 @@ where // Start from the rightmost (least significant) element and work left for i in (0..N).rev() { - let old_msb = (self.0[i].clone() >> msb_shift) & T::from(1); // Extract MSB that will be lost - self.0[i] = (self.0[i].clone() << 1u8) | carry; + let old_msb = (self.0[i] >> msb_shift) & T::from(1); // Extract MSB that will be lost + self.0[i] = (self.0[i] << 1u8) | carry; carry = old_msb; } // Note: carry from leftmost element is discarded (overflow) @@ -144,7 +144,7 @@ where fn pop(&mut self) -> bool { // Extract rightmost bit from least significant element - let bit = (self.0[N - 1].clone() & T::from(1)) != T::from(0); + let bit = (self.0[N - 1] & T::from(1)) != T::from(0); // Shift all elements right, carrying underflow from left to right let mut carry = T::from(0); @@ -153,8 +153,8 @@ where // Start from the leftmost (most significant) element and work right for i in 0..N { - let old_lsb = self.0[i].clone() & T::from(1); // Extract LSB that will be lost - self.0[i] = (self.0[i].clone() >> 1u8) | (carry << msb_shift); + let old_lsb = self.0[i] & T::from(1); // Extract LSB that will be lost + self.0[i] = (self.0[i] >> 1u8) | (carry << msb_shift); carry = old_lsb; } @@ -163,7 +163,7 @@ where fn top(&self) -> bool { // Return rightmost bit from least significant element without modifying - (self.0[N - 1].clone() & T::from(1)) != T::from(0) + (self.0[N - 1] & T::from(1)) != T::from(0) } } @@ -312,8 +312,8 @@ mod tests { // CURRENT BEHAVIOR: Empty stack returns false (was Some(false) before API change) // This behavior is now the intended design - no depth tracking needed - assert_eq!(bitstack.pop(), false, "Empty stack returns false"); - assert_eq!(bitstack.top(), false, "Empty stack top() returns false"); + assert!(!bitstack.pop(), "Empty stack returns false"); + assert!(!bitstack.top(), "Empty stack top() returns false"); // Test that underflow doesn't panic (at least it's safe) assert_eq!( diff --git a/picojson/src/tokenizer/mod.rs b/picojson/src/ujson/mod.rs similarity index 100% rename from picojson/src/tokenizer/mod.rs rename to picojson/src/ujson/mod.rs diff --git a/picojson/src/tokenizer/tokenizer/mod.rs b/picojson/src/ujson/tokenizer/mod.rs similarity index 99% rename from picojson/src/tokenizer/tokenizer/mod.rs rename to picojson/src/ujson/tokenizer/mod.rs index 8cc339d..b7f5292 100644 --- a/picojson/src/tokenizer/tokenizer/mod.rs +++ b/picojson/src/ujson/tokenizer/mod.rs @@ -1110,8 +1110,8 @@ mod tests { (consumed, &store[..index]) } - fn collect_with_result<'a, 'b, 'c>( - parser: &'c mut Tokenizer, + fn collect_with_result<'a, 'b>( + parser: &mut Tokenizer, data: &'b [u8], store: &'a mut [Event], ) -> Result<(usize, &'a [Event]), Error> { diff --git a/picojson/src/tokenizer/tokenizer/testdata/i_structure_500_nested_arrays.json b/picojson/src/ujson/tokenizer/testdata/i_structure_500_nested_arrays.json similarity index 100% rename from picojson/src/tokenizer/tokenizer/testdata/i_structure_500_nested_arrays.json rename to picojson/src/ujson/tokenizer/testdata/i_structure_500_nested_arrays.json diff --git a/picojson/tests/json_number_ergonomics_test.rs b/picojson/tests/json_number_ergonomics_test.rs new file mode 100644 index 0000000..3694604 --- /dev/null +++ b/picojson/tests/json_number_ergonomics_test.rs @@ -0,0 +1,254 @@ +// Integration test for JsonNumber ergonomic APIs +use picojson::{Event, PullParser}; + +#[test] +fn test_json_number_display_trait() { + let json = r#"{"int": 42, "big": 12345678901234567890, "float": 3.25}"#; + let mut scratch = [0u8; 128]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + // Skip to first number + assert_eq!(parser.next_event().unwrap(), Event::StartObject); + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + + // Test Display trait on integer + if let Event::Number(num) = parser.next_event().unwrap() { + let displayed = format!("{}", num); + assert_eq!(displayed, "42"); + } else { + panic!("Expected Number event"); + } + + // Skip to big number + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + + // Test Display trait on overflow + if let Event::Number(num) = parser.next_event().unwrap() { + let displayed = format!("{}", num); + assert_eq!(displayed, "12345678901234567890"); // Should show raw string for overflow + } else { + panic!("Expected Number event"); + } + + // Skip to float + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + + // Test Display trait on float (configuration dependent) + #[cfg(feature = "float-error")] + { + // float-error should return an error when encountering floats + let result = parser.next_event(); + assert!( + result.is_err(), + "Expected error for float with float-error configuration" + ); + } + #[cfg(not(feature = "float-error"))] + { + if let Event::Number(num) = parser.next_event().unwrap() { + let displayed = format!("{}", num); + #[cfg(feature = "float")] + assert_eq!(displayed, "3.25"); + #[cfg(all(not(feature = "float"), feature = "float-truncate"))] + assert_eq!(displayed, "3"); // Float truncated to integer + #[cfg(all(not(feature = "float"), not(feature = "float-truncate")))] + assert_eq!(displayed, "3.25"); // Raw string when float disabled + } else { + panic!("Expected Number event"); + } + } +} + +#[test] +fn test_json_number_deref_trait() { + let json = r#"[123, -456]"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartArray); + + // Test Deref trait (enables &*num syntax) + if let Event::Number(num) = parser.next_event().unwrap() { + let s: &str = &*num; // This uses Deref + assert_eq!(s, "123"); + + // Also test direct dereference + assert_eq!(*num, *"123"); + } else { + panic!("Expected Number event"); + } + + // Test negative number + if let Event::Number(num) = parser.next_event().unwrap() { + let s: &str = &*num; + assert_eq!(s, "-456"); + } else { + panic!("Expected Number event"); + } +} + +#[test] +fn test_json_number_as_ref_trait() { + let json = r#"{"zero": 0, "negative": -999}"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartObject); + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + + // Test AsRef trait + if let Event::Number(num) = parser.next_event().unwrap() { + let s: &str = num.as_ref(); // This uses AsRef + assert_eq!(s, "0"); + + // Function that takes AsRef + fn takes_str_ref>(value: T) -> String { + value.as_ref().to_uppercase() + } + assert_eq!(takes_str_ref(num), "0"); + } else { + panic!("Expected Number event"); + } + + // Test with negative number + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + if let Event::Number(num) = parser.next_event().unwrap() { + let s: &str = num.as_ref(); + assert_eq!(s, "-999"); + + // Test AsRef works with generic functions + fn string_length>(value: T) -> usize { + value.as_ref().len() + } + assert_eq!(string_length(num), 4); // "-999".len() + } else { + panic!("Expected Number event"); + } +} + +#[test] +fn test_json_number_parse_method() { + let json = r#"{"value": 42}"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartObject); + assert!(matches!(parser.next_event().unwrap(), Event::Key(_))); + + if let Event::Number(num) = parser.next_event().unwrap() { + // Test parse method with different types + let as_u32: u32 = num.parse().unwrap(); + assert_eq!(as_u32, 42); + + let as_i64: i64 = num.parse().unwrap(); + assert_eq!(as_i64, 42); + + let as_f64: f64 = num.parse().unwrap(); + assert_eq!(as_f64, 42.0); + + // Test that it's using the string representation + assert_eq!(num.as_str(), "42"); + } else { + panic!("Expected Number event"); + } +} + +#[test] +fn test_json_number_type_checking_methods() { + let json = r#"[42, 3.25]"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartArray); + + // Test integer type checking + if let Event::Number(num) = parser.next_event().unwrap() { + assert!(num.is_integer()); + assert!(!num.is_float()); + } else { + panic!("Expected Number event"); + } + + // Test float type checking (configuration dependent) + #[cfg(feature = "float-error")] + { + // float-error should return an error when encountering floats + let result = parser.next_event(); + assert!( + result.is_err(), + "Expected error for float with float-error configuration" + ); + } + #[cfg(not(feature = "float-error"))] + { + if let Event::Number(num) = parser.next_event().unwrap() { + assert!(!num.is_integer()); + assert!(num.is_float()); + } else { + panic!("Expected Number event"); + } + } +} + +#[test] +#[cfg(feature = "float")] +fn test_json_number_as_f64_method() { + let json = r#"[42, 3.25, 1e10]"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartArray); + + // Integer converted to f64 + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_f64(), Some(42.0)); + } else { + panic!("Expected Number event"); + } + + // Float value + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_f64(), Some(3.25)); + } else { + panic!("Expected Number event"); + } + + // Scientific notation + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_f64(), Some(1e10)); + } else { + panic!("Expected Number event"); + } +} + +#[test] +fn test_json_number_as_int_method() { + let json = r#"[42, -123, 12345678901234567890]"#; + let mut scratch = [0u8; 64]; + let mut parser = PullParser::with_buffer(json, &mut scratch); + + assert_eq!(parser.next_event().unwrap(), Event::StartArray); + + // Regular integer + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_int(), Some(42)); + } else { + panic!("Expected Number event"); + } + + // Negative integer + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_int(), Some(-123)); + } else { + panic!("Expected Number event"); + } + + // Overflow integer + if let Event::Number(num) = parser.next_event().unwrap() { + assert_eq!(num.as_int(), None); // Should be None due to overflow + // But string representation still available + assert_eq!(num.as_str(), "12345678901234567890"); + } else { + panic!("Expected Number event"); + } +}