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..31dcba1b4 --- /dev/null +++ b/exercises/practice/pov/.meta/example.rs @@ -0,0 +1,89 @@ +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Tree { + label: T, + children: Vec>>, +} + +impl Tree { + pub fn new(label: T) -> Self { + Self { + label, + children: Default::default(), + } + } + + pub fn with_children(label: T, children: Vec) -> Self { + Self { + label, + children: children.into_iter().map(Box::new).collect(), + } + } + + pub fn label(&self) -> T { + self.label.clone() + } + + pub fn children(&self) -> Vec<&Self> { + self.children.iter().map(Box::deref).collect() + } + + 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) + 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 + } + + 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/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera new file mode 100644 index 000000000..51d2a95b4 --- /dev/null +++ b/exercises/practice/pov/.meta/test_template.tera @@ -0,0 +1,82 @@ +{%- 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.description }} +mod {{ test_group.cases[0].property | make_ident }} { + use pov::*; + use pretty_assertions::assert_eq; +{%- if test_group.cases[0].property == "fromPov" -%} + use super::test_util::tree_to_sorted; +{%- endif %} + +{% 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_eq!(tree_to_sorted(result), tree_to_sorted(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::*; + use std::fmt::Debug; + + pub fn tree_to_sorted( + tree_opt: Option>, + ) -> Option> { + tree_opt.map(sorter) + } + + fn sorter(tree: Tree) -> Tree { + let mut children = tree.children(); + children.sort_unstable_by_key(|child| child.label()); + Tree::with_children( + tree.label(), + children.into_iter().cloned().map(|c| sorter(c)).collect(), + ) + } +} 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..4c3572c9f --- /dev/null +++ b/exercises/practice/pov/Cargo.toml @@ -0,0 +1,12 @@ +[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] + +[dev-dependencies] +pretty_assertions = "*" \ No newline at end of file diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs new file mode 100644 index 000000000..6d69b82a7 --- /dev/null +++ b/exercises/practice/pov/src/lib.rs @@ -0,0 +1,34 @@ +use std::fmt::Debug; + +#[derive(Clone, Debug, PartialEq)] +pub struct Tree { + _remove_this: std::marker::PhantomData, +} + +impl Tree { + pub fn new(label: T) -> Self { + todo!("Implement a function that creates a new Tree with given {label:?}"); + } + + pub fn with_children(label: T, children: Vec) -> Self { + todo!("Implement a function that creates a new Tree with given {label:?} and {children:?}"); + } + + pub fn label(&self) -> T { + todo!("Implement getter for label."); + } + + pub fn children(&self) -> Vec<&Self> { + todo!("Implement getter for children."); + } + + pub fn pov_from(&self, from: &T) -> Option { + todo!("Implement a function that reparents Tree with {from:?} as root."); + } + + pub fn path_to(&self, from: &T, to: &T) -> 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/tests/pov.rs b/exercises/practice/pov/tests/pov.rs new file mode 100644 index 000000000..66594895a --- /dev/null +++ b/exercises/practice/pov/tests/pov.rs @@ -0,0 +1,404 @@ +/// Reroot a tree so that its root is the specified node. +mod from_pov { + use super::test_util::tree_to_sorted; + use pov::*; + use pretty_assertions::assert_eq; + + #[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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(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_eq!(tree_to_sorted(result), tree_to_sorted(expected)); + } +} + +/// Given two nodes, find the path between them +mod path_to { + use pov::*; + use pretty_assertions::assert_eq; + + #[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::*; + use std::fmt::Debug; + + pub fn tree_to_sorted(tree_opt: Option>) -> Option> { + tree_opt.map(sorter) + } + + fn sorter(tree: Tree) -> Tree { + let mut children = tree.children(); + children.sort_unstable_by_key(|child| child.label()); + Tree::with_children( + tree.label(), + children.into_iter().cloned().map(|c| sorter(c)).collect(), + ) + } +} diff --git a/rust-tooling/Cargo.lock b/rust-tooling/Cargo.lock index 2e98226bc..f54d88314 100644 --- a/rust-tooling/Cargo.lock +++ b/rust-tooling/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -370,6 +370,7 @@ dependencies = [ "glob", "inquire", "models", + "regex", "serde_json", "slug", "tera", @@ -816,9 +817,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -828,9 +829,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -839,9 +840,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" diff --git a/rust-tooling/generate/Cargo.toml b/rust-tooling/generate/Cargo.toml index 2525ee049..6c7ace8e7 100644 --- a/rust-tooling/generate/Cargo.toml +++ b/rust-tooling/generate/Cargo.toml @@ -20,6 +20,7 @@ inquire = "0.6.2" models = { version = "0.1.0", path = "../models" } serde_json = { version = "1.0.105", features = ["arbitrary_precision", "preserve_order"] } slug = "0.1.5" +regex = "1.11.1" tera = "1.19.1" utils = { version = "0.1.0", path = "../utils" } uuid = { version = "1.4.1", features = ["v4"] } diff --git a/rust-tooling/generate/src/custom_filters.rs b/rust-tooling/generate/src/custom_filters.rs index dcef8b45c..27c3d3336 100644 --- a/rust-tooling/generate/src/custom_filters.rs +++ b/rust-tooling/generate/src/custom_filters.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; - +use regex::Regex; use tera::{Result, Value}; type Filter = fn(&Value, &HashMap) -> Result; @@ -27,6 +27,7 @@ pub fn make_ident(value: &Value, _args: &HashMap) -> Result) -> Result String { + let re = Regex::new(r"([a-z0-9])([A-Z])").unwrap(); + let value = re.replace_all(value, "${1}_${2}"); + let re2 = Regex::new(r"([A-Z])([A-Z][a-z])").unwrap(); + let value = re2.replace_all(&value, "${1}_${2}"); + String::from(value) +} + pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { let Some(value) = value.as_number() else { return Err(tera::Error::call_filter(