granit-parser
Advanced tools
+161
| # API Review — 1.0.0 public API cleanup | ||
| Review of the uncommitted working-tree changes on top of `6567777` ("Preparing for 1.0.0 release"), | ||
| covering the scanner/parser/parser-stack API cleanup before the 1.0.0 release. | ||
| **Overall verdict:** the cleanup is coherent, well-documented, and mechanically sound. The public | ||
| surface shrinks to exactly what the CHANGELOG says, all removed items are either documented as | ||
| breaking or were never externally reachable, and the whole matrix builds and tests clean. One | ||
| finding should be addressed before tagging 1.0.0 (finding 1); the rest are design decisions to | ||
| confirm consciously now, because 1.0 is the last cheap opportunity. | ||
| ## Verification performed | ||
| - `cargo test --all-features` — all suites pass (including the 402-case YAML test suite and 4 doctests). | ||
| - `cargo check --no-default-features` and `--no-default-features --features error_messages` — no_std builds pass. | ||
| - `cargo clippy --all-targets --all-features` — zero warnings (crate is `clippy::pedantic`). | ||
| - `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features` — no broken intra-doc links after the removals. | ||
| - `tools/walk` (path dependency on the crate) builds against the new API. | ||
| - Grepped `src/`, `examples/`, `README.md`, `tools/` for stale uses of every removed item | ||
| (`get_error`, `next_token`, `fetch_*`, `TEncoding`, `Event::Nothing`, `Comment::span`, | ||
| `get_anchor_offset`, `new_str`, `Scanner`-`resolve`): none remain. | ||
| - Compared old vs. new `src/lib.rs` re-exports and cross-checked every CHANGELOG bullet against the | ||
| actual API diff (see "CHANGELOG accuracy" below). | ||
| - Empirically tested the new `FusedIterator` impls with a scratch crate (finding 1). | ||
| ## Findings | ||
| ### 1. `FusedIterator` for `ParserStack` violates the trait contract (fix before tagging) | ||
| `src/parser_stack.rs:605` adds `impl FusedIterator for ParserStack`, but `prepare_for_push()` | ||
| resets the exhaustion latch (`self.stream_end_emitted = false`, `src/parser_stack.rs:299`). Pushing | ||
| a parser after the iterator has returned `None` makes `next()` return `Some` again — reactivation | ||
| is a deliberate, tested feature (`parser_stack_push_after_peeked_empty_stream_end_reactivates_stack`). | ||
| `FusedIterator` guarantees "once `None`, always `None`", and `Iterator::fuse()` specializes on the | ||
| marker to become a transparent pass-through. Verified with a scratch crate against this working tree: | ||
| ```rust | ||
| let mut stack: ParserStack<'_, Empty<char>, StrInput<'_>> = ParserStack::new(); | ||
| stack.push_str_parser(Parser::new_from_str("a: 1"), "first".into()); | ||
| while stack.next().is_some() {} | ||
| assert!(stack.next().is_none()); | ||
| stack.push_str_parser(Parser::new_from_str("b: 2"), "second".into()); | ||
| assert!(stack.next().is_some()); // Some after None | ||
| // and through fuse(): | ||
| assert!((&mut stack).fuse().next().is_some()); // fuse() does not protect — pass-through | ||
| ``` | ||
| This is not unsafe (the trait is a safe marker), but downstream generic code and iterator adaptors | ||
| may rely on the guarantee. This is the same reason `std::sync::mpsc::TryIter` does *not* implement | ||
| `FusedIterator` — new items can appear after `None`. | ||
| Options, best first: | ||
| - **Remove `impl FusedIterator for ParserStack`** and document that pushing after exhaustion | ||
| resumes iteration. The other three impls (`Parser`, `Scanner`, `ReplayParser`) genuinely latch | ||
| and are correct — keep them. Adjust the CHANGELOG bullet accordingly. | ||
| - Alternatively, drop the `stream_end_emitted = false` reset in `prepare_for_push`. The existing | ||
| reactivation test still passes (it reactivates via a *peeked* `StreamEnd`, which never sets the | ||
| latch), but push-after-consumed-`None` would silently stay dead — an asymmetry that seems worse | ||
| than removing the marker. | ||
| Note the `ParserTrait::next_event` contract is unaffected either way; this is purely about the | ||
| `Iterator`/`FusedIterator` marker. | ||
| ### 2. `Event` and `TokenType` are exhaustive — confirm this is the intended 1.0 contract | ||
| `ErrorKind` is `#[non_exhaustive]` (`src/error.rs:140`) — good. `Event`, `TokenType`, and | ||
| `Placement` are exhaustively matchable, so adding any variant during 1.x is semver-major. The | ||
| crate's own history shows the risk: comment support added `Event::Comment`/`TokenType::Comment` | ||
| variants during 0.0.x. If any new event/token kind (or a new `Placement` such as "below") is | ||
| plausible in 1.x, mark those enums `#[non_exhaustive]` now; after 1.0 the change itself is breaking. | ||
| Counterpoint to weigh: forcing consumers (serde-saphyr) into exhaustive matches is a legitimate | ||
| design choice for a parser event model, and `ScalarStyle`/`StructureStyle` are closed by the YAML | ||
| spec. This is a decision to make consciously, not necessarily a defect — but it is one-way after 1.0. | ||
| ### 3. `Comment` (and `Token`) keep public fields while `Tag` went private | ||
| `Tag` fields became private with a complete accessor/constructor set, but `Comment` still exposes | ||
| `pub text` / `pub placement` (relied on by tests and presumably consumers), and `Token` is a tuple | ||
| struct with two `pub` fields. Public fields make the structs literal-constructible, so adding any | ||
| field in 1.x is breaking (same pressure that just forced removing `Comment.span` as a breaking | ||
| change). Either privatize `Comment` for symmetry (it already has `new` + `with_placement` + | ||
| `trimmed_text`; only field *reads* would need accessors), or accept the layout freeze deliberately. | ||
| `Token`'s `(Span, TokenType)` shape is stable enough that freezing it seems fine. | ||
| ### 4. Dual public paths for `ParserStack`/`ReplayParser` | ||
| Both `granit_parser::ParserStack` (new root re-export) and `granit_parser::parser_stack::ParserStack` | ||
| (pre-existing `pub mod`) are now public API, and both paths must be kept for all of 1.x. If the root | ||
| re-export is meant to be the canonical path (tests were migrated to it), consider making the module | ||
| private and re-exporting only from the root, matching how `parser` and `scanner` are already | ||
| private modules. Low stakes, but it halves the frozen surface. | ||
| ### 5. README code block uses rustdoc hidden-line syntax but is never doctested | ||
| The new scanner example in `README.md` ends with `# Ok::<(), granit_parser::ScanError>(())`. | ||
| The README is not included via `#![doc = include_str!(...)]` and there is no README doctest harness, | ||
| so (a) the snippet is not compile-checked, and (b) the `#` line renders literally on GitHub and | ||
| crates.io. The API usage itself is correct (the identical `collect::<Result<Vec<_>, _>>()` pattern | ||
| is exercised in `tests/comment.rs`). Suggest either dropping the hidden line in favor of | ||
| `.expect(...)`, or wiring the README into doctests. | ||
| ### 6. Informational | ||
| - **api-compat workflow**: baseline switched to the `1.0.0` tag, which does not exist yet, so the | ||
| job self-skips (green badge, "1.0 API compatibility") until the release is tagged. Tag naming | ||
| matches the repo convention (existing tags have no `v` prefix). Just be aware the badge is | ||
| vacuous until the tag is pushed — after that it starts enforcing 1.0 compatibility for real. | ||
| - **CHANGELOG folded v0.0.8 into v1.0.0**: verified safe — crates.io's latest release is 0.0.7 and | ||
| there is no 0.0.8 git tag, so 0.0.8 was never published and its notes correctly move under 1.0.0. | ||
| - **Scanner errors are now terminal**: the old public API could drive the scanner past a first | ||
| error (the deleted test `scanner_reports_misplaced_bracket_when_resumed_after_unclosed_bracket` | ||
| documented recovering a second error). The new latched behavior is a deliberate improvement, | ||
| is stated in the CHANGELOG, and is enshrined by the new `scanner_error_is_terminal` test. | ||
| ## CHANGELOG accuracy (cross-checked bullet by bullet) | ||
| Every breaking-change bullet matches the actual diff: scanner iterator item type and method | ||
| removals; `Event::Nothing` removal; `TEncoding` removal (it was an unnameable type — private | ||
| module, never re-exported, single variant — so the unit `StreamStart` is strictly better); | ||
| `Comment` span removal; `Tag` field privatization; `kind() -> &ErrorKind`; `ScanError::new` | ||
| consolidation (`new_str` was already removed in `6567777`); `anchor_offset` renames; | ||
| `resolve` → `push_include` merge; `AnyParser` privatization. The API-additions list | ||
| (`ParseResult`, `ParserStack`, `ReplayParser` re-exports; `FusedIterator` impls; iterable | ||
| `ReplayParser`) is also accurate — modulo finding 1, which may remove `ParserStack` from the | ||
| `FusedIterator` bullet. | ||
| Items privatized *without* CHANGELOG entries — `ScanResult`, `MarkerOffsets`, `Chomping`, | ||
| `QueuedComment` (deleted) — were verified unreachable from outside the crate before this change | ||
| (private `scanner` module, never re-exported), so no entries are needed. The changelog documents | ||
| exactly the real breakage, no more, no less. | ||
| ## Verified-good details | ||
| - **Scanner fallible iterator**: the deferred-error machinery survives the `get_error` removal — | ||
| errors found behind already-scanned comment tokens still drain the comments first and then | ||
| surface as the iterator's single `Err` item (`next_queued_token`, `src/scanner.rs:1469`). The | ||
| `failed` latch makes the iterator genuinely fused after both an error and clean EOF | ||
| (`stream_end_produced` keeps returning `Ok(None)`). | ||
| - **`Parser` / `ReplayParser` fusedness**: `Parser::stream_end_emitted` is never reset by any | ||
| public path; `ReplayParser` wraps `vec::IntoIter` with no refill method. Both impls are correct. | ||
| - **`Tag` privatization leaves no capability gap**: constructors `Tag::new` (clones handle into | ||
| `original_handle` — sensible default, documented) and `Tag::with_original_handle` cover | ||
| construction; `handle()`/`suffix()`/`original_handle()` plus the pre-existing | ||
| `parts`/`original_parts`/`original`/`core_suffix`/`suffix_in_namespace` helpers cover all reads. | ||
| README's ergonomic-helpers list was updated to match. | ||
| - **`ScanError` changes**: `kind() -> &ErrorKind` is consistent with `marker() -> &Marker`, and | ||
| `ErrorKind: Clone` keeps ownership available (`.kind().clone()`, as the test helpers do). | ||
| `new(marker, impl Into<String>)` is a clean consolidation. | ||
| - **`QueuedComment` shim deletion**: storing the span only on the `Token` removed ~60 lines of | ||
| mirroring code and a per-comment `Span` duplication; the internal queue now stores the public | ||
| `Comment` directly. | ||
| - **Scanner public surface after cleanup** is pleasingly small: `new`, `stream_started`, | ||
| `stream_ended`, `mark`, plus the `Iterator`/`FusedIterator` impls. Queue management | ||
| (`fetch_next_token`, `fetch_more_tokens`, `next_token`) is fully private, as the CHANGELOG states. | ||
| - **Test migration quality**: tests were rewritten to the public API idioms (`collect::<Result<_, _>>()`, | ||
| `find_map(Result::err)`), and renamed to describe the new semantics | ||
| (`scanner_error_is_terminal`, `iterator_next_emits_error_and_then_stays_empty`) rather than | ||
| merely patched to compile. |
| coverage: | ||
| status: | ||
| project: | ||
| default: | ||
| target: 80% | ||
| patch: | ||
| default: | ||
| target: 50% |
| use std::io; | ||
| use granit_parser::{ErrorKind, Event, InputIoError, Parser, ScanError}; | ||
| /// Iterator adapter that adds parser error reporting and a UTF-8 byte limit. | ||
| /// | ||
| /// A byte reader must decode its input before constructing this adapter because the YAML parser | ||
| /// consumes Rust `char` values, not raw bytes. The decoder expresses each read as | ||
| /// `io::Result<char>`: `Ok(c)` is a decoded character, `Err(error)` is a read failure, and the | ||
| /// iterator's `None` is clean EOF. | ||
| struct CheckedChars<I> { | ||
| /// Decoded character source wrapped by this adapter. | ||
| input: I, | ||
| /// Number of UTF-8 bytes represented by characters returned so far. | ||
| bytes_read: usize, | ||
| /// Maximum number of UTF-8 bytes accepted from the source. | ||
| byte_limit: usize, | ||
| /// Prevents the source from being polled again after EOF or an error. | ||
| finished: bool, | ||
| } | ||
| impl<I> CheckedChars<I> { | ||
| fn new(input: I, byte_limit: usize) -> Self { | ||
| Self { | ||
| input, | ||
| bytes_read: 0, | ||
| byte_limit, | ||
| finished: false, | ||
| } | ||
| } | ||
| } | ||
| /// Implementing `Iterator` makes `CheckedChars` a lazy character source for the parser. | ||
| /// `next()` is called once whenever the parser needs another character. | ||
| impl<I> Iterator for CheckedChars<I> | ||
| where | ||
| I: Iterator<Item = io::Result<char>>, | ||
| { | ||
| type Item = Result<char, ErrorKind>; | ||
| fn next(&mut self) -> Option<Self::Item> { | ||
| // Do not poll the source after EOF or after reporting the first error. | ||
| if self.finished { | ||
| return None; | ||
| } | ||
| let Some(next) = self.input.next() else { | ||
| // `None` from the source means clean EOF. Returning `None` forwards that EOF to the | ||
| // parser without manufacturing an error. | ||
| self.finished = true; | ||
| return None; | ||
| }; | ||
| match next { | ||
| Ok(c) => { | ||
| let char_bytes = c.len_utf8(); | ||
| if char_bytes > self.byte_limit.saturating_sub(self.bytes_read) { | ||
| self.finished = true; | ||
| Some(Err(ErrorKind::InputByteLimitExceeded { | ||
| limit: self.byte_limit, | ||
| })) | ||
| } else { | ||
| self.bytes_read += char_bytes; | ||
| Some(Ok(c)) | ||
| } | ||
| } | ||
| Err(error) => { | ||
| // Report the reader failure as an input error. Setting `finished` guarantees that | ||
| // neither this adapter nor the parser will read beyond the failure. | ||
| self.finished = true; | ||
| Some(Err(ErrorKind::InputIo { | ||
| // With the crate's `std` feature, this retains the original `io::Error` | ||
| // alongside its portable message instead of discarding it after formatting. | ||
| error: InputIoError::from(error), | ||
| })) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| fn main() -> Result<(), ScanError> { | ||
| let yaml = "service:\n enabled: true\n retries: 3\n"; | ||
| // This in-memory source cannot fail, so wrap each character in `Ok`. A real reader-backed UTF-8 | ||
| // decoder would have the same `Iterator<Item = io::Result<char>>` interface and return its I/O | ||
| // failures as `Err` items. | ||
| let decoded = yaml.chars().map(Ok::<_, io::Error>); | ||
| // Construct the lazy adapter. No input is read until the parser starts requesting characters. | ||
| let input = CheckedChars::new(decoded, 1024); | ||
| // `new_from_fallible_iter` distinguishes the adapter's clean EOF (`None`) from its source | ||
| // failures (`Some(Err(...))`). The `?` below returns either source or YAML syntax failures. | ||
| for next in Parser::new_from_fallible_iter(input) { | ||
| let (event, _) = next?; | ||
| if let Event::Scalar(value, ..) = event { | ||
| println!("{value}"); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } |
+920
| //! Parser and scanner error types. | ||
| #[cfg(feature = "std")] | ||
| use alloc::sync::Arc; | ||
| use alloc::{ | ||
| string::{String, ToString}, | ||
| vec::Vec, | ||
| }; | ||
| use core::fmt; | ||
| use crate::scanner::Marker; | ||
| /// Details of an I/O failure reported by an input adapter. | ||
| /// | ||
| /// This error is primarily intended for terminal failures such as a missing file, insufficient | ||
| /// permissions, or a failed read, where an exact character position is usually not meaningful. | ||
| /// Streaming inputs may be read ahead by a small lookahead window. Once an adapter reports an I/O | ||
| /// failure, the parser reports it at its current marker; consequently, a few successfully read | ||
| /// characters that were already buffered ahead of that marker may not be scanned or emitted. | ||
| /// | ||
| /// The human-readable message is available in every build. With the `std` feature enabled, an | ||
| /// instance constructed from `std::io::Error` also retains that original error and exposes it | ||
| /// through `InputIoError::io_error` and the standard error source chain. | ||
| /// | ||
| /// Equality and hashing use the portable message. The optional retained `std` error does not | ||
| /// participate, so these operations have the same behavior with and without the `std` feature. | ||
| #[derive(Clone, Debug)] | ||
| pub struct InputIoError { | ||
| message: String, | ||
| #[cfg(feature = "std")] | ||
| source: Option<Arc<std::io::Error>>, | ||
| } | ||
| impl InputIoError { | ||
| /// Create I/O error details from a portable message. | ||
| /// | ||
| /// This constructor is available in `no_std` builds. It does not retain a typed source error. | ||
| #[must_use] | ||
| pub fn from_message(message: impl Into<String>) -> Self { | ||
| Self { | ||
| message: message.into(), | ||
| #[cfg(feature = "std")] | ||
| source: None, | ||
| } | ||
| } | ||
| /// Create I/O error details while retaining the original [`std::io::Error`]. | ||
| #[cfg(feature = "std")] | ||
| #[must_use] | ||
| pub fn from_io(error: std::io::Error) -> Self { | ||
| Self { | ||
| message: error.to_string(), | ||
| source: Some(Arc::new(error)), | ||
| } | ||
| } | ||
| /// Return the portable human-readable error message. | ||
| #[must_use] | ||
| pub fn message(&self) -> &str { | ||
| &self.message | ||
| } | ||
| /// Return the retained [`std::io::Error`], when one is available. | ||
| #[cfg(feature = "std")] | ||
| #[must_use] | ||
| pub fn io_error(&self) -> Option<&std::io::Error> { | ||
| self.source.as_deref() | ||
| } | ||
| /// Recover the retained [`std::io::Error`] when this is its only owner. | ||
| /// | ||
| /// # Errors | ||
| /// Returns the original `InputIoError` when it was created from a portable message or when | ||
| /// another clone still shares the retained error. | ||
| #[cfg(feature = "std")] | ||
| pub fn try_into_io_error(self) -> Result<std::io::Error, Self> { | ||
| let Self { message, source } = self; | ||
| let Some(source) = source else { | ||
| return Err(Self { | ||
| message, | ||
| source: None, | ||
| }); | ||
| }; | ||
| match Arc::try_unwrap(source) { | ||
| Ok(error) => Ok(error), | ||
| Err(source) => Err(Self { | ||
| message, | ||
| source: Some(source), | ||
| }), | ||
| } | ||
| } | ||
| } | ||
| #[cfg(feature = "std")] | ||
| impl From<std::io::Error> for InputIoError { | ||
| fn from(error: std::io::Error) -> Self { | ||
| Self::from_io(error) | ||
| } | ||
| } | ||
| impl PartialEq for InputIoError { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.message == other.message | ||
| } | ||
| } | ||
| impl Eq for InputIoError {} | ||
| impl core::hash::Hash for InputIoError { | ||
| fn hash<H: core::hash::Hasher>(&self, state: &mut H) { | ||
| core::hash::Hash::hash(&self.message, state); | ||
| } | ||
| } | ||
| impl fmt::Display for InputIoError { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str(&self.message) | ||
| } | ||
| } | ||
| impl core::error::Error for InputIoError { | ||
| fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { | ||
| #[cfg(feature = "std")] | ||
| { | ||
| self.source | ||
| .as_deref() | ||
| .map(|error| error as &(dyn core::error::Error + 'static)) | ||
| } | ||
| #[cfg(not(feature = "std"))] | ||
| { | ||
| None | ||
| } | ||
| } | ||
| } | ||
| /// Machine-readable category for a [`ScanError`]. | ||
| #[derive(Clone, PartialEq, Debug, Eq, Hash)] | ||
| #[non_exhaustive] | ||
| pub enum ErrorKind { | ||
| /// Too many consecutive comments were buffered before a collection entry. | ||
| TooManyComments, | ||
| /// Reading from the input source failed. | ||
| InputIo { | ||
| /// Portable details and, with the `std` feature, an optional retained I/O error. | ||
| error: InputIoError, | ||
| }, | ||
| /// The input source was not valid text in the adapter's expected encoding. | ||
| InputDecoding { | ||
| /// Human-readable details supplied by the input adapter. | ||
| message: String, | ||
| }, | ||
| /// The raw input exceeded a configured byte limit. | ||
| InputByteLimitExceeded { | ||
| /// Maximum number of raw input bytes accepted by the adapter. | ||
| limit: usize, | ||
| }, | ||
| /// Input ended while parsing a flow sequence. | ||
| UnexpectedEofFlowSequence, | ||
| /// Input ended while parsing a flow mapping. | ||
| UnexpectedEofFlowMapping, | ||
| /// Input ended while parsing an implicit flow mapping. | ||
| UnexpectedEofImplicitFlowMapping, | ||
| /// Input ended while parsing a block sequence. | ||
| UnexpectedEofBlockSequence, | ||
| /// Input ended while parsing a block mapping. | ||
| UnexpectedEofBlockMapping, | ||
| /// Input ended unexpectedly in another parser state. | ||
| UnexpectedEof, | ||
| /// A stream-start token was expected. | ||
| ExpectedStreamStart, | ||
| /// More than one YAML version directive was found for a document. | ||
| DuplicateVersionDirective, | ||
| /// The YAML major version is unsupported. | ||
| UnsupportedYamlMajorVersion, | ||
| /// A tag directive handle was declared more than once for a document. | ||
| DuplicateTagDirective, | ||
| /// A document-start token was expected. | ||
| ExpectedDocumentStart, | ||
| /// A directive followed an implicit document without an explicit document end. | ||
| MissingDocumentEndBeforeDirective, | ||
| /// The parser ran out of representable anchor identifiers. | ||
| AnchorCountOverflow, | ||
| /// An alias referred to an unknown anchor. | ||
| UnknownAnchor, | ||
| /// The parser did not find expected node content. | ||
| ExpectedNodeContent, | ||
| /// A block mapping key was expected. | ||
| ExpectedBlockMappingKey, | ||
| /// A flow mapping separator or closing brace was expected. | ||
| ExpectedFlowMappingSeparator, | ||
| /// A flow sequence separator or closing bracket was expected. | ||
| ExpectedFlowSequenceSeparator, | ||
| /// A block sequence entry indicator was expected. | ||
| ExpectedBlockSequenceEntry, | ||
| /// A tag used a handle that was not declared. | ||
| UndeclaredTagHandle, | ||
| /// No include resolver was configured for a parser stack. | ||
| MissingIncludeResolver, | ||
| /// An error supplied by an external parser adapter or resolver. | ||
| Custom(String), | ||
| /// A parser-stack entry contained multiple documents where only one is supported. | ||
| MultipleDocumentsUnsupported, | ||
| /// An input advertised byte offsets but did not provide the requested slice. | ||
| InputOffsetsWithoutSlice, | ||
| /// An input advertised slicing but did not provide the requested slice. | ||
| InputSlicingUnavailable, | ||
| /// A tag did not begin with the expected exclamation mark. | ||
| ExpectedTagBang, | ||
| /// A tag directive handle did not end with the expected exclamation mark. | ||
| ExpectedTagDirectiveBang, | ||
| /// A global tag started with an invalid character. | ||
| InvalidGlobalTagCharacter, | ||
| /// A required simple key was not followed by a value indicator. | ||
| SimpleKeyExpected, | ||
| /// A previously saved simple key was no longer valid. | ||
| InvalidSimpleKey, | ||
| /// Invalid content followed a document-end marker. | ||
| InvalidDocumentEnd, | ||
| /// Indentation was invalid for the current parser context. | ||
| InvalidIndentation, | ||
| /// A byte-order mark appeared inside a document. | ||
| BomInsideDocument, | ||
| /// An unexpected reserved character was encountered. | ||
| UnexpectedCharacter { | ||
| /// The character that was encountered. | ||
| character: char, | ||
| }, | ||
| /// A tab was used in a context where it is not allowed. | ||
| TabNotAllowed, | ||
| /// A tab was used in block indentation. | ||
| TabInBlockIndentation, | ||
| /// A comment interrupted a multiline plain scalar. | ||
| CommentInterceptedScalar, | ||
| /// Required whitespace was not found. | ||
| ExpectedWhitespace, | ||
| /// A comment was not separated from the preceding token by whitespace. | ||
| CommentNotSeparated, | ||
| /// A directive did not end with a comment or line break. | ||
| InvalidDirectiveTerminator, | ||
| /// A YAML version directive did not contain the expected digit or dot. | ||
| MissingYamlVersionSeparator, | ||
| /// A directive name was missing. | ||
| MissingDirectiveName, | ||
| /// A directive name contained an invalid character. | ||
| InvalidDirectiveName, | ||
| /// A YAML version component exceeded the supported length. | ||
| YamlVersionTooLong, | ||
| /// A YAML version component was missing. | ||
| MissingYamlVersion, | ||
| /// A tag directive did not end with whitespace or a line break. | ||
| InvalidTagDirectiveTerminator, | ||
| /// A tag token did not end with valid separation whitespace. | ||
| InvalidTagTerminator, | ||
| /// A tag URI was missing. | ||
| MissingTagUri, | ||
| /// A verbatim tag was missing its closing angle bracket. | ||
| UnclosedVerbatimTag, | ||
| /// A tag contained an invalid percent escape. | ||
| InvalidTagEscape, | ||
| /// A tag escape started with an invalid UTF-8 byte. | ||
| InvalidTagUtf8LeadingByte, | ||
| /// A tag escape contained an invalid trailing UTF-8 byte. | ||
| InvalidTagUtf8TrailingByte, | ||
| /// A tag escape did not decode to one valid Unicode scalar value. | ||
| InvalidTagUtf8, | ||
| /// An anchor or alias name was missing. | ||
| MissingAnchorOrAliasName, | ||
| /// A flow collection closing bracket was misplaced. | ||
| MisplacedFlowCollectionEnd, | ||
| /// A flow collection was closed with the wrong bracket type. | ||
| MismatchedFlowCollectionEnd { | ||
| /// The bracket that opened the flow collection. | ||
| open: char, | ||
| /// The bracket that closed the flow collection. | ||
| close: char, | ||
| }, | ||
| /// A flow collection was not closed. | ||
| UnclosedFlowCollection { | ||
| /// The bracket that opened the flow collection. | ||
| open: char, | ||
| }, | ||
| /// The supported flow nesting limit was exceeded. | ||
| RecursionLimitExceeded, | ||
| /// A block entry indicator appeared inside a flow collection. | ||
| BlockEntryInFlowCollection, | ||
| /// A block sequence entry appeared in a context that does not allow it. | ||
| BlockSequenceEntryNotAllowed, | ||
| /// A block entry indicator was followed by invalid whitespace. | ||
| InvalidBlockEntryWhitespace, | ||
| /// A block scalar used an indentation indicator of zero. | ||
| ZeroBlockScalarIndent, | ||
| /// A block scalar header did not end with a comment or line break. | ||
| InvalidBlockScalarHeader, | ||
| /// Block scalar content began with a tab. | ||
| TabAtBlockScalarStart, | ||
| /// A block scalar content line had invalid indentation. | ||
| InvalidBlockScalarIndent, | ||
| /// A document indicator appeared inside a quoted scalar. | ||
| DocumentIndicatorInQuotedScalar, | ||
| /// A quoted scalar was not closed. | ||
| UnclosedQuotedScalar, | ||
| /// A tab was used as indentation. | ||
| TabInIndentation, | ||
| /// A multiline quoted scalar had invalid indentation. | ||
| InvalidQuotedScalarIndent, | ||
| /// Invalid content followed a single-quoted scalar. | ||
| InvalidTrailingSingleQuotedScalar, | ||
| /// Invalid content followed a double-quoted scalar. | ||
| InvalidTrailingDoubleQuotedScalar, | ||
| /// A quoted scalar contained an unknown escape character. | ||
| UnknownQuotedScalarEscape, | ||
| /// A quoted scalar escape did not contain the expected hexadecimal digits. | ||
| InvalidQuotedScalarHexEscape, | ||
| /// A low-surrogate escape did not contain the expected hexadecimal digits. | ||
| InvalidLowSurrogateHexEscape, | ||
| /// A surrogate pair contained an invalid low surrogate. | ||
| InvalidLowSurrogate, | ||
| /// A high surrogate was not followed by a low surrogate. | ||
| MissingLowSurrogate, | ||
| /// A low surrogate appeared without a preceding high surrogate. | ||
| UnpairedLowSurrogate, | ||
| /// A quoted scalar escape did not represent a valid Unicode scalar value. | ||
| InvalidUnicodeEscape, | ||
| /// A flow scalar started at invalid indentation. | ||
| InvalidFlowScalarIndent, | ||
| /// A plain scalar began with a dash followed by a flow indicator. | ||
| PlainScalarStartsWithDashFlowIndicator, | ||
| /// A tab appeared where a plain scalar could not accept it. | ||
| TabInPlainScalar, | ||
| /// A plain scalar ended before consuming any content. | ||
| UnexpectedEndOfPlainScalar, | ||
| /// A mapping key appeared in a context that does not allow one. | ||
| MappingKeyNotAllowed, | ||
| /// A flow mapping value indicator was adjacent to a collection start. | ||
| FlowMappingValueAdjacentCollection, | ||
| /// A mapping value indicator was followed by invalid whitespace. | ||
| InvalidMappingValueWhitespace, | ||
| /// A value indicator was placed illegally in an implicit flow mapping. | ||
| InvalidColonPlacement, | ||
| /// A mapping value appeared in a context that does not allow one. | ||
| MappingValueNotAllowed, | ||
| } | ||
| #[cfg(feature = "error_messages")] | ||
| impl fmt::Display for ErrorKind { | ||
| #[allow(clippy::too_many_lines)] | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| match self { | ||
| Self::TooManyComments => { | ||
| f.write_str("too many consecutive comments before resolving collection entry") | ||
| } | ||
| Self::InputIo { error } => write!(f, "input I/O error: {error}"), | ||
| Self::InputDecoding { message } => { | ||
| write!(f, "input decoding error: {message}") | ||
| } | ||
| Self::InputByteLimitExceeded { limit } => { | ||
| write!(f, "input exceeds the configured limit of {limit} bytes") | ||
| } | ||
| Self::UnexpectedEofFlowSequence => { | ||
| f.write_str("unexpected EOF while parsing a flow sequence") | ||
| } | ||
| Self::UnexpectedEofFlowMapping => { | ||
| f.write_str("unexpected EOF while parsing a flow mapping") | ||
| } | ||
| Self::UnexpectedEofImplicitFlowMapping => { | ||
| f.write_str("unexpected EOF while parsing an implicit flow mapping") | ||
| } | ||
| Self::UnexpectedEofBlockSequence => { | ||
| f.write_str("unexpected EOF while parsing a block sequence") | ||
| } | ||
| Self::UnexpectedEofBlockMapping => { | ||
| f.write_str("unexpected EOF while parsing a block mapping") | ||
| } | ||
| Self::UnexpectedEof => f.write_str("unexpected eof"), | ||
| Self::ExpectedStreamStart => f.write_str("did not find expected <stream-start>"), | ||
| Self::DuplicateVersionDirective => f.write_str("duplicate version directive"), | ||
| Self::UnsupportedYamlMajorVersion => { | ||
| f.write_str("unsupported YAML major version") | ||
| } | ||
| Self::DuplicateTagDirective => f.write_str( | ||
| "the TAG directive must only be given at most once per handle in the same document", | ||
| ), | ||
| Self::ExpectedDocumentStart => { | ||
| f.write_str("did not find expected <document start>") | ||
| } | ||
| Self::MissingDocumentEndBeforeDirective => { | ||
| f.write_str("missing explicit document end marker before directive") | ||
| } | ||
| Self::AnchorCountOverflow => { | ||
| f.write_str("while parsing anchor, anchor count exceeded supported limit") | ||
| } | ||
| Self::UnknownAnchor => f.write_str("while parsing node, found unknown anchor"), | ||
| Self::ExpectedNodeContent => { | ||
| f.write_str("while parsing a node, did not find expected node content") | ||
| } | ||
| Self::ExpectedBlockMappingKey => { | ||
| f.write_str("while parsing a block mapping, did not find expected key") | ||
| } | ||
| Self::ExpectedFlowMappingSeparator => { | ||
| f.write_str("while parsing a flow mapping, did not find expected ',' or '}'") | ||
| } | ||
| Self::ExpectedFlowSequenceSeparator => { | ||
| f.write_str("while parsing a flow sequence, expected ',' or ']'") | ||
| } | ||
| Self::ExpectedBlockSequenceEntry => f.write_str( | ||
| "while parsing a block collection, did not find expected '-' indicator", | ||
| ), | ||
| Self::UndeclaredTagHandle => f.write_str("the handle wasn't declared"), | ||
| Self::MissingIncludeResolver => { | ||
| f.write_str("No include resolver set for parser stack.") | ||
| } | ||
| Self::Custom(message) => f.write_str(message), | ||
| Self::MultipleDocumentsUnsupported => { | ||
| f.write_str("multiple documents not supported here") | ||
| } | ||
| Self::InputOffsetsWithoutSlice => f.write_str( | ||
| "internal error: input advertised offsets but did not provide a slice", | ||
| ), | ||
| Self::InputSlicingUnavailable => f.write_str( | ||
| "internal error: input advertised slicing but did not provide a slice", | ||
| ), | ||
| Self::ExpectedTagBang => { | ||
| f.write_str("while scanning a tag, did not find expected '!'") | ||
| } | ||
| Self::ExpectedTagDirectiveBang => { | ||
| f.write_str("while parsing a tag directive, did not find expected '!'") | ||
| } | ||
| Self::InvalidGlobalTagCharacter => f.write_str("invalid global tag character"), | ||
| Self::SimpleKeyExpected => f.write_str("simple key expected ':'"), | ||
| Self::InvalidSimpleKey => f.write_str("simple key is no longer valid"), | ||
| Self::InvalidDocumentEnd => { | ||
| f.write_str("invalid content after document end marker") | ||
| } | ||
| Self::InvalidIndentation => f.write_str("invalid indentation"), | ||
| Self::BomInsideDocument => { | ||
| f.write_str("a BOM must not appear inside a document") | ||
| } | ||
| Self::UnexpectedCharacter { character } => { | ||
| write!(f, "unexpected character: `{}'", character.escape_default()) | ||
| } | ||
| Self::TabNotAllowed => f.write_str("tabs disallowed in this context"), | ||
| Self::TabInBlockIndentation => { | ||
| f.write_str("tabs disallowed within this context (block indentation)") | ||
| } | ||
| Self::CommentInterceptedScalar => { | ||
| f.write_str("comment intercepting the multiline text") | ||
| } | ||
| Self::ExpectedWhitespace => f.write_str("expected whitespace"), | ||
| Self::CommentNotSeparated => { | ||
| f.write_str("comments must be separated from other tokens by whitespace") | ||
| } | ||
| Self::InvalidDirectiveTerminator => f.write_str( | ||
| "while scanning a directive, did not find expected comment or line break", | ||
| ), | ||
| Self::MissingYamlVersionSeparator => f.write_str( | ||
| "while scanning a YAML directive, did not find expected digit or '.' character", | ||
| ), | ||
| Self::MissingDirectiveName => f.write_str( | ||
| "while scanning a directive, could not find expected directive name", | ||
| ), | ||
| Self::InvalidDirectiveName => f.write_str( | ||
| "while scanning a directive, found unexpected non-alphabetical character", | ||
| ), | ||
| Self::YamlVersionTooLong => { | ||
| f.write_str("while scanning a YAML directive, found extremely long version number") | ||
| } | ||
| Self::MissingYamlVersion => f.write_str( | ||
| "while scanning a YAML directive, did not find expected version number", | ||
| ), | ||
| Self::InvalidTagDirectiveTerminator => { | ||
| f.write_str("while scanning TAG, did not find expected whitespace or line break") | ||
| } | ||
| Self::InvalidTagTerminator => f.write_str( | ||
| "while scanning a tag, did not find expected whitespace or line break", | ||
| ), | ||
| Self::MissingTagUri => { | ||
| f.write_str("while parsing a tag, did not find expected tag URI") | ||
| } | ||
| Self::UnclosedVerbatimTag => { | ||
| f.write_str("while scanning a verbatim tag, did not find the expected '>'") | ||
| } | ||
| Self::InvalidTagEscape => { | ||
| f.write_str("while parsing a tag, found an invalid escape sequence") | ||
| } | ||
| Self::InvalidTagUtf8LeadingByte => { | ||
| f.write_str("while parsing a tag, found an incorrect leading UTF-8 byte") | ||
| } | ||
| Self::InvalidTagUtf8TrailingByte => { | ||
| f.write_str("while parsing a tag, found an incorrect trailing UTF-8 byte") | ||
| } | ||
| Self::InvalidTagUtf8 => { | ||
| f.write_str("while parsing a tag, found an invalid UTF-8 codepoint") | ||
| } | ||
| Self::MissingAnchorOrAliasName => f.write_str( | ||
| "while scanning an anchor or alias, did not find expected alphabetic or numeric character", | ||
| ), | ||
| Self::MisplacedFlowCollectionEnd => f.write_str("misplaced bracket"), | ||
| Self::MismatchedFlowCollectionEnd { open, close } => { | ||
| write!(f, "mismatched bracket '{open}' closed by '{close}'") | ||
| } | ||
| Self::UnclosedFlowCollection { open } => { | ||
| write!(f, "unclosed bracket '{open}'") | ||
| } | ||
| Self::RecursionLimitExceeded => f.write_str("recursion limit exceeded"), | ||
| Self::BlockEntryInFlowCollection => { | ||
| f.write_str(r#""-" is only valid inside a block"#) | ||
| } | ||
| Self::BlockSequenceEntryNotAllowed => { | ||
| f.write_str("block sequence entries are not allowed in this context") | ||
| } | ||
| Self::InvalidBlockEntryWhitespace => { | ||
| f.write_str("'-' must be followed by a valid YAML whitespace") | ||
| } | ||
| Self::ZeroBlockScalarIndent => f.write_str( | ||
| "while scanning a block scalar, found an indentation indicator equal to 0", | ||
| ), | ||
| Self::InvalidBlockScalarHeader => f.write_str( | ||
| "while scanning a block scalar, did not find expected comment or line break", | ||
| ), | ||
| Self::TabAtBlockScalarStart => { | ||
| f.write_str("a block scalar content cannot start with a tab") | ||
| } | ||
| Self::InvalidBlockScalarIndent => { | ||
| f.write_str("wrongly indented line in block scalar") | ||
| } | ||
| Self::DocumentIndicatorInQuotedScalar => f.write_str( | ||
| "while scanning a quoted scalar, found unexpected document indicator", | ||
| ), | ||
| Self::UnclosedQuotedScalar => f.write_str("unclosed quote"), | ||
| Self::TabInIndentation => f.write_str("tab cannot be used as indentation"), | ||
| Self::InvalidQuotedScalarIndent => { | ||
| f.write_str("invalid indentation in multiline quoted scalar") | ||
| } | ||
| Self::InvalidTrailingSingleQuotedScalar => { | ||
| f.write_str("invalid trailing content after single-quoted scalar") | ||
| } | ||
| Self::InvalidTrailingDoubleQuotedScalar => { | ||
| f.write_str("invalid trailing content after double-quoted scalar") | ||
| } | ||
| Self::UnknownQuotedScalarEscape => { | ||
| f.write_str("while parsing a quoted scalar, found unknown escape character") | ||
| } | ||
| Self::InvalidQuotedScalarHexEscape => f.write_str( | ||
| "while parsing a quoted scalar, did not find expected hexadecimal number", | ||
| ), | ||
| Self::InvalidLowSurrogateHexEscape => f.write_str( | ||
| "while parsing a quoted scalar, did not find expected hexadecimal number for low surrogate", | ||
| ), | ||
| Self::InvalidLowSurrogate => { | ||
| f.write_str("while parsing a quoted scalar, found invalid low surrogate") | ||
| } | ||
| Self::MissingLowSurrogate => f.write_str( | ||
| "while parsing a quoted scalar, found high surrogate without following low surrogate", | ||
| ), | ||
| Self::UnpairedLowSurrogate => { | ||
| f.write_str("while parsing a quoted scalar, found unpaired low surrogate") | ||
| } | ||
| Self::InvalidUnicodeEscape => f.write_str( | ||
| "while parsing a quoted scalar, found invalid Unicode character escape code", | ||
| ), | ||
| Self::InvalidFlowScalarIndent => { | ||
| f.write_str("invalid indentation in flow construct") | ||
| } | ||
| Self::PlainScalarStartsWithDashFlowIndicator => { | ||
| f.write_str("plain scalar cannot start with '-' followed by ,[]{}") | ||
| } | ||
| Self::TabInPlainScalar => { | ||
| f.write_str("while scanning a plain scalar, found a tab") | ||
| } | ||
| Self::UnexpectedEndOfPlainScalar => f.write_str("unexpected end of plain scalar"), | ||
| Self::MappingKeyNotAllowed => { | ||
| f.write_str("mapping keys are not allowed in this context") | ||
| } | ||
| Self::FlowMappingValueAdjacentCollection => { | ||
| f.write_str("':' may not precede any of `[{` in flow mapping") | ||
| } | ||
| Self::InvalidMappingValueWhitespace => { | ||
| f.write_str("':' must be followed by a valid YAML whitespace") | ||
| } | ||
| Self::InvalidColonPlacement => f.write_str("illegal placement of ':' indicator"), | ||
| Self::MappingValueNotAllowed => { | ||
| f.write_str("mapping values are not allowed in this context") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| #[cfg(not(feature = "error_messages"))] | ||
| impl fmt::Display for ErrorKind { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str("") | ||
| } | ||
| } | ||
| /// An error that occurred while reading, scanning, or parsing YAML. | ||
| #[derive(Clone, PartialEq, Debug, Eq)] | ||
| pub struct ScanError { | ||
| /// The position at which the error happened in the source. | ||
| mark: Marker, | ||
| /// Machine-readable error category. | ||
| kind: ErrorKind, | ||
| /// Source names captured by a parser stack before its failing entry is removed. | ||
| source_stack: Vec<String>, | ||
| } | ||
| impl ScanError { | ||
| /// Create an externally supplied error from a location and message. | ||
| /// | ||
| /// The message is stored in [`ErrorKind::Custom`]. This is useful for adapters that | ||
| /// participate in parser APIs, such as a custom [`ParserTrait`](crate::ParserTrait) | ||
| /// implementation or a [`ParserStack`](crate::ParserStack) include resolver. | ||
| #[must_use] | ||
| #[cold] | ||
| pub fn new(loc: Marker, message: impl Into<String>) -> ScanError { | ||
| Self::from_kind(loc, ErrorKind::Custom(message.into())) | ||
| } | ||
| #[must_use] | ||
| #[cold] | ||
| pub(crate) fn from_kind(loc: Marker, kind: ErrorKind) -> ScanError { | ||
| ScanError { | ||
| mark: loc, | ||
| kind, | ||
| source_stack: Vec::new(), | ||
| } | ||
| } | ||
| #[must_use] | ||
| pub(crate) fn with_source_stack(mut self, source_stack: Vec<String>) -> Self { | ||
| self.source_stack = source_stack; | ||
| self | ||
| } | ||
| #[cold] | ||
| pub(crate) fn into_result<T>(self) -> Result<T, ScanError> { | ||
| Err(self) | ||
| } | ||
| /// Return the marker pointing to the error in the source. | ||
| #[must_use] | ||
| pub fn marker(&self) -> &Marker { | ||
| &self.mark | ||
| } | ||
| /// Return the machine-readable error category. | ||
| #[must_use] | ||
| pub fn kind(&self) -> &ErrorKind { | ||
| &self.kind | ||
| } | ||
| /// Extract the input I/O error details without cloning them. | ||
| /// | ||
| /// # Errors | ||
| /// Returns the original scan error unchanged when it has a different error category. | ||
| pub fn try_into_input_io_error(self) -> Result<InputIoError, Self> { | ||
| let Self { | ||
| mark, | ||
| kind, | ||
| source_stack, | ||
| } = self; | ||
| match kind { | ||
| ErrorKind::InputIo { error } => Ok(error), | ||
| kind => Err(Self { | ||
| mark, | ||
| kind, | ||
| source_stack, | ||
| }), | ||
| } | ||
| } | ||
| /// Return source names captured by a parser stack, from bottom to top. | ||
| #[must_use] | ||
| pub fn source_stack(&self) -> &[String] { | ||
| &self.source_stack | ||
| } | ||
| /// Render the error as a human-readable description. | ||
| /// | ||
| /// Parser-stack errors include their nested source names. The result remains | ||
| /// empty when the `error_messages` feature is disabled. | ||
| #[must_use] | ||
| pub fn info(&self) -> String { | ||
| let mut info = self.kind.to_string(); | ||
| if !info.is_empty() && self.source_stack().len() > 1 { | ||
| info.push_str("\nwhile parsing "); | ||
| info.push_str(&self.source_stack().join(" -> ")); | ||
| } | ||
| info | ||
| } | ||
| } | ||
| impl fmt::Display for ScanError { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| write!( | ||
| f, | ||
| "{} at char {} line {} column {}", | ||
| self.info(), | ||
| self.mark.index(), | ||
| self.mark.line(), | ||
| self.mark.col() + 1 | ||
| ) | ||
| } | ||
| } | ||
| impl core::error::Error for ScanError { | ||
| fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { | ||
| match &self.kind { | ||
| ErrorKind::InputIo { error } => Some(error), | ||
| _ => None, | ||
| } | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
| mod tests { | ||
| #[cfg(feature = "error_messages")] | ||
| use alloc::format; | ||
| #[cfg(feature = "error_messages")] | ||
| use alloc::string::String; | ||
| use alloc::string::ToString; | ||
| use super::{ErrorKind, InputIoError, ScanError}; | ||
| use crate::scanner::Marker; | ||
| #[cfg(feature = "error_messages")] | ||
| #[test] | ||
| fn constructor_retains_kind_and_derives_info() { | ||
| let marker = Marker::new(3, 2, 1); | ||
| let error = ScanError::from_kind(marker, ErrorKind::ExpectedWhitespace); | ||
| assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace); | ||
| assert_eq!(error.kind().to_string(), "expected whitespace"); | ||
| assert_eq!(error.info(), "expected whitespace"); | ||
| } | ||
| #[cfg(feature = "error_messages")] | ||
| #[test] | ||
| fn parameterized_kind_constructs_info() { | ||
| let marker = Marker::new(3, 2, 1); | ||
| let error = ScanError::from_kind( | ||
| marker, | ||
| ErrorKind::MismatchedFlowCollectionEnd { | ||
| open: '[', | ||
| close: '}', | ||
| }, | ||
| ); | ||
| assert_eq!(error.info(), "mismatched bracket '[' closed by '}'"); | ||
| assert_eq!( | ||
| format!("{error}"), | ||
| "mismatched bracket '[' closed by '}' at char 3 line 2 column 2" | ||
| ); | ||
| } | ||
| #[cfg(feature = "error_messages")] | ||
| #[test] | ||
| fn input_error_kinds_construct_info() { | ||
| assert_eq!( | ||
| ErrorKind::InputIo { | ||
| error: InputIoError::from_message("connection reset") | ||
| } | ||
| .to_string(), | ||
| "input I/O error: connection reset" | ||
| ); | ||
| assert_eq!( | ||
| ErrorKind::InputDecoding { | ||
| message: String::from("invalid utf-8") | ||
| } | ||
| .to_string(), | ||
| "input decoding error: invalid utf-8" | ||
| ); | ||
| assert_eq!( | ||
| ErrorKind::InputByteLimitExceeded { limit: 4096 }.to_string(), | ||
| "input exceeds the configured limit of 4096 bytes" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn message_only_input_io_error_has_no_source() { | ||
| use core::error::Error as _; | ||
| let error = InputIoError::from_message("portable failure"); | ||
| assert_eq!(error.message(), "portable failure"); | ||
| assert!(error.source().is_none()); | ||
| } | ||
| #[cfg(feature = "std")] | ||
| #[test] | ||
| fn std_input_io_error_is_retained_in_scan_error_source_chain() { | ||
| use core::error::Error as _; | ||
| use std::io; | ||
| let details = InputIoError::from(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed")); | ||
| assert_eq!(details.message(), "pipe closed"); | ||
| assert_eq!( | ||
| details | ||
| .io_error() | ||
| .expect("std construction should retain io::Error") | ||
| .kind(), | ||
| io::ErrorKind::BrokenPipe | ||
| ); | ||
| let error = ScanError::from_kind( | ||
| Marker::new(3, 2, 1), | ||
| ErrorKind::InputIo { | ||
| error: details.clone(), | ||
| }, | ||
| ); | ||
| let input_error = error | ||
| .source() | ||
| .and_then(|source| source.downcast_ref::<InputIoError>()) | ||
| .expect("ScanError should expose InputIoError as its source"); | ||
| let io_error = input_error | ||
| .source() | ||
| .and_then(|source| source.downcast_ref::<io::Error>()) | ||
| .expect("InputIoError should expose the retained io::Error"); | ||
| assert_eq!(io_error.kind(), io::ErrorKind::BrokenPipe); | ||
| assert_eq!(details, *input_error); | ||
| } | ||
| #[cfg(feature = "std")] | ||
| #[test] | ||
| fn unique_std_input_io_error_can_be_recovered() { | ||
| use std::io; | ||
| let details = InputIoError::from(io::Error::from_raw_os_error(12_345)); | ||
| let error = details | ||
| .try_into_io_error() | ||
| .expect("a uniquely owned io::Error should be recoverable"); | ||
| assert_eq!(error.raw_os_error(), Some(12_345)); | ||
| } | ||
| #[cfg(feature = "std")] | ||
| #[test] | ||
| fn shared_std_input_io_error_can_be_recovered_after_other_clone_is_dropped() { | ||
| use std::io; | ||
| let details = InputIoError::from(io::Error::from_raw_os_error(12_345)); | ||
| let other = details.clone(); | ||
| let details = details | ||
| .try_into_io_error() | ||
| .expect_err("a shared io::Error cannot be moved out"); | ||
| drop(other); | ||
| let error = details | ||
| .try_into_io_error() | ||
| .expect("the last owner should recover the io::Error"); | ||
| assert_eq!(error.raw_os_error(), Some(12_345)); | ||
| } | ||
| #[cfg(feature = "std")] | ||
| #[test] | ||
| fn scan_error_moves_input_io_error_out_without_cloning() { | ||
| use std::io; | ||
| let error = ScanError::from_kind( | ||
| Marker::new(3, 2, 1), | ||
| ErrorKind::InputIo { | ||
| error: InputIoError::from(io::Error::from_raw_os_error(12_345)), | ||
| }, | ||
| ); | ||
| let details = error | ||
| .try_into_input_io_error() | ||
| .expect("input I/O details should be extractable"); | ||
| let error = details | ||
| .try_into_io_error() | ||
| .expect("extracting the scan error should retain unique ownership"); | ||
| assert_eq!(error.raw_os_error(), Some(12_345)); | ||
| } | ||
| #[test] | ||
| fn extracting_input_io_error_preserves_other_scan_errors() { | ||
| let error = ScanError::from_kind(Marker::new(3, 2, 1), ErrorKind::ExpectedWhitespace); | ||
| let error = error | ||
| .try_into_input_io_error() | ||
| .expect_err("a non-I/O scan error should be returned unchanged"); | ||
| assert_eq!(error.marker(), &Marker::new(3, 2, 1)); | ||
| assert_eq!(error.kind(), &ErrorKind::ExpectedWhitespace); | ||
| } | ||
| #[cfg(feature = "error_messages")] | ||
| #[test] | ||
| fn public_constructor_copies_custom_message() { | ||
| let marker = Marker::new(3, 2, 1); | ||
| let mut message = String::from("adapter failed"); | ||
| let error = ScanError::new(marker, &message); | ||
| message.clear(); | ||
| assert_eq!( | ||
| error.kind(), | ||
| &ErrorKind::Custom(String::from("adapter failed")) | ||
| ); | ||
| assert_eq!(error.info(), "adapter failed"); | ||
| } | ||
| #[cfg(not(feature = "error_messages"))] | ||
| #[test] | ||
| fn disabled_error_messages_are_empty() { | ||
| let marker = Marker::new(3, 2, 1); | ||
| let error = ScanError::from_kind( | ||
| marker, | ||
| ErrorKind::MismatchedFlowCollectionEnd { | ||
| open: '[', | ||
| close: '}', | ||
| }, | ||
| ); | ||
| assert!(error.kind().to_string().is_empty()); | ||
| assert!(error.info().is_empty()); | ||
| } | ||
| } |
| use granit_parser::{ | ||
| ErrorKind, Event, EventReceiver, FallibleBufferedInput, InputIoError, Parser, ScanError, | ||
| Scanner, | ||
| }; | ||
| fn io_error(message: &str) -> ErrorKind { | ||
| ErrorKind::InputIo { | ||
| error: InputIoError::from_message(message), | ||
| } | ||
| } | ||
| #[test] | ||
| fn fallible_iterator_clean_eof_emits_stream_end() { | ||
| let input = "key: value\n".chars().map(Ok::<_, ErrorKind>); | ||
| let events = Parser::new_from_fallible_iter(input) | ||
| .collect::<Result<Vec<_>, _>>() | ||
| .expect("clean EOF should finish parsing"); | ||
| assert!(matches!(events.last(), Some((Event::StreamEnd, _)))); | ||
| } | ||
| #[test] | ||
| fn source_error_cannot_be_mistaken_for_clean_eof() { | ||
| let input = "key: value\n" | ||
| .chars() | ||
| .map(Ok) | ||
| .chain(core::iter::once(Err(io_error("connection reset")))); | ||
| let mut parser = Parser::new_from_fallible_iter(input); | ||
| let mut emitted_stream_end = false; | ||
| let error = loop { | ||
| match parser.next() { | ||
| Some(Ok((event, _))) => emitted_stream_end |= matches!(event, Event::StreamEnd), | ||
| Some(Err(error)) => break error, | ||
| None => panic!("source failure was silently treated as EOF"), | ||
| } | ||
| }; | ||
| assert_eq!(error.kind(), &io_error("connection reset")); | ||
| assert!(!emitted_stream_end); | ||
| assert!(parser.next().is_none(), "a source error must be terminal"); | ||
| } | ||
| #[test] | ||
| fn source_error_takes_priority_over_eof_derived_syntax_error() { | ||
| let input = core::iter::once(Ok('[')).chain(core::iter::once(Err(io_error("read failed")))); | ||
| let error = Parser::new_from_fallible_iter(input) | ||
| .find_map(Result::err) | ||
| .expect("source failure should be reported"); | ||
| assert_eq!(error.kind(), &io_error("read failed")); | ||
| } | ||
| #[test] | ||
| fn scanner_iterator_emits_source_error() { | ||
| let input = core::iter::once(Err(io_error("scanner read failed"))); | ||
| let mut scanner = Scanner::new(FallibleBufferedInput::new(input)); | ||
| let error = scanner | ||
| .by_ref() | ||
| .find_map(Result::err) | ||
| .expect("scanner should emit the source error"); | ||
| assert_eq!(error.kind(), &io_error("scanner read failed")); | ||
| assert!(scanner.next().is_none(), "a source error must be terminal"); | ||
| } | ||
| struct ErrorThenPanic { | ||
| next: usize, | ||
| } | ||
| impl Iterator for ErrorThenPanic { | ||
| type Item = Result<char, ErrorKind>; | ||
| fn next(&mut self) -> Option<Self::Item> { | ||
| let item = match self.next { | ||
| 0 => Ok('a'), | ||
| 1 => Err(io_error("terminal failure")), | ||
| _ => panic!("fallible input was polled after its first error"), | ||
| }; | ||
| self.next += 1; | ||
| Some(item) | ||
| } | ||
| } | ||
| #[test] | ||
| fn source_is_not_polled_after_error() { | ||
| let mut parser = Parser::new_from_fallible_iter(ErrorThenPanic { next: 0 }); | ||
| let error = parser | ||
| .find_map(Result::err) | ||
| .expect("source failure should be reported"); | ||
| assert_eq!(error.kind(), &io_error("terminal failure")); | ||
| } | ||
| struct Sink; | ||
| impl EventReceiver<'static> for Sink { | ||
| fn on_event(&mut self, _event: Event<'static>) {} | ||
| } | ||
| #[test] | ||
| fn receiver_api_returns_byte_limit_error() { | ||
| let input = "key: value".chars().map(Ok).chain(core::iter::once(Err( | ||
| ErrorKind::InputByteLimitExceeded { limit: 8 }, | ||
| ))); | ||
| let mut parser = Parser::new_from_fallible_iter(input); | ||
| let error: ScanError = parser | ||
| .load(&mut Sink, true) | ||
| .expect_err("byte limit failure should stop receiver loading"); | ||
| assert_eq!( | ||
| error.kind(), | ||
| &ErrorKind::InputByteLimitExceeded { limit: 8 } | ||
| ); | ||
| } |
| { | ||
| "git": { | ||
| "sha1": "66f3df3edcbeed550f4d9f5665fde4fd544de1d4" | ||
| "sha1": "f0b7631a54dcf1ec10ad52b4ac7fb87ee8583bf4" | ||
| }, | ||
| "path_in_vcs": "" | ||
| } |
@@ -1,2 +0,2 @@ | ||
| name: API 0.0.3 compatibility | ||
| name: API 1.0.0 compatibility | ||
@@ -14,3 +14,3 @@ on: | ||
| env: | ||
| BASELINE_REF: "0.0.3" | ||
| BASELINE_REF: "1.0.0" | ||
@@ -17,0 +17,0 @@ jobs: |
+30
-6
@@ -12,3 +12,3 @@ # This file is automatically @generated by Cargo. | ||
| "anstyle", | ||
| "anstyle-parse", | ||
| "anstyle-parse 0.2.7", | ||
| "anstyle-query", | ||
@@ -22,2 +22,17 @@ "anstyle-wincon", | ||
| [[package]] | ||
| name = "anstream" | ||
| version = "1.0.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" | ||
| dependencies = [ | ||
| "anstyle", | ||
| "anstyle-parse 1.0.0", | ||
| "anstyle-query", | ||
| "anstyle-wincon", | ||
| "colorchoice", | ||
| "is_terminal_polyfill", | ||
| "utf8parse", | ||
| ] | ||
| [[package]] | ||
| name = "anstyle" | ||
@@ -38,2 +53,11 @@ version = "1.0.14" | ||
| [[package]] | ||
| name = "anstyle-parse" | ||
| version = "1.0.0" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" | ||
| dependencies = [ | ||
| "utf8parse", | ||
| ] | ||
| [[package]] | ||
| name = "anstyle-query" | ||
@@ -80,3 +104,3 @@ version = "1.1.5" | ||
| dependencies = [ | ||
| "anstream", | ||
| "anstream 0.6.21", | ||
| "anstyle", | ||
@@ -119,3 +143,3 @@ "clap_lex", | ||
| name = "granit-parser" | ||
| version = "0.0.7" | ||
| version = "1.0.0-rc.1" | ||
| dependencies = [ | ||
@@ -141,7 +165,7 @@ "arraydeque", | ||
| name = "libtest-mimic" | ||
| version = "0.8.1" | ||
| version = "0.8.2" | ||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" | ||
| checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" | ||
| dependencies = [ | ||
| "anstream", | ||
| "anstream 1.0.0", | ||
| "anstyle", | ||
@@ -148,0 +172,0 @@ "clap", |
+19
-4
@@ -16,3 +16,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| name = "granit-parser" | ||
| version = "0.0.7" | ||
| version = "1.0.0-rc.1" | ||
| authors = [ | ||
@@ -52,3 +52,9 @@ "Ethiraric <ethiraric@gmail.com>", | ||
| [features] | ||
| debug_prints = [] | ||
| debug_prints = ["std"] | ||
| default = [ | ||
| "error_messages", | ||
| "std", | ||
| ] | ||
| error_messages = [] | ||
| std = [] | ||
@@ -76,2 +82,7 @@ [lib] | ||
| [[example]] | ||
| name = "fallible_reader" | ||
| path = "examples/fallible_reader.rs" | ||
| required-features = ["std"] | ||
| [[example]] | ||
| name = "include_stack" | ||
@@ -121,2 +132,6 @@ path = "examples/include_stack.rs" | ||
| [[test]] | ||
| name = "fallible_input" | ||
| path = "tests/fallible_input.rs" | ||
| [[test]] | ||
| name = "flow_collections_fuzz_regression" | ||
@@ -215,6 +230,6 @@ path = "tests/flow_collections_fuzz_regression.rs" | ||
| [dependencies.smallvec] | ||
| version = ">= 1.0.0, < 2.0.0" | ||
| version = ">=1.6.1, < 2.0.0" | ||
| [dev-dependencies.libtest-mimic] | ||
| version = "=0.8.1" | ||
| version = "0.8.2" | ||
@@ -221,0 +236,0 @@ [lints.rust] |
+76
-0
| # Changelog | ||
| ## v1.0.0 | ||
| **Breaking Changes**: | ||
| - Made `Scanner` a fallible, fused iterator with | ||
| `Item = Result<Token<'input>, ScanError>`. Scanner failures are now emitted once and cannot be | ||
| mistaken for clean EOF. Use `scanner.next()` for incremental scanning or | ||
| `scanner.collect::<Result<Vec<_>, _>>()` to collect tokens. Removed the public `get_error`, | ||
| `next_token`, `fetch_next_token`, and `fetch_more_tokens` methods; queue management is now an | ||
| implementation detail. | ||
| - Removed the internal-only `Event::Nothing` variant. | ||
| - Marked `Event`, `TokenType`, and `Placement` as non-exhaustive. Downstream matches must include a | ||
| wildcard arm so new variants can be added in compatible releases. | ||
| - Removed the redundant UTF-8-only `TEncoding` payload. `TokenType::StreamStart` is now a unit | ||
| variant; the scanner consumes decoded Rust `char` values regardless of their original encoding. | ||
| - Removed the duplicated `Span` from scanner `Comment`. The companion `Token` remains the sole | ||
| source span, and `Comment::new` now accepts only the comment text. | ||
| - Made `Comment` and `Token` fields private. Use `Comment::text`, `Comment::placement`, and | ||
| `Comment::into_text`, or `Token::span`, `Token::token_type`, and `Token::into_parts` to inspect | ||
| them; use their `new` constructors to create values. | ||
| - Made `Tag` fields private. Use `Tag::handle`, `Tag::suffix`, `Tag::original_handle`, `Tag::parts`, | ||
| and `Tag::original_parts` to inspect them, and use the existing constructors to create tags. | ||
| - Changed `ScanError::kind` to return `&ErrorKind` instead of cloning the error category. | ||
| - Consolidated the old `ScanError::new` and convenience `ScanError::new_str` constructors into | ||
| `ScanError::new(marker, impl Into<String>)` and removed `new_str`. | ||
| - `ScanError` now stores a structured `ErrorKind`, and `ScanError::info` renders and returns an owned | ||
| `String` instead of borrowing a stored message. | ||
| - With `std` enabled, retaining arbitrary `std::io::Error` values means `InputIoError` and public | ||
| types containing it no longer guarantee the `UnwindSafe` or `RefUnwindSafe` auto traits. | ||
| - Renamed `Parser::get_anchor_offset` and `ReplayParser::get_anchor_offset` to `anchor_offset`. | ||
| - Removed the duplicate `ParserStack::resolve` method; use `ParserStack::push_include`. Also removed | ||
| the internal `AnyParser` enum from the public API. | ||
| - Made the `parser_stack` module private. Import `ParserStack` and `ReplayParser` directly from the | ||
| crate root. | ||
| **API Additions**: | ||
| - Re-exported `ParseResult` at the crate root. | ||
| - Re-exported `ParserStack` and `ReplayParser` at the crate root. | ||
| - Implemented `FusedIterator` for `Parser`, `Scanner`, `ReplayParser`, and `ParserStack`, and made | ||
| `ReplayParser` directly iterable. | ||
| **Features and Fixes**: | ||
| - Added `FallibleBufferedInput` and `Parser::new_from_fallible_iter` for | ||
| `Iterator<Item = Result<char, ErrorKind>>` sources. Clean iterator exhaustion remains EOF, while | ||
| an error item now terminates parsing as a `ScanError` without polling the source again. Existing | ||
| `BufferedInput` and `Parser::new_from_iter` APIs remain unchanged. | ||
| - Added `ErrorKind::InputIo`, `ErrorKind::InputDecoding`, and | ||
| `ErrorKind::InputByteLimitExceeded` for machine-readable streaming source failures. The provided | ||
| `examples/fallible_reader.rs` defines a documented `CheckedChars` iterator adapter demonstrating | ||
| per-character I/O handling, UTF-8 byte limits, clean EOF, and terminal source errors. | ||
| - Added the optional `std` feature and the stable `InputIoError` wrapper. Message-only construction | ||
| remains available under `no_std`; with `std`, construction from `std::io::Error` retains the | ||
| original error through `InputIoError::io_error()` and the standard error source chain. | ||
| - Made `debug_prints` enable the new `std` feature explicitly. | ||
| - Added the default `Input::take_source_error` hook so existing custom `Input` implementations stay | ||
| source-compatible while fallible inputs can distinguish source failure from EOF. | ||
| - Fixed literal NUL being treated as end-of-input and rejected raw DEL and C0/C1 controls other | ||
| than TAB, LF, CR, and NEL in scalars and comments. The valid double-quoted `"\0"` escape remains | ||
| supported. | ||
| - Added the public, non-exhaustive `ErrorKind` enum and `ScanError::kind()` for matching errors | ||
| without relying on human-readable text. Variants carry relevant structured details where needed, | ||
| such as the opening and closing characters for mismatched flow collections. `ScanError` now | ||
| stores its marker and `ErrorKind`, while descriptions are rendered by `ErrorKind`'s `Display` | ||
| implementation. | ||
| - `ScanError::new(marker, message)` constructs externally supplied errors for parser adapters and | ||
| `ParserStack` include resolvers, copying the message into `ErrorKind::Custom(String)`. | ||
| - `ParserStack` errors now retain their nested source names in `ScanError::source_stack()` and | ||
| include that context in human-readable error messages. | ||
| - Added the default `error_messages` feature. Disabling default features removes human-readable | ||
| error text: `ErrorKind`'s `Display` implementation and `ScanError::info()` return an empty string, | ||
| while structured error kinds and source markers remain available. | ||
| ## v0.0.7 | ||
@@ -3,0 +79,0 @@ - Added `YamlVersion` and changed `Event::DocumentStart` to |
@@ -1,4 +0,2 @@ | ||
| use granit_parser::{ | ||
| parser_stack::ParserStack, Event, Parser, ParserTrait, ScanError, StrInput, Tag, | ||
| }; | ||
| use granit_parser::{Event, Parser, ParserStack, ParserTrait, ScanError, StrInput, Tag}; | ||
@@ -14,9 +12,6 @@ type IncludeStack = ParserStack<'static, core::iter::Empty<char>, StrInput<'static>>; | ||
| fn resolve_include(name: &str) -> Result<String, ScanError> { | ||
| fn resolve_include(name: &str) -> &'static str { | ||
| match name { | ||
| "something.yaml" => Ok("included: &included_anchor from-include\n".to_string()), | ||
| _ => Err(ScanError::new_str( | ||
| granit_parser::Marker::new(0, 1, 0), | ||
| "include not found", | ||
| )), | ||
| "something.yaml" => "included: &included_anchor from-include\n", | ||
| _ => unreachable!("the example only resolves its static include"), | ||
| } | ||
@@ -32,3 +27,3 @@ } | ||
| stack.push_str_parser(Parser::new_from_str(ROOT_YAML), "root.yaml".to_string()); | ||
| stack.set_resolver(resolve_include); | ||
| stack.set_borrowed_resolver(|name| Ok(resolve_include(name))); | ||
@@ -35,0 +30,0 @@ let mut anchored_scalars = Vec::new(); |
+42
-31
@@ -8,8 +8,10 @@ # granit-parser | ||
| [](https://docs.rs/granit-parser) | ||
| [](https://codecov.io/gh/bourumir-wyngs/granit-parser) | ||
| [](https://badge.socket.dev/cargo/package/granit-parser/0.0.1) | ||
| [](https://socket.dev/cargo/package/granit-parser) | ||
| [](https://deps.rs/repo/github/bourumir-wyngs/granit-parser) | ||
| [](https://github.com/bourumir-wyngs/granit-parser/actions/workflows/api-compat.yml) | ||
| [](https://crates.io/crates/granit-parser) | ||
| [](https://crates.io/crates/granit-parser) | ||
| [](https://github.com/bourumir-wyngs/granit-parser/actions/workflows/api-compat.yml) | ||
| [](https://crates.io/crates/granit-parser) | ||
@@ -32,3 +34,5 @@ | ||
| `granit-parser`’s 0.0.1 or 0.0.2 public API is very similar to that of `saphyr-parser`, so it is typically an easy replacement. Later versions emit style and comment information, you need to adjust your code to handle or discard them. | ||
| `granit-parser` 1.0 stabilizes the public API after the experimental 0.0.x releases. The 1.0 | ||
| migration, including scanner and parser-stack replacements, is documented in | ||
| [`CHANGELOG.md`](CHANGELOG.md). | ||
@@ -138,32 +142,6 @@ See [releases](https://github.com/bourumir-wyngs/granit-parser/releases) | ||
| ``` | ||
| ## Event API choices | ||
| Use [`try_load`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.try_load) | ||
| when a receiver may return a validation or application error and parsing should | ||
| stop immediately. It accepts | ||
| [`TryEventReceiver`](https://docs.rs/granit-parser/latest/granit_parser/trait.TryEventReceiver.html) | ||
| or | ||
| [`TrySpannedEventReceiver`](https://docs.rs/granit-parser/latest/granit_parser/trait.TrySpannedEventReceiver.html) | ||
| and returns | ||
| [`TryLoadError`](https://docs.rs/granit-parser/latest/granit_parser/enum.TryLoadError.html) to | ||
| distinguish parser errors from receiver errors. | ||
| Event-only receivers receive comment events as `Event::Comment(text, placement)`. | ||
| Spanned receivers receive the same event plus the comment span in | ||
| [`on_event`](https://docs.rs/granit-parser/latest/granit_parser/trait.SpannedEventReceiver.html#tymethod.on_event). | ||
| When using [`resolve`](https://docs.rs/granit-parser/latest/granit_parser/parser_stack/struct.ParserStack.html#method.resolve) | ||
| or [`push_include`](https://docs.rs/granit-parser/latest/granit_parser/parser_stack/struct.ParserStack.html#method.push_include) | ||
| on `ParserStack`, comment events | ||
| from included documents are forwarded through the normal event stream. Their | ||
| spans remain local to the included source, matching the existing span behavior | ||
| for other included-document events. | ||
| Use the iterator API when the caller should pull events and decide when to stop | ||
| parsing. [`load`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.load) | ||
| is `infallible`. | ||
| ## Key differences from saphyr-parser | ||
| All changes are intentionally scoped around correctness, compliance, and interoperability. | ||
| All changes are intentionally scoped around correctness, compliance, and interoperability. This list summarizes | ||
| differences done from the time of forking (0.0.6). Saphyr-parser may also have changes in its later releases. | ||
@@ -240,3 +218,33 @@ ### YAML compliance fixes | ||
| ### Error handling without grepping message text | ||
| Granit-parser uses the [`ErrorKind`](https://docs.rs/granit-parser/latest/granit_parser/enum.ErrorKind.html) enum to encode the error kind, and this enum may carry additional metadata. This is useful when messages need to be translated (built-in messages are English), or when the using code needs to handle some error specifically without greping for message text. | ||
| ### Fallible streaming input | ||
| [`Parser::new_from_iter`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.new_from_iter) remains available for genuinely infallible sources. Do not use it for an adapter that turns I/O, decoding, or size-limit failures into EOF: an incomplete YAML prefix may itself be a valid document. | ||
| Use [`Parser::new_from_fallible_iter`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.new_from_fallible_iter) for a streaming source that can fail. It accepts an [`Iterator`](https://doc.rust-lang.org/std/iter/trait.Iterator.html) whose items are [`Result<char, ErrorKind>`](https://doc.rust-lang.org/std/result/enum.Result.html). The iterator protocol has three distinct outcomes: | ||
| * `Some(Ok(character))` supplies another character. | ||
| * `None` reports clean physical end-of-input. | ||
| * `Some(Err(error_kind))` reports a terminal source failure. | ||
| The parser returns a [`ScanError`](https://docs.rs/granit-parser/latest/granit_parser/struct.ScanError.html) carrying that same [`ErrorKind`](https://docs.rs/granit-parser/latest/granit_parser/enum.ErrorKind.html) and never polls the source again. | ||
| See the [fallible reader example](examples/fallible_reader.rs). | ||
| The lower-level [`Scanner`](https://docs.rs/granit-parser/latest/granit_parser/struct.Scanner.html) | ||
| is also fallible. Its iterator item is `Result<Token, ScanError>`, so collecting all tokens keeps | ||
| scanner failures distinct from clean EOF: | ||
| ```rust | ||
| use granit_parser::{Scanner, StrInput}; | ||
| let tokens = Scanner::new(StrInput::new("key: value\n")) | ||
| .collect::<Result<Vec<_>, _>>() | ||
| .expect("valid YAML should scan"); | ||
| assert!(!tokens.is_empty()); | ||
| ``` | ||
| ### Security | ||
@@ -260,2 +268,5 @@ | ||
| - `Tag::parts` | ||
| - `Tag::handle` | ||
| - `Tag::suffix` | ||
| - `Tag::original_handle` | ||
| - `Tag::original_parts` | ||
@@ -262,0 +273,0 @@ - `Tag::original` |
@@ -75,3 +75,3 @@ //! Holds functions to determine if a character belongs to a specific character set. | ||
| 'A'..='F' => (c as u32) - ('A' as u32) + 10, | ||
| _ => unreachable!(), | ||
| _ => unreachable!("as_hex called with a non-hexadecimal character"), | ||
| } | ||
@@ -104,3 +104,3 @@ } | ||
| #[must_use] | ||
| fn is_printable(c: char) -> bool { | ||
| pub(crate) fn is_printable(c: char) -> bool { | ||
| matches!( | ||
@@ -107,0 +107,0 @@ c as u32, |
+54
-22
@@ -8,2 +8,4 @@ //! Utilities to create a source of input to the parser. | ||
| use crate::error::ErrorKind; | ||
| pub(crate) mod buffered; | ||
@@ -13,3 +15,3 @@ pub(crate) mod str; | ||
| #[allow(clippy::module_name_repetitions)] | ||
| pub use buffered::BufferedInput; | ||
| pub use buffered::{BufferedInput, FallibleBufferedInput}; | ||
@@ -192,2 +194,17 @@ /// A trait for inputs that can provide borrowed slices with a specific lifetime. | ||
| /// Take a terminal error reported by the underlying source. | ||
| /// | ||
| /// Infallible inputs use the default implementation. Fallible streaming inputs latch their | ||
| /// first source error and return it here so the scanner can distinguish the failure from clean | ||
| /// end-of-input. Once an implementation reports an error, it must not read from its source | ||
| /// again. | ||
| /// | ||
| /// Source adapters should use an input-related [`ErrorKind`] such as | ||
| /// [`ErrorKind::InputIo`], [`ErrorKind::InputDecoding`], or | ||
| /// [`ErrorKind::InputByteLimitExceeded`]. | ||
| #[inline] | ||
| fn take_source_error(&mut self) -> Option<ErrorKind> { | ||
| None | ||
| } | ||
| /// Look for the next character and return it. | ||
@@ -291,6 +308,6 @@ /// | ||
| /// # Errors | ||
| /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event, | ||
| /// the first tuple element will contain the number of characters consumed prior to reaching | ||
| /// the `#`. | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) { | ||
| /// Returns [`ErrorKind::CommentNotSeparated`] if a comment is encountered without preceding | ||
| /// whitespace. In that event, the first tuple element contains the number of characters | ||
| /// consumed prior to reaching the `#`. | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) { | ||
| let mut encountered_tab = false; | ||
@@ -311,6 +328,3 @@ let mut has_yaml_ws = false; | ||
| '#' if !encountered_tab && !has_yaml_ws => { | ||
| return ( | ||
| chars_consumed, | ||
| Err("comments must be separated from other tokens by whitespace"), | ||
| ); | ||
| return (chars_consumed, Err(ErrorKind::CommentNotSeparated)); | ||
| } | ||
@@ -460,11 +474,11 @@ '#' => { | ||
| /// Check whether the next character is [a z]. | ||
| /// Check whether the input is at its physical end. | ||
| /// | ||
| /// The character must have previously been fetched through [`lookahead`] | ||
| /// The default implementation infers end-of-input from the `\0` sentinel returned by | ||
| /// [`Self::peek`]. Inputs that can distinguish a literal NUL from end-of-input should override | ||
| /// this method. | ||
| /// The next position must have previously been fetched through [`Self::lookahead`]. | ||
| /// | ||
| /// # Return | ||
| /// Returns true if the character is [a z], false otherwise. | ||
| /// | ||
| /// [`lookahead`]: Input::lookahead | ||
| /// [a z]: is_z | ||
| /// Returns true if the input is exhausted, false otherwise. | ||
| #[inline] | ||
@@ -517,5 +531,5 @@ fn next_is_z(&self) -> bool { | ||
| /// Skip characters from the input until a [breakz] is found. | ||
| /// Skip printable characters until a [breakz] or non-printable character is found. | ||
| /// | ||
| /// The characters are consumed from the input. | ||
| /// The stopping character is not consumed. | ||
| /// | ||
@@ -530,3 +544,6 @@ /// # Return | ||
| let mut count = 0; | ||
| while !is_breakz(self.look_ch()) { | ||
| while { | ||
| let c = self.look_ch(); | ||
| !is_breakz(c) && crate::char_traits::is_printable(c) | ||
| } { | ||
| count += 1; | ||
@@ -619,2 +636,19 @@ self.skip(); | ||
| } | ||
| /// Consume a chunk of plain-scalar characters without materializing them. | ||
| /// | ||
| /// Stable inputs use this while the scanner retains the source as a borrowed slice and only | ||
| /// promotes it to an owned buffer if YAML folding changes the scalar contents. | ||
| fn skip_plain_scalar_chunk(&mut self, count: usize, flow_level_gt_0: bool) -> (bool, usize) { | ||
| let mut chars_consumed = 0; | ||
| for _ in 0..count { | ||
| self.lookahead(1); | ||
| if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) { | ||
| return (true, chars_consumed); | ||
| } | ||
| self.skip(); | ||
| chars_consumed += 1; | ||
| } | ||
| (false, chars_consumed) | ||
| } | ||
| } | ||
@@ -661,2 +695,3 @@ | ||
| use super::{Input, SkipTabs}; | ||
| use crate::error::ErrorKind; | ||
@@ -721,8 +756,5 @@ struct MinimalInput; | ||
| assert_eq!(consumed, 0); | ||
| assert_eq!( | ||
| result.err(), | ||
| Some("comments must be separated from other tokens by whitespace") | ||
| ); | ||
| assert_eq!(result.err(), Some(ErrorKind::CommentNotSeparated)); | ||
| assert_eq!(input.peek(), '#'); | ||
| } | ||
| } |
+132
-0
| use crate::char_traits::is_breakz; | ||
| use crate::error::ErrorKind; | ||
| use crate::input::{BorrowedInput, Input}; | ||
@@ -183,2 +184,7 @@ | ||
| } | ||
| #[inline] | ||
| fn next_is_z(&self) -> bool { | ||
| self.source_exhausted && self.real_buffered == 0 | ||
| } | ||
| } | ||
@@ -195,2 +201,128 @@ | ||
| /// Adapter that exposes successful items to [`BufferedInput`] and latches the first source error. | ||
| struct FallibleChars<T: Iterator<Item = Result<char, ErrorKind>>> { | ||
| input: T, | ||
| error: Option<ErrorKind>, | ||
| finished: bool, | ||
| } | ||
| impl<T: Iterator<Item = Result<char, ErrorKind>>> FallibleChars<T> { | ||
| fn new(input: T) -> Self { | ||
| Self { | ||
| input, | ||
| error: None, | ||
| finished: false, | ||
| } | ||
| } | ||
| } | ||
| impl<T: Iterator<Item = Result<char, ErrorKind>>> Iterator for FallibleChars<T> { | ||
| type Item = char; | ||
| fn next(&mut self) -> Option<Self::Item> { | ||
| if self.finished { | ||
| return None; | ||
| } | ||
| match self.input.next() { | ||
| Some(Ok(c)) => Some(c), | ||
| Some(Err(error)) => { | ||
| self.error = Some(error); | ||
| self.finished = true; | ||
| None | ||
| } | ||
| None => { | ||
| self.finished = true; | ||
| None | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /// A buffered wrapper around a fallible iterator of characters. | ||
| /// | ||
| /// The iterator uses its normal `None` return value for clean end-of-input and returns source | ||
| /// failures as `Some(Err(error))`, where `error` is an [`ErrorKind`]. The first error is latched, | ||
| /// parsing becomes terminal, and the underlying iterator is never polled again. | ||
| #[allow(clippy::module_name_repetitions)] | ||
| pub struct FallibleBufferedInput<T: Iterator<Item = Result<char, ErrorKind>>> { | ||
| inner: BufferedInput<FallibleChars<T>>, | ||
| } | ||
| impl<T: Iterator<Item = Result<char, ErrorKind>>> FallibleBufferedInput<T> { | ||
| /// Create a buffered input over a fallible character iterator. | ||
| pub fn new(input: T) -> Self { | ||
| Self { | ||
| inner: BufferedInput::new(FallibleChars::new(input)), | ||
| } | ||
| } | ||
| } | ||
| impl<T: Iterator<Item = Result<char, ErrorKind>>> Input for FallibleBufferedInput<T> { | ||
| #[inline] | ||
| fn lookahead(&mut self, count: usize) { | ||
| self.inner.lookahead(count); | ||
| } | ||
| #[inline] | ||
| fn buflen(&self) -> usize { | ||
| self.inner.buflen() | ||
| } | ||
| #[inline] | ||
| fn bufmaxlen(&self) -> usize { | ||
| self.inner.bufmaxlen() | ||
| } | ||
| #[inline] | ||
| fn raw_read_ch(&mut self) -> char { | ||
| self.inner.raw_read_ch() | ||
| } | ||
| #[inline] | ||
| fn raw_read_non_breakz_ch(&mut self) -> Option<char> { | ||
| self.inner.raw_read_non_breakz_ch() | ||
| } | ||
| #[inline] | ||
| fn skip(&mut self) { | ||
| self.inner.skip(); | ||
| } | ||
| #[inline] | ||
| fn skip_n(&mut self, count: usize) { | ||
| self.inner.skip_n(count); | ||
| } | ||
| #[inline] | ||
| fn peek(&self) -> char { | ||
| self.inner.peek() | ||
| } | ||
| #[inline] | ||
| fn peek_nth(&self, n: usize) -> char { | ||
| self.inner.peek_nth(n) | ||
| } | ||
| #[inline] | ||
| fn next_is_z(&self) -> bool { | ||
| self.inner.next_is_z() | ||
| } | ||
| #[inline] | ||
| fn take_source_error(&mut self) -> Option<ErrorKind> { | ||
| self.inner.input.error.take() | ||
| } | ||
| } | ||
| /// `FallibleBufferedInput` is a streaming input and cannot provide stable borrowed slices. | ||
| impl<T: Iterator<Item = Result<char, ErrorKind>>> BorrowedInput<'static> | ||
| for FallibleBufferedInput<T> | ||
| { | ||
| #[inline] | ||
| fn slice_borrowed(&self, _start: usize, _end: usize) -> Option<&'static str> { | ||
| None | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
@@ -197,0 +329,0 @@ mod tests { |
+57
-97
| use crate::{ | ||
| char_traits::{is_blank_or_breakz, is_breakz, is_flow}, | ||
| char_traits::is_breakz, | ||
| error::ErrorKind, | ||
| input::{BorrowedInput, Input, SkipTabs}, | ||
@@ -47,2 +48,41 @@ }; | ||
| } | ||
| /// Find the end of the next plain-scalar chunk in the current input buffer. | ||
| /// | ||
| /// Returns its byte length and character count. This keeps the existing batched scanner in one | ||
| /// place so callers can either materialize the chunk or retain it as a borrowed source slice. | ||
| fn plain_scalar_chunk_len(&self, flow_level_gt_0: bool) -> (usize, usize) { | ||
| let bytes = self.buffer.as_bytes(); | ||
| let mut byte_pos = 0; | ||
| let mut chars_consumed = 0; | ||
| while byte_pos < bytes.len() { | ||
| let byte = bytes[byte_pos]; | ||
| if byte < 0x80 { | ||
| let character = byte as char; | ||
| if crate::char_traits::is_blank_or_breakz(character) | ||
| || flow_level_gt_0 && crate::char_traits::is_flow(character) | ||
| { | ||
| break; | ||
| } | ||
| if character == ':' { | ||
| let next_byte = bytes.get(byte_pos + 1).copied().unwrap_or(0); | ||
| // A non-ASCII character cannot be blank, breakz, or a flow indicator. | ||
| let stops_scalar = next_byte < 0x80 | ||
| && (crate::char_traits::is_blank_or_breakz(next_byte as char) | ||
| || flow_level_gt_0 && crate::char_traits::is_flow(next_byte as char)); | ||
| if stops_scalar { | ||
| break; | ||
| } | ||
| } | ||
| byte_pos += 1; | ||
| } else { | ||
| let character = self.buffer[byte_pos..].chars().next().unwrap(); | ||
| byte_pos += character.len_utf8(); | ||
| } | ||
| chars_consumed += 1; | ||
| } | ||
| (byte_pos, chars_consumed) | ||
| } | ||
| } | ||
@@ -69,6 +109,2 @@ | ||
| fn buf_is_empty(&self) -> bool { | ||
| self.buflen() == 0 | ||
| } | ||
| #[inline] | ||
@@ -173,18 +209,2 @@ fn raw_read_ch(&mut self) -> char { | ||
| #[inline] | ||
| fn look_ch(&mut self) -> char { | ||
| self.lookahead(1); | ||
| self.peek() | ||
| } | ||
| #[inline] | ||
| fn next_char_is(&self, c: char) -> bool { | ||
| self.peek() == c | ||
| } | ||
| #[inline] | ||
| fn nth_char_is(&self, n: usize, c: char) -> bool { | ||
| self.peek_nth(n) == c | ||
| } | ||
| #[inline] | ||
| fn next_2_are(&self, c1: char, c2: char) -> bool { | ||
@@ -243,3 +263,3 @@ let mut chars = self.buffer.chars(); | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) { | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) { | ||
| assert!(!matches!(skip_tabs, SkipTabs::Result(..))); | ||
@@ -277,6 +297,3 @@ | ||
| if !encountered_tab && !has_yaml_ws { | ||
| return ( | ||
| chars_consumed, | ||
| Err("comments must be separated from other tokens by whitespace"), | ||
| ); | ||
| return (chars_consumed, Err(ErrorKind::CommentNotSeparated)); | ||
| } | ||
@@ -336,14 +353,2 @@ | ||
| #[allow(clippy::inline_always)] | ||
| #[inline(always)] | ||
| fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool { | ||
| let nc = self.peek_nth(1); | ||
| match self.peek() { | ||
| // indicators can end a plain scalar, see 7.3.3. Plain Style | ||
| ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false, | ||
| c if in_flow && is_flow(c) => false, | ||
| _ => true, | ||
| } | ||
| } | ||
| #[inline] | ||
@@ -377,3 +382,3 @@ fn next_is_blank_or_break(&self) -> bool { | ||
| fn next_is_z(&self) -> bool { | ||
| self.buffer.is_empty() || self.buffer.as_bytes()[0] == 0 | ||
| self.buffer.is_empty() | ||
| } | ||
@@ -410,3 +415,3 @@ | ||
| for (i, c) in self.buffer.char_indices() { | ||
| if is_breakz(c) { | ||
| if is_breakz(c) || !crate::char_traits::is_printable(c) { | ||
| break; | ||
@@ -490,58 +495,13 @@ } | ||
| ) -> (bool, usize) { | ||
| let bytes = self.buffer.as_bytes(); | ||
| let len = bytes.len(); | ||
| let mut byte_pos = 0; | ||
| let mut chars_consumed = 0; | ||
| while byte_pos < len { | ||
| let b = bytes[byte_pos]; | ||
| if b < 0x80 { | ||
| let c = b as char; | ||
| if crate::char_traits::is_blank_or_breakz(c) { | ||
| out.push_str(&self.buffer[..byte_pos]); | ||
| self.buffer = &self.buffer[byte_pos..]; | ||
| return (true, chars_consumed); | ||
| } | ||
| if flow_level_gt_0 && crate::char_traits::is_flow(c) { | ||
| out.push_str(&self.buffer[..byte_pos]); | ||
| self.buffer = &self.buffer[byte_pos..]; | ||
| return (true, chars_consumed); | ||
| } | ||
| if c == ':' { | ||
| let next_byte = if byte_pos + 1 < len { | ||
| bytes[byte_pos + 1] | ||
| } else { | ||
| 0 | ||
| }; | ||
| // ASCII optimization: if next_byte >= 0x80, it is not blank/breakz/flow | ||
| let is_stop = if next_byte < 0x80 { | ||
| let nc = next_byte as char; | ||
| crate::char_traits::is_blank_or_breakz(nc) | ||
| || (flow_level_gt_0 && crate::char_traits::is_flow(nc)) | ||
| } else { | ||
| false | ||
| }; | ||
| if is_stop { | ||
| out.push_str(&self.buffer[..byte_pos]); | ||
| self.buffer = &self.buffer[byte_pos..]; | ||
| return (true, chars_consumed); | ||
| } | ||
| } | ||
| byte_pos += 1; | ||
| chars_consumed += 1; | ||
| } else { | ||
| let mut chars = self.buffer[byte_pos..].chars(); | ||
| let c = chars.next().unwrap(); | ||
| byte_pos += c.len_utf8(); | ||
| chars_consumed += 1; | ||
| } | ||
| } | ||
| let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0); | ||
| out.push_str(&self.buffer[..byte_pos]); | ||
| self.buffer = &self.buffer[byte_pos..]; | ||
| // If we reached here, we consumed the whole string (EOF). | ||
| // EOF is effectively a stop condition (breakz). | ||
| (true, chars_consumed) | ||
| } | ||
| fn skip_plain_scalar_chunk(&mut self, _count: usize, flow_level_gt_0: bool) -> (bool, usize) { | ||
| let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0); | ||
| self.buffer = &self.buffer[byte_pos..]; | ||
| (true, chars_consumed) | ||
| } | ||
| } | ||
@@ -595,3 +555,6 @@ | ||
| use crate::input::{BorrowedInput, Input, SkipTabs}; | ||
| use crate::{ | ||
| error::ErrorKind, | ||
| input::{BorrowedInput, Input, SkipTabs}, | ||
| }; | ||
@@ -714,6 +677,3 @@ use super::StrInput; | ||
| assert_eq!(consumed, 0); | ||
| assert_eq!( | ||
| result.err(), | ||
| Some("comments must be separated from other tokens by whitespace") | ||
| ); | ||
| assert_eq!(result.err(), Some(ErrorKind::CommentNotSeparated)); | ||
| assert_eq!(input.peek(), '#'); | ||
@@ -720,0 +680,0 @@ } |
+20
-9
@@ -66,2 +66,12 @@ // Copyright 2015, Yuheng Chen. | ||
| //! | ||
| //! #### `error_messages` (enabled by default) | ||
| //! Provides human-readable text through [`ErrorKind`]'s `Display` implementation and | ||
| //! [`ScanError::info`]. Disabling this feature makes both render an empty string while retaining | ||
| //! machine-readable error kinds and source markers. | ||
| //! | ||
| //! #### `std` | ||
| //! Retains the original `std::io::Error` inside [`InputIoError`] when constructed through | ||
| //! `InputIoError::from_io` or its `From<std::io::Error>` implementation. Without this feature, | ||
| //! [`InputIoError::from_message`] remains available for portable `no_std` error reporting. | ||
| //! | ||
| //! #### `debug_prints` | ||
@@ -75,3 +85,3 @@ //! Enables the `debug` module and usage of debug prints in the scanner and the parser. Do not | ||
| //! | ||
| //! This feature is _not_ `no_std` compatible. | ||
| //! This feature enables `std` and is _not_ `no_std` compatible. | ||
@@ -82,6 +92,5 @@ #![forbid(unsafe_code)] | ||
| #[macro_use] | ||
| extern crate alloc; | ||
| #[cfg(feature = "debug_prints")] | ||
| #[cfg(feature = "std")] | ||
| extern crate std; | ||
@@ -92,15 +101,17 @@ | ||
| mod debug; | ||
| mod error; | ||
| pub mod input; | ||
| mod parser; | ||
| /// A stack-based parser implementation. | ||
| pub mod parser_stack; | ||
| mod parser_stack; | ||
| mod scanner; | ||
| pub use crate::input::{str::StrInput, BorrowedInput, BufferedInput, Input}; | ||
| pub use crate::error::{ErrorKind, InputIoError, ScanError}; | ||
| pub use crate::input::{str::StrInput, BorrowedInput, BufferedInput, FallibleBufferedInput, Input}; | ||
| pub use crate::parser::{ | ||
| Event, EventReceiver, Parser, ParserTrait, SpannedEventReceiver, StructureStyle, Tag, | ||
| TryEventReceiver, TryLoadError, TrySpannedEventReceiver, YamlVersion, | ||
| Event, EventReceiver, ParseResult, Parser, ParserTrait, SpannedEventReceiver, StructureStyle, | ||
| Tag, TryEventReceiver, TryLoadError, TrySpannedEventReceiver, YamlVersion, | ||
| }; | ||
| pub use crate::parser_stack::{ParserStack, ReplayParser}; | ||
| pub use crate::scanner::{ | ||
| Comment, Marker, Placement, ScalarStyle, ScanError, Scanner, Span, Token, TokenType, | ||
| Comment, Marker, Placement, ScalarStyle, Scanner, Span, Token, TokenType, | ||
| }; |
+133
-65
| use crate::{ | ||
| error::{ErrorKind, ScanError}, | ||
| input::{str::StrInput, BorrowedInput, BufferedInput}, | ||
| parser::{Event, ParseResult, Parser, ParserTrait, SpannedEventReceiver}, | ||
| scanner::{ScanError, Span}, | ||
| scanner::Span, | ||
| }; | ||
| use alloc::{boxed::Box, string::String, vec::Vec}; | ||
| use alloc::{borrow::Cow, boxed::Box, string::String, vec::Vec}; | ||
| /// A lightweight parser that replays a pre-collected event stream. | ||
| pub struct ReplayParser<'input> { | ||
| events: Vec<(Event<'input>, Span)>, | ||
| index: usize, | ||
| events: alloc::vec::IntoIter<(Event<'input>, Span)>, | ||
| anchor_offset: usize, | ||
@@ -20,4 +20,3 @@ } | ||
| Self { | ||
| events, | ||
| index: 0, | ||
| events: events.into_iter(), | ||
| anchor_offset, | ||
@@ -29,3 +28,3 @@ } | ||
| #[must_use] | ||
| pub fn get_anchor_offset(&self) -> usize { | ||
| pub fn anchor_offset(&self) -> usize { | ||
| self.anchor_offset | ||
@@ -55,8 +54,7 @@ } | ||
| fn peek(&mut self) -> Option<Result<&(Event<'input>, Span), ScanError>> { | ||
| self.events.get(self.index).map(Ok) | ||
| self.events.as_slice().first().map(Ok) | ||
| } | ||
| fn next_event(&mut self) -> Option<ParseResult<'input>> { | ||
| let event = self.events.get(self.index).cloned()?; | ||
| self.index += 1; | ||
| let event = self.events.next()?; | ||
| self.advance_anchor_offset(&event.0); | ||
@@ -87,4 +85,14 @@ Some(Ok(event)) | ||
| impl<'input> Iterator for ReplayParser<'input> { | ||
| type Item = ParseResult<'input>; | ||
| fn next(&mut self) -> Option<Self::Item> { | ||
| self.next_event() | ||
| } | ||
| } | ||
| impl core::iter::FusedIterator for ReplayParser<'_> {} | ||
| /// A wrapper for different types of parsers. | ||
| pub enum AnyParser<'input, I, T> | ||
| enum AnyParser<'input, I, T> | ||
| where | ||
@@ -129,8 +137,8 @@ I: Iterator<Item = char>, | ||
| { | ||
| fn get_anchor_offset(&self) -> usize { | ||
| fn anchor_offset(&self) -> usize { | ||
| match self { | ||
| AnyParser::String { parser, .. } => parser.get_anchor_offset(), | ||
| AnyParser::Iter { parser, .. } => parser.get_anchor_offset(), | ||
| AnyParser::Custom { parser, .. } => parser.get_anchor_offset(), | ||
| AnyParser::Replay { parser, .. } => parser.get_anchor_offset(), | ||
| AnyParser::String { parser, .. } => parser.anchor_offset(), | ||
| AnyParser::Iter { parser, .. } => parser.anchor_offset(), | ||
| AnyParser::Custom { parser, .. } => parser.anchor_offset(), | ||
| AnyParser::Replay { parser, .. } => parser.anchor_offset(), | ||
| } | ||
@@ -161,3 +169,4 @@ } | ||
| /// like every other event span from an included parser. `ParserStack` does not attach file names, | ||
| /// source IDs, or other include provenance to events or spans. | ||
| /// source IDs, or other include provenance to events or spans. Errors do retain the nested source | ||
| /// names through [`ScanError::source_stack`]. | ||
| pub struct ParserStack<'input, I = core::iter::Empty<char>, T = StrInput<'input>> | ||
@@ -173,3 +182,3 @@ where | ||
| #[allow(clippy::type_complexity)] | ||
| include_resolver: Option<Box<dyn FnMut(&str) -> Result<String, ScanError> + 'input>>, | ||
| include_resolver: Option<Box<dyn FnMut(&str) -> Result<Cow<'input, str>, ScanError> + 'input>>, | ||
| } | ||
@@ -194,3 +203,3 @@ | ||
| /// Set the resolver used by [`Self::resolve`] and [`Self::push_include`]. | ||
| /// Set the resolver used by [`Self::push_include`]. | ||
| /// | ||
@@ -200,9 +209,21 @@ /// The resolver receives the include name and returns the included YAML source text. | ||
| &mut self, | ||
| resolver: impl FnMut(&str) -> Result<String, ScanError> + 'input, | ||
| mut resolver: impl FnMut(&str) -> Result<String, ScanError> + 'input, | ||
| ) { | ||
| self.include_resolver = Some(Box::new(resolver)); | ||
| self.include_resolver = Some(Box::new(move |name| resolver(name).map(Cow::Owned))); | ||
| } | ||
| /// Resolves an include string using the include resolver. | ||
| /// Set an include resolver whose source text can be borrowed for the stack's input lifetime. | ||
| /// | ||
| /// Unlike [`Self::set_resolver`], this path lets scalar, comment, anchor, and tag token text in | ||
| /// included documents borrow directly from the returned source. The included document is still | ||
| /// validated eagerly so resolution errors retain the same timing and source-stack context. | ||
| pub fn set_borrowed_resolver( | ||
| &mut self, | ||
| mut resolver: impl FnMut(&str) -> Result<&'input str, ScanError> + 'input, | ||
| ) { | ||
| self.include_resolver = Some(Box::new(move |name| resolver(name).map(Cow::Borrowed))); | ||
| } | ||
| /// Resolve an include by name and push the resulting parser onto the stack. | ||
| /// | ||
| /// Comment events from the included content are preserved. Their spans are local to the | ||
@@ -215,38 +236,68 @@ /// included content returned by the resolver, matching the existing behavior for all included | ||
| /// included content cannot be parsed. | ||
| pub fn resolve(&mut self, include_str: &str) -> Result<(), ScanError> { | ||
| if let Some(resolver) = &mut self.include_resolver { | ||
| let content = resolver(include_str)?; | ||
| let mut parser = Parser::new_from_iter(content.chars().collect::<Vec<_>>().into_iter()); | ||
| if let Some(parent) = self.parsers.last() { | ||
| parser.set_anchor_offset(parent.get_anchor_offset()); | ||
| pub fn push_include(&mut self, include_str: &str) -> Result<(), ScanError> { | ||
| let resolved = match &mut self.include_resolver { | ||
| Some(resolver) => resolver(include_str), | ||
| None => { | ||
| return Err(self.contextualize_include_error( | ||
| ScanError::from_kind( | ||
| crate::scanner::Marker::new(0, 1, 0), | ||
| ErrorKind::MissingIncludeResolver, | ||
| ), | ||
| include_str, | ||
| )); | ||
| } | ||
| let mut events = Vec::new(); | ||
| while let Some(event) = parser.next_event() { | ||
| events.push(event?); | ||
| }; | ||
| let content = match resolved { | ||
| Ok(content) => content, | ||
| Err(error) => return Err(self.contextualize_include_error(error, include_str)), | ||
| }; | ||
| let inherited_anchor_offset = self.parsers.last().map(AnyParser::anchor_offset); | ||
| let (events, next_anchor_offset) = match content { | ||
| Cow::Borrowed(content) => { | ||
| let mut parser = Parser::new_from_str(content); | ||
| if let Some(anchor_offset) = inherited_anchor_offset { | ||
| parser.set_anchor_offset(anchor_offset); | ||
| } | ||
| let mut events = Vec::new(); | ||
| while let Some(event) = parser.next_event() { | ||
| match event { | ||
| Ok(event) => events.push(event), | ||
| Err(error) => { | ||
| return Err(self.contextualize_include_error(error, include_str)); | ||
| } | ||
| } | ||
| } | ||
| (events, parser.anchor_offset()) | ||
| } | ||
| Cow::Owned(content) => { | ||
| let mut parser = | ||
| Parser::new_from_iter(content.chars().collect::<Vec<_>>().into_iter()); | ||
| if let Some(anchor_offset) = inherited_anchor_offset { | ||
| parser.set_anchor_offset(anchor_offset); | ||
| } | ||
| let mut events = Vec::new(); | ||
| while let Some(event) = parser.next_event() { | ||
| match event { | ||
| Ok(event) => events.push(event), | ||
| Err(error) => { | ||
| return Err(self.contextualize_include_error(error, include_str)); | ||
| } | ||
| } | ||
| } | ||
| (events, parser.anchor_offset()) | ||
| } | ||
| }; | ||
| self.push_replay_parser( | ||
| ReplayParser::new(events, parser.get_anchor_offset()), | ||
| include_str.into(), | ||
| ); | ||
| Ok(()) | ||
| } else { | ||
| Err(ScanError::new( | ||
| crate::scanner::Marker::new(0, 1, 0), | ||
| String::from("No include resolver set for parser stack."), | ||
| )) | ||
| } | ||
| self.push_replay_parser( | ||
| ReplayParser::new(events, next_anchor_offset), | ||
| include_str.into(), | ||
| ); | ||
| Ok(()) | ||
| } | ||
| /// Resolves an include by name and pushes the resulting parser onto the stack. | ||
| /// | ||
| /// This is an alias for [`Self::resolve`] with a name that reads naturally in | ||
| /// include-oriented consumers: `stack.push_include("config.yaml")?`. | ||
| /// Comment spans from the included content are local to that included source. | ||
| /// | ||
| /// # Errors | ||
| /// Returns `ScanError` if no resolver is configured, include resolution fails, or the | ||
| /// included content cannot be parsed. | ||
| pub fn push_include(&mut self, include_name: &str) -> Result<(), ScanError> { | ||
| self.resolve(include_name) | ||
| fn contextualize_include_error(&self, error: ScanError, include_str: &str) -> ScanError { | ||
| let mut source_stack = self.stack(); | ||
| source_stack.push(include_str.into()); | ||
| error.with_source_stack(source_stack) | ||
| } | ||
@@ -258,3 +309,2 @@ | ||
| } | ||
| self.stream_end_emitted = false; | ||
| } | ||
@@ -269,3 +319,3 @@ | ||
| if let Some(parent) = self.parsers.last() { | ||
| parser.set_anchor_offset(parent.get_anchor_offset()); | ||
| parser.set_anchor_offset(parent.anchor_offset()); | ||
| } | ||
@@ -286,3 +336,3 @@ self.parsers.push(AnyParser::String { parser, name }); | ||
| if let Some(parent) = self.parsers.last() { | ||
| parser.set_anchor_offset(parent.get_anchor_offset()); | ||
| parser.set_anchor_offset(parent.anchor_offset()); | ||
| } | ||
@@ -299,3 +349,3 @@ self.parsers.push(AnyParser::Iter { parser, name }); | ||
| if let Some(parent) = self.parsers.last() { | ||
| parser.set_anchor_offset(parent.get_anchor_offset()); | ||
| parser.set_anchor_offset(parent.anchor_offset()); | ||
| } | ||
@@ -312,4 +362,4 @@ self.parsers.push(AnyParser::Custom { parser, name }); | ||
| if let Some(parent) = self.parsers.last() { | ||
| let inherited = parent.get_anchor_offset(); | ||
| parser.set_anchor_offset(parser.get_anchor_offset().max(inherited)); | ||
| let inherited = parent.anchor_offset(); | ||
| parser.set_anchor_offset(parser.anchor_offset().max(inherited)); | ||
| } | ||
@@ -332,3 +382,3 @@ | ||
| if let Some(parent) = self.parsers.last() { | ||
| parser.set_anchor_offset(parent.get_anchor_offset()); | ||
| parser.set_anchor_offset(parent.anchor_offset()); | ||
| } | ||
@@ -342,3 +392,3 @@ self.parsers.push(AnyParser::Custom { parser, name }); | ||
| pub fn current_anchor_offset(&self) -> usize { | ||
| self.parsers.last().map_or(0, AnyParser::get_anchor_offset) | ||
| self.parsers.last().map_or(0, AnyParser::anchor_offset) | ||
| } | ||
@@ -360,5 +410,13 @@ | ||
| fn contextualize_error(&self, error: ScanError) -> ScanError { | ||
| if self.parsers.len() > 1 { | ||
| error.with_source_stack(self.stack()) | ||
| } else { | ||
| error | ||
| } | ||
| } | ||
| fn propagate_anchor_offset_from_popped(&mut self, popped: &AnyParser<'input, I, T>) { | ||
| if let Some(parent) = self.parsers.last_mut() { | ||
| let next_offset = parent.get_anchor_offset().max(popped.get_anchor_offset()); | ||
| let next_offset = parent.anchor_offset().max(popped.anchor_offset()); | ||
| parent.set_anchor_offset(next_offset); | ||
@@ -408,2 +466,3 @@ } | ||
| Some(Err(e)) => { | ||
| let e = self.contextualize_error(e); | ||
| self.pop_parser_and_propagate_anchor_offset(); | ||
@@ -430,9 +489,11 @@ return e.into_result(); | ||
| Some(Ok(_)) => { | ||
| self.pop_parser_and_propagate_anchor_offset(); | ||
| return Err(ScanError::new_str( | ||
| let error = self.contextualize_error(ScanError::from_kind( | ||
| span.start, | ||
| "multiple documents not supported here", | ||
| ErrorKind::MultipleDocumentsUnsupported, | ||
| )); | ||
| self.pop_parser_and_propagate_anchor_offset(); | ||
| return Err(error); | ||
| } | ||
| Some(Err(e)) => { | ||
| let e = self.contextualize_error(e); | ||
| self.pop_parser_and_propagate_anchor_offset(); | ||
@@ -562,1 +623,8 @@ return Err(e); | ||
| } | ||
| impl<'input, I, T> core::iter::FusedIterator for ParserStack<'input, I, T> | ||
| where | ||
| I: Iterator<Item = char>, | ||
| T: BorrowedInput<'input>, | ||
| { | ||
| } |
+3
-3
@@ -477,5 +477,5 @@ #![allow(clippy::bool_assert_comparison)] | ||
| { | ||
| assert!(a == b); | ||
| assert!(a == c); | ||
| assert!(a == d); | ||
| assert_eq!(a, b); | ||
| assert_eq!(a, c); | ||
| assert_eq!(a, d); | ||
| } | ||
@@ -482,0 +482,0 @@ } |
+123
-106
@@ -13,10 +13,3 @@ use std::{borrow::Cow, cell::Cell, rc::Rc}; | ||
| fn scanner_tokens(source: &str) -> Result<Vec<Token<'_>>, ScanError> { | ||
| let mut scanner = Scanner::new(StrInput::new(source)); | ||
| let mut tokens = Vec::new(); | ||
| while let Some(token) = scanner.next_token()? { | ||
| tokens.push(token); | ||
| } | ||
| Ok(tokens) | ||
| Scanner::new(StrInput::new(source)).collect() | ||
| } | ||
@@ -65,3 +58,3 @@ | ||
| Ok(_) => {} | ||
| Err(error) => return (read.get(), error.info().to_owned()), | ||
| Err(error) => return (read.get(), error.info()), | ||
| } | ||
@@ -138,11 +131,6 @@ } | ||
| fn comment_type_is_a_convenience_container() { | ||
| let span = Span::new( | ||
| Marker::new(0, 1, 0).with_byte_offset(Some(0)), | ||
| Marker::new(11, 1, 11).with_byte_offset(Some(11)), | ||
| ); | ||
| let comment = Comment::new(span, " payload "); | ||
| let comment = Comment::new(" payload "); | ||
| assert_eq!(comment.span, span); | ||
| assert_eq!(comment.text, " payload "); | ||
| assert_eq!(comment.placement, Placement::Free); | ||
| assert_eq!(comment.text(), " payload "); | ||
| assert_eq!(comment.placement(), Placement::Free); | ||
| assert_eq!(comment.as_ref(), " payload "); | ||
@@ -154,9 +142,5 @@ assert_eq!(comment.trimmed_text(), "payload"); | ||
| fn comment_type_preserves_single_space_payload() { | ||
| let span = Span::new( | ||
| Marker::new(0, 1, 0).with_byte_offset(Some(0)), | ||
| Marker::new(2, 1, 2).with_byte_offset(Some(2)), | ||
| ); | ||
| let comment = Comment::new(span, " "); | ||
| let comment = Comment::new(" "); | ||
| assert_eq!(comment.text, " "); | ||
| assert_eq!(comment.text(), " "); | ||
| assert_eq!(comment.trimmed_text(), ""); | ||
@@ -184,10 +168,10 @@ } | ||
| ); | ||
| let token = Token( | ||
| let token = Token::new( | ||
| span, | ||
| TokenType::Comment(Comment::new(span, " payload").with_placement(Placement::Right)), | ||
| TokenType::Comment(Comment::new(" payload").with_placement(Placement::Right)), | ||
| ); | ||
| assert_eq!(token.0.slice(yaml), Some("# payload")); | ||
| assert!(matches!(token.1, TokenType::Comment(ref comment) | ||
| if comment.text == " payload" && comment.placement == Placement::Right)); | ||
| assert_eq!(token.span().slice(yaml), Some("# payload")); | ||
| assert!(matches!(token.token_type(), TokenType::Comment(comment) | ||
| if comment.text() == " payload" && comment.placement() == Placement::Right)); | ||
| } | ||
@@ -198,8 +182,8 @@ | ||
| let yaml = "# top\n # indented\nkey: value # trailing\n#eof"; | ||
| let tokens = Scanner::new(StrInput::new(yaml)).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens(yaml).expect("valid YAML should scan without errors"); | ||
| let comments: Vec<_> = tokens | ||
| .iter() | ||
| .filter_map(|Token(span, token)| match token { | ||
| TokenType::Comment(comment) => Some((comment.text.as_ref(), span.slice(yaml))), | ||
| .filter_map(|token| match token.token_type() { | ||
| TokenType::Comment(comment) => Some((comment.text(), token.span().slice(yaml))), | ||
| _ => None, | ||
@@ -223,8 +207,8 @@ }) | ||
| let yaml = "# own line\nkey: value # right\n"; | ||
| let tokens = Scanner::new(StrInput::new(yaml)).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens(yaml).expect("valid YAML should scan without errors"); | ||
| let comments: Vec<_> = tokens | ||
| .iter() | ||
| .filter_map(|Token(_, token)| match token { | ||
| TokenType::Comment(comment) => Some((comment.text.as_ref(), comment.placement)), | ||
| .filter_map(|token| match token.token_type() { | ||
| TokenType::Comment(comment) => Some((comment.text(), comment.placement())), | ||
| _ => None, | ||
@@ -250,4 +234,6 @@ }) | ||
| for (yaml, expected_text) in cases { | ||
| let comment = Scanner::new(StrInput::new(yaml)) | ||
| .find_map(|Token(_, token)| match token { | ||
| let comment = scanner_tokens(yaml) | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .find_map(|token| match token.into_parts().1 { | ||
| TokenType::Comment(comment) => Some(comment), | ||
@@ -258,4 +244,4 @@ _ => None, | ||
| assert_eq!(comment.text, expected_text, "{yaml:?}"); | ||
| assert_eq!(comment.placement, Placement::Right, "{yaml:?}"); | ||
| assert_eq!(comment.text(), expected_text, "{yaml:?}"); | ||
| assert_eq!(comment.placement(), Placement::Right, "{yaml:?}"); | ||
| } | ||
@@ -266,8 +252,9 @@ } | ||
| fn scanner_emits_trailing_comment_after_plain_scalar_token() { | ||
| let tokens = Scanner::new(StrInput::new("key: value # trailing\n")).collect::<Vec<Token<'_>>>(); | ||
| let tokens = | ||
| scanner_tokens("key: value # trailing\n").expect("valid YAML should scan without errors"); | ||
| let value_index = tokens | ||
| .iter() | ||
| .position(|Token(_, token)| { | ||
| matches!(token, TokenType::Scalar(ScalarStyle::Plain, value) if value == "value") | ||
| .position(|token| { | ||
| matches!(token.token_type(), TokenType::Scalar(ScalarStyle::Plain, value) if value == "value") | ||
| }) | ||
@@ -277,4 +264,4 @@ .expect("plain scalar token should be emitted"); | ||
| .iter() | ||
| .position(|Token(_, token)| { | ||
| matches!(token, TokenType::Comment(comment) if comment.text == " trailing") | ||
| .position(|token| { | ||
| matches!(token.token_type(), TokenType::Comment(comment) if comment.text() == " trailing") | ||
| }) | ||
@@ -369,11 +356,12 @@ .expect("comment token should be emitted"); | ||
| for case in cases { | ||
| let tokens = Scanner::new(StrInput::new(case.yaml)).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens(case.yaml).expect("valid YAML should scan without errors"); | ||
| let syntax_index = tokens | ||
| .iter() | ||
| .position(|Token(_, token)| (case.syntax_matches)(token)) | ||
| .position(|token| (case.syntax_matches)(token.token_type())) | ||
| .expect("syntax token should be emitted"); | ||
| let comment_index = tokens | ||
| .iter() | ||
| .position(|Token(_, token)| { | ||
| matches!(token, TokenType::Comment(comment) if comment.text == case.expected_comment) | ||
| .position(|token| { | ||
| matches!(token.token_type(), TokenType::Comment(comment) | ||
| if comment.text() == case.expected_comment) | ||
| }) | ||
@@ -389,11 +377,11 @@ .expect("comment token should be emitted"); | ||
| let yaml = "key: | # block scalar header\n body\n"; | ||
| let tokens = Scanner::new(StrInput::new(yaml)).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens(yaml).expect("valid YAML should scan without errors"); | ||
| assert!(tokens.iter().any( | ||
| |Token(_, token)| matches!(token, TokenType::Comment(comment) | ||
| if comment.text == " block scalar header") | ||
| )); | ||
| assert!(tokens.iter().any(|Token(_, token)| { | ||
| matches!(token, TokenType::Scalar(ScalarStyle::Literal, value) if value == "body\n") | ||
| assert!(tokens.iter().any(|token| { | ||
| matches!(token.token_type(), TokenType::Comment(comment) | ||
| if comment.text() == " block scalar header") | ||
| })); | ||
| assert!(tokens.iter().any(|token| { | ||
| matches!(token.token_type(), TokenType::Scalar(ScalarStyle::Literal, value) if value == "body\n") | ||
| })); | ||
| } | ||
@@ -404,9 +392,14 @@ | ||
| let yaml = "#\n# \n"; | ||
| let comments: Vec<_> = Scanner::new(StrInput::new(yaml)) | ||
| .filter_map(|Token(span, token)| match token { | ||
| TokenType::Comment(comment) => Some(( | ||
| comment.text.into_owned(), | ||
| span.slice(yaml).map(str::to_owned), | ||
| )), | ||
| _ => None, | ||
| let comments: Vec<_> = scanner_tokens(yaml) | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .filter_map(|token| { | ||
| let (span, token_type) = token.into_parts(); | ||
| match token_type { | ||
| TokenType::Comment(comment) => Some(( | ||
| comment.into_text().into_owned(), | ||
| span.slice(yaml).map(str::to_owned), | ||
| )), | ||
| _ => None, | ||
| } | ||
| }) | ||
@@ -427,6 +420,11 @@ .collect(); | ||
| let yaml = "# crlf\r\nkey: value\n"; | ||
| let comment = Scanner::new(StrInput::new(yaml)) | ||
| .find_map(|Token(span, token)| match token { | ||
| TokenType::Comment(comment) => Some((comment.text.into_owned(), span)), | ||
| _ => None, | ||
| let comment = scanner_tokens(yaml) | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .find_map(|token| { | ||
| let (span, token_type) = token.into_parts(); | ||
| match token_type { | ||
| TokenType::Comment(comment) => Some((comment.into_text().into_owned(), span)), | ||
| _ => None, | ||
| } | ||
| }) | ||
@@ -442,10 +440,15 @@ .expect("comment token should be emitted"); | ||
| let yaml = "# ž🎵\n"; | ||
| let comment = Scanner::new(StrInput::new(yaml)) | ||
| .find_map(|Token(span, token)| match token { | ||
| TokenType::Comment(comment) => Some((comment, span)), | ||
| _ => None, | ||
| let comment = scanner_tokens(yaml) | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .find_map(|token| { | ||
| let (span, token_type) = token.into_parts(); | ||
| match token_type { | ||
| TokenType::Comment(comment) => Some((comment, span)), | ||
| _ => None, | ||
| } | ||
| }) | ||
| .expect("comment token should be emitted"); | ||
| assert_eq!(comment.0.text, " ž🎵"); | ||
| assert_eq!(comment.0.text(), " ž🎵"); | ||
| assert_eq!(comment.1.slice(yaml), Some("# ž🎵")); | ||
@@ -460,5 +463,7 @@ assert_eq!(comment.1.start.index(), 0); | ||
| fn scanner_comment_text_is_borrowed_for_str_input() { | ||
| let comment = Scanner::new(StrInput::new("# borrowed\n")) | ||
| .find_map(|Token(_, token)| match token { | ||
| TokenType::Comment(comment) => Some(comment.text), | ||
| let comment = scanner_tokens("# borrowed\n") | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .find_map(|token| match token.into_parts().1 { | ||
| TokenType::Comment(comment) => Some(comment.into_text()), | ||
| _ => None, | ||
@@ -477,4 +482,7 @@ }) | ||
| let comment = Scanner::new(BufferedInput::new("# streamed\n".chars())) | ||
| .find_map(|Token(_, token)| match token { | ||
| TokenType::Comment(comment) => Some(comment.text), | ||
| .collect::<Result<Vec<_>, _>>() | ||
| .expect("valid YAML should scan without errors") | ||
| .into_iter() | ||
| .find_map(|token| match token.into_parts().1 { | ||
| TokenType::Comment(comment) => Some(comment.into_text()), | ||
| _ => None, | ||
@@ -499,10 +507,17 @@ }) | ||
| scanner | ||
| .filter_map(|Token(span, token)| match token { | ||
| TokenType::Comment(comment) => Some(( | ||
| comment.text.into_owned(), | ||
| comment.placement, | ||
| span.start.index(), | ||
| span.end.index(), | ||
| )), | ||
| _ => None, | ||
| .map(|token| token.expect("valid YAML should scan without errors")) | ||
| .filter_map(|token| { | ||
| let (span, token_type) = token.into_parts(); | ||
| match token_type { | ||
| TokenType::Comment(comment) => { | ||
| let placement = comment.placement(); | ||
| Some(( | ||
| comment.into_text().into_owned(), | ||
| placement, | ||
| span.start.index(), | ||
| span.end.index(), | ||
| )) | ||
| } | ||
| _ => None, | ||
| } | ||
| }) | ||
@@ -541,3 +556,3 @@ .collect() | ||
| for (yaml, style, expected_value) in cases { | ||
| let tokens = Scanner::new(StrInput::new(yaml)).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens(yaml).expect("valid YAML should scan without errors"); | ||
@@ -547,8 +562,8 @@ assert!( | ||
| .iter() | ||
| .any(|Token(_, token)| matches!(token, TokenType::Comment(_))), | ||
| .any(|token| matches!(token.token_type(), TokenType::Comment(_))), | ||
| "{yaml:?}" | ||
| ); | ||
| assert!( | ||
| tokens.iter().any(|Token(_, token)| { | ||
| matches!(token, TokenType::Scalar(scalar_style, value) | ||
| tokens.iter().any(|token| { | ||
| matches!(token.token_type(), TokenType::Scalar(scalar_style, value) | ||
| if *scalar_style == style && value == expected_value) | ||
@@ -567,7 +582,9 @@ }), | ||
| let error = loop { | ||
| match scanner.next_token() { | ||
| Ok(Some(Token(_, TokenType::Comment(_)))) => saw_comment = true, | ||
| Ok(Some(_)) => {} | ||
| Ok(None) => panic!("expected scanner error"), | ||
| Err(error) => break error, | ||
| match scanner.next() { | ||
| Some(Ok(token)) if matches!(token.token_type(), TokenType::Comment(_)) => { | ||
| saw_comment = true; | ||
| } | ||
| Some(Ok(_)) => {} | ||
| Some(Err(error)) => break error, | ||
| None => panic!("expected scanner error"), | ||
| } | ||
@@ -585,10 +602,10 @@ }; | ||
| fn scanner_treats_unseparated_hash_after_plain_scalar_as_content() { | ||
| let tokens = Scanner::new(StrInput::new("key: value#bad\n")).collect::<Vec<Token<'_>>>(); | ||
| let tokens = scanner_tokens("key: value#bad\n").expect("valid YAML should scan without errors"); | ||
| assert!(tokens.iter().any(|Token(_, token)| { | ||
| matches!(token, TokenType::Scalar(ScalarStyle::Plain, value) if value == "value#bad") | ||
| assert!(tokens.iter().any(|token| { | ||
| matches!(token.token_type(), TokenType::Scalar(ScalarStyle::Plain, value) if value == "value#bad") | ||
| })); | ||
| assert!(!tokens | ||
| .iter() | ||
| .any(|Token(_, token)| matches!(token, TokenType::Comment(_)))); | ||
| .any(|token| matches!(token.token_type(), TokenType::Comment(_)))); | ||
| } | ||
@@ -626,3 +643,3 @@ | ||
| .iter() | ||
| .filter_map(|Token(_, token)| match token { | ||
| .filter_map(|token| match token.token_type() { | ||
| TokenType::Scalar(ScalarStyle::Plain, value) => Some(value.as_ref()), | ||
@@ -634,7 +651,7 @@ _ => None, | ||
| .iter() | ||
| .filter_map(|Token(span, token)| match token { | ||
| .filter_map(|token| match token.token_type() { | ||
| TokenType::Comment(comment) => Some(( | ||
| comment.text.as_ref(), | ||
| comment.placement, | ||
| span.slice(case.yaml), | ||
| comment.text(), | ||
| comment.placement(), | ||
| token.span().slice(case.yaml), | ||
| )), | ||
@@ -666,6 +683,6 @@ _ => None, | ||
| let error = loop { | ||
| match scanner.next_token() { | ||
| Ok(Some(_)) => {} | ||
| Ok(None) => panic!("expected scanner error"), | ||
| Err(error) => break error, | ||
| match scanner.next() { | ||
| Some(Ok(_)) => {} | ||
| Some(Err(error)) => break error, | ||
| None => panic!("expected scanner error"), | ||
| } | ||
@@ -1258,3 +1275,3 @@ }; | ||
| Event::Comment(text, _) => Some(format!("Comment({text})")), | ||
| Event::Nothing | Event::Alias(_) | Event::MappingStart(..) | Event::MappingEnd => None, | ||
| _ => None, | ||
| }) | ||
@@ -1261,0 +1278,0 @@ .collect(); |
| use granit_parser::{ | ||
| input::SkipTabs, BorrowedInput, Event, Input, Parser, ScanError, Scanner, Span, StrInput, Token, | ||
| input::SkipTabs, BorrowedInput, ErrorKind, Event, Input, Parser, ScanError, Scanner, Span, | ||
| StrInput, Token, | ||
| }; | ||
@@ -67,3 +68,3 @@ | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) { | ||
| fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) { | ||
| self.inner.skip_ws_to_eol(skip_tabs) | ||
@@ -90,7 +91,11 @@ } | ||
| fn scanner_tokens_fast_path(source: &str) -> Vec<Token<'_>> { | ||
| Scanner::new(StrInput::new(source)).collect() | ||
| Scanner::new(StrInput::new(source)) | ||
| .collect::<Result<_, _>>() | ||
| .expect("valid YAML should scan without errors") | ||
| } | ||
| fn scanner_tokens_comment_enabled(source: &str) -> Vec<Token<'_>> { | ||
| Scanner::new(CommentEnabledStrInput::new(source)).collect() | ||
| Scanner::new(CommentEnabledStrInput::new(source)) | ||
| .collect::<Result<_, _>>() | ||
| .expect("valid YAML should scan without errors") | ||
| } | ||
@@ -97,0 +102,0 @@ |
@@ -12,3 +12,3 @@ use granit_parser::{Event, Parser, ScalarStyle, ScanError, StructureStyle}; | ||
| if let Err(error) = event { | ||
| return error.info().to_owned(); | ||
| return error.info(); | ||
| } | ||
@@ -15,0 +15,0 @@ } |
@@ -23,3 +23,3 @@ use granit_parser::{Event, Parser, ScalarStyle, StructureStyle}; | ||
| fn first_error_info(yaml: &str) -> Option<String> { | ||
| Parser::new_from_str(yaml).find_map(|event| event.err().map(|err| err.info().to_owned())) | ||
| Parser::new_from_str(yaml).find_map(|event| event.err().map(|err| err.info())) | ||
| } | ||
@@ -26,0 +26,0 @@ |
@@ -1,3 +0,33 @@ | ||
| use granit_parser::{BufferedInput, Input, Parser, StrInput}; | ||
| use granit_parser::{BufferedInput, ErrorKind, Event, Input, Marker, Parser, ScanError, StrInput}; | ||
| fn first_str_error(input: &str) -> ScanError { | ||
| Parser::new_from_str(input) | ||
| .find_map(Result::err) | ||
| .expect("input should fail") | ||
| } | ||
| fn first_iter_error(input: &str) -> ScanError { | ||
| Parser::new_from_iter(input.chars()) | ||
| .find_map(Result::err) | ||
| .expect("input should fail") | ||
| } | ||
| fn first_str_scalar(input: &str) -> String { | ||
| Parser::new_from_str(input) | ||
| .find_map(|item| match item.expect("input should parse") { | ||
| (Event::Scalar(value, ..), _) => Some(value.into_owned()), | ||
| _ => None, | ||
| }) | ||
| .expect("input should contain a scalar") | ||
| } | ||
| fn first_iter_scalar(input: &str) -> String { | ||
| Parser::new_from_iter(input.chars()) | ||
| .find_map(|item| match item.expect("input should parse") { | ||
| (Event::Scalar(value, ..), _) => Some(value.into_owned()), | ||
| _ => None, | ||
| }) | ||
| .expect("input should contain a scalar") | ||
| } | ||
| #[test] | ||
@@ -85,1 +115,51 @@ fn parser_iterator_terminates_after_scan_error() { | ||
| } | ||
| #[test] | ||
| fn non_printable_source_characters_are_rejected_by_both_inputs() { | ||
| for character in ['\0', '\u{1}'] { | ||
| let inputs = [ | ||
| format!("key: before{character}after\n"), | ||
| format!("'before{character}after'\n"), | ||
| format!("\"before{character}after\"\n"), | ||
| format!("key: |\n before{character}after\n"), | ||
| format!("key: >\n before{character}after\n"), | ||
| format!("# before{character}after\nkey: value\n"), | ||
| ]; | ||
| for input in &inputs { | ||
| assert_eq!( | ||
| first_str_error(input).kind(), | ||
| &ErrorKind::UnexpectedCharacter { character }, | ||
| "string input accepted {input:?}", | ||
| ); | ||
| assert_eq!( | ||
| first_iter_error(input).kind(), | ||
| &ErrorKind::UnexpectedCharacter { character }, | ||
| "iterator input accepted {input:?}", | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| #[test] | ||
| fn invalid_indentation_diagnostics_match_between_input_backends() { | ||
| let input = "a:\n [\nfoo]\n"; | ||
| let str_error = first_str_error(input); | ||
| let iter_error = first_iter_error(input); | ||
| for error in [&str_error, &iter_error] { | ||
| assert_eq!(error.kind(), &ErrorKind::InvalidIndentation); | ||
| assert_eq!(error.marker(), &Marker::new(7, 3, 0)); | ||
| } | ||
| assert_eq!(str_error.marker().byte_offset(), Some(7)); | ||
| assert_eq!(iter_error.marker().byte_offset(), None); | ||
| } | ||
| #[test] | ||
| fn escaped_nul_in_double_quoted_scalar_remains_valid() { | ||
| let input = "\"\\0\"\n"; | ||
| assert_eq!(first_str_scalar(input), "\0"); | ||
| assert_eq!(first_iter_scalar(input), "\0"); | ||
| } |
@@ -7,4 +7,4 @@ //! Coverage tests for rarely-exercised parser and input paths. | ||
| use granit_parser::{ | ||
| input::SkipTabs, BufferedInput, Event, Input, Parser, Placement, ScanError, StrInput, | ||
| TryEventReceiver, TryLoadError, | ||
| input::SkipTabs, BufferedInput, ErrorKind, Event, Input, Parser, Placement, ScanError, | ||
| StrInput, TryEventReceiver, TryLoadError, | ||
| }; | ||
@@ -21,3 +21,3 @@ | ||
| if let Err(error) = event { | ||
| return error.info().to_owned(); | ||
| return error.info(); | ||
| } | ||
@@ -80,9 +80,6 @@ } | ||
| assert_eq!(consumed, 0); | ||
| let Err(message) = result else { | ||
| let Err(kind) = result else { | ||
| panic!("expected an error for a comment without leading whitespace"); | ||
| }; | ||
| assert_eq!( | ||
| message, | ||
| "comments must be separated from other tokens by whitespace" | ||
| ); | ||
| assert_eq!(kind, ErrorKind::CommentNotSeparated); | ||
| // The '#' itself must not be consumed. | ||
@@ -242,5 +239,3 @@ assert_eq!(input.look_ch(), '#'); | ||
| #[test] | ||
| fn next_event_after_receiver_error_on_stream_end_with_comments_reports_eof() { | ||
| // With comments possible, resuming after the scanner is exhausted asks the | ||
| // scanner for another token and reports an EOF error instead. | ||
| fn next_event_after_receiver_error_on_stream_end_with_comments_returns_stream_end() { | ||
| let mut parser = Parser::new_from_str("# hello\nfoo\n"); | ||
@@ -252,8 +247,5 @@ let mut receiver = RejectStreamEnd; | ||
| let error = parser | ||
| .next_event() | ||
| .expect("expected an EOF error") | ||
| .unwrap_err(); | ||
| assert_eq!(error.info(), "unexpected eof"); | ||
| let (event, _) = parser.next_event().expect("stream end event").unwrap(); | ||
| assert_eq!(event, Event::StreamEnd); | ||
| assert!(parser.next_event().is_none()); | ||
| } |
@@ -29,3 +29,3 @@ //! Coverage tests for block scalars, quoted scalars, plain scalars and error paths in | ||
| if let Err(error) = event { | ||
| return error.info().to_owned(); | ||
| return error.info(); | ||
| } | ||
@@ -32,0 +32,0 @@ } |
@@ -170,5 +170,5 @@ //! Coverage tests for scanner fallback paths around tags, tag directives, anchors and flow | ||
| assert_eq!(value, "bar"); | ||
| assert_eq!(tag.handle, "tag:example.com,2000:app/"); | ||
| assert_eq!(tag.suffix, "foo"); | ||
| assert_eq!(tag.original_handle, "!e!"); | ||
| assert_eq!(tag.handle(), "tag:example.com,2000:app/"); | ||
| assert_eq!(tag.suffix(), "foo"); | ||
| assert_eq!(tag.original_handle(), "!e!"); | ||
| } | ||
@@ -198,4 +198,4 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "tag:example/"); | ||
| assert_eq!(tag.suffix, "x"); | ||
| assert_eq!(tag.handle(), "tag:example/"); | ||
| assert_eq!(tag.suffix(), "x"); | ||
| } | ||
@@ -221,4 +221,4 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "app/"); | ||
| assert_eq!(tag.suffix, "x"); | ||
| assert_eq!(tag.handle(), "app/"); | ||
| assert_eq!(tag.suffix(), "x"); | ||
| } | ||
@@ -233,3 +233,3 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(format!("{}{}", tag.handle, tag.suffix), "tag:example"); | ||
| assert_eq!(format!("{}{}", tag.handle(), tag.suffix()), "tag:example"); | ||
| } | ||
@@ -257,25 +257,12 @@ | ||
| #[test] | ||
| fn scanner_reports_misplaced_bracket_when_resumed_after_unclosed_bracket() { | ||
| // The public `Scanner` API can be driven past a first error. The `---` marker inside the | ||
| // unclosed flow sequence pops the flow marker while reporting the first error; resuming | ||
| // then reaches `]` with a non-zero flow level but no recorded opening bracket. | ||
| fn scanner_error_is_terminal() { | ||
| let mut scanner = Scanner::new(StrInput::new("[\n--- ]\n")); | ||
| let first = loop { | ||
| match scanner.next_token() { | ||
| Ok(Some(_)) => {} | ||
| Ok(None) => panic!("expected a first scan error"), | ||
| Err(error) => break error, | ||
| } | ||
| }; | ||
| assert_eq!(first.info(), "unclosed bracket '['"); | ||
| let error = scanner | ||
| .by_ref() | ||
| .find_map(Result::err) | ||
| .expect("scanner should emit an error"); | ||
| let second = loop { | ||
| match scanner.next_token() { | ||
| Ok(Some(_)) => {} | ||
| Ok(None) => panic!("expected a second scan error"), | ||
| Err(error) => break error, | ||
| } | ||
| }; | ||
| assert_eq!(second.info(), "misplaced bracket"); | ||
| assert_eq!(error.info(), "unclosed bracket '['"); | ||
| assert!(scanner.next().is_none()); | ||
| } | ||
@@ -295,5 +282,5 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "tag:e/"); | ||
| assert_eq!(tag.suffix, "foo"); | ||
| assert_eq!(tag.original_handle, "!e!"); | ||
| assert_eq!(tag.handle(), "tag:e/"); | ||
| assert_eq!(tag.suffix(), "foo"); | ||
| assert_eq!(tag.original_handle(), "!e!"); | ||
| } | ||
@@ -320,4 +307,4 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "!"); | ||
| assert_eq!(tag.suffix, "foo"); | ||
| assert_eq!(tag.handle(), "!"); | ||
| assert_eq!(tag.suffix(), "foo"); | ||
| } | ||
@@ -400,4 +387,4 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "app/"); | ||
| assert_eq!(tag.suffix, "x"); | ||
| assert_eq!(tag.handle(), "app/"); | ||
| assert_eq!(tag.suffix(), "x"); | ||
| } | ||
@@ -412,4 +399,4 @@ | ||
| assert_eq!(value, "1"); | ||
| assert_eq!(tag.handle, "!"); | ||
| assert_eq!(tag.suffix, "abc"); | ||
| assert_eq!(tag.handle(), "!"); | ||
| assert_eq!(tag.suffix(), "abc"); | ||
| } | ||
@@ -416,0 +403,0 @@ |
+4
-4
@@ -133,3 +133,3 @@ #![allow(clippy::bool_assert_comparison)] | ||
| event, | ||
| Event::MappingStart(_, _, Some(tag)) if tag.suffix == "omap" | ||
| Event::MappingStart(_, _, Some(tag)) if tag.suffix() == "omap" | ||
| ) | ||
@@ -158,3 +158,3 @@ }) | ||
| Event::Scalar(value, _, _, Some(tag)) | ||
| if value.as_ref() == "value" && tag.suffix == "str" | ||
| if value.as_ref() == "value" && tag.suffix() == "str" | ||
| ) | ||
@@ -181,3 +181,3 @@ }) | ||
| Event::Scalar(value, _, _, Some(tag)) | ||
| if value.as_ref() == "value" && tag.suffix == "str" | ||
| if value.as_ref() == "value" && tag.suffix() == "str" | ||
| ) | ||
@@ -269,3 +269,3 @@ }) | ||
| Event::Scalar(value, _, _, Some(tag)) | ||
| if value.is_empty() && tag.suffix == "str" | ||
| if value.is_empty() && tag.suffix() == "str" | ||
| ) | ||
@@ -272,0 +272,0 @@ }) |
@@ -116,3 +116,3 @@ #![allow(dead_code)] | ||
| str_error.info().to_owned() | ||
| str_error.info() | ||
| } | ||
@@ -119,0 +119,0 @@ |
+191
-54
| extern crate alloc; | ||
| use alloc::{ | ||
| borrow::Cow, | ||
| string::{String, ToString}, | ||
@@ -10,5 +11,5 @@ vec, | ||
| use granit_parser::{ | ||
| parser_stack::{ParserStack, ReplayParser}, | ||
| BorrowedInput, Event, Marker, Parser, ParserTrait, Placement, ScalarStyle, ScanError, Span, | ||
| SpannedEventReceiver, StrInput, StructureStyle, TryEventReceiver, TryLoadError, | ||
| BorrowedInput, ErrorKind, Event, Marker, Parser, ParserStack, ParserTrait, Placement, | ||
| ReplayParser, ScalarStyle, ScanError, Span, SpannedEventReceiver, StrInput, StructureStyle, | ||
| TryEventReceiver, TryLoadError, | ||
| }; | ||
@@ -58,2 +59,29 @@ | ||
| fn stack_after_first_parent_anchor() -> MyStack<'static> { | ||
| let mut stack = ParserStack::new(); | ||
| stack.push_str_parser( | ||
| Parser::new_from_str("before: &parent parent-value\nafter: &after resumed-parent-value\n"), | ||
| "parent.yaml".to_string(), | ||
| ); | ||
| loop { | ||
| let event = stack.next_event().unwrap().unwrap().0; | ||
| if let Event::Scalar(value, _, anchor_id, _) = event { | ||
| if value == "parent-value" { | ||
| assert_eq!(anchor_id, 1); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| assert_eq!(stack.current_anchor_offset(), 2); | ||
| stack | ||
| } | ||
| fn assert_include_anchor_offset_is_inherited(events: &[Event]) { | ||
| assert_eq!(find_anchor_id(events, "included-value"), Some(2)); | ||
| assert!(events.iter().any(|event| matches!(event, Event::Alias(2)))); | ||
| assert_eq!(find_anchor_id(events, "resumed-parent-value"), Some(3)); | ||
| } | ||
| fn format_events(events: &[Event]) -> Vec<String> { | ||
@@ -146,3 +174,3 @@ events | ||
| #[test] | ||
| fn nested_parser_scan_error_after_document_end_is_propagated_verbatim() { | ||
| fn nested_parser_scan_error_after_document_end_preserves_kind_and_adds_source_stack() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
@@ -159,3 +187,8 @@ stack.push_str_parser(Parser::new_from_str("parent: value"), "parent".to_string()); | ||
| Some(Err(err)) => { | ||
| assert_eq!(err.info(), "unclosed bracket '['"); | ||
| assert_eq!(err.kind(), &ErrorKind::UnclosedFlowCollection { open: '[' }); | ||
| assert_eq!( | ||
| err.info(), | ||
| "unclosed bracket '['\nwhile parsing parent -> child" | ||
| ); | ||
| assert_eq!(err.source_stack(), ["parent", "child"]); | ||
| assert_eq!(stack.stack(), vec!["parent".to_string()]); | ||
@@ -484,10 +517,7 @@ break; | ||
| } else { | ||
| Err(granit_parser::ScanError::new( | ||
| granit_parser::Marker::new(0, 1, 0), | ||
| "Not found".to_string(), | ||
| )) | ||
| unreachable!("this test only resolves inc1") | ||
| } | ||
| }); | ||
| stack.resolve("inc1").unwrap(); | ||
| stack.push_include("inc1").unwrap(); | ||
@@ -517,2 +547,115 @@ let events = collect_events(&mut stack).unwrap(); | ||
| #[test] | ||
| fn borrowed_include_resolver_preserves_borrowed_event_text() { | ||
| const INCLUDED: &str = "included: borrowed-value\n"; | ||
| let mut stack: MyStack<'static> = ParserStack::new(); | ||
| stack.set_borrowed_resolver(|name| { | ||
| assert_eq!(name, "borrowed.yaml"); | ||
| Ok(INCLUDED) | ||
| }); | ||
| stack.push_include("borrowed.yaml").unwrap(); | ||
| while let Some(event) = stack.next_event() { | ||
| if let Event::Scalar(value, _, _, _) = event.unwrap().0 { | ||
| if value == "borrowed-value" { | ||
| assert!(matches!(value, Cow::Borrowed("borrowed-value"))); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| panic!("expected the included scalar"); | ||
| } | ||
| #[test] | ||
| fn borrowed_include_resolver_still_validates_eagerly() { | ||
| let mut stack: MyStack<'static> = ParserStack::new(); | ||
| stack.push_str_parser(Parser::new_from_str("root: value\n"), "root".to_string()); | ||
| stack.set_borrowed_resolver(|_| Ok("included: [unclosed\n")); | ||
| let error = stack.push_include("invalid.yaml").unwrap_err(); | ||
| assert_eq!( | ||
| error.kind(), | ||
| &ErrorKind::UnclosedFlowCollection { open: '[' } | ||
| ); | ||
| assert_eq!(error.source_stack(), ["root", "invalid.yaml"]); | ||
| } | ||
| #[test] | ||
| fn empty_stack_include_preserves_fresh_parser_anchor_offset() { | ||
| const INCLUDED: &str = "definition: &anchor anchored\nalias: *anchor\n"; | ||
| let mut owned: MyStack<'static> = ParserStack::new(); | ||
| owned.set_resolver(|_| Ok(INCLUDED.to_string())); | ||
| owned.push_include("owned.yaml").unwrap(); | ||
| let owned_events = collect_events(&mut owned).unwrap(); | ||
| assert_eq!(find_anchor_id(&owned_events, "anchored"), Some(1)); | ||
| assert!(owned_events | ||
| .iter() | ||
| .any(|event| matches!(event, Event::Alias(1)))); | ||
| let mut borrowed: MyStack<'static> = ParserStack::new(); | ||
| borrowed.set_borrowed_resolver(|_| Ok(INCLUDED)); | ||
| borrowed.push_include("borrowed.yaml").unwrap(); | ||
| let borrowed_events = collect_events(&mut borrowed).unwrap(); | ||
| assert_eq!(find_anchor_id(&borrowed_events, "anchored"), Some(1)); | ||
| assert!(borrowed_events | ||
| .iter() | ||
| .any(|event| matches!(event, Event::Alias(1)))); | ||
| } | ||
| #[test] | ||
| fn owned_include_inherits_and_propagates_parent_anchor_offset() { | ||
| let mut stack = stack_after_first_parent_anchor(); | ||
| stack.set_resolver(|name| { | ||
| assert_eq!(name, "included.yaml"); | ||
| Ok("definition: &included included-value\nalias: *included\n".to_string()) | ||
| }); | ||
| stack.push_include("included.yaml").unwrap(); | ||
| let events = collect_events(&mut stack).unwrap(); | ||
| assert_include_anchor_offset_is_inherited(&events); | ||
| } | ||
| #[test] | ||
| fn borrowed_include_inherits_and_propagates_parent_anchor_offset() { | ||
| const INCLUDED: &str = "definition: &included included-value\nalias: *included\n"; | ||
| let mut stack = stack_after_first_parent_anchor(); | ||
| stack.set_borrowed_resolver(|name| { | ||
| assert_eq!(name, "included.yaml"); | ||
| Ok(INCLUDED) | ||
| }); | ||
| stack.push_include("included.yaml").unwrap(); | ||
| let events = collect_events(&mut stack).unwrap(); | ||
| assert_include_anchor_offset_is_inherited(&events); | ||
| } | ||
| #[test] | ||
| fn replay_parser_moves_owned_event_text() { | ||
| let value = "owned replay value".to_string(); | ||
| let value_ptr = value.as_ptr(); | ||
| let mut replay = ReplayParser::new( | ||
| vec![( | ||
| Event::Scalar(Cow::Owned(value), ScalarStyle::Plain, 0, None), | ||
| test_span(), | ||
| )], | ||
| 1, | ||
| ); | ||
| let event = replay.next().unwrap().unwrap().0; | ||
| let Event::Scalar(Cow::Owned(value), _, _, _) = event else { | ||
| panic!("expected an owned scalar"); | ||
| }; | ||
| assert_eq!(value.as_ptr(), value_ptr); | ||
| } | ||
| #[test] | ||
| fn test_unexpected_eof_is_reported() { | ||
@@ -655,3 +798,3 @@ let mut stack: MyStack = ParserStack::new(); | ||
| assert_eq!(replay.get_anchor_offset(), 8); | ||
| assert_eq!(replay.anchor_offset(), 8); | ||
| assert_eq!( | ||
@@ -803,3 +946,3 @@ format_events(&recv.events), | ||
| #[test] | ||
| fn parser_stack_resolve_preserves_included_comment_events_and_local_spans() { | ||
| fn parser_stack_include_preserves_comment_events_and_local_spans() { | ||
| let included = "# inc\nincluded: value\n"; | ||
@@ -816,3 +959,3 @@ let mut stack: MyStack = ParserStack::new(); | ||
| stack | ||
| .resolve("included.yaml") | ||
| .push_include("included.yaml") | ||
| .expect("include should resolve"); | ||
@@ -848,3 +991,3 @@ | ||
| #[test] | ||
| fn parser_stack_push_str_after_exhaustion_reactivates_stack() { | ||
| fn parser_stack_push_str_after_exhaustion_stays_exhausted() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
@@ -857,16 +1000,4 @@ | ||
| let events = collect_events(&mut stack).unwrap(); | ||
| assert_eq!( | ||
| format_events(&events), | ||
| vec![ | ||
| "StreamStart", | ||
| "DocStart", | ||
| "MapStart", | ||
| "Scalar(b)", | ||
| "Scalar(2)", | ||
| "MapEnd", | ||
| "DocEnd", | ||
| "StreamEnd" | ||
| ] | ||
| ); | ||
| assert!(stack.next_event().is_none()); | ||
| assert!(stack.peek().is_none()); | ||
| } | ||
@@ -899,6 +1030,6 @@ | ||
| #[test] | ||
| fn parser_stack_resolve_without_resolver_reports_error() { | ||
| fn parser_stack_include_without_resolver_reports_error() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
| let err = stack.resolve("missing").unwrap_err(); | ||
| let err = stack.push_include("missing").unwrap_err(); | ||
@@ -911,2 +1042,21 @@ assert!(err | ||
| #[test] | ||
| fn parser_stack_resolver_can_construct_a_scan_error() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
| stack.push_str_parser(Parser::new_from_str("root: value"), "root".to_string()); | ||
| stack.set_resolver(|_| Err(ScanError::new(Marker::new(0, 1, 0), "include not found"))); | ||
| let err = stack.push_include("missing").unwrap_err(); | ||
| assert_eq!( | ||
| err.kind(), | ||
| &ErrorKind::Custom("include not found".to_string()) | ||
| ); | ||
| assert_eq!( | ||
| err.info(), | ||
| "include not found\nwhile parsing root -> missing" | ||
| ); | ||
| assert_eq!(err.source_stack(), ["root", "missing"]); | ||
| } | ||
| #[test] | ||
| fn parser_stack_push_include_resolves_included_content() { | ||
@@ -917,6 +1067,3 @@ let mut stack: MyStack = ParserStack::new(); | ||
| "child" => Ok("b: 2".to_string()), | ||
| _ => Err(ScanError::new( | ||
| Marker::new(0, 1, 0), | ||
| "Not found".to_string(), | ||
| )), | ||
| _ => unreachable!("this test only resolves child"), | ||
| }); | ||
@@ -948,10 +1095,7 @@ | ||
| #[test] | ||
| fn parser_stack_push_include_after_exhaustion_reactivates_stack() { | ||
| fn parser_stack_push_include_after_exhaustion_stays_exhausted() { | ||
| let mut stack: MyStack = ParserStack::new(); | ||
| stack.set_resolver(|name| match name { | ||
| "child" => Ok("b: 2".to_string()), | ||
| _ => Err(ScanError::new( | ||
| Marker::new(0, 1, 0), | ||
| "Not found".to_string(), | ||
| )), | ||
| _ => unreachable!("this test only resolves child"), | ||
| }); | ||
@@ -964,16 +1108,4 @@ | ||
| let events = collect_events(&mut stack).unwrap(); | ||
| assert_eq!( | ||
| format_events(&events), | ||
| vec![ | ||
| "StreamStart", | ||
| "DocStart", | ||
| "MapStart", | ||
| "Scalar(b)", | ||
| "Scalar(2)", | ||
| "MapEnd", | ||
| "DocEnd", | ||
| "StreamEnd" | ||
| ] | ||
| ); | ||
| assert!(stack.next_event().is_none()); | ||
| assert!(stack.peek().is_none()); | ||
| } | ||
@@ -1002,3 +1134,8 @@ | ||
| Err(err) => { | ||
| assert_eq!(err.info(), "multiple documents not supported here"); | ||
| assert_eq!(err.kind(), &ErrorKind::MultipleDocumentsUnsupported); | ||
| assert_eq!( | ||
| err.info(), | ||
| "multiple documents not supported here\nwhile parsing root -> two.yaml" | ||
| ); | ||
| assert_eq!(err.source_stack(), ["root", "two.yaml"]); | ||
| assert_eq!(stack.stack(), vec!["root".to_string()]); | ||
@@ -1005,0 +1142,0 @@ saw_error = true; |
+26
-11
@@ -1,2 +0,2 @@ | ||
| use granit_parser::{Event, EventReceiver, Parser}; | ||
| use granit_parser::{ErrorKind, Event, EventReceiver, Parser, ScanError}; | ||
@@ -70,7 +70,6 @@ struct Collector(Vec<Event<'static>>); | ||
| fn first_error(input: &str) -> (String, usize) { | ||
| let err = Parser::new_from_str(input) | ||
| fn first_error(input: &str) -> ScanError { | ||
| Parser::new_from_str(input) | ||
| .find_map(Result::err) | ||
| .expect("input should fail"); | ||
| (err.info().to_owned(), err.marker().index()) | ||
| .expect("input should fail") | ||
| } | ||
@@ -80,5 +79,13 @@ | ||
| fn mismatched_sequence_closed_by_mapping_brace_reports_mismatch() { | ||
| let (err, index) = first_error("[}"); | ||
| assert_eq!(err, "mismatched bracket '[' closed by '}'"); | ||
| assert_eq!(index, 0); | ||
| let error = first_error("[}"); | ||
| assert_eq!( | ||
| error.kind(), | ||
| &ErrorKind::MismatchedFlowCollectionEnd { | ||
| open: '[', | ||
| close: '}', | ||
| } | ||
| ); | ||
| assert_eq!(error.info(), "mismatched bracket '[' closed by '}'"); | ||
| assert_eq!(error.marker().index(), 0); | ||
| } | ||
@@ -88,5 +95,13 @@ | ||
| fn mismatched_mapping_closed_by_sequence_bracket_reports_mismatch() { | ||
| let (err, index) = first_error("{]"); | ||
| assert_eq!(err, "mismatched bracket '{' closed by ']'"); | ||
| assert_eq!(index, 0); | ||
| let error = first_error("{]"); | ||
| assert_eq!( | ||
| error.kind(), | ||
| &ErrorKind::MismatchedFlowCollectionEnd { | ||
| open: '{', | ||
| close: ']', | ||
| } | ||
| ); | ||
| assert_eq!(error.info(), "mismatched bracket '{' closed by ']'"); | ||
| assert_eq!(error.marker().index(), 0); | ||
| } |
@@ -363,3 +363,3 @@ use std::{ | ||
| fn on_event(&mut self, ev: Event<'input>, span: Span) { | ||
| if matches!(ev, Event::Comment(..) | Event::Nothing) { | ||
| if matches!(ev, Event::Comment(..)) { | ||
| return; | ||
@@ -413,3 +413,4 @@ } | ||
| Event::Alias(idx) => format!("=ALI *{idx}"), | ||
| Event::Comment(..) | Event::Nothing => unreachable!("comments are ignored above"), | ||
| Event::Comment(..) => unreachable!("comments are ignored above"), | ||
| _ => return, | ||
| }; | ||
@@ -444,3 +445,3 @@ self.events.push(line); | ||
| if let Some(tag) = tag { | ||
| format!(" <{}{}>", tag.handle, tag.suffix) | ||
| format!(" <{}{}>", tag.handle(), tag.suffix()) | ||
| } else { | ||
@@ -447,0 +448,0 @@ String::new() |
@@ -13,3 +13,3 @@ use granit_parser::{Event, Parser, Span, SpannedEventReceiver}; | ||
| fn on_event(&mut self, ev: Event<'a>, span: Span) { | ||
| eprintln!(" \x1B[;34m\u{21B3} {:?}\x1B[;m", &ev); | ||
| eprintln!(" \x1B[;34m\u{21B3} {ev:?}\x1B[;m"); | ||
| self.events.push((ev, span)); | ||
@@ -16,0 +16,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display