diff --git a/Cargo.toml b/Cargo.toml index 7bc449a..65630a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ repository = "https://github.com/chyh1990/yaml-rust" readme = "README.md" [dependencies] +derivative = "1" linked-hash-map = ">=0.0.9, <0.6" [dev-dependencies] +indoc = "0.3" quickcheck = "0.7" diff --git a/src/lib.rs b/src/lib.rs index 40cff18..e061541 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ //! # Examples //! //! ``` -//! use yaml_rust::{YamlLoader, YamlEmitter}; +//! use yaml_rust::{YamlLoader, YamlEmitter, YamlNode}; //! //! let docs = YamlLoader::load_from_str("[1, 2, 3]").unwrap(); //! let doc = &docs[0]; // select the first document @@ -44,6 +44,7 @@ allow(match_same_arms, should_implement_trait) )] +extern crate derivative; extern crate linked_hash_map; pub mod emitter; @@ -54,8 +55,8 @@ pub mod yaml; // reexport key APIs pub use emitter::{EmitError, YamlEmitter}; pub use parser::Event; -pub use scanner::ScanError; -pub use yaml::{Yaml, YamlLoader}; +pub use scanner::{Marker, ScanError}; +pub use yaml::{Node, Yaml, YamlLoader, YamlMarked, YamlNode}; #[cfg(test)] mod tests { diff --git a/src/scanner.rs b/src/scanner.rs index 6f4fa58..1cd261f 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -18,7 +18,7 @@ pub enum TScalarStyle { Foled, } -#[derive(Clone, Copy, PartialEq, Debug, Eq)] +#[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Eq, Ord, Hash)] pub struct Marker { index: usize, line: usize, diff --git a/src/yaml/ast.rs b/src/yaml/ast.rs new file mode 100644 index 0000000..e1c8af6 --- /dev/null +++ b/src/yaml/ast.rs @@ -0,0 +1,421 @@ +use super::parse_f64::parse_f64; +use derivative::Derivative; +use linked_hash_map::LinkedHashMap; +use scanner::Marker; +use std::ops::Index; +use std::{hash, string, vec}; + +pub trait YamlNode { + type Child: YamlNode + Eq + hash::Hash + Ord; + + fn as_bool(&self) -> Option; + fn as_f64(&self) -> Option; + fn as_i64(&self) -> Option; + fn as_str(&self) -> Option<&str>; + fn as_hash(&self) -> Option<&LinkedHashMap>; + fn as_vec(&self) -> Option<&Vec>; + + fn into_bool(self) -> Option; + fn into_f64(self) -> Option; + fn into_i64(self) -> Option; + fn into_string(self) -> Option; + fn into_hash(self) -> Option>; + fn into_vec(self) -> Option>; + + fn is_null(&self) -> bool; + fn is_badvalue(&self) -> bool; + fn is_array(&self) -> bool; + + fn bad_value() -> &'static Self; +} + +macro_rules! define_as ( + ($enum_name:ident, $name:ident, $t:ty, $yt:ident) => ( +fn $name(&self) -> Option<$t> { + match *self { + $enum_name::$yt(v) => Some(v), + _ => None + } +} + ); +); + +macro_rules! define_as_ref ( + ($enum_name:ident, $name:ident, $t:ty, $yt:ident) => ( +fn $name(&self) -> Option<$t> { + match self { + $enum_name::$yt(v) => Some(v), + _ => None + } +} + ); +); + +macro_rules! define_into ( + ($enum_name:ident, $name:ident, $t:ty, $yt:ident) => ( +fn $name(self) -> Option<$t> { + match self { + $enum_name::$yt(v) => Some(v), + _ => None + } +} + ); +); + +macro_rules! yaml_enum ( + ($enum_name:ident, $child_type:ty, $bad_value:ident) => ( +/// A YAML node is stored as this `Yaml` enumeration, which provides an easy way to +/// access your YAML document. The `YamlMarked` enumeration mirrors `Yaml`, but pairs each +/// child node with a source location marker. +/// +/// # Examples +/// +/// ``` +/// use yaml_rust::{Yaml, YamlNode}; +/// let foo = Yaml::from_str("-123"); // convert the string to the appropriate YAML type +/// assert_eq!(foo.as_i64().unwrap(), -123); +/// +/// // iterate over an Array +/// let vec = Yaml::Array(vec![Yaml::Integer(1), Yaml::Integer(2)]); +/// for v in vec.as_vec().unwrap() { +/// assert!(v.as_i64().is_some()); +/// } +/// ``` +#[derive(Clone, PartialEq, PartialOrd, Debug, Eq, Ord, Hash)] +pub enum $enum_name { + /// 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), + /// YAML int is stored as i64. + Integer(i64), + /// YAML scalar. + String(string::String), + /// YAML bool, e.g. `true` or `false`. + Boolean(bool), + /// YAML array, can be accessed as a `Vec`. + Array(Vec<$child_type>), + /// YAML hash, can be accessed as a `LinkedHashMap`. + /// + /// Insertion order will match the order of insertion into the map. + Hash(LinkedHashMap<$child_type, $child_type>), + /// Alias, not fully supported yet. + Alias(usize), + /// YAML null, e.g. `null` or `~`. + Null, + /// Accessing a nonexistent node via the Index trait returns `BadValue`. This + /// simplifies error handling in the calling code. Invalid type conversion also + /// returns `BadValue`. + BadValue, +} + +static $bad_value: $enum_name = $enum_name::BadValue; + +impl YamlNode for $enum_name { + type Child = $child_type; + + define_as!($enum_name, as_bool, bool, Boolean); + define_as!($enum_name, as_i64, i64, Integer); + + define_as_ref!($enum_name, as_str, &str, String); + define_as_ref!($enum_name, as_hash, &LinkedHashMap, Hash); + define_as_ref!($enum_name, as_vec, &Vec, Array); + + define_into!($enum_name, into_bool, bool, Boolean); + define_into!($enum_name, into_i64, i64, Integer); + define_into!($enum_name, into_string, String, String); + define_into!($enum_name, into_hash, LinkedHashMap, Hash); + define_into!($enum_name, into_vec, Vec, Array); + + fn is_null(&self) -> bool { + match self { + $enum_name::Null => true, + _ => false, + } + } + + fn is_badvalue(&self) -> bool { + match self { + $enum_name::BadValue => true, + _ => false, + } + } + + fn is_array(&self) -> bool { + match self { + $enum_name::Array(_) => true, + _ => false, + } + } + + fn as_f64(&self) -> Option { + match self { + $enum_name::Real(v) => parse_f64(v), + _ => None, + } + } + + fn into_f64(self) -> Option { + match self { + $enum_name::Real(ref v) => parse_f64(v), + _ => None, + } + } + + #[inline] + fn bad_value() -> &'static Self { + &$bad_value + } +} + ); +); + +yaml_enum!(Yaml, Yaml, BAD_VALUE_YAML); +yaml_enum!(YamlMarked, Node, BAD_VALUE_YAML_MARKED); + +pub type Array = Vec; +pub type Hash = LinkedHashMap; + +pub type ArrayNode = Vec; +pub type HashNode = LinkedHashMap; + +/// A `Node` is a YAML AST node paired with a source location marker. +#[derive(Clone, Debug, Derivative, Ord, PartialOrd)] +#[derivative(Eq, Hash, PartialEq)] +pub struct Node( + pub YamlMarked, + #[derivative(Hash = "ignore")] + #[derivative(PartialEq = "ignore")] + pub Option, +); + +impl Node { + pub fn marker(&self) -> Option { + self.1 + } + + pub fn value(&self) -> &YamlMarked { + &self.0 + } + + pub fn into_value(self) -> YamlMarked { + self.0 + } +} + +macro_rules! node_method_ref ( + ($name:ident, $t:ty) => ( +fn $name(&self) -> $t { + self.value().$name() +} + ); +); + +macro_rules! node_method_owned ( + ($name:ident, $t: ty) => ( +fn $name(self) -> $t { + self.into_value().$name() +} + ); +); + +static BAD_VALUE_NODE: Node = Node(YamlMarked::BadValue, None); +impl YamlNode for Node { + type Child = Node; + + node_method_ref!(as_bool, Option); + node_method_ref!(as_f64, Option); + node_method_ref!(as_i64, Option); + node_method_ref!(as_str, Option<&str>); + node_method_ref!(as_hash, Option<&LinkedHashMap>); + node_method_ref!(as_vec, Option<&Vec>); + + node_method_owned!(into_bool, Option); + node_method_owned!(into_i64, Option); + node_method_owned!(into_f64, Option); + node_method_owned!(into_string, Option); + node_method_owned!(into_hash, Option>); + node_method_owned!(into_vec, Option>); + + node_method_ref!(is_null, bool); + node_method_ref!(is_badvalue, bool); + node_method_ref!(is_array, bool); + + #[inline] + fn bad_value() -> &'static Self { + &BAD_VALUE_NODE + } +} + +impl From for Yaml { + fn from(yaml: YamlMarked) -> Self { + match yaml { + YamlMarked::Real(s) => Yaml::Real(s), + YamlMarked::Integer(i) => Yaml::Integer(i), + YamlMarked::String(s) => Yaml::String(s), + YamlMarked::Boolean(b) => Yaml::Boolean(b), + YamlMarked::Array(v) => Yaml::Array(v.into_iter().map(|Node(y, _)| y.into()).collect()), + YamlMarked::Hash(h) => Yaml::Hash( + h.into_iter() + .map(|(Node(k, _), Node(v, _))| (k.into(), v.into())) + .collect(), + ), + YamlMarked::Alias(i) => Yaml::Alias(i), + YamlMarked::Null => Yaml::Null, + YamlMarked::BadValue => Yaml::BadValue, + } + } +} + +impl From for Node { + fn from(yaml: Yaml) -> Self { + match yaml { + Yaml::Real(s) => Node(YamlMarked::Real(s), None), + Yaml::Integer(i) => Node(YamlMarked::Integer(i), None), + Yaml::String(s) => Node(YamlMarked::String(s), None), + Yaml::Boolean(b) => Node(YamlMarked::Boolean(b), None), + Yaml::Array(v) => Node( + YamlMarked::Array(v.into_iter().map(From::from).collect()), + None, + ), + Yaml::Hash(h) => Node( + YamlMarked::Hash(h.into_iter().map(|(k, v)| (k.into(), v.into())).collect()), + None, + ), + Yaml::Alias(i) => Node(YamlMarked::Alias(i), None), + Yaml::Null => Node(YamlMarked::Null, None), + Yaml::BadValue => Node(YamlMarked::BadValue, None), + } + } +} + +impl From for Yaml { + fn from(node: Node) -> Self { + node.into_value().into() + } +} + +#[cfg_attr(feature = "cargo-clippy", allow(should_implement_trait))] +impl YamlMarked { + // Not implementing FromStr because there is no possibility of Error. + // This function falls back to YamlMarked::String if nothing else matches. + pub fn from_str(v: &str) -> Self { + if v.starts_with("0x") { + let n = i64::from_str_radix(&v[2..], 16); + if n.is_ok() { + return YamlMarked::Integer(n.unwrap()); + } + } + if v.starts_with("0o") { + let n = i64::from_str_radix(&v[2..], 8); + if n.is_ok() { + return YamlMarked::Integer(n.unwrap()); + } + } + if v.starts_with('+') && v[1..].parse::().is_ok() { + return YamlMarked::Integer(v[1..].parse::().unwrap()); + } + match v { + "~" | "null" => YamlMarked::Null, + "true" => YamlMarked::Boolean(true), + "false" => YamlMarked::Boolean(false), + _ if v.parse::().is_ok() => YamlMarked::Integer(v.parse::().unwrap()), + // try parsing as f64 + _ if parse_f64(v).is_some() => YamlMarked::Real(v.to_owned()), + _ => YamlMarked::String(v.to_owned()), + } + } +} + +#[cfg_attr(feature = "cargo-clippy", allow(should_implement_trait))] +impl Yaml { + pub fn from_str(v: &str) -> Self { + YamlMarked::from_str(v).into() + } +} + +impl<'a> Index<&'a str> for Yaml { + type Output = Yaml; + + fn index(&self, idx: &'a str) -> &Yaml { + let key = Yaml::String(idx.to_owned()); + match self.as_hash() { + Some(h) => h.get(&key).unwrap_or(&Yaml::bad_value()), + None => &Yaml::bad_value(), + } + } +} + +impl<'a> Index<&'a str> for YamlMarked { + type Output = Node; + + fn index(&self, idx: &'a str) -> &Node { + let key = Node(YamlMarked::String(idx.to_owned()), None); + match self.as_hash() { + Some(h) => h.get(&key).unwrap_or(&Node::bad_value()), + None => &Node::bad_value(), + } + } +} + +impl<'a> Index<&'a str> for Node { + type Output = Node; + + fn index(&self, idx: &'a str) -> &Node { + self.value().index(idx) + } +} + +impl Index for Yaml { + type Output = Yaml; + + fn index(&self, idx: usize) -> &Yaml { + if let Some(v) = self.as_vec() { + v.get(idx).unwrap_or(&Yaml::bad_value()) + } else if let Some(v) = self.as_hash() { + let key = Yaml::Integer(idx as i64); + v.get(&key).unwrap_or(&Yaml::bad_value()) + } else { + &Yaml::bad_value() + } + } +} + +impl Index for YamlMarked { + type Output = Node; + + fn index(&self, idx: usize) -> &Node { + if let Some(v) = self.as_vec() { + v.get(idx).unwrap_or(&Node::bad_value()) + } else if let Some(v) = self.as_hash() { + let key = Node(YamlMarked::Integer(idx as i64), None); + v.get(&key).unwrap_or(&Node::bad_value()) + } else { + &Node::bad_value() + } + } +} + +impl Index for Node { + type Output = Node; + + fn index(&self, idx: usize) -> &Node { + self.value().index(idx) + } +} + +macro_rules! define_into_iter ( + ($yaml_type:ty, $child_type:ty) => ( +impl IntoIterator for $yaml_type { + type Item = $child_type; + type IntoIter = vec::IntoIter<$child_type>; + + fn into_iter(self) -> Self::IntoIter { + self.into_vec().unwrap_or_else(Vec::new).into_iter() + } +} + ); +); + +define_into_iter!(Yaml, Yaml); +define_into_iter!(YamlMarked, Node); +define_into_iter!(Node, Node); diff --git a/src/yaml/mod.rs b/src/yaml/mod.rs new file mode 100644 index 0000000..365d0ec --- /dev/null +++ b/src/yaml/mod.rs @@ -0,0 +1,6 @@ +mod ast; +mod parse_f64; +mod yaml_loader; + +pub use self::ast::*; +pub use self::yaml_loader::YamlLoader; diff --git a/src/yaml/parse_f64.rs b/src/yaml/parse_f64.rs new file mode 100644 index 0000000..4059858 --- /dev/null +++ b/src/yaml/parse_f64.rs @@ -0,0 +1,12 @@ +use std::f64; + +// parse f64 as Core schema +// See: https://github.com/chyh1990/yaml-rust/issues/51 +pub fn parse_f64(v: &str) -> Option { + match v { + ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY), + "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY), + ".nan" | "NaN" | ".NAN" => Some(f64::NAN), + _ => v.parse::().ok(), + } +} diff --git a/src/yaml.rs b/src/yaml/yaml_loader.rs similarity index 63% rename from src/yaml.rs rename to src/yaml/yaml_loader.rs index fe112cc..cbcadf7 100644 --- a/src/yaml.rs +++ b/src/yaml/yaml_loader.rs @@ -1,82 +1,21 @@ -use linked_hash_map::LinkedHashMap; +use super::ast::{ArrayNode, HashNode, Node, Yaml, YamlMarked, YamlNode}; +use super::parse_f64::parse_f64; use parser::*; use scanner::{Marker, ScanError, TScalarStyle, TokenType}; 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. -/// -/// # Examples -/// -/// ``` -/// use yaml_rust::Yaml; -/// let foo = Yaml::from_str("-123"); // convert the string to the appropriate YAML type -/// assert_eq!(foo.as_i64().unwrap(), -123); -/// -/// // iterate over an Array -/// let vec = Yaml::Array(vec![Yaml::Integer(1), Yaml::Integer(2)]); -/// for v in vec.as_vec().unwrap() { -/// assert!(v.as_i64().is_some()); -/// } -/// ``` -#[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), - /// YAML int is stored as i64. - Integer(i64), - /// YAML scalar. - String(string::String), - /// YAML bool, e.g. `true` or `false`. - Boolean(bool), - /// YAML array, can be accessed as a `Vec`. - Array(self::Array), - /// YAML hash, can be accessed as a `LinkedHashMap`. - /// - /// Insertion order will match the order of insertion into the map. - Hash(self::Hash), - /// Alias, not fully supported yet. - Alias(usize), - /// YAML null, e.g. `null` or `~`. - Null, - /// Accessing a nonexistent node via the Index trait returns `BadValue`. This - /// simplifies error handling in the calling code. Invalid type conversion also - /// returns `BadValue`. - BadValue, -} - -pub type Array = Vec; -pub type Hash = LinkedHashMap; - -// parse f64 as Core schema -// See: https://github.com/chyh1990/yaml-rust/issues/51 -fn parse_f64(v: &str) -> Option { - match v { - ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY), - "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY), - ".nan" | "NaN" | ".NAN" => Some(f64::NAN), - _ => v.parse::().ok(), - } -} +use std::{i64, mem}; pub struct YamlLoader { - docs: Vec, + docs: Vec, // states // (current node, anchor_id) tuple - doc_stack: Vec<(Yaml, usize)>, - key_stack: Vec, - anchor_map: BTreeMap, + doc_stack: Vec<(Node, usize)>, + key_stack: Vec, + anchor_map: BTreeMap, } impl MarkedEventReceiver for YamlLoader { - fn on_event(&mut self, ev: Event, _: Marker) { + fn on_event(&mut self, ev: Event, marker: Marker) { // println!("EV {:?}", ev); match ev { Event::DocumentStart => { @@ -85,21 +24,24 @@ impl MarkedEventReceiver for YamlLoader { Event::DocumentEnd => { match self.doc_stack.len() { // empty document - 0 => self.docs.push(Yaml::BadValue), + 0 => self.docs.push(Node(YamlMarked::BadValue, Some(marker))), 1 => self.docs.push(self.doc_stack.pop().unwrap().0), _ => unreachable!(), } } Event::SequenceStart(aid) => { - self.doc_stack.push((Yaml::Array(Vec::new()), aid)); + self.doc_stack + .push((Node(YamlMarked::Array(ArrayNode::new()), Some(marker)), aid)); } Event::SequenceEnd => { let node = self.doc_stack.pop().unwrap(); self.insert_new_node(node); } Event::MappingStart(aid) => { - self.doc_stack.push((Yaml::Hash(Hash::new()), aid)); - self.key_stack.push(Yaml::BadValue); + self.doc_stack + .push((Node(YamlMarked::Hash(HashNode::new()), Some(marker)), aid)); + self.key_stack + .push(Node(YamlMarked::BadValue, Some(marker))); } Event::MappingEnd => { self.key_stack.pop().unwrap(); @@ -107,8 +49,8 @@ impl MarkedEventReceiver for YamlLoader { self.insert_new_node(node); } Event::Scalar(v, style, aid, tag) => { - let node = if style != TScalarStyle::Plain { - Yaml::String(v) + let yaml = if style != TScalarStyle::Plain { + YamlMarked::String(v) } else if let Some(TokenType::Tag(ref handle, ref suffix)) = tag { // XXX tag:yaml.org,2002: if handle == "!!" { @@ -116,38 +58,38 @@ impl MarkedEventReceiver for YamlLoader { "bool" => { // "true" or "false" match v.parse::() { - Err(_) => Yaml::BadValue, - Ok(v) => Yaml::Boolean(v), + Err(_) => YamlMarked::BadValue, + Ok(v) => YamlMarked::Boolean(v), } } "int" => match v.parse::() { - Err(_) => Yaml::BadValue, - Ok(v) => Yaml::Integer(v), + Err(_) => YamlMarked::BadValue, + Ok(v) => YamlMarked::Integer(v), }, "float" => match parse_f64(&v) { - Some(_) => Yaml::Real(v), - None => Yaml::BadValue, + Some(_) => YamlMarked::Real(v), + None => YamlMarked::BadValue, }, "null" => match v.as_ref() { - "~" | "null" => Yaml::Null, - _ => Yaml::BadValue, + "~" | "null" => YamlMarked::Null, + _ => YamlMarked::BadValue, }, - _ => Yaml::String(v), + _ => YamlMarked::String(v), } } else { - Yaml::String(v) + YamlMarked::String(v) } } else { // Datatype is not specified, or unrecognized - Yaml::from_str(&v) + YamlMarked::from_str(&v) }; - + let node = Node(yaml, Some(marker)); self.insert_new_node((node, aid)); } Event::Alias(id) => { let n = match self.anchor_map.get(&id) { - Some(v) => v.clone(), - None => Yaml::BadValue, + Some(v) => Node(v.clone(), Some(marker)), + None => Node(YamlMarked::BadValue, Some(marker)), }; self.insert_new_node((n, 0)); } @@ -158,25 +100,25 @@ impl MarkedEventReceiver for YamlLoader { } impl YamlLoader { - fn insert_new_node(&mut self, node: (Yaml, usize)) { + fn insert_new_node(&mut self, node: (Node, usize)) { // valid anchor id starts from 1 if node.1 > 0 { - self.anchor_map.insert(node.1, node.0.clone()); + self.anchor_map.insert(node.1, (node.0).0.clone()); } if self.doc_stack.is_empty() { self.doc_stack.push(node); } else { let parent = self.doc_stack.last_mut().unwrap(); match *parent { - (Yaml::Array(ref mut v), _) => v.push(node.0), - (Yaml::Hash(ref mut h), _) => { + (Node(YamlMarked::Array(ref mut v), _), _) => v.push(node.0), + (Node(YamlMarked::Hash(ref mut h), _), _) => { let cur_key = self.key_stack.last_mut().unwrap(); // current node is a key - if cur_key.is_badvalue() { + if cur_key.0.is_badvalue() { *cur_key = node.0; // current node is a value } else { - let mut newkey = Yaml::BadValue; + let mut newkey = Node(YamlMarked::BadValue, (node.0).marker()); mem::swap(&mut newkey, cur_key); h.insert(newkey, node.0); } @@ -187,6 +129,11 @@ impl YamlLoader { } pub fn load_from_str(source: &str) -> Result, ScanError> { + let ast = Self::load_from_str_with_markers(source)?; + Ok(ast.into_iter().map(|n| n.into()).collect()) + } + + pub fn load_from_str_with_markers(source: &str) -> Result, ScanError> { let mut loader = YamlLoader { docs: Vec::new(), doc_stack: Vec::new(), @@ -199,176 +146,12 @@ impl YamlLoader { } } -macro_rules! define_as ( - ($name:ident, $t:ident, $yt:ident) => ( -pub fn $name(&self) -> Option<$t> { - match *self { - Yaml::$yt(v) => Some(v), - _ => None - } -} - ); -); - -macro_rules! define_as_ref ( - ($name:ident, $t:ty, $yt:ident) => ( -pub fn $name(&self) -> Option<$t> { - match *self { - Yaml::$yt(ref v) => Some(v), - _ => None - } -} - ); -); - -macro_rules! define_into ( - ($name:ident, $t:ty, $yt:ident) => ( -pub fn $name(self) -> Option<$t> { - match self { - Yaml::$yt(v) => Some(v), - _ => None - } -} - ); -); - -impl Yaml { - define_as!(as_bool, bool, Boolean); - define_as!(as_i64, i64, Integer); - - define_as_ref!(as_str, &str, String); - define_as_ref!(as_hash, &Hash, Hash); - define_as_ref!(as_vec, &Array, Array); - - define_into!(into_bool, bool, Boolean); - define_into!(into_i64, i64, Integer); - define_into!(into_string, String, String); - define_into!(into_hash, Hash, Hash); - define_into!(into_vec, Array, Array); - - pub fn is_null(&self) -> bool { - match *self { - Yaml::Null => true, - _ => false, - } - } - - pub fn is_badvalue(&self) -> bool { - match *self { - Yaml::BadValue => true, - _ => false, - } - } - - pub fn is_array(&self) -> bool { - match *self { - Yaml::Array(_) => true, - _ => false, - } - } - - pub fn as_f64(&self) -> Option { - match *self { - Yaml::Real(ref v) => parse_f64(v), - _ => None, - } - } - - pub fn into_f64(self) -> Option { - match self { - Yaml::Real(ref v) => parse_f64(v), - _ => None, - } - } -} - -#[cfg_attr(feature = "cargo-clippy", allow(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. - pub fn from_str(v: &str) -> Yaml { - if v.starts_with("0x") { - let n = i64::from_str_radix(&v[2..], 16); - if n.is_ok() { - return Yaml::Integer(n.unwrap()); - } - } - if v.starts_with("0o") { - let n = i64::from_str_radix(&v[2..], 8); - if n.is_ok() { - return Yaml::Integer(n.unwrap()); - } - } - if v.starts_with('+') && v[1..].parse::().is_ok() { - return Yaml::Integer(v[1..].parse::().unwrap()); - } - match v { - "~" | "null" => Yaml::Null, - "true" => Yaml::Boolean(true), - "false" => Yaml::Boolean(false), - _ if v.parse::().is_ok() => Yaml::Integer(v.parse::().unwrap()), - // try parsing as f64 - _ if parse_f64(v).is_some() => Yaml::Real(v.to_owned()), - _ => Yaml::String(v.to_owned()), - } - } -} - -static BAD_VALUE: Yaml = Yaml::BadValue; -impl<'a> Index<&'a str> for Yaml { - type Output = Yaml; - - fn index(&self, idx: &'a str) -> &Yaml { - let key = Yaml::String(idx.to_owned()); - match self.as_hash() { - Some(h) => h.get(&key).unwrap_or(&BAD_VALUE), - None => &BAD_VALUE, - } - } -} - -impl Index for Yaml { - type Output = Yaml; - - fn index(&self, idx: usize) -> &Yaml { - if let Some(v) = self.as_vec() { - v.get(idx).unwrap_or(&BAD_VALUE) - } else if let Some(v) = self.as_hash() { - let key = Yaml::Integer(idx as i64); - v.get(&key).unwrap_or(&BAD_VALUE) - } else { - &BAD_VALUE - } - } -} - -impl IntoIterator for Yaml { - type Item = Yaml; - type IntoIter = YamlIter; - - fn into_iter(self) -> Self::IntoIter { - YamlIter { - yaml: self.into_vec().unwrap_or_else(Vec::new).into_iter(), - } - } -} - -pub struct YamlIter { - yaml: vec::IntoIter, -} - -impl Iterator for YamlIter { - type Item = Yaml; - - fn next(&mut self) -> Option { - self.yaml.next() - } -} - +#[cfg_attr(feature = "cargo-clippy", allow(clippy::float_cmp))] #[cfg(test)] mod test { use std::f64; use yaml::*; + #[test] fn test_coerce() { let s = "--- @@ -463,6 +246,7 @@ a1: &DEFAULT assert_eq!(doc.as_str().unwrap(), ""); } + #[cfg_attr(feature = "cargo-clippy", allow(clippy::cyclomatic_complexity))] #[test] fn test_plain_datatype() { let s = " diff --git a/tests/test_markers.rs b/tests/test_markers.rs new file mode 100644 index 0000000..833fa65 --- /dev/null +++ b/tests/test_markers.rs @@ -0,0 +1,173 @@ +#[macro_use] +extern crate indoc; +extern crate yaml_rust; + +use yaml_rust::{Node, YamlLoader, YamlMarked}; + +type R = Result>; + +#[test] +fn test_top_level_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + - a + - b + "# + ))?; + let Node(_, marker) = docs[0]; + assert_eq!(marker.unwrap().line(), 1, "line"); + assert_eq!(marker.unwrap().col(), 0, "col"); + Ok(()) +} + +#[test] +fn test_top_level_location_in_non_initial_document() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + - a + - b + --- + foo: 1 + bar: 2 + "# + ))?; + let Node(_, marker) = docs[1]; + assert_eq!(marker.unwrap().line(), 4, "line"); + // TODO: column is given as 3, but I expected 0 + // assert_eq!(marker.unwrap().col(), 0, "col"); + Ok(()) +} + +#[test] +fn test_array_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + items: + - a + - b + "# + ))?; + match &docs[0] { + Node(YamlMarked::Hash(ref hash), _) => { + let (_, array) = hash.front().unwrap(); + assert_eq!(array.marker().unwrap().line(), 2, "line"); + assert_eq!(array.marker().unwrap().col(), 4, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +} + +#[test] +fn test_array_element_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + items: + - a + - b + "# + ))?; + match &docs[0] { + Node(YamlMarked::Hash(ref hash), _) => { + let (_, node) = hash.front().unwrap(); + match node { + Node(YamlMarked::Array(array), _) => { + let elem = &array[1]; + assert_eq!(elem.marker().unwrap().line(), 3, "line"); + assert_eq!(elem.marker().unwrap().col(), 6, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expectd an array but got {:#?}", yaml))?, + } + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +} + +#[test] +fn test_hash_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + - 1 + - foo: 1 + bar: 2 + "# + ))?; + match &docs[0] { + Node(YamlMarked::Array(ref array), _) => { + let hash = &array[1]; + assert_eq!(hash.marker().unwrap().line(), 2, "line"); + // TODO: column is given as 5, but I expected 2 + // assert_eq!(hash.marker().unwrap().col(), 2, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +} + +#[test] +fn test_hash_key_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + --- + foo: bar + "# + ))?; + match &docs[0] { + Node(YamlMarked::Hash(ref hash), _) => { + let (key, _) = hash.front().unwrap(); + let Node(_, key_marker) = key; + assert_eq!(key_marker.unwrap().line(), 2, "line"); + assert_eq!(key_marker.unwrap().col(), 0, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +} + +#[test] +fn test_hash_value_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + --- + foo: bar + "# + ))?; + match &docs[0] { + Node(YamlMarked::Hash(ref hash), _) => { + let (_, value) = hash.front().unwrap(); + let Node(_, value_marker) = value; + assert_eq!(value_marker.unwrap().line(), 2, "line"); + assert_eq!(value_marker.unwrap().col(), 5, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +} + +#[test] +fn test_alias_location() -> R<()> { + let docs = YamlLoader::load_from_str_with_markers(indoc!( + r#" + items: + - &first a + - b + - *first + "# + ))?; + match &docs[0] { + Node(YamlMarked::Hash(ref hash), _) => { + let (_, node) = hash.front().unwrap(); + match node { + Node(YamlMarked::Array(ref array), _) => { + let elem = &array[2]; + assert_eq!(elem.marker().unwrap().line(), 4, "line"); + assert_eq!(elem.marker().unwrap().col(), 6, "col"); + Ok(()) + } + Node(yaml, _) => Err(format!("expected an array but got {:#?}", yaml))?, + } + } + Node(yaml, _) => Err(format!("expected a hash but got {:#?}", yaml))?, + } +}