From 213dfec79a4a7a8bd91030808e872d85900ad224 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 12:49:54 +0900 Subject: [PATCH] Initial draft of POV, also added camel->snake custom filter --- .gitignore | 1 + config.json | 9 + exercises/practice/pov/.docs/instructions.md | 41 ++ exercises/practice/pov/.gitignore | 2 + exercises/practice/pov/.meta/config.json | 20 + exercises/practice/pov/.meta/example.rs | 35 ++ .../practice/pov/.meta/test_template.tera | 94 ++++ exercises/practice/pov/.meta/tests.toml | 55 +++ exercises/practice/pov/Cargo.toml | 9 + exercises/practice/pov/src/lib.rs | 84 ++++ exercises/practice/pov/tests/pov.rs | 428 ++++++++++++++++++ rust-tooling/generate/src/custom_filters.rs | 22 + 12 files changed, 800 insertions(+) create mode 100644 exercises/practice/pov/.docs/instructions.md create mode 100644 exercises/practice/pov/.gitignore create mode 100644 exercises/practice/pov/.meta/config.json create mode 100644 exercises/practice/pov/.meta/example.rs create mode 100644 exercises/practice/pov/.meta/test_template.tera create mode 100644 exercises/practice/pov/.meta/tests.toml create mode 100644 exercises/practice/pov/Cargo.toml create mode 100644 exercises/practice/pov/src/lib.rs create mode 100644 exercises/practice/pov/tests/pov.rs diff --git a/.gitignore b/.gitignore index 3eea4c1c0..69320c607 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tmp /bin/configlet exercises/*/*/Cargo.lock clippy.log +.idea .vscode .prob-spec problem-specifications diff --git a/config.json b/config.json index a36553efd..0b7d079f8 100644 --- a/config.json +++ b/config.json @@ -1572,6 +1572,15 @@ "lists", "unsafe" ] + }, + { + "slug": "pov", + "name": "POV", + "uuid": "bf7b7309-3d34-4893-a584-f7742502e012", + "practices": [], + "prerequisites": [], + "difficulty": 10, + "topics": [] } ], "foregone": [ diff --git a/exercises/practice/pov/.docs/instructions.md b/exercises/practice/pov/.docs/instructions.md new file mode 100644 index 000000000..0fdeed225 --- /dev/null +++ b/exercises/practice/pov/.docs/instructions.md @@ -0,0 +1,41 @@ +# Instructions + +Reparent a tree on a selected node. + +A [tree][wiki-tree] is a special type of [graph][wiki-graph] where all nodes are connected but there are no cycles. +That means, there is exactly one path to get from one node to another for any pair of nodes. + +This exercise is all about re-orientating a tree to see things from a different point of view. +For example family trees are usually presented from the ancestor's perspective: + +```text + +------0------+ + | | | + +-1-+ +-2-+ +-3-+ + | | | | | | + 4 5 6 7 8 9 +``` + +But there is no inherent direction in a tree. +The same information can be presented from the perspective of any other node in the tree, by pulling it up to the root and dragging its relationships along with it. +So the same tree from 6's perspective would look like: + +```text + 6 + | + +-----2-----+ + | | + 7 +-----0-----+ + | | + +-1-+ +-3-+ + | | | | + 4 5 8 9 +``` + +This lets us more simply describe the paths between two nodes. +So for example the path from 6-9 (which in the first tree goes up to the root and then down to a different leaf node) can be seen to follow the path 6-2-0-3-9. + +This exercise involves taking an input tree and re-orientating it from the point of view of one of the nodes. + +[wiki-graph]: https://en.wikipedia.org/wiki/Tree_(graph_theory) +[wiki-tree]: https://en.wikipedia.org/wiki/Graph_(discrete_mathematics) diff --git a/exercises/practice/pov/.gitignore b/exercises/practice/pov/.gitignore new file mode 100644 index 000000000..96ef6c0b9 --- /dev/null +++ b/exercises/practice/pov/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/exercises/practice/pov/.meta/config.json b/exercises/practice/pov/.meta/config.json new file mode 100644 index 000000000..076d72325 --- /dev/null +++ b/exercises/practice/pov/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "devcyjung" + ], + "files": { + "solution": [ + "src/lib.rs", + "Cargo.toml" + ], + "test": [ + "tests/pov.rs" + ], + "example": [ + ".meta/example.rs" + ] + }, + "blurb": "Reparent a graph on a selected node.", + "source": "Adaptation of exercise from 4clojure", + "source_url": "https://github.com/oxalorg/4ever-clojure" +} diff --git a/exercises/practice/pov/.meta/example.rs b/exercises/practice/pov/.meta/example.rs new file mode 100644 index 000000000..63d6e6b13 --- /dev/null +++ b/exercises/practice/pov/.meta/example.rs @@ -0,0 +1,35 @@ +#[derive(Clone, PartialEq, Eq)] +pub struct Tree {} + +impl Tree { + #[must_use] + pub fn new(_label: &str) -> Self { + todo!("Implement a function that creates a new Tree with given label"); + } + + #[must_use] + pub fn with_children(_label: &str, _children: &[Tree]) -> Self { + todo!("Implement a function that creates a new Tree with given label and Children"); + } + + #[must_use] + pub fn get_label(&self) -> String { + todo!("Implement getter for label."); + } + + #[must_use] + pub fn get_children(&self) -> Vec<&Self> { + todo!("Implement getter for children."); + } + + #[must_use] + pub fn pov_from(self, _from: &str) -> Option { + todo!("Implement a function that reparents Tree with 'from' as root."); + } + + #[must_use] + pub fn path_to(self, _from: &str, _to: &str) -> Option> + { + todo!("Implement a function that returns the list of labels in the shortest path from 'from' to 'to'"); + } +} diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera new file mode 100644 index 000000000..840f8b2c1 --- /dev/null +++ b/exercises/practice/pov/.meta/test_template.tera @@ -0,0 +1,94 @@ +{%- macro render_tree(tree) -%} +{%- if tree.children -%} + Tree::with_children( + "{{ tree.label }}".to_string(), + vec![ + {%- for child in tree.children -%} + {{ self::render_tree(tree=child) }}, + {%- endfor -%} + ], + ) +{%- else -%} + Tree::new("{{ tree.label }}".to_string()) +{%- endif -%} +{%- endmacro -%} + +{%- macro render_vec(values) -%} +vec![ + {%- for value in values -%} + "{{ value }}".to_string(), + {%- endfor -%} +] +{%- endmacro -%} + +{% for test_group in cases %} +/// {{ test_group.cases[0].property | camel_to_snake }}() tests +/// {{ test_group.description }} +{%- for comment in test_group.comments %} +/// {{ comment }} +{%- endfor %} +mod {{ test_group.description | make_ident }} { + use pov::*; + +{% for test in test_group.cases %} +#[test] +#[ignore] +fn {{ test.description | make_ident }}() { + let input = {{ self::render_tree(tree=test.input.tree) }}; + let from = "{{ test.input.from }}".to_string(); + {%- if test.property == "fromPov" -%} + let result = input.pov_from(&from); + {%- if not test.expected -%} + let expected: Option> = None; + {%- else -%} + let expected = Some({{ self::render_tree(tree=test.expected) }}); + {%- endif -%} + assert!(crate::test_util::tree_option_eq(result, expected)); + {%- elif test.property == "pathTo" -%} + let to = "{{ test.input.to }}".to_string(); + let result = input.path_to(&from, &to); + {%- if not test.expected -%} + let expected: Option> = None; + {%- else -%} + let expected = Some({{ self::render_vec(values=test.expected) }}); + {%- endif -%} + assert_eq!(result, expected); + {%- else -%} + Invalid property: {{ test.property }} + {%- endif -%} +} +{% endfor %} +} +{% endfor %} +mod test_util { + use pov::*; + + pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { + match (lhs, rhs) { + (None, None) => true, + (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), + _ => false, + } + } + + pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { + let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); + let (mut l_children, mut r_children) = ( + lhs.get_children(), + rhs.get_children(), + ); + if l_label == r_label && l_children.len() == r_children.len() { + if l_children.len() == 0 { + return true; + } + let key_fn = |child: &&Tree| child.get_label(); + l_children.sort_unstable_by_key(key_fn); + r_children.sort_unstable_by_key(key_fn); + return l_children + .iter() + .zip(r_children.iter()) + .all(|(&lc, &rc)| tree_eq(lc, rc)); + } + false + } +} \ No newline at end of file diff --git a/exercises/practice/pov/.meta/tests.toml b/exercises/practice/pov/.meta/tests.toml new file mode 100644 index 000000000..bfa0bb630 --- /dev/null +++ b/exercises/practice/pov/.meta/tests.toml @@ -0,0 +1,55 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[1b3cd134-49ad-4a7d-8376-7087b7e70792] +description = "Reroot a tree so that its root is the specified node. -> Results in the same tree if the input tree is a singleton" + +[0778c745-0636-40de-9edd-25a8f40426f6] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with a parent and one sibling" + +[fdfdef0a-4472-4248-8bcf-19cf33f9c06e] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with a parent and many siblings" + +[cbcf52db-8667-43d8-a766-5d80cb41b4bb] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with new root deeply nested in tree" + +[e27fa4fa-648d-44cd-90af-d64a13d95e06] +description = "Reroot a tree so that its root is the specified node. -> Moves children of the new root to same level as former parent" + +[09236c7f-7c83-42cc-87a1-25afa60454a3] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a complex tree with cousins" + +[f41d5eeb-8973-448f-a3b0-cc1e019a4193] +description = "Reroot a tree so that its root is the specified node. -> Errors if target does not exist in a singleton tree" + +[9dc0a8b3-df02-4267-9a41-693b6aff75e7] +description = "Reroot a tree so that its root is the specified node. -> Errors if target does not exist in a large tree" + +[02d1f1d9-428d-4395-b026-2db35ffa8f0a] +description = "Given two nodes, find the path between them -> Can find path to parent" + +[d0002674-fcfb-4cdc-9efa-bfc54e3c31b5] +description = "Given two nodes, find the path between them -> Can find path to sibling" + +[c9877cd1-0a69-40d4-b362-725763a5c38f] +description = "Given two nodes, find the path between them -> Can find path to cousin" + +[9fb17a82-2c14-4261-baa3-2f3f234ffa03] +description = "Given two nodes, find the path between them -> Can find path not involving root" + +[5124ed49-7845-46ad-bc32-97d5ac7451b2] +description = "Given two nodes, find the path between them -> Can find path from nodes other than x" + +[f52a183c-25cc-4c87-9fc9-0e7f81a5725c] +description = "Given two nodes, find the path between them -> Errors if destination does not exist" + +[f4fe18b9-b4a2-4bd5-a694-e179155c2149] +description = "Given two nodes, find the path between them -> Errors if source does not exist" diff --git a/exercises/practice/pov/Cargo.toml b/exercises/practice/pov/Cargo.toml new file mode 100644 index 000000000..87c6c85d1 --- /dev/null +++ b/exercises/practice/pov/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pov" +version = "0.1.0" +edition = "2024" + +# Not all libraries from crates.io are available in Exercism's test runner. +# The full list of available libraries is here: +# https://github.com/exercism/rust-test-runner/blob/main/local-registry/Cargo.toml +[dependencies] diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs new file mode 100644 index 000000000..33ce33a04 --- /dev/null +++ b/exercises/practice/pov/src/lib.rs @@ -0,0 +1,84 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, PartialEq)] +pub struct Tree { + label: T, + children: Vec>>, +} + +impl Tree { + #[must_use] + pub fn new(label: T) -> Self { + Self { label, children: Default::default() } + } + + #[must_use] + pub fn with_children(label: T, children: Vec>) -> Self { + Self { label, children: children.into_iter().map(Box::new).collect() } + } + + #[must_use] + pub fn get_label(&self) -> T { + self.label.clone() + } + + #[must_use] + pub fn get_children(&self) -> Vec<&Self> { + self.children.iter().map(Box::deref).collect() + } + + #[must_use] + pub fn pov_from(&self, from: &T) -> Option { + // list of (child, parent, child's index in parent.children) + let mut lookup = vec![(self, None, None)]; + let mut stack = vec![self]; + while let Some(parent) = stack.pop() { + if &parent.label == from { + return self.reparent(parent, lookup.as_slice()).into(); + } + lookup.extend( + parent.children.iter() + .map(Box::deref).enumerate() + .map(|(i, child)| (child, Some(parent), Some(i))) + ); + stack.extend(parent.children.iter().map(Box::deref)); + } + None + } + + // lookup is list of (child, parent, child's index in parent.children) + #[must_use] + fn reparent(&self, parent: &Self, lookup: &[(&Self, Option<&Self>, Option)]) -> Self { + let mut new_root = parent.clone(); + let mut current = parent; + let mut clone_mut = &mut new_root; + let find_parent = |child| lookup.iter().find(|(c, _p, _i)| *c == child); + while let Some(&(_, Some(parent), Some(index))) = find_parent(current) { + let mut parent_clone = parent.clone(); + parent_clone.children.swap_remove(index); + clone_mut.children.push(Box::new(parent_clone)); + current = parent; + let new_box = clone_mut.children.last_mut().expect("We just inserted node, this is not empty"); + clone_mut = new_box.deref_mut(); + } + new_root + } + + #[must_use] + pub fn path_to(&self, from: &T, to: &T) -> Option> + { + if from != &self.label { + return self.pov_from(from).and_then(|pov| pov.path_to(from, to)); + } + if to == &self.label { + return Some(vec![self.label.clone()]); + } + for child in self.children.iter() { + if let Some(mut path) = child.path_to(&child.label, to) { + path.insert(0, self.label.clone()); + return Some(path); + } + } + None + } +} diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs new file mode 100644 index 000000000..e1dc8f7ed --- /dev/null +++ b/exercises/practice/pov/tests/pov.rs @@ -0,0 +1,428 @@ +/// from_pov() tests +/// Reroot a tree so that its root is the specified node. +/// In this way, the tree is presented from the point of view of the specified node. +/// +/// If appropriate for your track, you may test that the input tree is not modified. +/// +/// Note that when rerooting upon a target node that has both parents and children, +/// it does not matter whether the former parent comes before or after the former children. +/// Please account for this when checking correctness of the resulting trees. +/// One suggested method is to only check two things: +/// 1) The root of the returned tree is the root that was passed in to from_pov. +/// 2) The sorted edge list of the returned tree is the same as the sorted edge list of the expected tree. +mod reroot_a_tree_so_that_its_root_is_the_specified_node { + use pov::*; + + #[test] + fn results_in_the_same_tree_if_the_input_tree_is_a_singleton() { + let input = Tree::new("x".to_string()); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::new("x".to_string())); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_a_parent_and_one_sibling() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::new("x".to_string()), Tree::new("sibling".to_string())], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![Tree::new("sibling".to_string())], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_a_parent_and_many_siblings() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_new_root_deeply_nested_in_tree() { + let input = Tree::with_children( + "level-0".to_string(), + vec![Tree::with_children( + "level-1".to_string(), + vec![Tree::with_children( + "level-2".to_string(), + vec![Tree::with_children( + "level-3".to_string(), + vec![Tree::new("x".to_string())], + )], + )], + )], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "level-3".to_string(), + vec![Tree::with_children( + "level-2".to_string(), + vec![Tree::with_children( + "level-1".to_string(), + vec![Tree::new("level-0".to_string())], + )], + )], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn moves_children_of_the_new_root_to_same_level_as_former_parent() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + )], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + Tree::new("parent".to_string()), + ], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_complex_tree_with_cousins() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![ + Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ), + Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + ), + ], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-1".to_string()), + Tree::new("kid-0".to_string()), + Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + Tree::with_children( + "grandparent".to_string(), + vec![Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + )], + ), + ], + ), + ], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn errors_if_target_does_not_exist_in_a_singleton_tree() { + let input = Tree::new("x".to_string()); + let from = "nonexistent".to_string(); + let result = input.pov_from(&from); + let expected: Option> = None; + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn errors_if_target_does_not_exist_in_a_large_tree() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "nonexistent".to_string(); + let result = input.pov_from(&from); + let expected: Option> = None; + assert!(crate::test_util::tree_option_eq(result, expected)); + } +} + +/// path_to() tests +/// Given two nodes, find the path between them +/// A typical implementation would first reroot the tree on one of the two nodes. +/// +/// If appropriate for your track, you may test that the input tree is not modified. +mod given_two_nodes_find_the_path_between_them { + use pov::*; + + #[test] + #[ignore] + fn can_find_path_to_parent() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::new("x".to_string()), Tree::new("sibling".to_string())], + ); + let from = "x".to_string(); + let to = "parent".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["x".to_string(), "parent".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_to_sibling() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "x".to_string(); + let to = "b".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["x".to_string(), "parent".to_string(), "b".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_to_cousin() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![ + Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ), + Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + ), + ], + ); + let from = "x".to_string(); + let to = "cousin-1".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec![ + "x".to_string(), + "parent".to_string(), + "grandparent".to_string(), + "uncle".to_string(), + "cousin-1".to_string(), + ]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_not_involving_root() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("x".to_string()), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + )], + ); + let from = "x".to_string(); + let to = "sibling-1".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec![ + "x".to_string(), + "parent".to_string(), + "sibling-1".to_string(), + ]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_from_nodes_other_than_x() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "a".to_string(); + let to = "c".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["a".to_string(), "parent".to_string(), "c".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn errors_if_destination_does_not_exist() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "x".to_string(); + let to = "nonexistent".to_string(); + let result = input.path_to(&from, &to); + let expected: Option> = None; + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn errors_if_source_does_not_exist() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "nonexistent".to_string(); + let to = "x".to_string(); + let result = input.path_to(&from, &to); + let expected: Option> = None; + assert_eq!(result, expected); + } +} + +mod test_util { + use pov::*; + + pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { + match (lhs, rhs) { + (None, None) => true, + (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), + _ => false, + } + } + + pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { + let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); + let (mut l_children, mut r_children) = (lhs.get_children(), rhs.get_children()); + if l_label == r_label && l_children.len() == r_children.len() { + if l_children.len() == 0 { + return true; + } + let key_fn = |child: &&Tree| child.get_label(); + l_children.sort_unstable_by_key(key_fn); + r_children.sort_unstable_by_key(key_fn); + return l_children + .iter() + .zip(r_children.iter()) + .all(|(&lc, &rc)| tree_eq(lc, rc)); + } + false + } +} diff --git a/rust-tooling/generate/src/custom_filters.rs b/rust-tooling/generate/src/custom_filters.rs index dcef8b45c..b92eeef27 100644 --- a/rust-tooling/generate/src/custom_filters.rs +++ b/rust-tooling/generate/src/custom_filters.rs @@ -8,6 +8,7 @@ pub static CUSTOM_FILTERS: &[(&str, Filter)] = &[ ("to_hex", to_hex), ("make_ident", make_ident), ("fmt_num", fmt_num), + ("camel_to_snake", camel_to_snake), ]; pub fn to_hex(value: &Value, _args: &HashMap) -> Result { @@ -56,3 +57,24 @@ pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { let pretty_num = String::from_utf8(pretty_digits).unwrap_or_default(); Ok(Value::String(pretty_num)) } + +pub fn camel_to_snake(value: &Value, _args: &HashMap) -> Result { + let Some(input) = value.as_str() else { + return Err(tera::Error::call_filter( + "camel_to_snake filter expects a string", + "serde_json::value::Value::as_str", + )); + }; + let mut result = String::with_capacity(input.len() << 1); + let mut peek_iter = input.chars().peekable(); + while let Some(ch) = peek_iter.next() { + result.push(ch); + if let Some(&next_ch) = peek_iter.peek() { + if ch.is_lowercase() && next_ch.is_uppercase() { + result.push('_'); + } + } + } + result.shrink_to_fit(); + Ok(Value::String(result.to_lowercase())) +} \ No newline at end of file