diff --git a/saphyr/src/lib.rs b/saphyr/src/lib.rs index ee432ca..7ccd4db 100644 --- a/saphyr/src/lib.rs +++ b/saphyr/src/lib.rs @@ -13,21 +13,14 @@ //! yaml-rust = "0.4" //! ``` //! -//! And this in your crate root: -//! -//! ```rust -//! extern crate yaml_rust; -//! ``` -//! -//! Parse a string into `Vec` and then serialize it as a YAML string. -//! //! # Examples +//! Parse a string into `Vec` and then serialize it as a YAML string. //! //! ``` //! use yaml_rust::{YamlLoader, YamlEmitter}; //! //! let docs = YamlLoader::load_from_str("[1, 2, 3]").unwrap(); -//! let doc = &docs[0]; // select the first document +//! let doc = &docs[0]; // select the first YAML document //! assert_eq!(doc[0].as_i64().unwrap(), 1); // access elements by index //! //! let mut out_str = String::new(); @@ -61,65 +54,3 @@ pub use crate::emitter::{EmitError, YamlEmitter}; pub use crate::parser::Event; pub use crate::scanner::ScanError; pub use crate::yaml::{Yaml, YamlLoader}; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_api() { - let s = " -# from yaml-cpp example -- name: Ogre - position: [0, 5, 0] - powers: - - name: Club - damage: 10 - - name: Fist - damage: 8 -- name: Dragon - position: [1, 0, 10] - powers: - - name: Fire Breath - damage: 25 - - name: Claws - damage: 15 -- name: Wizard - position: [5, -3, 0] - powers: - - name: Acid Rain - damage: 50 - - name: Staff - damage: 3 -"; - let docs = YamlLoader::load_from_str(s).unwrap(); - let doc = &docs[0]; - - assert_eq!(doc[0]["name"].as_str().unwrap(), "Ogre"); - - let mut writer = String::new(); - { - let mut emitter = YamlEmitter::new(&mut writer); - emitter.dump(doc).unwrap(); - } - - assert!(!writer.is_empty()); - } - - fn try_fail(s: &str) -> Result, ScanError> { - let t = YamlLoader::load_from_str(s)?; - Ok(t) - } - - #[test] - fn test_fail() { - let s = " -# syntax error -scalar -key: [1, 2]] -key1:a2 -"; - assert!(YamlLoader::load_from_str(s).is_err()); - assert!(try_fail(s).is_err()); - } -} diff --git a/saphyr/src/parser.rs b/saphyr/src/parser.rs index 90539f1..c885856 100644 --- a/saphyr/src/parser.rs +++ b/saphyr/src/parser.rs @@ -29,57 +29,145 @@ enum State { End, } -/// `Event` is used with the low-level event base parsing API, -/// see `EventReceiver` trait. +/// An event generated by the YAML parser. +/// +/// Events are used in the low-level event-based API (push parser). The API entrypoint is the +/// [`EventReceiver`] trait. #[derive(Clone, PartialEq, Debug, Eq)] pub enum Event { - /// Reserved for internal use + /// Reserved for internal use. Nothing, + /// Event generated at the very beginning of parsing. StreamStart, + /// Last event that will be generated by the parser. Signals EOF. StreamEnd, + /// The YAML start document directive (`---`). DocumentStart, + /// The YAML end document directive (`...`). DocumentEnd, - /// Refer to an anchor ID - Alias(usize), + /// A YAML Alias. + Alias( + /// The anchor ID the alias refers to. + usize, + ), /// Value, style, anchor_id, tag Scalar(String, TScalarStyle, usize, Option), - /// Anchor ID - SequenceStart(usize), + SequenceStart( + /// The anchor ID of the start of the squence. + usize, + ), SequenceEnd, - /// Anchor ID - MappingStart(usize), + MappingStart( + /// The anchor ID of the start of the mapping. + usize, + ), MappingEnd, } impl Event { + /// Create an empty scalar. fn empty_scalar() -> Event { // a null scalar Event::Scalar("~".to_owned(), TScalarStyle::Plain, 0, None) } + /// Create an empty scalar with the given anchor. fn empty_scalar_with_anchor(anchor: usize, tag: Option) -> Event { Event::Scalar(String::new(), TScalarStyle::Plain, anchor, tag) } } +/// A YAML parser. #[derive(Debug)] -#[allow(dead_code)] pub struct Parser { scanner: Scanner, states: Vec, state: State, - marks: Vec, token: Option, current: Option<(Event, Marker)>, anchors: HashMap, anchor_id: usize, } +/// Trait to be implemented in order to use the low-level parsing API. +/// +/// The low-level parsing API is event-based (a push parser), calling [`EventReceiver::on_event`] +/// for each YAML [`Event`] that occurs. +/// The [`EventReceiver`] trait only receives events. In order to receive both events and their +/// location in the source, use [`MarkedEventReceiver`]. Note that [`EventReceiver`]s implement +/// [`MarkedEventReceiver`] automatically. +/// +/// # Event hierarchy +/// The event stream starts with an [`Event::StreamStart`] event followed by an +/// [`Event::DocumentStart`] event. If the YAML document starts with a mapping (an object), an +/// [`Event::MappingStart`] event is emitted. If it starts with a sequence (an array), an +/// [`Event::SequenceStart`] event is emitted. Otherwise, an [`Event::Scalar`] event is emitted. +/// +/// In a mapping, key-values are sent as consecutive events. The first event after an +/// [`Event::MappingStart`] will be the key, and following its value. If the mapping contains no +/// sub-mapping or sub-sequence, then even events (starting from 0) will always be keys and odd +/// ones will always be values. The mapping ends when an [`Event::MappingEnd`] event is received. +/// +/// In a sequence, values are sent consecutively until the [`Event::SequenceEnd`] event. +/// +/// If a value is a sub-mapping or a sub-sequence, an [`Event::MappingStart`] or +/// [`Event::SequenceStart`] event will be sent respectively. Following events until the associated +/// [`Event::MappingStart`] or [`Event::SequenceEnd`] (beware of nested mappings or sequences) will +/// be part of the value and not another key-value pair or element in the sequence. +/// +/// For instance, the following yaml: +/// ```yaml +/// a: b +/// c: +/// d: e +/// f: +/// - g +/// - h +/// ``` +/// will emit (indented and commented for lisibility): +/// ```text +/// StreamStart, DocumentStart, MappingStart, +/// Scalar("a", ..), Scalar("b", ..) +/// Scalar("c", ..), MappingStart, Scalar("d", ..), Scalar("e", ..), MappingEnd, +/// Scalar("f", ..), SequenceStart, Scalar("g", ..), Scalar("h", ..), SequenceEnd, +/// MappingEnd, DocumentEnd, StreamEnd +/// ``` +/// +/// # Example +/// ``` +/// # use yaml_rust::parser::{Event, EventReceiver, Parser}; +/// # +/// /// Sink of events. Collects them into an array. +/// struct EventSink { +/// events: Vec, +/// } +/// +/// /// Implement `on_event`, pushing into `self.events`. +/// impl EventReceiver for EventSink { +/// fn on_event(&mut self, ev: Event) { +/// self.events.push(ev); +/// } +/// } +/// +/// /// Load events from a yaml string. +/// fn str_to_events(yaml: &str) -> Vec { +/// let mut sink = EventSink { events: Vec::new() }; +/// let mut parser = Parser::new(yaml.chars()); +/// // Load events using our sink as the receiver. +/// parser.load(&mut sink, true).unwrap(); +/// sink.events +/// } +/// ``` pub trait EventReceiver { + /// Handler called for each YAML event that is emitted by the parser. fn on_event(&mut self, ev: Event); } +/// Trait to be implemented for using the low-level parsing API. +/// +/// Functionally similar to [`EventReceiver`], but receives a [`Marker`] as well as the event. pub trait MarkedEventReceiver { + /// Handler called for each event that occurs. fn on_event(&mut self, ev: Event, _mark: Marker); } @@ -92,12 +180,12 @@ impl MarkedEventReceiver for R { pub type ParseResult = Result<(Event, Marker), ScanError>; impl> Parser { + /// Crate a new instance of a parser from the given input of characters. pub fn new(src: T) -> Parser { Parser { scanner: Scanner::new(src), states: Vec::new(), state: State::StreamStart, - marks: Vec::new(), token: None, current: None, @@ -107,6 +195,10 @@ impl> Parser { } } + /// Try to load the next event and return it, but do not consuming it from `self`. + /// + /// Any subsequent call to [`Parser::peek`] will return the same value, until a call to + /// [`Parser::next`] or [`Parser::load`]. pub fn peek(&mut self) -> Result<&(Event, Marker), ScanError> { if let Some(ref x) = self.current { Ok(x) @@ -116,10 +208,11 @@ impl> Parser { } } + /// Try to load the next event and return it, consuming it from `self`. pub fn next(&mut self) -> ParseResult { - match self.current { + match self.current.take() { None => self.parse(), - Some(_) => Ok(self.current.take().unwrap()), + Some(v) => Ok(v), } } @@ -170,6 +263,16 @@ impl> Parser { Ok((ev, mark)) } + /// Load the YAML from the stream in `self`, pushing events into `recv`. + /// + /// The contents of the stream are parsed and the corresponding events are sent into the + /// recveiver. For detailed explanations about how events work, see [`EventReceiver`]. + /// + /// If `multi` is set to `true`, the parser will allow parsing of multiple YAML documents + /// inside the stream. + /// + /// Note that any [`EventReceiver`] is also a [`MarkedEventReceiver`], so implementing the + /// former is enough to call this function. pub fn load( &mut self, recv: &mut R, diff --git a/saphyr/src/yaml.rs b/saphyr/src/yaml.rs index 939cb58..67b8b76 100644 --- a/saphyr/src/yaml.rs +++ b/saphyr/src/yaml.rs @@ -4,12 +4,8 @@ use crate::parser::{Event, MarkedEventReceiver, Parser}; use crate::scanner::{Marker, ScanError, TScalarStyle, TokenType}; use linked_hash_map::LinkedHashMap; use std::collections::BTreeMap; -use std::f64; -use std::i64; use std::mem; use std::ops::Index; -use std::string; -use std::vec; /// A YAML node is stored as this `Yaml` enumeration, which provides an easy way to /// access your YAML document. @@ -30,20 +26,20 @@ use std::vec; #[derive(Clone, PartialEq, PartialOrd, Debug, Eq, Ord, Hash)] pub enum Yaml { /// Float types are stored as String and parsed on demand. - /// Note that f64 does NOT implement Eq trait and can NOT be stored in BTreeMap. - Real(string::String), + /// Note that `f64` does NOT implement Eq trait and can NOT be stored in `BTreeMap`. + Real(String), /// YAML int is stored as i64. Integer(i64), /// YAML scalar. - String(string::String), + String(String), /// YAML bool, e.g. `true` or `false`. Boolean(bool), /// YAML array, can be accessed as a `Vec`. - Array(self::Array), + Array(Array), /// YAML hash, can be accessed as a `LinkedHashMap`. /// /// Insertion order will match the order of insertion into the map. - Hash(self::Hash), + Hash(Hash), /// Alias, not fully supported yet. Alias(usize), /// YAML null, e.g. `null` or `~`. @@ -68,6 +64,9 @@ fn parse_f64(v: &str) -> Option { } } +/// Main structure for quickly parsing YAML. +/// +/// See [`YamlLoader::load_from_str`]. pub struct YamlLoader { docs: Vec, // states @@ -188,14 +187,28 @@ impl YamlLoader { } } + /// Load the given string as a set of YAML documents. + /// + /// The `source` is interpreted as YAML documents and is parsed. Parsing succeeds if and only + /// if all documents are parsed successfully. An error in a latter document prevents the former + /// from being returned. pub fn load_from_str(source: &str) -> Result, ScanError> { + Self::load_from_iter(source.chars()) + } + + /// Load the contents of the given iterator as a set of YAML documents. + /// + /// The `source` is interpreted as YAML documents and is parsed. Parsing succeeds if and only + /// if all documents are parsed successfully. An error in a latter document prevents the former + /// from being returned. + pub fn load_from_iter>(source: I) -> Result, ScanError> { let mut loader = YamlLoader { docs: Vec::new(), doc_stack: Vec::new(), key_stack: Vec::new(), anchor_map: BTreeMap::new(), }; - let mut parser = Parser::new(source.chars()); + let mut parser = Parser::new(source); parser.load(&mut loader, true)?; Ok(loader.docs) } @@ -251,23 +264,28 @@ impl Yaml { define_into!(into_hash, Hash, Hash); define_into!(into_vec, Array, Array); - /// Returns the is null of this [`Yaml`]. + /// Return whether `self` is a [`Yaml::Null`] node. #[must_use] pub fn is_null(&self) -> bool { matches!(*self, Yaml::Null) } - /// Returns the is badvalue of this [`Yaml`]. + /// Return whether `self` is a [`Yaml::BadValue`] node. #[must_use] pub fn is_badvalue(&self) -> bool { matches!(*self, Yaml::BadValue) } + /// Return whether `self` is a [`Yaml::Array`] node. #[must_use] pub fn is_array(&self) -> bool { matches!(*self, Yaml::Array(_)) } + /// Return the `f64` value contained in this YAML node. + /// + /// If the node is not a [`Yaml::Real`] YAML node or its contents is not a valid `f64` string, + /// `None` is returned. #[must_use] pub fn as_f64(&self) -> Option { if let Yaml::Real(ref v) = self { @@ -277,20 +295,35 @@ impl Yaml { } } + /// Return the `f64` value contained in this YAML node. + /// + /// If the node is not a [`Yaml::Real`] YAML node or its contents is not a valid `f64` string, + /// `None` is returned. #[must_use] pub fn into_f64(self) -> Option { - if let Yaml::Real(ref v) = self { - parse_f64(v) - } else { - None - } + self.as_f64() } } #[cfg_attr(feature = "cargo-clippy", allow(clippy::should_implement_trait))] impl Yaml { - // Not implementing FromStr because there is no possibility of Error. - // This function falls back to Yaml::String if nothing else matches. + /// Convert a string to a [`Yaml`] node. + /// + /// [`Yaml`] does not implement [`std::str::FromStr`] since conversion may not fail. This + /// function falls back to [`Yaml::String`] if nothing else matches. + /// + /// # Examples + /// ``` + /// # use yaml_rust::yaml::Yaml; + /// assert!(matches!(Yaml::from_str("42"), Yaml::Integer(42))); + /// assert!(matches!(Yaml::from_str("0x2A"), Yaml::Integer(42))); + /// assert!(matches!(Yaml::from_str("0o52"), Yaml::Integer(42))); + /// assert!(matches!(Yaml::from_str("~"), Yaml::Null)); + /// assert!(matches!(Yaml::from_str("null"), Yaml::Null)); + /// assert!(matches!(Yaml::from_str("true"), Yaml::Boolean(true))); + /// assert!(matches!(Yaml::from_str("3.14"), Yaml::Real(_))); + /// assert!(matches!(Yaml::from_str("foo"), Yaml::String(_))); + /// ``` #[must_use] pub fn from_str(v: &str) -> Yaml { if let Some(number) = v.strip_prefix("0x") { @@ -362,8 +395,9 @@ impl IntoIterator for Yaml { } } +/// An iterator over a [`Yaml`] node. pub struct YamlIter { - yaml: vec::IntoIter, + yaml: std::vec::IntoIter, } impl Iterator for YamlIter { @@ -373,378 +407,3 @@ impl Iterator for YamlIter { self.yaml.next() } } - -#[cfg(test)] -#[allow(clippy::bool_assert_comparison)] -#[allow(clippy::float_cmp)] -mod test { - use crate::yaml::{vec, Yaml, YamlLoader}; - use std::f64; - #[test] - fn test_coerce() { - let s = "--- -a: 1 -b: 2.2 -c: [1, 2] -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out[0]; - assert_eq!(doc["a"].as_i64().unwrap(), 1i64); - assert_eq!(doc["b"].as_f64().unwrap(), 2.2f64); - assert_eq!(doc["c"][1].as_i64().unwrap(), 2i64); - assert!(doc["d"][0].is_badvalue()); - } - - #[test] - fn test_empty_doc() { - let s: String = String::new(); - YamlLoader::load_from_str(&s).unwrap(); - let s: String = "---".to_owned(); - assert_eq!(YamlLoader::load_from_str(&s).unwrap()[0], Yaml::Null); - } - - #[test] - fn test_parser() { - let s: String = " -# comment -a0 bb: val -a1: - b1: 4 - b2: d -a2: 4 # i'm comment -a3: [1, 2, 3] -a4: - - - a1 - - a2 - - 2 -a5: 'single_quoted' -a6: \"double_quoted\" -a7: 你好 -" - .to_owned(); - let out = YamlLoader::load_from_str(&s).unwrap(); - let doc = &out[0]; - assert_eq!(doc["a7"].as_str().unwrap(), "你好"); - } - - #[test] - fn test_multi_doc() { - let s = " -'a scalar' ---- -'a scalar' ---- -'a scalar' -"; - let out = YamlLoader::load_from_str(s).unwrap(); - assert_eq!(out.len(), 3); - } - - #[test] - fn test_anchor() { - let s = " -a1: &DEFAULT - b1: 4 - b2: d -a2: *DEFAULT -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out[0]; - assert_eq!(doc["a2"]["b1"].as_i64().unwrap(), 4); - } - - #[test] - fn test_bad_anchor() { - let s = " -a1: &DEFAULT - b1: 4 - b2: *DEFAULT -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out[0]; - assert_eq!(doc["a1"]["b2"], Yaml::BadValue); - } - - #[test] - fn test_github_27() { - // https://github.com/chyh1990/yaml-rust/issues/27 - let s = "&a"; - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out[0]; - assert_eq!(doc.as_str().unwrap(), ""); - } - - #[test] - fn test_plain_datatype() { - let s = " -- 'string' -- \"string\" -- string -- 123 -- -321 -- 1.23 -- -1e4 -- ~ -- null -- true -- false -- !!str 0 -- !!int 100 -- !!float 2 -- !!null ~ -- !!bool true -- !!bool false -- 0xFF -# bad values -- !!int string -- !!float string -- !!bool null -- !!null val -- 0o77 -- [ 0xF, 0xF ] -- +12345 -- [ true, false ] -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out[0]; - - assert_eq!(doc[0].as_str().unwrap(), "string"); - assert_eq!(doc[1].as_str().unwrap(), "string"); - assert_eq!(doc[2].as_str().unwrap(), "string"); - assert_eq!(doc[3].as_i64().unwrap(), 123); - assert_eq!(doc[4].as_i64().unwrap(), -321); - assert_eq!(doc[5].as_f64().unwrap(), 1.23); - assert_eq!(doc[6].as_f64().unwrap(), -1e4); - assert!(doc[7].is_null()); - assert!(doc[8].is_null()); - assert_eq!(doc[9].as_bool().unwrap(), true); - assert_eq!(doc[10].as_bool().unwrap(), false); - assert_eq!(doc[11].as_str().unwrap(), "0"); - assert_eq!(doc[12].as_i64().unwrap(), 100); - assert_eq!(doc[13].as_f64().unwrap(), 2.0); - assert!(doc[14].is_null()); - assert_eq!(doc[15].as_bool().unwrap(), true); - assert_eq!(doc[16].as_bool().unwrap(), false); - assert_eq!(doc[17].as_i64().unwrap(), 255); - assert!(doc[18].is_badvalue()); - assert!(doc[19].is_badvalue()); - assert!(doc[20].is_badvalue()); - assert!(doc[21].is_badvalue()); - assert_eq!(doc[22].as_i64().unwrap(), 63); - assert_eq!(doc[23][0].as_i64().unwrap(), 15); - assert_eq!(doc[23][1].as_i64().unwrap(), 15); - assert_eq!(doc[24].as_i64().unwrap(), 12345); - assert!(doc[25][0].as_bool().unwrap()); - assert!(!doc[25][1].as_bool().unwrap()); - } - - #[test] - fn test_bad_hyphen() { - // See: https://github.com/chyh1990/yaml-rust/issues/23 - let s = "{-"; - assert!(YamlLoader::load_from_str(s).is_err()); - } - - #[test] - fn test_issue_65() { - // See: https://github.com/chyh1990/yaml-rust/issues/65 - let b = "\n\"ll\\\"ll\\\r\n\"ll\\\"ll\\\r\r\r\rU\r\r\rU"; - assert!(YamlLoader::load_from_str(b).is_err()); - } - - #[test] - fn test_bad_docstart() { - assert!(YamlLoader::load_from_str("---This used to cause an infinite loop").is_ok()); - assert_eq!( - YamlLoader::load_from_str("----"), - Ok(vec![Yaml::String(String::from("----"))]) - ); - assert_eq!( - YamlLoader::load_from_str("--- #here goes a comment"), - Ok(vec![Yaml::Null]) - ); - assert_eq!( - YamlLoader::load_from_str("---- #here goes a comment"), - Ok(vec![Yaml::String(String::from("----"))]) - ); - } - - #[test] - fn test_plain_datatype_with_into_methods() { - let s = " -- 'string' -- \"string\" -- string -- 123 -- -321 -- 1.23 -- -1e4 -- true -- false -- !!str 0 -- !!int 100 -- !!float 2 -- !!bool true -- !!bool false -- 0xFF -- 0o77 -- +12345 -- -.INF -- .NAN -- !!float .INF -"; - let mut out = YamlLoader::load_from_str(s).unwrap().into_iter(); - let mut doc = out.next().unwrap().into_iter(); - - assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); - assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); - assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), 123); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), -321); - assert_eq!(doc.next().unwrap().into_f64().unwrap(), 1.23); - assert_eq!(doc.next().unwrap().into_f64().unwrap(), -1e4); - assert_eq!(doc.next().unwrap().into_bool().unwrap(), true); - assert_eq!(doc.next().unwrap().into_bool().unwrap(), false); - assert_eq!(doc.next().unwrap().into_string().unwrap(), "0"); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), 100); - assert_eq!(doc.next().unwrap().into_f64().unwrap(), 2.0); - assert_eq!(doc.next().unwrap().into_bool().unwrap(), true); - assert_eq!(doc.next().unwrap().into_bool().unwrap(), false); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), 255); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), 63); - assert_eq!(doc.next().unwrap().into_i64().unwrap(), 12345); - assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::NEG_INFINITY); - assert!(doc.next().unwrap().into_f64().is_some()); - assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::INFINITY); - } - - #[test] - fn test_hash_order() { - let s = "--- -b: ~ -a: ~ -c: ~ -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let first = out.into_iter().next().unwrap(); - let mut iter = first.into_hash().unwrap().into_iter(); - assert_eq!( - Some((Yaml::String("b".to_owned()), Yaml::Null)), - iter.next() - ); - assert_eq!( - Some((Yaml::String("a".to_owned()), Yaml::Null)), - iter.next() - ); - assert_eq!( - Some((Yaml::String("c".to_owned()), Yaml::Null)), - iter.next() - ); - assert_eq!(None, iter.next()); - } - - #[test] - fn test_integer_key() { - let s = " -0: - important: true -1: - important: false -"; - let out = YamlLoader::load_from_str(s).unwrap(); - let first = out.into_iter().next().unwrap(); - assert_eq!(first[0]["important"].as_bool().unwrap(), true); - } - - #[test] - fn test_indentation_equality() { - let four_spaces = YamlLoader::load_from_str( - r#" -hash: - with: - indentations -"#, - ) - .unwrap() - .into_iter() - .next() - .unwrap(); - - let two_spaces = YamlLoader::load_from_str( - r#" -hash: - with: - indentations -"#, - ) - .unwrap() - .into_iter() - .next() - .unwrap(); - - let one_space = YamlLoader::load_from_str( - r#" -hash: - with: - indentations -"#, - ) - .unwrap() - .into_iter() - .next() - .unwrap(); - - let mixed_spaces = YamlLoader::load_from_str( - r#" -hash: - with: - indentations -"#, - ) - .unwrap() - .into_iter() - .next() - .unwrap(); - - assert_eq!(four_spaces, two_spaces); - assert_eq!(two_spaces, one_space); - assert_eq!(four_spaces, mixed_spaces); - } - - #[test] - fn test_two_space_indentations() { - // https://github.com/kbknapp/clap-rs/issues/965 - - let s = r#" -subcommands: - - server: - about: server related commands -subcommands2: - - server: - about: server related commands -subcommands3: - - server: - about: server related commands - "#; - - let out = YamlLoader::load_from_str(s).unwrap(); - let doc = &out.into_iter().next().unwrap(); - - println!("{doc:#?}"); - assert_eq!(doc["subcommands"][0]["server"], Yaml::Null); - assert!(doc["subcommands2"][0]["server"].as_hash().is_some()); - assert!(doc["subcommands3"][0]["server"].as_hash().is_some()); - } - - #[test] - fn test_recursion_depth_check_objects() { - let s = "{a:".repeat(10_000) + &"}".repeat(10_000); - assert!(YamlLoader::load_from_str(&s).is_err()); - } - - #[test] - fn test_recursion_depth_check_arrays() { - let s = "[".repeat(10_000) + &"]".repeat(10_000); - assert!(YamlLoader::load_from_str(&s).is_err()); - } -} diff --git a/saphyr/tests/basic.rs b/saphyr/tests/basic.rs new file mode 100644 index 0000000..1ec8751 --- /dev/null +++ b/saphyr/tests/basic.rs @@ -0,0 +1,424 @@ +#![allow(clippy::bool_assert_comparison)] +#![allow(clippy::float_cmp)] + +use std::vec; +use yaml_rust::{Yaml, YamlEmitter, YamlLoader}; + +#[test] +fn test_api() { + let s = " +# from yaml-cpp example +- name: Ogre + position: [0, 5, 0] + powers: + - name: Club + damage: 10 + - name: Fist + damage: 8 +- name: Dragon + position: [1, 0, 10] + powers: + - name: Fire Breath + damage: 25 + - name: Claws + damage: 15 +- name: Wizard + position: [5, -3, 0] + powers: + - name: Acid Rain + damage: 50 + - name: Staff + damage: 3 +"; + let docs = YamlLoader::load_from_str(s).unwrap(); + let doc = &docs[0]; + + assert_eq!(doc[0]["name"].as_str().unwrap(), "Ogre"); + + let mut writer = String::new(); + { + let mut emitter = YamlEmitter::new(&mut writer); + emitter.dump(doc).unwrap(); + } + + assert!(!writer.is_empty()); +} + +#[test] +fn test_fail() { + let s = " +# syntax error +scalar +key: [1, 2]] +key1:a2 +"; + assert!(YamlLoader::load_from_str(s).is_err()); +} + +#[test] +fn test_coerce() { + let s = "--- +a: 1 +b: 2.2 +c: [1, 2] +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out[0]; + assert_eq!(doc["a"].as_i64().unwrap(), 1i64); + assert_eq!(doc["b"].as_f64().unwrap(), 2.2f64); + assert_eq!(doc["c"][1].as_i64().unwrap(), 2i64); + assert!(doc["d"][0].is_badvalue()); +} + +#[test] +fn test_empty_doc() { + let s: String = String::new(); + YamlLoader::load_from_str(&s).unwrap(); + let s: String = "---".to_owned(); + assert_eq!(YamlLoader::load_from_str(&s).unwrap()[0], Yaml::Null); +} + +#[test] +fn test_parser() { + let s: String = " +# comment +a0 bb: val +a1: + b1: 4 + b2: d +a2: 4 # i'm comment +a3: [1, 2, 3] +a4: + - - a1 + - a2 + - 2 +a5: 'single_quoted' +a6: \"double_quoted\" +a7: 你好 +" + .to_owned(); + let out = YamlLoader::load_from_str(&s).unwrap(); + let doc = &out[0]; + assert_eq!(doc["a7"].as_str().unwrap(), "你好"); +} + +#[test] +fn test_multi_doc() { + let s = " +'a scalar' +--- +'a scalar' +--- +'a scalar' +"; + let out = YamlLoader::load_from_str(s).unwrap(); + assert_eq!(out.len(), 3); +} + +#[test] +fn test_anchor() { + let s = " +a1: &DEFAULT + b1: 4 + b2: d +a2: *DEFAULT +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out[0]; + assert_eq!(doc["a2"]["b1"].as_i64().unwrap(), 4); +} + +#[test] +fn test_bad_anchor() { + let s = " +a1: &DEFAULT + b1: 4 + b2: *DEFAULT +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out[0]; + assert_eq!(doc["a1"]["b2"], Yaml::BadValue); +} + +#[test] +fn test_github_27() { + // https://github.com/chyh1990/yaml-rust/issues/27 + let s = "&a"; + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out[0]; + assert_eq!(doc.as_str().unwrap(), ""); +} + +#[test] +fn test_plain_datatype() { + let s = " +- 'string' +- \"string\" +- string +- 123 +- -321 +- 1.23 +- -1e4 +- ~ +- null +- true +- false +- !!str 0 +- !!int 100 +- !!float 2 +- !!null ~ +- !!bool true +- !!bool false +- 0xFF +# bad values +- !!int string +- !!float string +- !!bool null +- !!null val +- 0o77 +- [ 0xF, 0xF ] +- +12345 +- [ true, false ] +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out[0]; + + assert_eq!(doc[0].as_str().unwrap(), "string"); + assert_eq!(doc[1].as_str().unwrap(), "string"); + assert_eq!(doc[2].as_str().unwrap(), "string"); + assert_eq!(doc[3].as_i64().unwrap(), 123); + assert_eq!(doc[4].as_i64().unwrap(), -321); + assert_eq!(doc[5].as_f64().unwrap(), 1.23); + assert_eq!(doc[6].as_f64().unwrap(), -1e4); + assert!(doc[7].is_null()); + assert!(doc[8].is_null()); + assert_eq!(doc[9].as_bool().unwrap(), true); + assert_eq!(doc[10].as_bool().unwrap(), false); + assert_eq!(doc[11].as_str().unwrap(), "0"); + assert_eq!(doc[12].as_i64().unwrap(), 100); + assert_eq!(doc[13].as_f64().unwrap(), 2.0); + assert!(doc[14].is_null()); + assert_eq!(doc[15].as_bool().unwrap(), true); + assert_eq!(doc[16].as_bool().unwrap(), false); + assert_eq!(doc[17].as_i64().unwrap(), 255); + assert!(doc[18].is_badvalue()); + assert!(doc[19].is_badvalue()); + assert!(doc[20].is_badvalue()); + assert!(doc[21].is_badvalue()); + assert_eq!(doc[22].as_i64().unwrap(), 63); + assert_eq!(doc[23][0].as_i64().unwrap(), 15); + assert_eq!(doc[23][1].as_i64().unwrap(), 15); + assert_eq!(doc[24].as_i64().unwrap(), 12345); + assert!(doc[25][0].as_bool().unwrap()); + assert!(!doc[25][1].as_bool().unwrap()); +} + +#[test] +fn test_bad_hyphen() { + // See: https://github.com/chyh1990/yaml-rust/issues/23 + let s = "{-"; + assert!(YamlLoader::load_from_str(s).is_err()); +} + +#[test] +fn test_issue_65() { + // See: https://github.com/chyh1990/yaml-rust/issues/65 + let b = "\n\"ll\\\"ll\\\r\n\"ll\\\"ll\\\r\r\r\rU\r\r\rU"; + assert!(YamlLoader::load_from_str(b).is_err()); +} + +#[test] +fn test_bad_docstart() { + assert!(YamlLoader::load_from_str("---This used to cause an infinite loop").is_ok()); + assert_eq!( + YamlLoader::load_from_str("----"), + Ok(vec![Yaml::String(String::from("----"))]) + ); + assert_eq!( + YamlLoader::load_from_str("--- #here goes a comment"), + Ok(vec![Yaml::Null]) + ); + assert_eq!( + YamlLoader::load_from_str("---- #here goes a comment"), + Ok(vec![Yaml::String(String::from("----"))]) + ); +} + +#[test] +fn test_plain_datatype_with_into_methods() { + let s = " +- 'string' +- \"string\" +- string +- 123 +- -321 +- 1.23 +- -1e4 +- true +- false +- !!str 0 +- !!int 100 +- !!float 2 +- !!bool true +- !!bool false +- 0xFF +- 0o77 +- +12345 +- -.INF +- .NAN +- !!float .INF +"; + let mut out = YamlLoader::load_from_str(s).unwrap().into_iter(); + let mut doc = out.next().unwrap().into_iter(); + + assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); + assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); + assert_eq!(doc.next().unwrap().into_string().unwrap(), "string"); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), 123); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), -321); + assert_eq!(doc.next().unwrap().into_f64().unwrap(), 1.23); + assert_eq!(doc.next().unwrap().into_f64().unwrap(), -1e4); + assert_eq!(doc.next().unwrap().into_bool().unwrap(), true); + assert_eq!(doc.next().unwrap().into_bool().unwrap(), false); + assert_eq!(doc.next().unwrap().into_string().unwrap(), "0"); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), 100); + assert_eq!(doc.next().unwrap().into_f64().unwrap(), 2.0); + assert_eq!(doc.next().unwrap().into_bool().unwrap(), true); + assert_eq!(doc.next().unwrap().into_bool().unwrap(), false); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), 255); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), 63); + assert_eq!(doc.next().unwrap().into_i64().unwrap(), 12345); + assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::NEG_INFINITY); + assert!(doc.next().unwrap().into_f64().is_some()); + assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::INFINITY); +} + +#[test] +fn test_hash_order() { + let s = "--- +b: ~ +a: ~ +c: ~ +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let first = out.into_iter().next().unwrap(); + let mut iter = first.into_hash().unwrap().into_iter(); + assert_eq!( + Some((Yaml::String("b".to_owned()), Yaml::Null)), + iter.next() + ); + assert_eq!( + Some((Yaml::String("a".to_owned()), Yaml::Null)), + iter.next() + ); + assert_eq!( + Some((Yaml::String("c".to_owned()), Yaml::Null)), + iter.next() + ); + assert_eq!(None, iter.next()); +} + +#[test] +fn test_integer_key() { + let s = " +0: + important: true +1: + important: false +"; + let out = YamlLoader::load_from_str(s).unwrap(); + let first = out.into_iter().next().unwrap(); + assert_eq!(first[0]["important"].as_bool().unwrap(), true); +} + +#[test] +fn test_indentation_equality() { + let four_spaces = YamlLoader::load_from_str( + r#" +hash: + with: + indentations +"#, + ) + .unwrap() + .into_iter() + .next() + .unwrap(); + + let two_spaces = YamlLoader::load_from_str( + r#" +hash: + with: + indentations +"#, + ) + .unwrap() + .into_iter() + .next() + .unwrap(); + + let one_space = YamlLoader::load_from_str( + r#" +hash: + with: + indentations +"#, + ) + .unwrap() + .into_iter() + .next() + .unwrap(); + + let mixed_spaces = YamlLoader::load_from_str( + r#" +hash: + with: + indentations +"#, + ) + .unwrap() + .into_iter() + .next() + .unwrap(); + + assert_eq!(four_spaces, two_spaces); + assert_eq!(two_spaces, one_space); + assert_eq!(four_spaces, mixed_spaces); +} + +#[test] +fn test_two_space_indentations() { + // https://github.com/kbknapp/clap-rs/issues/965 + + let s = r#" +subcommands: + - server: + about: server related commands +subcommands2: + - server: + about: server related commands +subcommands3: + - server: + about: server related commands + "#; + + let out = YamlLoader::load_from_str(s).unwrap(); + let doc = &out.into_iter().next().unwrap(); + + println!("{doc:#?}"); + assert_eq!(doc["subcommands"][0]["server"], Yaml::Null); + assert!(doc["subcommands2"][0]["server"].as_hash().is_some()); + assert!(doc["subcommands3"][0]["server"].as_hash().is_some()); +} + +#[test] +fn test_recursion_depth_check_objects() { + let s = "{a:".repeat(10_000) + &"}".repeat(10_000); + assert!(YamlLoader::load_from_str(&s).is_err()); +} + +#[test] +fn test_recursion_depth_check_arrays() { + let s = "[".repeat(10_000) + &"]".repeat(10_000); + assert!(YamlLoader::load_from_str(&s).is_err()); +}