Skip to content

Commit

Permalink
Merge pull request #84 from projectsyn/feat/compose-node-name
Browse files Browse the repository at this point in the history
Add support for config option `compose-node-name`
  • Loading branch information
simu authored Feb 14, 2024
2 parents 059a60d + 0537541 commit 8a7e524
Show file tree
Hide file tree
Showing 18 changed files with 574 additions and 34 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ The implementation currently supports the following features of Kapicorp Reclass
* References in class names
* Loading classes with relative names
* Loading Reclass configuration options from `reclass-config.yaml`
* The Reclass option `componse_node_name`
** reclass-rs provides a non-compatible mode for `compose_node_name` which preserves literal dots in node names

The following Kapicorp Reclass features aren't supported:

* Ignoring overwritten missing references
* Inventory Queries
* The Reclass option `ignore_class_notfound_regexp`
* The Reclass option `componse_node_name`
* The Reclass option `allow_none_override` can't be set to `False`
* The Reclass `yaml_git` and `mixed` storage types
* Any Reclass option which is not mentioned explicitly here or above
Expand Down
65 changes: 65 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
use anyhow::{anyhow, Result};
use pyo3::prelude::*;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use crate::fsutil::to_lexical_normal;

/// Flags to change reclass-rs behavior to be compaible with Python reclass
#[pyclass]
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum CompatFlag {
/// This flag enables Python Reclass-compatible rendering of fields `path` and `parts` in
/// `NodeInfoMeta` when Reclass option `compose-node-name` is enabled.
///
/// By default, if this flag isn't enabled, reclass-rs will preserve literal dots in the node's
/// file path when rendering fields `path` and `parts` in `NodeInfoMeta` when
/// `compose-node-name` is enabled.
ComposeNodeNameLiteralDots,
}

#[pymethods]
impl CompatFlag {
fn __hash__(&self) -> u64 {
let mut h = DefaultHasher::new();
self.hash(&mut h);
h.finish()
}
}

impl TryFrom<&str> for CompatFlag {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self> {
match value {
"compose-node-name-literal-dots"
| "compose_node_name_literal_dots"
| "ComposeNodeNameLiteralDots" => Ok(Self::ComposeNodeNameLiteralDots),
_ => Err(anyhow!("Unknown compatibility flag '{value}'")),
}
}
}

#[pyclass]
#[derive(Clone, Debug, Default)]
pub struct Config {
Expand All @@ -21,6 +58,12 @@ pub struct Config {
/// Whether to ignore included classes which don't exist (yet)
#[pyo3(get)]
pub ignore_class_notfound: bool,
/// Whether to treat nested files in `nodes_path` as node definitions
#[pyo3(get)]
pub compose_node_name: bool,
/// Python Reclass compatibility flags. See `CompatFlag` for available flags.
#[pyo3(get)]
pub compatflags: HashSet<CompatFlag>,
}

impl Config {
Expand Down Expand Up @@ -70,6 +113,8 @@ impl Config {
nodes_path: to_lexical_normal(&npath, true).display().to_string(),
classes_path: to_lexical_normal(&cpath, true).display().to_string(),
ignore_class_notfound: ignore_class_notfound.unwrap_or(false),
compose_node_name: false,
compatflags: HashSet::new(),
})
}

Expand Down Expand Up @@ -113,6 +158,26 @@ impl Config {
"Expected value of config key 'ignore_class_notfound' to be a boolean"
))?;
}
"compose_node_name" => {
self.compose_node_name = v.as_bool().ok_or(anyhow!(
"Expected value of config key 'compose_node_name' to be a boolean"
))?;
}
"reclass_rs_compat_flags" => {
let flags = v.as_sequence().ok_or(anyhow!(
"Expected value of config key 'reclass_rs_compat_flags' to be a list"
))?;
for f in flags {
let f = f
.as_str()
.ok_or(anyhow!("Expected compatibility flag to be a string"))?;
if let Ok(flag) = CompatFlag::try_from(f) {
self.compatflags.insert(flag);
} else {
eprintln!("Unknown compatibility flag '{f}', ignoring...");
}
}
}
_ => {
eprintln!(
"reclass-config.yml entry '{kstr}={vstr}' not implemented yet, ignoring..."
Expand Down
158 changes: 158 additions & 0 deletions src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,162 @@ mod inventory_tests {

assert_eq!(inv.classes, expected_classes);
}

use crate::types::Value;
fn literal(v: &str) -> Value {
Value::Literal(v.to_string())
}

fn sequence(i: &[&str]) -> Value {
Value::Sequence(i.iter().map(|s| literal(s)).collect::<Vec<Value>>())
}

fn check_compose_node_name_shared(inv: &Inventory, invabsdir: &str) {
let mut nodes = inv.nodes.keys().collect::<Vec<_>>();
nodes.sort();
assert_eq!(
nodes,
vec!["a", "a.1", "b.1", "c.1", "c._c.1", "d", "d1", "d2"]
);

for n in nodes {
if n == "a.1" {
// not checking a.1 here, since it's the one node that is rendered differently
// between compat- and nocompat-mode.
continue;
}
let node = &inv.nodes[n];
assert_eq!(node.reclass.node, *n);
assert_eq!(node.reclass.name, *n);
let expected_full_name = node.parameters.get(&"node_name".into()).unwrap();
let expected_short_name = node.parameters.get(&"short_name".into()).unwrap();
let expected_path = node.parameters.get(&"path".into()).unwrap();
let expected_parts = node.parameters.get(&"parts".into()).unwrap();
let expected_uri_suffix = node.parameters.get(&"uri_suffix".into()).unwrap();
assert_eq!(
node.reclass.uri,
format!(
"yaml_fs://{invabsdir}/nodes/{}",
expected_uri_suffix.as_str().unwrap()
)
);
let params_reclass_name = node
.parameters
.get(&"_reclass_".into())
.unwrap()
.get(&"name".into())
.unwrap();
assert_eq!(
params_reclass_name.get(&"full".into()),
Some(expected_full_name)
);
assert_eq!(
params_reclass_name.get(&"short".into()),
Some(expected_short_name)
);
assert_eq!(params_reclass_name.get(&"path".into()), Some(expected_path));
assert_eq!(
params_reclass_name.get(&"parts".into()),
Some(expected_parts)
)
}
}

#[test]
fn test_render_compose_node_name_pycompat() {
let mut c = crate::Config::new(
Some("./tests/inventory-compose-node-name"),
None,
None,
None,
)
.unwrap();
c.load_from_file("reclass-config-compat.yml").unwrap();
let r = Reclass::new_from_config(c).unwrap();

let inv = Inventory::render(&r).unwrap();

let invabsdir = std::fs::canonicalize("./tests/inventory-compose-node-name").unwrap();
let invabsdir = invabsdir.to_str().unwrap();
check_compose_node_name_shared(&inv, invabsdir);

let node = &inv.nodes["a.1"];
assert_eq!(node.reclass.node, "a.1");
assert_eq!(node.reclass.name, "a.1");
assert_eq!(
node.reclass.uri,
format!("yaml_fs://{invabsdir}/nodes/a.1.yml")
);
let params_reclass_name = node
.parameters
.get(&"_reclass_".into())
.unwrap()
.get(&"name".into())
.unwrap();
assert_eq!(
params_reclass_name.get(&"full".into()),
Some(&literal("a.1"))
);
assert_eq!(
params_reclass_name.get(&"short".into()),
Some(&literal("1"))
);
assert_eq!(
params_reclass_name.get(&"path".into()),
Some(&literal("a/1"))
);
assert_eq!(
params_reclass_name.get(&"parts".into()),
Some(&sequence(&["a", "1"]))
);
}

#[test]
fn test_render_compose_node_name() {
let mut c = crate::Config::new(
Some("./tests/inventory-compose-node-name"),
None,
None,
None,
)
.unwrap();
c.load_from_file("reclass-config.yml").unwrap();
let r = Reclass::new_from_config(c).unwrap();

let inv = Inventory::render(&r).unwrap();

let invabsdir = std::fs::canonicalize("./tests/inventory-compose-node-name").unwrap();
let invabsdir = invabsdir.to_str().unwrap();
check_compose_node_name_shared(&inv, invabsdir);

let node = &inv.nodes["a.1"];
assert_eq!(node.reclass.node, "a.1");
assert_eq!(node.reclass.name, "a.1");
assert_eq!(
node.reclass.uri,
format!("yaml_fs://{invabsdir}/nodes/a.1.yml")
);
let params_reclass_name = node
.parameters
.get(&"_reclass_".into())
.unwrap()
.get(&"name".into())
.unwrap();
assert_eq!(
params_reclass_name.get(&"full".into()),
Some(&literal("a.1"))
);
assert_eq!(
params_reclass_name.get(&"short".into()),
Some(&literal("a.1"))
);
assert_eq!(
params_reclass_name.get(&"path".into()),
Some(&literal("a.1"))
);
assert_eq!(
params_reclass_name.get(&"parts".into()),
Some(&sequence(&["a.1"]))
);
}
}
Loading

0 comments on commit 8a7e524

Please sign in to comment.