#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
#![allow(clippy::redundant_field_names)]
#![forbid(unsafe_code)]
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
ops::{Deref, RangeBounds},
};
use line_index::LineIndex;
use serde::Serialize;
use thiserror::Error;
use tree_sitter::{Language, Node, Parser};
use tree_sitter_iter::TreeIter;
trait NodeExt {
fn is_anchor(&self) -> bool;
fn is_alias(&self) -> bool;
fn is_comment(&self) -> bool;
fn is_block_node(&self) -> bool;
fn is_flow_node(&self) -> bool;
fn is_block_or_flow_node(&self) -> bool;
fn is_block_mapping(&self) -> bool;
fn is_flow_mapping(&self) -> bool;
fn is_mapping(&self) -> bool;
fn is_block_mapping_pair(&self) -> bool;
fn is_flow_pair(&self) -> bool;
fn is_pair(&self) -> bool;
fn is_block_sequence(&self) -> bool;
fn is_flow_sequence(&self) -> bool;
fn is_sequence(&self) -> bool;
fn is_block_sequence_item(&self) -> bool;
fn is_block_scalar(&self) -> bool;
fn is_document(&self) -> bool;
fn is_plain_scalar(&self) -> bool;
fn is_single_quote_scalar(&self) -> bool;
fn is_double_quote_scalar(&self) -> bool;
fn is_quoted_scalar(&self) -> bool;
fn is_scalar(&self) -> bool;
}
impl NodeExt for Node<'_> {
fn is_anchor(&self) -> bool {
self.kind() == "anchor"
}
fn is_alias(&self) -> bool {
self.kind() == "alias"
}
fn is_comment(&self) -> bool {
self.kind() == "comment"
}
fn is_block_node(&self) -> bool {
self.kind() == "block_node"
}
fn is_flow_node(&self) -> bool {
self.kind() == "flow_node"
}
fn is_block_or_flow_node(&self) -> bool {
self.is_block_node() || self.is_flow_node()
}
fn is_block_mapping(&self) -> bool {
self.kind() == "block_mapping"
}
fn is_flow_mapping(&self) -> bool {
self.kind() == "flow_mapping"
}
fn is_mapping(&self) -> bool {
self.is_block_mapping() || self.is_flow_mapping()
}
fn is_block_mapping_pair(&self) -> bool {
self.kind() == "block_mapping_pair"
}
fn is_flow_pair(&self) -> bool {
self.kind() == "flow_pair"
}
fn is_pair(&self) -> bool {
self.is_block_mapping_pair() || self.is_flow_pair()
}
fn is_block_sequence(&self) -> bool {
self.kind() == "block_sequence"
}
fn is_flow_sequence(&self) -> bool {
self.kind() == "flow_sequence"
}
fn is_sequence(&self) -> bool {
self.is_block_sequence() || self.is_flow_sequence()
}
fn is_block_sequence_item(&self) -> bool {
self.kind() == "block_sequence_item"
}
fn is_block_scalar(&self) -> bool {
self.kind() == "block_scalar"
}
fn is_document(&self) -> bool {
self.kind() == "document"
}
fn is_plain_scalar(&self) -> bool {
self.kind() == "plain_scalar"
}
fn is_single_quote_scalar(&self) -> bool {
self.kind() == "single_quote_scalar"
}
fn is_double_quote_scalar(&self) -> bool {
self.kind() == "double_quote_scalar"
}
fn is_quoted_scalar(&self) -> bool {
self.is_single_quote_scalar() || self.is_double_quote_scalar()
}
fn is_scalar(&self) -> bool {
self.is_plain_scalar() || self.is_quoted_scalar() || self.is_block_scalar()
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("malformed or unsupported tree-sitter grammar")]
InvalidLanguage(#[from] tree_sitter::LanguageError),
#[error("input is not valid YAML")]
InvalidInput,
#[error("expected mapping containing key `{0}`")]
ExpectedMapping(String),
#[error("expected list for index `[{0}]`")]
ExpectedList(usize),
#[error("mapping has no key `{0}`")]
ExhaustedMapping(String),
#[error("index `[{0}]` exceeds list size ({1})")]
ExhaustedList(usize, usize),
#[error("unexpected node: `{0}`")]
UnexpectedNode(String),
#[error("syntax node `{0}` is missing named child `{1}`")]
MissingChild(String, String),
#[error("syntax node `{0}` is missing child field `{1}`")]
MissingChildField(String, &'static str),
#[error("route error: {0}")]
Other(String),
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Route<'a> {
route: Vec<Component<'a>>,
}
impl<'a> Route<'a> {
pub fn is_empty(&self) -> bool {
self.route.is_empty()
}
pub fn with_key(&self, component: impl Into<Component<'a>>) -> Self {
let mut components = self.route.clone();
components.push(component.into());
Self::from(components)
}
pub fn with_keys(&self, components: impl IntoIterator<Item = Component<'a>>) -> Self {
let mut new_route = self.route.clone();
new_route.extend(components);
Self::from(new_route)
}
pub fn parent(&self) -> Option<Self> {
if self.is_empty() {
None
} else {
let mut route = self.route.clone();
route.truncate(self.route.len() - 1);
Some(Self::from(route))
}
}
}
#[macro_export]
macro_rules! route {
($($key:expr),* $(,)?) => {
$crate::Route::from(
vec![$($crate::Component::from($key)),*]
)
};
() => {
$crate::Route::default()
};
}
impl<'a> From<Vec<Component<'a>>> for Route<'a> {
fn from(route: Vec<Component<'a>>) -> Self {
Self { route }
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Component<'a> {
Key(Cow<'a, str>),
Index(usize),
}
impl From<usize> for Component<'_> {
fn from(index: usize) -> Self {
Component::Index(index)
}
}
impl<'a> From<&'a str> for Component<'a> {
fn from(key: &'a str) -> Self {
Component::Key(key.into())
}
}
impl From<String> for Component<'_> {
fn from(key: String) -> Self {
Component::Key(key.into())
}
}
#[derive(Debug)]
pub struct Location {
pub byte_span: (usize, usize),
pub point_span: ((usize, usize), (usize, usize)),
}
impl From<Node<'_>> for Location {
fn from(node: Node<'_>) -> Self {
let start_point = node.start_position();
let end_point = node.end_position();
Self {
byte_span: (node.start_byte(), node.end_byte()),
point_span: (
(start_point.row, start_point.column),
(end_point.row, end_point.column),
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FeatureKind {
BlockMapping,
BlockSequence,
FlowMapping,
FlowSequence,
Scalar,
}
#[derive(Debug)]
pub struct Feature<'tree> {
_node: Node<'tree>,
pub location: Location,
pub context: Option<Location>,
}
impl Feature<'_> {
pub fn parent(&self) -> Option<Feature<'_>> {
self._node.parent().map(Feature::from)
}
pub fn kind(&self) -> FeatureKind {
let node = if self._node.is_block_or_flow_node() {
self._node
.child(0)
.expect("internal error: expected child of block_node/flow_node")
} else {
self._node
};
match () {
_ if node.is_block_mapping() => FeatureKind::BlockMapping,
_ if node.is_block_sequence() => FeatureKind::BlockSequence,
_ if node.is_flow_mapping() => FeatureKind::FlowMapping,
_ if node.is_flow_sequence() => FeatureKind::FlowSequence,
_ if node.is_scalar() => FeatureKind::Scalar,
_ => unreachable!("unexpected feature kind: {}", node.kind()),
}
}
pub fn is_multiline(&self) -> bool {
self.location.point_span.0.0 != self.location.point_span.1.0
}
}
impl RangeBounds<usize> for &Feature<'_> {
fn start_bound(&self) -> std::ops::Bound<&usize> {
std::ops::Bound::Included(&self.location.byte_span.0)
}
fn end_bound(&self) -> std::ops::Bound<&usize> {
std::ops::Bound::Excluded(&self.location.byte_span.1)
}
}
impl<'tree> From<Node<'tree>> for Feature<'tree> {
fn from(node: Node<'tree>) -> Self {
Feature {
_node: node,
location: Location::from(node),
context: node.parent().map(Location::from),
}
}
}
#[derive(Copy, Clone, Debug)]
enum QueryMode {
Pretty,
KeyOnly,
Exact,
}
#[derive(Clone)]
struct SourceTree {
source: String,
tree: tree_sitter::Tree,
}
impl Deref for SourceTree {
type Target = tree_sitter::Tree;
fn deref(&self) -> &Self::Target {
&self.tree
}
}
type AnchorMap<'tree> = HashMap<&'tree str, BTreeMap<usize, Node<'tree>>>;
self_cell::self_cell!(
struct Tree {
owner: SourceTree,
#[covariant]
dependent: AnchorMap,
}
);
impl Tree {
fn build(inner: SourceTree) -> Result<Self, QueryError> {
Tree::try_new(SourceTree::clone(&inner), |tree| {
let mut anchor_map: AnchorMap = HashMap::new();
for anchor in TreeIter::new(tree).filter(|n| n.is_anchor()) {
let anchor_name = &anchor
.utf8_text(tree.source.as_bytes())
.expect("impossible: anchor name should be UTF-8 by construction")[1..];
let parent = anchor.parent().ok_or_else(|| {
QueryError::UnexpectedNode("anchor node has no parent".into())
})?;
let mut cursor = parent.walk();
let sibling = parent
.named_children(&mut cursor)
.find(|child| !child.is_anchor() && !child.is_comment())
.ok_or_else(|| {
QueryError::UnexpectedNode("anchor has no non-comment sibling".into())
})?;
anchor_map
.entry(anchor_name)
.or_default()
.insert(anchor.start_byte(), sibling);
}
Ok(anchor_map)
})
}
}
impl Clone for Tree {
fn clone(&self) -> Self {
Self::build(self.borrow_owner().clone())
.expect("impossible: cloning a Tree preserves invariants")
}
}
impl Deref for Tree {
type Target = tree_sitter::Tree;
fn deref(&self) -> &Self::Target {
&self.borrow_owner().tree
}
}
#[derive(Clone)]
pub struct Document {
tree: Tree,
line_index: LineIndex,
}
impl Document {
pub fn new(source: impl Into<String>) -> Result<Self, QueryError> {
let source = source.into();
let mut parser = Parser::new();
let language: Language = tree_sitter_yaml::LANGUAGE.into();
parser.set_language(&language)?;
let tree = parser
.parse(&source, None)
.expect("impossible: tree-sitter parsing should never fail");
if tree.root_node().has_error() {
return Err(QueryError::InvalidInput);
}
let line_index = LineIndex::new(&source);
let source_tree = SourceTree {
source: source,
tree,
};
Ok(Self {
tree: Tree::build(source_tree)?,
line_index,
})
}
pub fn line_index(&self) -> &LineIndex {
&self.line_index
}
pub fn source(&self) -> &str {
&self.tree.borrow_owner().source
}
fn resolve_anchor(&self, name: &str, position: usize) -> Option<Node<'_>> {
self.tree
.borrow_dependent()
.get(name)?
.range(..position)
.next_back()
.map(|(_, node)| *node)
}
pub fn top_feature(&self) -> Result<Feature<'_>, QueryError> {
let top_node = self.top_object()?;
Ok(top_node.into())
}
pub fn range_spanned_by_comment(&self, start: usize, end: usize) -> bool {
let root = self.tree.root_node();
match root.named_descendant_for_byte_range(start, end) {
Some(child) => child.is_comment(),
None => false,
}
}
pub fn offset_inside_comment(&self, offset: usize) -> bool {
self.range_spanned_by_comment(offset, offset)
}
pub fn query_exists(&self, route: &Route) -> bool {
self.query_node(route, QueryMode::Exact).is_ok()
}
pub fn query_pretty(&self, route: &Route) -> Result<Feature<'_>, QueryError> {
self.query_node(route, QueryMode::Pretty).map(|n| n.into())
}
pub fn query_exact(&self, route: &Route) -> Result<Option<Feature<'_>>, QueryError> {
let node = self.query_node(route, QueryMode::Exact)?;
if node.is_pair() {
Ok(None)
} else {
Ok(Some(node.into()))
}
}
pub fn query_key_only(&self, route: &Route) -> Result<Feature<'_>, QueryError> {
if !matches!(route.route.last(), Some(Component::Key(_))) {
return Err(QueryError::Other(
"route must end with a key component for key-only routes".into(),
));
}
self.query_node(route, QueryMode::KeyOnly).map(|n| n.into())
}
pub fn extract(&self, feature: &Feature) -> &str {
&self.source()[feature.location.byte_span.0..feature.location.byte_span.1]
}
pub fn extract_with_leading_whitespace<'a>(&'a self, feature: &Feature) -> &'a str {
let mut start_idx = feature.location.byte_span.0;
let pre_slice = &self.source()[0..start_idx];
if let Some(last_newline) = pre_slice.rfind('\n') {
if self.source()[last_newline + 1..start_idx]
.bytes()
.all(|b| b == b' ')
{
start_idx = last_newline + 1
}
}
&self.source()[start_idx..feature.location.byte_span.1]
}
pub fn feature_comments<'tree>(&'tree self, feature: &Feature<'tree>) -> Vec<Feature<'tree>> {
let start_line = feature.location.point_span.0.0;
let end_line = feature.location.point_span.1.0;
fn trawl<'tree>(
node: &Node<'tree>,
start_line: usize,
end_line: usize,
) -> Vec<Feature<'tree>> {
let mut comments = vec![];
let mut cur = node.walk();
if node.end_position().row < start_line || node.start_position().row > end_line {
return comments;
}
comments.extend(
node.named_children(&mut cur)
.filter(|c| {
c.is_comment()
&& c.start_position().row >= start_line
&& c.end_position().row <= end_line
})
.map(|c| c.into()),
);
for child in node.children(&mut cur) {
comments.extend(trawl(&child, start_line, end_line));
}
comments
}
trawl(&self.tree.root_node(), start_line, end_line)
}
pub fn has_anchors(&self) -> bool {
!self.tree.borrow_dependent().is_empty()
}
fn top_object(&self) -> Result<Node<'_>, QueryError> {
let stream = self.tree.root_node();
let mut cur = stream.walk();
let document = stream
.named_children(&mut cur)
.find(|c| c.is_document())
.ok_or_else(|| QueryError::MissingChild(stream.kind().into(), "document".into()))?;
let top_node = document
.named_children(&mut cur)
.find(|c| c.is_block_or_flow_node())
.ok_or_else(|| QueryError::Other("document has no block_node or flow_node".into()))?;
Ok(top_node)
}
fn query_node(&self, route: &Route, mode: QueryMode) -> Result<Node<'_>, QueryError> {
let mut focus_node = self.top_object()?;
for component in &route.route {
match self.descend(&focus_node, component) {
Ok(next) => focus_node = next,
Err(e) => return Err(e),
}
}
let pre_resolution_node = focus_node;
focus_node = match focus_node.child(0) {
Some(child) if child.is_alias() => {
let alias_name = child
.utf8_text(self.source().as_bytes())
.expect("impossible: alias name should be UTF-8 by construction");
self.resolve_anchor(&alias_name[1..], child.start_byte())
.ok_or_else(|| QueryError::Other(format!("unknown alias: {}", alias_name)))?
}
Some(child) if child.is_anchor() => {
let mut cursor = focus_node.walk();
focus_node
.named_children(&mut cursor)
.find(|n| !n.is_anchor())
.unwrap_or(focus_node)
}
_ => focus_node,
};
let was_alias = pre_resolution_node.id() != focus_node.id();
let structural_node = if was_alias {
pre_resolution_node
} else {
focus_node
};
focus_node = match mode {
QueryMode::Pretty => {
if matches!(route.route.last(), Some(Component::Key(_)))
&& !structural_node.is_pair()
{
structural_node
.parent()
.expect("missing parent of focus node")
} else {
structural_node
}
}
QueryMode::KeyOnly => {
let parent_node = if structural_node.is_pair() {
structural_node
} else if structural_node.is_block_scalar() {
structural_node
.parent()
.expect("missing parent of focus node")
.parent()
.expect("missing grandparent of focus node")
} else {
structural_node
.parent()
.expect("missing parent of focus node")
};
if parent_node.is_flow_mapping() {
let mut cur = parent_node.walk();
parent_node
.named_children(&mut cur)
.find(|n| n.is_flow_node())
.ok_or_else(|| {
QueryError::MissingChildField(parent_node.kind().into(), "flow_node")
})?
} else {
parent_node.child_by_field_name("key").ok_or_else(|| {
QueryError::MissingChildField(parent_node.kind().into(), "key")
})?
}
}
QueryMode::Exact => focus_node,
};
if matches!(mode, QueryMode::Pretty)
&& matches!(route.route.last(), Some(Component::Key(_)))
&& !focus_node.is_block_mapping_pair()
{
focus_node = focus_node.parent().expect("missing parent of focus node")
}
Ok(focus_node)
}
fn descend<'b>(
&'b self,
node: &Node<'b>,
component: &Component,
) -> Result<Node<'b>, QueryError> {
let mut child = {
let mut cursor = node.walk();
node.named_children(&mut cursor)
.find(|n| !n.is_anchor())
.ok_or_else(|| {
QueryError::Other(format!(
"node of kind {} has no non-anchor child",
node.kind()
))
})?
};
if child.is_alias() {
let alias_name = node
.utf8_text(self.source().as_bytes())
.expect("impossible: alias name should be UTF-8 by construction");
child = self
.resolve_anchor(&alias_name[1..], node.start_byte())
.ok_or_else(|| QueryError::Other(format!("unknown alias: {}", alias_name)))?;
}
if child.is_mapping() {
match component {
Component::Key(key) => self.descend_mapping(&child, key),
Component::Index(idx) => Err(QueryError::ExpectedList(*idx)),
}
} else if child.is_sequence() {
match component {
Component::Index(idx) => self.descend_sequence(&child, *idx),
Component::Key(key) => Err(QueryError::ExpectedMapping(key.to_string())),
}
} else {
Err(QueryError::UnexpectedNode(child.kind().into()))
}
}
fn descend_mapping<'b>(&self, node: &Node<'b>, expected: &str) -> Result<Node<'b>, QueryError> {
let mut cur = node.walk();
for child in node.named_children(&mut cur) {
let key = if child.is_pair() {
child
.child_by_field_name("key")
.ok_or_else(|| QueryError::MissingChildField(child.kind().into(), "key"))?
} else if child.is_flow_node() {
child
} else {
continue;
};
let key_value = {
let mut cursor = key.walk();
let scalar = key.named_children(&mut cursor).find(|n| !n.is_anchor());
match scalar {
Some(scalar) => {
let key_value = scalar
.utf8_text(self.source().as_bytes())
.expect("impossible: value for key should be UTF-8 by construction");
if scalar.is_quoted_scalar() {
let mut chars = key_value.chars();
chars.next();
chars.next_back();
chars.as_str()
} else {
key_value
}
}
None => key
.utf8_text(self.source().as_bytes())
.expect("impossible: key should be UTF-8 by construction"),
}
};
if key_value == expected {
return Ok(child.child_by_field_name("value").unwrap_or(child));
}
}
Err(QueryError::ExhaustedMapping(expected.into()))
}
fn flatten_sequence<'b>(&'b self, node: &Node<'b>) -> Result<Vec<Node<'b>>, QueryError> {
let mut children = vec![];
let mut cur = node.walk();
for child in node.named_children(&mut cur).filter(|child| {
child.is_block_sequence_item() || child.is_flow_node() || child.is_flow_pair()
}) {
let mut child = child;
if child.is_block_sequence_item() {
let mut cur = child.walk();
child = child
.named_children(&mut cur)
.find(|c| c.is_block_or_flow_node())
.ok_or_else(|| {
QueryError::MissingChild(child.kind().into(), "block_sequence_item".into())
})?;
}
children.push(child);
}
Ok(children)
}
fn descend_sequence<'b>(&'b self, node: &Node<'b>, idx: usize) -> Result<Node<'b>, QueryError> {
let children = self.flatten_sequence(node)?;
let Some(child) = children.get(idx) else {
return Err(QueryError::ExhaustedList(idx, children.len()));
};
if child.is_flow_pair() {
return Ok(child.child_by_field_name("value").unwrap_or(*child));
}
Ok(*child)
}
}
#[cfg(test)]
mod tests {
use std::vec;
use crate::{Component, Document, FeatureKind, QueryError, Route};
#[test]
fn test_document_preserves_leading_trailing_whitespace() {
let source = "\n\n foo: bar \n baz: quux \n\n";
let doc = Document::new(source).unwrap();
assert_eq!(doc.source(), source);
}
#[test]
fn test_query_parent() {
let route = route!("foo", "bar", "baz");
assert_eq!(
route.parent().unwrap().route,
[Component::Key("foo".into()), Component::Key("bar".into())]
);
let route = route!("foo");
assert!(route.parent().is_some());
let route = Route::from(vec![]);
assert!(route.parent().is_none());
}
#[test]
fn test_location_spanned_by_comment() {
let doc = Document::new(
r#"
foo: bar
# comment
baz: quux
"#,
)
.unwrap();
assert!(!doc.range_spanned_by_comment(1, 4));
assert!(doc.range_spanned_by_comment(13, 13));
assert!(doc.range_spanned_by_comment(13, 15));
assert!(!doc.range_spanned_by_comment(13, 21));
}
#[test]
fn test_offset_inside_comment() {
let doc = Document::new("foo: bar # abc def").unwrap();
let comment = doc.source().find('#').unwrap();
for idx in 0..doc.source().len() {
if idx < comment {
assert!(!doc.offset_inside_comment(idx));
} else {
assert!(doc.offset_inside_comment(idx));
}
}
}
#[test]
fn test_query_builder() {
let route = route!("foo", "bar", 1, 123, "lol");
assert_eq!(
route.route,
[
Component::Key("foo".into()),
Component::Key("bar".into()),
Component::Index(1),
Component::Index(123),
Component::Key("lol".into()),
]
)
}
#[test]
fn test_basic() {
let doc = r#"
foo: bar
baz:
sub:
keys:
abc:
- 123
- 456
- [a, b, c, {d: e}]
"#;
let doc = Document::new(doc).unwrap();
let route = Route {
route: vec![
Component::Key("baz".into()),
Component::Key("sub".into()),
Component::Key("keys".into()),
Component::Key("abc".into()),
Component::Index(2),
Component::Index(3),
],
};
assert_eq!(
doc.extract_with_leading_whitespace(&doc.query_pretty(&route).unwrap()),
"{d: e}"
);
}
#[test]
fn test_top_feature() {
let doc = r#"
foo: bar
baz:
abc: def
"#;
let doc = Document::new(doc).unwrap();
let feature = doc.top_feature().unwrap();
assert_eq!(doc.extract(&feature).trim(), doc.source().trim());
assert_eq!(feature.kind(), FeatureKind::BlockMapping);
}
#[test]
fn test_feature_comments() {
let doc = r#"
root: # rootlevel
a: 1 # foo
b: 2 # bar
c: 3
d: 4 # baz
e: [1, 2, {nested: key}] # quux
bar: # outside
# outside too
"#;
let doc = Document::new(doc).unwrap();
let route = Route {
route: vec![Component::Key("root".into())],
};
let feature = doc.query_pretty(&route).unwrap();
assert_eq!(
doc.feature_comments(&feature)
.iter()
.map(|f| doc.extract(f))
.collect::<Vec<_>>(),
&["# rootlevel", "# foo", "# bar", "# baz", "# quux"]
);
let route = Route {
route: vec![
Component::Key("root".into()),
Component::Key("e".into()),
Component::Index(1),
],
};
let feature = doc.query_pretty(&route).unwrap();
assert_eq!(
doc.feature_comments(&feature)
.iter()
.map(|f| doc.extract(f))
.collect::<Vec<_>>(),
&["# quux"]
);
}
#[test]
fn test_feature_kind() {
let doc = r#"
block-mapping:
foo: bar
"block-mapping-quoted":
foo: bar
block-sequence:
- foo
- bar
"block-sequence-quoted":
- foo
- bar
flow-mapping: {foo: bar}
flow-sequence: [foo, bar]
scalars:
- abc
- 'abc'
- "abc"
- 123
- -123
- 123.456
- true
- false
- null
- |
multiline
text
- >
folded
text
nested:
foo:
- bar
- baz
- { a: b }
- { c: }
"#;
let doc = Document::new(doc).unwrap();
for (route, expected_kind) in &[
(
vec![Component::Key("block-mapping".into())],
FeatureKind::BlockMapping,
),
(
vec![Component::Key("block-mapping-quoted".into())],
FeatureKind::BlockMapping,
),
(
vec![Component::Key("block-sequence".into())],
FeatureKind::BlockSequence,
),
(
vec![Component::Key("block-sequence-quoted".into())],
FeatureKind::BlockSequence,
),
(
vec![Component::Key("flow-mapping".into())],
FeatureKind::FlowMapping,
),
(
vec![Component::Key("flow-sequence".into())],
FeatureKind::FlowSequence,
),
(
vec![Component::Key("scalars".into()), Component::Index(0)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(1)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(2)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(3)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(4)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(5)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(6)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(7)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(8)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(9)],
FeatureKind::Scalar,
),
(
vec![Component::Key("scalars".into()), Component::Index(10)],
FeatureKind::Scalar,
),
(
vec![
Component::Key("nested".into()),
Component::Key("foo".into()),
Component::Index(2),
],
FeatureKind::FlowMapping,
),
(
vec![
Component::Key("nested".into()),
Component::Key("foo".into()),
Component::Index(3),
],
FeatureKind::FlowMapping,
),
] {
let route = Route::from(route.clone());
let feature = doc.query_exact(&route).unwrap().unwrap();
assert_eq!(feature.kind(), *expected_kind);
}
}
#[test]
fn test_duplicate_anchors() {
let test_cases: Vec<(&str, Vec<(Route, &str)>)> = vec![
(
"first: &x value1\nsecond: &x value2\nref: *x",
vec![(route!("ref"), "value2")],
),
(
"a1: &x old_x\nref_x: *x\na2: &x new_x\nref_x2: *x",
vec![(route!("ref_x"), "old_x"), (route!("ref_x2"), "new_x")],
),
(
"foo: [&x x, *x, &x y, *x]",
vec![
(route!("foo", 0), "x"),
(route!("foo", 1), "x"),
(route!("foo", 2), "y"),
(route!("foo", 3), "y"),
],
),
];
for (yaml, queries) in test_cases {
let doc = Document::new(yaml).unwrap();
for (route, expected) in queries {
let feature = doc.query_exact(&route).unwrap().unwrap();
assert_eq!(doc.extract(&feature), expected, "YAML: {}", yaml);
}
}
}
#[test]
fn test_anchor_map() {
let anchors = r#"
foo: &foo-anchor
bar: &bar-anchor
baz: quux
"#;
let doc = Document::new(anchors).unwrap();
let anchor_map = doc.tree.borrow_dependent();
assert_eq!(anchor_map.len(), 2);
assert_eq!(anchor_map["foo-anchor"].len(), 1);
assert_eq!(anchor_map["bar-anchor"].len(), 1);
assert_eq!(
anchor_map["foo-anchor"].values().next().unwrap().kind(),
"block_mapping"
);
assert_eq!(
anchor_map["bar-anchor"].values().next().unwrap().kind(),
"block_mapping"
);
}
#[test]
fn test_sequence_alias_not_flattened() {
let doc = r#"
defaults: &defaults
- a
- b
- c
list:
- *defaults
- d
- e
"#;
let doc = Document::new(doc).unwrap();
for (route, expected_kind, expected_value) in [
(
route!("list", 0),
FeatureKind::BlockSequence,
"- a\n - b\n - c",
),
(route!("list", 1), FeatureKind::Scalar, "d"),
(route!("list", 2), FeatureKind::Scalar, "e"),
] {
let feature = doc.query_exact(&route).unwrap().unwrap();
assert_eq!(feature.kind(), expected_kind);
assert_eq!(doc.extract(&feature).trim(), expected_value);
}
assert!(matches!(
doc.query_exact(&route!("list", 3)),
Err(QueryError::ExhaustedList(3, 3))
));
}
#[test]
fn test_inline_anchor_alias_patterns() {
let test_cases: Vec<(&str, Vec<(Route, &str)>)> = vec![
(
"foo: [&x v, *x]",
vec![(route!("foo", 0), "v"), (route!("foo", 1), "v")],
),
(
"foo: [a, &x v, *x]",
vec![
(route!("foo", 0), "a"),
(route!("foo", 1), "v"),
(route!("foo", 2), "v"),
],
),
(
"foo: [&a 1, &b 2, *a, *b]",
vec![
(route!("foo", 0), "1"),
(route!("foo", 1), "2"),
(route!("foo", 2), "1"),
(route!("foo", 3), "2"),
],
),
(
"top: { &a foo: &b bar, nested: *a, other: *b }",
vec![
(route!("top", "foo"), "bar"),
(route!("top", "nested"), "foo"),
(route!("top", "other"), "bar"),
],
),
(
"top: { &a k1: v1, &b k2: v2, ref1: *a, ref2: *b }",
vec![
(route!("top", "k1"), "v1"),
(route!("top", "k2"), "v2"),
(route!("top", "ref1"), "k1"),
(route!("top", "ref2"), "k2"),
],
),
(
"top: { seq: &x [a, b], ref: *x }",
vec![
(route!("top", "seq", 0), "a"),
(route!("top", "ref", 1), "b"),
],
),
(
"top: { map: &x {a: 1}, ref: *x }",
vec![
(route!("top", "map", "a"), "1"),
(route!("top", "ref", "a"), "1"),
],
),
(
r#"top: { &x "foo": bar, nested: *x }"#,
vec![
(route!("top", "foo"), "bar"),
(route!("top", "nested"), "\"foo\""),
],
),
(
"top: { &x 'foo': bar, nested: *x }",
vec![
(route!("top", "foo"), "bar"),
(route!("top", "nested"), "'foo'"),
],
),
];
for (yaml, queries) in test_cases {
let doc = Document::new(yaml).unwrap();
for (route, expected) in queries {
let feature = doc.query_exact(&route).unwrap().unwrap();
assert_eq!(doc.extract(&feature), expected, "YAML: {}", yaml);
}
}
}
}