use crate::{
NEW_ISSUE_LINK, paths,
yaml::{
LocatedError, SourceId, SourceIdLocation, SourceLocation, SourceMap,
SourcedYaml, yaml_parse_panic,
},
};
use derive_more::From;
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use saphyr::{AnnotatedMapping, Scalar, YamlData};
use std::{
collections::HashMap,
fmt::{self, Display},
path::{Path, PathBuf},
str::FromStr,
};
use thiserror::Error;
use winnow::{
ModalResult, Parser,
combinator::{alt, preceded, repeat, separated_pair},
error::EmptyError,
token::{take_until, take_while},
};
pub const REFERENCE_KEY: &str = "$ref";
type Result<T> = std::result::Result<T, LocatedError<ReferenceError>>;
impl SourcedYaml<'_> {
pub(super) fn resolve_references(
self,
source_map: &mut SourceMap,
) -> Result<Self> {
ReferenceLocations::scan(self, source_map)?
.build_graph()
.sort()?
.replace(source_map)
}
}
#[derive(Debug)]
struct ReferenceLocations<'input> {
references: Vec<(YamlPath<'input>, SourceReference)>,
document_map: DocumentMap<'input>,
}
impl<'input> ReferenceLocations<'input> {
fn scan(
value: SourcedYaml<'input>,
source_map: &mut SourceMap,
) -> Result<Self> {
let mut scanner = ReferenceScanner {
references: Vec::new(),
source_map,
unscanned_sources: IndexSet::new(),
};
let source_map = &scanner.source_map;
assert!(source_map.sources.len() <= 1);
let root_source_id = if source_map.sources.is_empty() {
SourceId::Memory
} else {
SourceId::File(0)
};
let mut document_map = DocumentMap {
root: value,
root_source_id,
additional: HashMap::new(),
};
let root_path = source_map
.get_path(root_source_id)
.and_then(|path| path.parent())
.map(Path::to_owned);
scanner.scan_source(
root_path.as_deref(),
YamlPath::new(root_source_id),
&document_map.root,
)?;
while let Some(source_id) = scanner.unscanned_sources.pop() {
let file_path = scanner
.source_map
.get_path(source_id)
.map(Path::to_owned)
.unwrap_or_else(|| {
panic!("Source map missing source {source_id:?}")
});
let value = match source_id {
SourceId::File(_) => SourcedYaml::load(&file_path, source_id)
.map_err(|error| LocatedError {
error: ReferenceError::Nested(Box::new(error.error)),
location: error.location,
})?,
SourceId::Memory => {
panic!("In-memory source cannot be referenced")
}
};
scanner.scan_source(
file_path.parent(),
YamlPath::new(source_id),
&value,
)?;
document_map.additional.insert(source_id, value);
}
Ok(Self {
references: scanner.references,
document_map,
})
}
fn build_graph(self) -> UnsortedGraph<'input> {
let mut graph: HashMap<SourceReference, ReferenceMetadata> =
HashMap::new();
for (path, reference) in &self.references {
graph
.entry(reference.clone())
.and_modify(|metadata| {
metadata.locations.push(path.clone());
})
.or_insert_with(|| {
let dependencies = self
.references
.iter()
.filter_map(|(other_path, other_reference)| {
if reference.depends_on(other_path) {
Some((
other_reference.clone(),
other_path.clone(),
))
} else {
None
}
})
.collect();
ReferenceMetadata {
locations: vec![path.clone()],
dependencies,
}
});
}
UnsortedGraph {
references: graph,
document_map: self.document_map,
}
}
}
#[derive(Debug)]
struct ReferenceScanner<'input, 'src> {
references: Vec<(YamlPath<'input>, SourceReference)>,
source_map: &'src mut SourceMap,
unscanned_sources: IndexSet<SourceId>,
}
impl<'input> ReferenceScanner<'input, '_> {
fn scan_source(
&mut self,
reference_dir: Option<&Path>,
path: YamlPath<'input>,
value: &SourcedYaml<'input>,
) -> Result<()> {
match &value.data {
YamlData::Value(_) => Ok(()),
YamlData::Sequence(sequence) => {
for (index, value) in sequence.iter().enumerate() {
self.scan_source(
reference_dir,
path.cons(index, value.location),
value,
)?;
}
Ok(())
}
YamlData::Mapping(mapping) => {
for (key, value) in mapping {
if key.data.as_str() == Some(REFERENCE_KEY) {
let reference = Reference::try_from_yaml(value)?;
self.add_reference(
reference_dir,
path.clone(),
reference,
);
} else {
self.scan_source(
reference_dir,
path.cons(key.clone(), value.location),
value,
)?;
}
}
Ok(())
}
YamlData::Tagged(_, value) => {
self.scan_source(reference_dir, path, value)
}
YamlData::Representation(_, _, _)
| YamlData::BadValue
| YamlData::Alias(_) => yaml_parse_panic(),
}
}
fn add_reference(
&mut self,
reference_dir: Option<&Path>,
path: YamlPath<'input>,
reference: Reference,
) {
let source_id = match &reference.source {
ReferenceSource::Local => path.source_id,
ReferenceSource::File(path) => {
let reference_dir = reference_dir.unwrap_or_else(|| {
panic!("File references disallowed from in-memory YAML")
});
let path = paths::normalize_path(reference_dir, path);
if let Some(source_id) = self.source_map.get_source_id(&path) {
source_id
} else {
let source_id = self.source_map.add_source(path.clone());
self.unscanned_sources.insert(source_id);
source_id
}
}
};
self.references.push((
path,
SourceReference {
reference,
source_id,
},
));
}
}
#[derive(Debug)]
struct ReferenceMetadata<'input> {
locations: Vec<YamlPath<'input>>,
dependencies: HashMap<SourceReference, YamlPath<'input>>,
}
#[derive(Debug)]
struct UnsortedGraph<'input> {
references: HashMap<SourceReference, ReferenceMetadata<'input>>,
document_map: DocumentMap<'input>,
}
impl<'input> UnsortedGraph<'input> {
fn sort(mut self) -> Result<SortedGraph<'input>> {
let mut sorted = IndexMap::new();
let mut independent: Vec<(SourceReference, ReferenceMetadata)> = self
.references
.extract_if(|_, metadata| metadata.dependencies.is_empty())
.collect();
let mut dependents = self.references;
while let Some((reference, metadata)) = independent.pop() {
assert!(
metadata.dependencies.is_empty(),
"Reference still has dependencies: {:?}",
metadata.dependencies
);
let newly_independent = dependents.extract_if(|_, metadata| {
metadata.dependencies.remove(&reference);
metadata.dependencies.is_empty()
});
independent.extend(newly_independent);
sorted.insert(reference, metadata);
}
if let Some(dependent) = dependents.values().next() {
let location =
dependent.dependencies.values().next().unwrap().location;
Err(LocatedError {
error: ReferenceError::CircularReference {
references: dependents
.into_keys()
.map(|reference| reference.reference)
.sorted()
.collect(),
},
location,
})
} else {
Ok(SortedGraph {
references: sorted,
document_map: self.document_map,
})
}
}
}
struct SortedGraph<'input> {
references: IndexMap<SourceReference, ReferenceMetadata<'input>>,
document_map: DocumentMap<'input>,
}
impl<'input> SortedGraph<'input> {
fn replace(
mut self,
source_map: &SourceMap,
) -> Result<SourcedYaml<'input>> {
for (reference, metadata) in self.references {
let resolved = reference.resolve(&self.document_map)?.clone();
for path in metadata.locations {
let document = self.document_map.get_mut(path.source_id);
let value = path.get(document);
let YamlData::Mapping(mapping) = &mut value.data else {
panic!(
"Expected path {path:?} to point to a mapping, \
but found {value:?}"
)
};
if mapping.len() == 1 {
*value = resolved.clone();
} else {
let YamlData::Mapping(to_spread) = resolved.clone().data
else {
return Err(LocatedError {
error: ReferenceError::ExpectedMapping {
reference: reference.reference,
parent: value.location.resolve(source_map),
},
location: resolved.location,
});
};
spread_mapping(mapping, to_spread);
}
}
}
Ok(self.document_map.root)
}
}
fn path_lookup<'input, 'value>(
value: &'value SourcedYaml<'input>,
path: &[String],
) -> std::result::Result<&'value SourcedYaml<'input>, SourceIdLocation> {
if let [first, rest @ ..] = path {
let location = value.location;
match &value.data {
YamlData::Value(_) => Err(location),
YamlData::Sequence(sequence) => {
let index: usize = first.parse().or(Err(location))?;
let inner = sequence.get(index).ok_or(location)?;
path_lookup(inner, rest)
}
YamlData::Mapping(mapping) => {
let inner = mapping
.get(&SourcedYaml::value_from_string(first.to_owned()))
.ok_or(location)?;
path_lookup(inner, rest)
}
YamlData::Tagged(_, value) => {
path_lookup(value, rest)
}
YamlData::Representation(_, _, _)
| YamlData::BadValue
| YamlData::Alias(_) => yaml_parse_panic(),
}
} else {
Ok(value)
}
}
fn spread_mapping<'input>(
mapping: &mut AnnotatedMapping<SourcedYaml<'input>>,
to_spread: AnnotatedMapping<SourcedYaml<'input>>,
) {
let mut vec: Vec<_> = Vec::with_capacity(mapping.len());
let mut to_spread = Some(to_spread);
for (key, value) in mapping.drain() {
if key.data.as_str() == Some(REFERENCE_KEY) {
vec.extend(to_spread.take().unwrap());
} else {
vec.push((key, value));
}
}
*mapping = vec.into_iter().collect();
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct SourceReference {
reference: Reference,
source_id: SourceId,
}
impl SourceReference {
fn resolve<'value, 'input>(
&self,
document_map: &'value DocumentMap<'input>,
) -> Result<&'value SourcedYaml<'input>> {
let document = document_map.get(self.source_id);
path_lookup(document, &self.reference.path).map_err(|location| {
LocatedError {
error: ReferenceError::NoResource(self.reference.clone()),
location,
}
})
}
fn depends_on(&self, location: &YamlPath) -> bool {
self.source_id == location.source_id
&& self
.reference
.path
.iter()
.zip(location.segments.iter())
.all(|(ref_part, location_part)| {
location_part == ref_part.as_str()
})
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Reference {
source: ReferenceSource,
path: Vec<String>,
}
impl Reference {
fn try_from_yaml(value: &SourcedYaml) -> Result<Self> {
if let YamlData::Value(Scalar::String(reference)) = &value.data {
let reference =
reference.parse::<Reference>().map_err(|error| {
LocatedError {
error,
location: value.location,
}
})?;
Ok(reference)
} else {
Err(LocatedError {
error: ReferenceError::NotAReference,
location: value.location,
})
}
}
}
impl From<Reference> for String {
fn from(value: Reference) -> Self {
value.to_string()
}
}
impl FromStr for Reference {
type Err = ReferenceError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
fn path(input: &mut &str) -> ModalResult<Vec<String>, EmptyError> {
let segment = preceded('/', take_while(1.., |c| c != '/'));
repeat(1.., segment)
.fold(Vec::new, |mut acc, item: &str| {
acc.push(item.to_owned());
acc
})
.parse_next(input)
}
let source =
|input: &mut &str| -> ModalResult<ReferenceSource, EmptyError> {
alt((
take_until(1.., "#")
.map(|path: &str| ReferenceSource::File(path.into())),
"".map(|_| ReferenceSource::Local),
))
.parse_next(input)
};
let (source, path) = separated_pair(source, '#', path)
.parse(s)
.map_err(|_| ReferenceError::InvalidReference(s.to_owned()))?;
Ok(Self { source, path })
}
}
impl Display for Reference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}#", self.source)?;
for component in &*self.path {
write!(f, "/{component}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum ReferenceSource {
Local,
File(PathBuf),
}
impl Display for ReferenceSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
ReferenceSource::Local => Ok(()),
ReferenceSource::File(path) => {
write!(f, "{}", path.display())
}
}
}
}
#[derive(Clone, Debug)]
struct YamlPath<'input> {
source_id: SourceId,
segments: Vec<YamlPathSegment<'input>>,
location: SourceIdLocation,
}
impl<'input> YamlPath<'input> {
fn new(source_id: SourceId) -> Self {
Self {
source_id,
segments: Vec::new(),
location: SourceIdLocation::default(),
}
}
fn cons(
&self,
segment: impl Into<YamlPathSegment<'input>>,
location: SourceIdLocation,
) -> Self {
let mut segments = self.segments.clone();
segments.push(segment.into());
Self {
source_id: self.source_id,
segments,
location,
}
}
fn get<'value>(
&self,
root: &'value mut SourcedYaml<'input>,
) -> &'value mut SourcedYaml<'input> {
let mut value: &'value mut SourcedYaml<'input> = root;
for segment in &self.segments {
match (segment, &mut value.data) {
(
YamlPathSegment::Sequence(index),
YamlData::Sequence(sequence),
) => {
value = &mut sequence[*index];
}
(YamlPathSegment::Mapping(key), YamlData::Mapping(mapping)) => {
value = &mut mapping[key];
}
(_, value) => panic!(
"Expected a sequence or mapping to match path {self:?} \
at segment {segment:?}, but found {value:?}"
),
}
}
value
}
}
#[derive(Clone, Debug, Eq, From, Hash, PartialEq)]
enum YamlPathSegment<'input> {
Sequence(usize),
Mapping(SourcedYaml<'input>),
}
#[cfg(test)]
impl From<&'static str> for YamlPathSegment<'static> {
fn from(value: &'static str) -> Self {
Self::Mapping(SourcedYaml::value_from_str(value))
}
}
impl PartialEq<str> for YamlPathSegment<'_> {
fn eq(&self, other: &str) -> bool {
match self {
Self::Sequence(index) => Ok(*index) == other.parse::<usize>(),
Self::Mapping(key) => key.data.as_str() == Some(other),
}
}
}
#[derive(Debug)]
struct DocumentMap<'input> {
root: SourcedYaml<'input>,
root_source_id: SourceId,
additional: HashMap<SourceId, SourcedYaml<'input>>,
}
impl<'input> DocumentMap<'input> {
fn get(&self, source_id: SourceId) -> &SourcedYaml<'input> {
if source_id == self.root_source_id {
&self.root
} else {
self.additional.get(&source_id).unwrap_or_else(|| {
panic!("Unregistered source `{source_id:?}`")
})
}
}
fn get_mut(&mut self, source_id: SourceId) -> &mut SourcedYaml<'input> {
if source_id == self.root_source_id {
&mut self.root
} else {
self.additional.get_mut(&source_id).unwrap_or_else(|| {
panic!("Unregistered source `{source_id:?}`")
})
}
}
}
#[derive(Debug, Error)]
pub enum ReferenceError {
#[error(
"References contain one or more cycles: {}",
references.iter().format(", "),
)]
CircularReference { references: Vec<Reference> },
#[error(
"Expected reference `{reference}` to refer to a mapping, as the \
referring parent at {parent} is a mapping with multiple fields. \
The referenced value must also be a mapping so it can be spread into \
the referring map"
)]
ExpectedMapping {
reference: Reference,
parent: SourceLocation,
},
#[error("Invalid reference: `{0}`")]
InvalidReference(String),
#[error(transparent)]
Nested(Box<super::YamlErrorKind>),
#[error("Resource does not exist: `{0}`")]
NoResource(Reference),
#[error("Not a reference")]
NotAReference,
#[error(
"Reference `{0}` was not resolved. Please report this as a bug! {link}",
link = NEW_ISSUE_LINK
)]
Unresolved(Reference),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TempDir, assert_err, temp_dir};
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::fs;
#[rstest]
#[case::no_reference("3", "3")]
#[case::simple_reference(
r##"
requests:
login:
username: "user"
password: "pass"
ref_login:
$ref: "#/requests/login"
"##,
r#"
requests:
login:
username: "user"
password: "pass"
ref_login:
username: "user"
password: "pass"
"#
)]
#[case::sequence(
r##"
base:
seq:
- a
- b
value:
$ref: "#/base/seq/1"
"##,
r"
base:
seq:
- a
- b
value: b
"
)]
#[case::nested(
// Referred value contains another reference that must be resolved
r##"
base:
url: test
requests:
details:
$ref: "#/requests/login"
login:
$ref: "#/base"
"##,
r"
base:
url: test
requests:
details:
url: test
login:
url: test
"
)]
#[case::nested_through(
// Referred value contains a reference that gets traversed through
r##"
base:
headers:
Content-Type: application/json
requests:
details:
headers:
$ref: "#/requests/login/headers"
login:
$ref: "#/base"
"##,
r"
base:
headers:
Content-Type: application/json
requests:
details:
headers:
Content-Type: application/json
login:
headers:
Content-Type: application/json
"
)]
#[case::spread(
r##"
base:
a: 1
b: 1
child:
a: 0
$ref: "#/base"
b: 2
"##,
r"
base:
a: 1
b: 1
child:
a: 1
b: 2
"
)]
fn test_reference(#[case] input: &str, #[case] expected: &str) {
let input = parse_yaml(input);
let expected = parse_yaml(expected);
assert_eq!(
input.resolve_references(&mut SourceMap::default()).unwrap(),
expected
);
}
#[rstest]
#[case::relative(
r#"
value:
$ref: "./file1.yml#/value"
"#,
&[("file1.yml", "value: test")],
"value: test",
)]
#[case::absolute(
r#"
value:
$ref: "{ROOT}/file1.yml#/value"
"#,
&[("file1.yml", "value: test")],
"value: test",
)]
#[case::local_in_subfile(
r#"
value:
$ref: "./file1.yml#/indirection"
"#,
// This inner reference should refer to file1.yml, NOT the root document
&[("file1.yml", r##"
value: test
indirection:
$ref: "#/value"
"##)],
"value: test",
)]
#[case::multi_file(
r#"
value:
$ref: "./file1.yml#/requests/r2/url"
"#,
&[
(
"file1.yml",
r#"
requests:
r1:
url: test
r2:
url: {"$ref": "file2.yml#/requests/r1/url"}
"#,
),
(
"file2.yml",
r#"
requests:
r1:
url: {"$ref": "file1.yml#/requests/r1/url"}
"#
),
],
r#"{"value": "test"}"#
)]
fn test_reference_file(
temp_dir: TempDir,
#[case] root: &str,
#[case] additional: &[(&str, &str)], #[case] expected: &str,
) {
for (path, yaml) in additional {
fs::write(temp_dir.join(path), yaml).unwrap();
}
let base_dir = temp_dir.to_str().unwrap().replace('\\', "\\\\");
let input = parse_yaml(
&root.replace("{ROOT}", &base_dir),
);
let expected = parse_yaml(expected);
let mut source_map = SourceMap::default();
source_map.add_source(temp_dir.join("root.yml"));
let actual = input.resolve_references(&mut source_map).unwrap();
assert_eq!(actual, expected);
}
#[rstest]
fn test_reference_file_cycle(temp_dir: TempDir) {
let file1 = "file1.yml";
let file2 = "file2.yml";
fs::write(
temp_dir.join(file1),
r#"
data:
$ref: "file2.yml#/data"
"#,
)
.unwrap();
fs::write(
temp_dir.join(file2),
r#"
data:
$ref: "file1.yml#/data"
"#,
)
.unwrap();
let yaml = fs::read_to_string(temp_dir.join(file1)).unwrap();
let input = parse_yaml(&yaml);
let mut source_map = SourceMap::default();
source_map.add_source(temp_dir.join("root.yml"));
let result = input.resolve_references(&mut source_map);
assert_err!(
result.map_err(|error| error.error),
"References contain one or more cycles: \
file1.yml#/data, file2.yml#/data"
);
}
#[rstest]
#[case::invalid_reference(
r#"ref_invalid:
$ref: "bad ref""#,
"Invalid reference: `bad ref`"
)]
#[case::not_a_reference(
"ref_invalid:
$ref: 3",
"Not a reference"
)]
#[case::too_many_segments(
r##"
value: 3
ref_invalid:
$ref: "#/value/x"
"##,
"Resource does not exist: `#/value/x`"
)]
#[case::sequence_index_out_of_bounds(
r##"
seq: [a, b]
ref_invalid:
$ref: "#/seq/2"
"##,
"Resource does not exist: `#/seq/2`"
)]
#[case::sequence_string_segment(
r##"
seq: [a, b]
ref_invalid:
$ref: "#/seq/w"
"##,
"Resource does not exist: `#/seq/w`"
)]
#[case::mapping_unknown_key(
r##"
map:
a: 1
b: 2
ref_invalid:
$ref: "#/map/c"
"##,
"Resource does not exist: `#/map/c`"
)]
#[case::spread_scalar(
r##"
map:
a: 1
b: 2
ref_invalid:
$ref: "#/map/a"
b: 1
"##,
"Expected reference `#/map/a` to refer to a mapping"
)]
#[case::cycle_self(
r##"ref_self:
$ref: "#/ref_self""##,
"References contain one or more cycles: #/ref_self"
)]
#[case::cycle_mutual(
r##"
ref1:
$ref: "#/ref2"
ref2:
$ref: "#/ref1"
"##,
"References contain one or more cycles: #/ref1, #/ref2"
)]
#[case::cycle_parent(
r##"
root:
inner:
$ref: "#/root"
"##,
"References contain one or more cycles: #/root"
)]
#[case::cycle_multiple(
// Multiple independent cycles
r##"
a: {"$ref": "#/b"}
b: {"$ref": "#/a"}
c: {"$ref": "#/d"}
d: {"$ref": "#/c"}
e: {"$ref": "#/f"}
f: 1
"##,
// References outside the cycles are NOT included in the error message
"References contain one or more cycles: #/a, #/b, #/c, #/d"
)]
#[case::io(
r#"
root:
$ref: "./other.yml#/root"
"#,
// Strategically omit the base of the path because it's absolute
if cfg!(unix) {
"other.yml: No such file or directory"
} else {
"other.yml: The system cannot find the file specified"
}
)]
fn test_errors(
temp_dir: TempDir,
#[case] input: &str,
#[case] expected_error: &str,
) {
let input = parse_yaml(input);
let mut source_map = SourceMap::default();
source_map.add_source(temp_dir.join("root.yml"));
let result = input.resolve_references(&mut source_map);
assert_err(result.map_err(LocatedError::into_error), expected_error);
}
#[rstest]
#[case::ref_below(SourceId::Memory, "#/a", vec!["a", "b"], true)]
#[case::ref_at(SourceId::Memory, "#/a/b", vec!["a", "b"], true)]
#[case::ref_above(SourceId::Memory, "#/a/b/c", vec!["a", "b"], true)]
#[case::disjoint(SourceId::Memory, "#/a/c", vec!["a", "b"], false)]
#[case::different_sources(SourceId::File(0), "#/a/b", vec!["a", "b"], false)]
fn test_depends_on(
#[case] reference_source_id: SourceId,
#[case] reference: &str,
#[case] segments: Vec<&'static str>,
#[case] is_child: bool,
) {
let path = YamlPath {
source_id: SourceId::Memory,
segments: segments.into_iter().map(YamlPathSegment::from).collect(),
location: SourceIdLocation::default(),
};
let reference = SourceReference {
source_id: reference_source_id,
reference: reference.parse::<Reference>().unwrap(),
};
assert_eq!(reference.depends_on(&path), is_child);
}
#[rstest]
#[case::local("#/a/b", ReferenceSource::Local, vec!["a", "b"])]
#[case::num_index("#/a/0", ReferenceSource::Local, vec!["a", "0"])]
#[case::file_local(
"./file.yml#/a/b",
ReferenceSource::File("./file.yml".into()),
vec!["a", "b"],
)]
#[case::file_absolute(
"/root/file.yml#/a/b",
ReferenceSource::File("/root/file.yml".into()),
vec!["a", "b"],
)]
#[case::file_tilde(
"~/file.yml#/a/b",
// This won't be expanded during parsing but it should parse
ReferenceSource::File("~/file.yml".into()),
vec!["a", "b"],
)]
fn test_parse_reference(
#[case] reference: &str,
#[case] expected_source: ReferenceSource,
#[case] expected_path: Vec<&str>,
) {
let actual = reference.parse::<Reference>().unwrap();
let expected = Reference {
source: expected_source,
path: expected_path.into_iter().map(String::from).collect(),
};
assert_eq!(actual, expected);
}
fn parse_yaml(yaml: &str) -> SourcedYaml<'static> {
SourcedYaml::load_from_str(yaml, SourceId::Memory).unwrap()
}
}