granit-parser
Advanced tools
| { | ||
| "git": { | ||
| "sha1": "d260e5b35b46abcf116846e60aea7042c02da807" | ||
| "sha1": "25a68dd9632194904c086e18766c27a249768d87" | ||
| }, | ||
| "path_in_vcs": "" | ||
| } |
@@ -58,4 +58,4 @@ name: Release | ||
| run: | | ||
| cargo fetch --locked | ||
| cargo +"${MSRV}" fetch --locked | ||
| cargo fetch | ||
| cargo +"${MSRV}" fetch | ||
@@ -66,21 +66,21 @@ - name: Check formatting | ||
| - name: Run clippy | ||
| run: cargo clippy --locked --offline --all-targets --all-features -- -D warnings | ||
| run: cargo clippy --offline --all-targets --all-features -- -D warnings | ||
| - name: Run tests | ||
| run: cargo test --locked --offline | ||
| run: cargo test --offline | ||
| - name: Run tests with debug_prints | ||
| run: cargo test --locked --offline --features debug_prints | ||
| run: cargo test --offline --features debug_prints | ||
| - name: Run no-default-features library tests | ||
| run: cargo test --locked --offline --lib --no-default-features | ||
| run: cargo test --offline --lib --no-default-features | ||
| - name: Check no_std target | ||
| run: cargo check --locked --offline --lib --no-default-features --target wasm32v1-none | ||
| run: cargo check --offline --lib --no-default-features --target wasm32v1-none | ||
| - name: Check MSRV | ||
| run: cargo +"${MSRV}" check --locked --offline --all-targets | ||
| run: cargo +"${MSRV}" check --offline --all-targets | ||
| - name: Verify crate packaging | ||
| run: cargo publish --locked --dry-run | ||
| run: cargo publish --dry-run | ||
@@ -117,2 +117,2 @@ publish: | ||
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | ||
| run: cargo publish --locked | ||
| run: cargo publish |
+1
-1
@@ -115,3 +115,3 @@ # This file is automatically @generated by Cargo. | ||
| name = "granit-parser" | ||
| version = "0.0.1" | ||
| version = "0.0.2" | ||
| dependencies = [ | ||
@@ -118,0 +118,0 @@ "arraydeque", |
+2
-2
@@ -16,3 +16,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| name = "granit-parser" | ||
| version = "0.0.1" | ||
| version = "0.0.2" | ||
| authors = [ | ||
@@ -34,3 +34,3 @@ "Ethiraric <ethiraric@gmail.com>", | ||
| autobenches = false | ||
| description = "A YAML 1.2 parser in pure Rust with strict compliance" | ||
| description = "A YAML parser in pure Rust with strict compliance" | ||
| documentation = "https://docs.rs/granit-parser/latest/granit_parser" | ||
@@ -37,0 +37,0 @@ readme = "README.md" |
+9
-0
| # Changelog | ||
| ## v0.0.2 | ||
| **Features**: | ||
| - Added `TryEventReceiver`, `TrySpannedEventReceiver`, `TryLoadError`, | ||
| `Parser::try_load`, and `ParserTrait::try_load` for receiver-style parsing | ||
| that can fail fast with an application error. The existing `load` API remains | ||
| unchanged for infallible receivers. | ||
| ## v0.0.1 | ||
@@ -4,0 +13,0 @@ |
+12
-3
@@ -8,2 +8,3 @@ # granit-parser | ||
| [](https://codecov.io/gh/bourumir-wyngs/granit-parser) | ||
| [](https://badge.socket.dev/cargo/package/granit-parser/0.0.1) | ||
@@ -17,7 +18,7 @@ [](https://crates.io/crates/granit-parser) | ||
| > | ||
| > — Ethiraric | ||
| > — [Ethiraric](https://crates.io/users/Ethiraric) | ||
| **granit-parser** is both YAML 1.1 and 1.2 compliant parser in pure Rust with strict compliance, no-std support, and spans for parser events. | ||
| This crate started as a fork of [`saphyr-parser`](https://crates.io/crates/saphyr-parser) that descends from [`yaml-rust`](https://github.com/chyh1990/yaml-rust), with influences from `libyaml` and `yaml-cpp`. The project has since diverged significantly and is now maintained as an independent project. | ||
| This crate started as a fork of [saphyr-parser](https://crates.io/crates/saphyr-parser) that descends from [yaml-rust](https://github.com/chyh1990/yaml-rust), with influences from [libyaml](https://crates.io/crates/libyaml) and [yaml-cpp](https://github.com/jbeder/yaml-cpp). The project has since diverged significantly and is now maintained as an independent project. | ||
@@ -28,3 +29,3 @@ Its primary goals are: | ||
| * compatibility with real-world YAML usage | ||
| * quickly incorporate the changes we need for the upstream dependency serde-saphyr | ||
| * quickly incorporate the changes we need for the upstream dependency [serde-saphyr](https://crates.io/crates/serde-saphyr). | ||
@@ -106,3 +107,11 @@ `granit-parser`’s public API is very similar to that of `saphyr-parser`, so it is typically an easy replacement. However, some changes are still breaking (crate rename, MSRV bump, lifetimes on events, Cow payloads, etc.). | ||
| ## Event API choices | ||
| Use the iterator API when the caller should pull events and decide when to stop | ||
| parsing. Use `Parser::load` for infallible receiver-style event sinks. If a | ||
| receiver may return a validation or application error and parsing should stop | ||
| immediately, use `Parser::try_load` with `TryEventReceiver` or | ||
| `TrySpannedEventReceiver`; it returns `TryLoadError::Scan` for parser errors and | ||
| `TryLoadError::Receiver` for receiver errors. | ||
| ## Key differences from saphyr-parser | ||
@@ -109,0 +118,0 @@ |
+56
-0
@@ -581,1 +581,57 @@ //! Utilities to create a source of input to the parser. | ||
| } | ||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::Input; | ||
| struct MinimalInput; | ||
| impl Input for MinimalInput { | ||
| fn lookahead(&mut self, _count: usize) {} | ||
| fn buflen(&self) -> usize { | ||
| 0 | ||
| } | ||
| fn bufmaxlen(&self) -> usize { | ||
| 0 | ||
| } | ||
| fn raw_read_ch(&mut self) -> char { | ||
| '\0' | ||
| } | ||
| fn raw_read_non_breakz_ch(&mut self) -> Option<char> { | ||
| None | ||
| } | ||
| fn skip(&mut self) {} | ||
| fn skip_n(&mut self, _count: usize) {} | ||
| fn peek(&self) -> char { | ||
| '\0' | ||
| } | ||
| fn peek_nth(&self, _n: usize) -> char { | ||
| '\0' | ||
| } | ||
| } | ||
| #[test] | ||
| fn default_slice_bytes_returns_none() { | ||
| let mut input = MinimalInput; | ||
| input.lookahead(4); | ||
| assert_eq!(input.buflen(), 0); | ||
| assert_eq!(input.bufmaxlen(), 0); | ||
| assert_eq!(input.raw_read_ch(), '\0'); | ||
| assert_eq!(input.raw_read_non_breakz_ch(), None); | ||
| input.skip(); | ||
| input.skip_n(2); | ||
| assert_eq!(input.peek(), '\0'); | ||
| assert_eq!(input.peek_nth(1), '\0'); | ||
| assert_eq!(input.byte_offset(), None); | ||
| assert_eq!(input.slice_bytes(0, 0), None); | ||
| } | ||
| } |
@@ -133,2 +133,42 @@ use crate::char_traits::is_breakz; | ||
| } | ||
| #[test] | ||
| fn raw_reads_bypass_buffer_and_report_eof() { | ||
| let mut input = BufferedInput::new("a".chars()); | ||
| assert_eq!(input.raw_read_ch(), 'a'); | ||
| assert_eq!(input.raw_read_ch(), '\0'); | ||
| } | ||
| #[test] | ||
| fn raw_read_non_breakz_pushes_break_back_into_buffer() { | ||
| let mut input = BufferedInput::new("a\n".chars()); | ||
| assert_eq!(input.raw_read_non_breakz_ch(), Some('a')); | ||
| assert_eq!(input.raw_read_non_breakz_ch(), None); | ||
| assert_eq!(input.buflen(), 1); | ||
| assert_eq!(input.peek(), '\n'); | ||
| let mut empty = BufferedInput::new("".chars()); | ||
| assert_eq!(empty.raw_read_non_breakz_ch(), None); | ||
| } | ||
| #[test] | ||
| fn skip_n_drains_buffered_characters() { | ||
| let mut input = BufferedInput::new("abcdef".chars()); | ||
| input.lookahead(5); | ||
| input.skip_n(2); | ||
| assert_eq!(input.buflen(), 3); | ||
| assert_eq!(input.peek(), 'c'); | ||
| assert_eq!(input.peek_nth(2), 'e'); | ||
| } | ||
| #[test] | ||
| fn streaming_input_never_borrows_slices() { | ||
| let input = BufferedInput::new("abc".chars()); | ||
| assert_eq!(BorrowedInput::slice_borrowed(&input, 0, 1), None); | ||
| } | ||
| } |
+122
-1
@@ -546,4 +546,6 @@ use crate::{ | ||
| mod test { | ||
| use crate::input::Input; | ||
| use alloc::string::String; | ||
| use crate::input::{BorrowedInput, Input, SkipTabs}; | ||
| use super::StrInput; | ||
@@ -582,2 +584,121 @@ | ||
| } | ||
| #[test] | ||
| fn raw_reads_track_byte_offsets_and_eof() { | ||
| let mut input = StrInput::new("aé"); | ||
| assert_eq!(input.raw_read_ch(), 'a'); | ||
| assert_eq!(input.byte_offset(), Some(1)); | ||
| assert_eq!(input.raw_read_ch(), 'é'); | ||
| assert_eq!(input.byte_offset(), Some(3)); | ||
| assert_eq!(input.raw_read_ch(), '\0'); | ||
| assert_eq!(input.byte_offset(), Some(3)); | ||
| } | ||
| #[test] | ||
| fn raw_read_non_breakz_stops_before_breakz() { | ||
| let mut input = StrInput::new("a\n"); | ||
| assert_eq!(input.raw_read_non_breakz_ch(), Some('a')); | ||
| assert_eq!(input.raw_read_non_breakz_ch(), None); | ||
| assert_eq!(input.peek(), '\n'); | ||
| let mut empty = StrInput::new(""); | ||
| assert_eq!(empty.raw_read_non_breakz_ch(), None); | ||
| } | ||
| #[test] | ||
| fn skip_handles_ascii_unicode_and_eof() { | ||
| let mut input = StrInput::new("éab"); | ||
| input.skip(); | ||
| assert_eq!(input.peek(), 'a'); | ||
| input.skip_n(8); | ||
| assert_eq!(input.peek(), '\0'); | ||
| input.skip(); | ||
| assert_eq!(input.peek(), '\0'); | ||
| } | ||
| #[test] | ||
| fn peeking_past_end_returns_nul() { | ||
| let ascii = StrInput::new("ab"); | ||
| assert_eq!(ascii.peek_nth(1), 'b'); | ||
| assert_eq!(ascii.peek_nth(3), '\0'); | ||
| let unicode = StrInput::new("éab"); | ||
| assert!(unicode.next_3_are('é', 'a', 'b')); | ||
| assert!(!unicode.next_3_are('é', 'a', 'c')); | ||
| } | ||
| #[test] | ||
| fn skip_ws_to_eol_without_tabs_stops_before_tab() { | ||
| let mut input = StrInput::new(" \t# comment\n"); | ||
| let (consumed, result) = input.skip_ws_to_eol(SkipTabs::No); | ||
| assert_eq!(consumed, 2); | ||
| let result = result.unwrap(); | ||
| assert!(!result.found_tabs()); | ||
| assert!(result.has_valid_yaml_ws()); | ||
| assert_eq!(input.peek(), '\t'); | ||
| } | ||
| #[test] | ||
| fn skip_ws_to_eol_skips_comments_after_whitespace() { | ||
| let mut input = StrInput::new(" # comment\nnext"); | ||
| let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes); | ||
| assert_eq!(consumed, 11); | ||
| let result = result.unwrap(); | ||
| assert!(!result.found_tabs()); | ||
| assert!(result.has_valid_yaml_ws()); | ||
| assert_eq!(input.peek(), '\n'); | ||
| } | ||
| #[test] | ||
| fn fetch_while_is_alpha_is_ascii_only() { | ||
| let mut input = StrInput::new("abc_123-é"); | ||
| let mut out = String::new(); | ||
| assert_eq!(input.fetch_while_is_alpha(&mut out), 8); | ||
| assert_eq!(out, "abc_123-"); | ||
| assert_eq!(input.peek(), 'é'); | ||
| } | ||
| #[test] | ||
| fn fetch_plain_scalar_chunk_handles_non_ascii_after_colon() { | ||
| let mut input = StrInput::new("a:é "); | ||
| let mut out = String::new(); | ||
| assert_eq!( | ||
| input.fetch_plain_scalar_chunk(&mut out, 16, false), | ||
| (true, 3) | ||
| ); | ||
| assert_eq!(out, "a:é"); | ||
| assert_eq!(input.peek(), ' '); | ||
| } | ||
| #[test] | ||
| fn fetch_plain_scalar_chunk_stops_at_flow_indicator() { | ||
| let mut input = StrInput::new("abc,def"); | ||
| let mut out = String::new(); | ||
| assert_eq!( | ||
| input.fetch_plain_scalar_chunk(&mut out, 16, true), | ||
| (true, 3) | ||
| ); | ||
| assert_eq!(out, "abc"); | ||
| assert_eq!(input.peek(), ','); | ||
| } | ||
| #[test] | ||
| fn borrowed_slices_use_original_input_lifetime() { | ||
| let input = StrInput::new("aéz"); | ||
| assert_eq!(BorrowedInput::slice_borrowed(&input, 1, 3), Some("é")); | ||
| assert_eq!(input.slice_bytes(3, 4), Some("z")); | ||
| } | ||
| } |
+4
-1
@@ -67,3 +67,6 @@ // Copyright 2015, Yuheng Chen. | ||
| pub use crate::input::{str::StrInput, BorrowedInput, BufferedInput, Input}; | ||
| pub use crate::parser::{Event, EventReceiver, Parser, ParserTrait, SpannedEventReceiver, Tag}; | ||
| pub use crate::parser::{ | ||
| Event, EventReceiver, Parser, ParserTrait, SpannedEventReceiver, Tag, TryEventReceiver, | ||
| TryLoadError, TrySpannedEventReceiver, | ||
| }; | ||
| pub use crate::scanner::{Marker, ScalarStyle, ScanError, Scanner, Span, Token, TokenType}; |
+19
-0
@@ -67,2 +67,21 @@ #![allow(clippy::bool_assert_comparison)] | ||
| #[test] | ||
| fn span_helpers_report_length_empty_and_byte_range() { | ||
| let span = granit_parser::Span::new( | ||
| Marker::new(2, 1, 2).with_byte_offset(Some(5)), | ||
| Marker::new(6, 1, 6).with_byte_offset(Some(13)), | ||
| ); | ||
| assert_eq!(span.len(), 4); | ||
| assert!(!span.is_empty()); | ||
| assert_eq!(span.byte_range(), Some(5..13)); | ||
| let empty = granit_parser::Span::empty(Marker::new(6, 1, 6).with_byte_offset(Some(13))); | ||
| assert!(empty.is_empty()); | ||
| assert_eq!(empty.byte_range(), Some(13..13)); | ||
| let without_byte_offsets = granit_parser::Span::new(Marker::new(0, 1, 0), Marker::new(1, 1, 1)); | ||
| assert_eq!(without_byte_offsets.byte_range(), None); | ||
| } | ||
| #[test] | ||
| fn test_plain() { | ||
@@ -69,0 +88,0 @@ assert_eq!( |
@@ -12,3 +12,3 @@ extern crate alloc; | ||
| BorrowedInput, Event, Marker, Parser, ParserTrait, ScalarStyle, Span, SpannedEventReceiver, | ||
| StrInput, | ||
| StrInput, TryEventReceiver, TryLoadError, | ||
| }; | ||
@@ -303,2 +303,20 @@ | ||
| struct TryTestReceiver<'input> { | ||
| events: Vec<Event<'input>>, | ||
| } | ||
| impl<'input> TryEventReceiver<'input> for TryTestReceiver<'input> { | ||
| type Error = &'static str; | ||
| fn on_event(&mut self, ev: Event<'input>) -> Result<(), Self::Error> { | ||
| let should_fail = matches!(&ev, Event::Scalar(value, ..) if value.as_ref() == "stop"); | ||
| self.events.push(ev); | ||
| if should_fail { | ||
| Err("stop requested") | ||
| } else { | ||
| Ok(()) | ||
| } | ||
| } | ||
| } | ||
| #[test] | ||
@@ -346,2 +364,25 @@ fn test_parser_stack_load() { | ||
| #[test] | ||
| fn test_parser_stack_try_load_stops_on_receiver_error() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
| stack.push_str_parser( | ||
| Parser::new_from_str("a: stop\nafter: value\n"), | ||
| "p1".to_string(), | ||
| ); | ||
| let mut recv = TryTestReceiver { events: Vec::new() }; | ||
| let err = stack.try_load(&mut recv, true).unwrap_err(); | ||
| assert_eq!(err, TryLoadError::Receiver("stop requested")); | ||
| assert!(recv | ||
| .events | ||
| .iter() | ||
| .any(|event| matches!(event, Event::Scalar(value, ..) if value == "stop"))); | ||
| assert!(!recv | ||
| .events | ||
| .iter() | ||
| .any(|event| matches!(event, Event::Scalar(value, ..) if value == "after"))); | ||
| } | ||
| #[test] | ||
| fn test_parser_stack_load_single() { | ||
@@ -348,0 +389,0 @@ let mut stack: MyStack = ParserStack::new(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display