Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for config option compose-node-name #84

Merged
merged 9 commits into from
Feb 14, 2024
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
Loading