From bb4738da611b46504176ae4dad3670be4e3b034e Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Sun, 27 Mar 2022 16:45:40 -0600 Subject: add API to resolve refs --- README.md | 2 +- src/lib.rs | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/ref-resolution.rs | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 tests/ref-resolution.rs diff --git a/README.md b/README.md index 02fcf1d..70d98ff 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ assert_eq!(kdl_schema::SCHEMA_SCHEMA.document.info.title[0].text, "KDL Schema"); ## conditions blocking version 1.0.0 +- documentation at all - good API for parsing from a file -- good API for resolving refs (as long as the ref is a simple global query by ID because using anything other than that as a ref is a weird lifehack and not idiomatic) - types actually match the schema (currently I'm omitting several things because the schema schema doesn't use them) - ergonomic builder API to define a schema in Rust in a non-ugly way - can generate KDL from schema object in Rust diff --git a/src/lib.rs b/src/lib.rs index c724687..6b61dea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,12 @@ pub trait BuildFromRef { fn ref_to(query: impl Into) -> Self; } +fn get_id_from_ref(r#ref: &str) -> Option<&str> { + r#ref + .strip_prefix(r#"[id=""#) + .and_then(|r#ref| r#ref.strip_suffix(r#""]"#)) +} + #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Schema { @@ -18,6 +24,48 @@ pub struct Schema { pub document: Document, } +impl Schema { + /// Panics if ref is not of the form `[id="foo"]`. + pub fn resolve_node_ref(&self, r#ref: &str) -> Option<&Node> { + let id = get_id_from_ref(r#ref).expect("invalid ref"); + self.document + .nodes + .iter() + .filter_map(|node| node.find_node_by_id(id)) + .next() + } + + /// Panics if ref is not of the form `[id="foo"]`. + pub fn resolve_prop_ref(&self, r#ref: &str) -> Option<&Prop> { + let id = get_id_from_ref(r#ref).expect("invalid ref"); + self.document + .nodes + .iter() + .filter_map(|node| node.find_prop_by_id(id)) + .next() + } + + /// Panics if ref is not of the form `[id="foo"]`. + pub fn resolve_value_ref(&self, r#ref: &str) -> Option<&Value> { + let id = get_id_from_ref(r#ref).expect("invalid ref"); + self.document + .nodes + .iter() + .filter_map(|node| node.find_value_by_id(id)) + .next() + } + + /// Panics if ref is not of the form `[id="foo"]`. + pub fn resolve_children_ref(&self, r#ref: &str) -> Option<&Children> { + let id = get_id_from_ref(r#ref).expect("invalid ref"); + self.document + .nodes + .iter() + .filter_map(|node| node.find_children_by_id(id)) + .next() + } +} + #[cfg(feature = "parse-knuffel")] impl Schema { pub fn parse( @@ -131,6 +179,50 @@ pub struct Node { pub children: Vec, } +impl Node { + fn find_node_by_id(&self, id: &str) -> Option<&Node> { + if self.id.as_deref() == Some(id) { + Some(self) + } else { + self.children + .iter() + .filter_map(|children| children.find_node_by_id(id)) + .next() + } + } + + fn find_prop_by_id(&self, id: &str) -> Option<&Prop> { + self.props + .iter() + .filter_map(|prop| prop.find_prop_by_id(id)) + .chain( + self.children + .iter() + .filter_map(|children| children.find_prop_by_id(id)), + ) + .next() + } + + fn find_value_by_id(&self, id: &str) -> Option<&Value> { + self.values + .iter() + .filter_map(|value| value.find_value_by_id(id)) + .chain( + self.children + .iter() + .filter_map(|children| children.find_value_by_id(id)), + ) + .next() + } + + fn find_children_by_id(&self, id: &str) -> Option<&Children> { + self.children + .iter() + .filter_map(|children| children.find_children_by_id(id)) + .next() + } +} + impl BuildFromRef for Node { fn ref_to(query: impl Into) -> Self { Self { @@ -157,6 +249,16 @@ pub struct Prop { pub validations: Vec, } +impl Prop { + fn find_prop_by_id(&self, id: &str) -> Option<&Prop> { + if self.id.as_deref() == Some(id) { + Some(self) + } else { + None + } + } +} + impl BuildFromRef for Prop { fn ref_to(query: impl Into) -> Self { Self { @@ -183,6 +285,16 @@ pub struct Value { pub validations: Vec, } +impl Value { + fn find_value_by_id(&self, id: &str) -> Option<&Value> { + if self.id.as_deref() == Some(id) { + Some(self) + } else { + None + } + } +} + impl BuildFromRef for Value { fn ref_to(query: impl Into) -> Self { Self { @@ -205,6 +317,40 @@ pub struct Children { pub nodes: Vec, } +impl Children { + fn find_node_by_id(&self, id: &str) -> Option<&Node> { + self.nodes + .iter() + .filter_map(|node| node.find_node_by_id(id)) + .next() + } + + fn find_prop_by_id(&self, id: &str) -> Option<&Prop> { + self.nodes + .iter() + .filter_map(|node| node.find_prop_by_id(id)) + .next() + } + + fn find_value_by_id(&self, id: &str) -> Option<&Value> { + self.nodes + .iter() + .filter_map(|node| node.find_value_by_id(id)) + .next() + } + + fn find_children_by_id(&self, id: &str) -> Option<&Children> { + if self.id.as_deref() == Some(id) { + Some(self) + } else { + self.nodes + .iter() + .filter_map(|node| node.find_children_by_id(id)) + .next() + } + } +} + impl BuildFromRef for Children { fn ref_to(query: impl Into) -> Self { Self { diff --git a/tests/ref-resolution.rs b/tests/ref-resolution.rs new file mode 100644 index 0000000..e6bb648 --- /dev/null +++ b/tests/ref-resolution.rs @@ -0,0 +1,145 @@ +use kdl_schema::{Node, SCHEMA_SCHEMA}; + +#[test] +fn node_ref_proper() { + assert_eq!( + SCHEMA_SCHEMA + .resolve_node_ref(r#"[id="tag-node"]"#) + .unwrap() + .name + .as_deref(), + Some("tag") + ); +} + +#[test] +fn node_ref_missing() { + assert_eq!( + SCHEMA_SCHEMA.resolve_node_ref(r#"[id="not-in-there-lol"]"#), + None + ); +} + +#[test] +#[should_panic] +fn node_ref_malformed() { + let _ = SCHEMA_SCHEMA.resolve_node_ref(r#"[description="hi"]"#); +} + +#[test] +fn prop_ref_proper() { + assert_eq!( + SCHEMA_SCHEMA + .resolve_prop_ref(r#"[id="info-time"]"#) + .unwrap() + .key + .as_deref(), + Some("time") + ); +} + +#[test] +fn prop_ref_missing() { + assert_eq!( + SCHEMA_SCHEMA.resolve_prop_ref(r#"[id="not-in-there-lol"]"#), + None + ); +} + +#[test] +#[should_panic] +fn prop_ref_malformed() { + let _ = SCHEMA_SCHEMA.resolve_prop_ref(r#"[description="hi"]"#); +} + +#[test] +fn value_ref_proper() { + assert_eq!( + SCHEMA_SCHEMA + .resolve_value_ref(r#"[id="info-person-name"]"#) + .unwrap() + .description + .as_deref(), + Some("Person name") + ); +} + +#[test] +fn value_ref_missing() { + assert_eq!( + SCHEMA_SCHEMA.resolve_value_ref(r#"[id="not-in-there-lol"]"#), + None + ); +} + +#[test] +#[should_panic] +fn value_ref_malformed() { + let _ = SCHEMA_SCHEMA.resolve_value_ref(r#"[description="hi"]"#); +} + +#[test] +fn children_ref_proper() { + assert_eq!( + SCHEMA_SCHEMA + .resolve_children_ref(r#"[id="validations"]"#) + .unwrap() + .description + .as_deref(), + Some("General value validations.") + ); +} + +#[test] +fn children_ref_missing() { + assert_eq!( + SCHEMA_SCHEMA.resolve_children_ref(r#"[id="not-in-there-lol"]"#), + None + ); +} + +#[test] +#[should_panic] +fn children_ref_malformed() { + let _ = SCHEMA_SCHEMA.resolve_children_ref(r#"[description="hi"]"#); +} + +#[test] +fn all_schema_schema_refs_resolve() { + fn all_refs_resolve(node: &Node) { + if let Some(r#ref) = &node.ref_ { + SCHEMA_SCHEMA + .resolve_node_ref(r#ref) + .unwrap_or_else(|| panic!("node ref {} not found", r#ref)); + } else { + for prop in &node.props { + if let Some(r#ref) = &prop.ref_ { + SCHEMA_SCHEMA + .resolve_prop_ref(r#ref) + .unwrap_or_else(|| panic!("prop ref {} not found", r#ref)); + } + } + for value in &node.values { + if let Some(r#ref) = &value.ref_ { + SCHEMA_SCHEMA + .resolve_value_ref(r#ref) + .unwrap_or_else(|| panic!("value ref {} not found", r#ref)); + } + } + for children in &node.children { + if let Some(r#ref) = &children.ref_ { + SCHEMA_SCHEMA + .resolve_children_ref(r#ref) + .unwrap_or_else(|| panic!("children ref {} not found", r#ref)); + } else { + for node in &children.nodes { + all_refs_resolve(node); + } + } + } + } + } + for node in &SCHEMA_SCHEMA.document.nodes { + all_refs_resolve(node); + } +} -- cgit v1.2.3