Repository: projectsyn/reclass-rs (language: python)

Issue:
Add support for config option `compose-node-name`
This PR adds support for the Reclass option `compose-node-name`.

Additionally, we implement support for compatibility flags (including helpers to set/unset/clear flags from Python), and introduce a first flag `ComposeNodeNameLiteralDots`. This flag configures reclass-rs to generate node metadata which matches Python reclass when `compose-node-name` is enabled.

The compatibility flag is required because Python reclass discards literal dots in node filepaths when `compose-node-name` is enabled, resulting in incorrect node metadata for  nodes which have literal dots in their filename (e.g. node `a.1` in the `test/inventory-compose-node-name` inventory).

By default, reclass-rs will preserve literal dots in the node metadata (`path`, and `part`) for node names when `compose-node-name` is enabled.

## Checklist
<!--
Remove items that do not apply. For completed items, change [ ] to [x].
-->

- [x] The PR has a meaningful title. The title will be used to auto generate the changelog
- [x] PR contains a single logical change (to build a better changelog).
- [x] Update the documentation.
- [x] Update tests.
- [x] Categorize the PR by setting a good title and adding one of the labels:
      `bug`, `enhancement`, `documentation`, `change`, `breaking`, `dependency`, `internal`
      as they show up in the changelog
- [x] Link this PR to related PRs or issues.

<!--
NOTE: these things are not required to open a PR and can be done afterwards,
while the PR is open.
-->



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index dd74803..c4b2802 100644
--- a/README.md
+++ b/README.md
@@ -19,10 +19,10 @@ The implementation currently supports the following features of Kapicorp Reclass
 * Nested references
 * References in class names
 * Loading classes with relative names
+* Loading Reclass configuration options from `reclass-config.yaml`
 
 The following Kapicorp Reclass features aren't supported:
 
-* Loading Reclass configuration options from `reclass-config.yaml`
 * Ignoring overwritten missing references
 * Inventory Queries
 * The Reclass option `ignore_class_notfound_regexp`
diff --git a/src/config.rs b/src/config.rs
index 32bfdec..430084c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -73,6 +73,56 @@ impl Config {
         })
     }
 
+    /// Load additional config options from the file at `<self.inventory_path>/<config_file>`.
+    ///
+    /// This method assumes that you've created a Config object with a suitable `inventory_path`.
+    ///
+    /// The method will print diagnostic messages for config options which aren't implemented yet.
+    pub fn load_from_file(&mut self, config_file: &str) -> Result<()> {
+        let mut cfg_path = PathBuf::from(&self.inventory_path);
+        cfg_path.push(config_file);
+
+        let cfg_file = std::fs::read_to_string(&cfg_path)?;
+        let cfg: serde_yaml::Value = serde_yaml::from_str(&cfg_file)?;
+        for (k, v) in cfg
+            .as_mapping()
+            .ok_or(anyhow!("Expected reclass config to be a Mapping"))?
+        {
+            let kstr = serde_yaml::to_string(k)?;
+            let vstr = serde_yaml::to_string(v)?;
+            let kstr = kstr.trim();
+            let vstr = vstr.trim();
+            match kstr {
+                "nodes_uri" => {
+                    let npath = cfg_path
+                        .with_file_name(vstr)
+                        .to_str()
+                        .ok_or(anyhow!("Can't create nodes path from config file"))?
+                        .to_owned();
+                    self.nodes_path = npath;
+                }
+                "classes_uri" => {
+                    self.classes_path = cfg_path
+                        .with_file_name(vstr)
+                        .to_str()
+                        .ok_or(anyhow!("Can't create nodes path from config file"))?
+                        .to_owned();
+                }
+                "ignore_class_notfound" => {
+                    self.ignore_class_notfound = v.as_bool().ok_or(anyhow!(
+                        "Expected value of config key 'ignore_class_notfound' to be a boolean"
+                    ))?;
+                }
+                _ => {
+                    eprintln!(
+                        "reclass-config.yml entry '{kstr}={vstr}' not implemented yet, ignoring..."
+                    );
+                }
+            }
+        }
+        Ok(())
+    }
+
     /// Construct path to node from `self.inventory_path`, `self.nodes_path` and the provided path
     /// to the node relative to the inventory nodes directory.
     pub(crate) fn node_path(&self, npath: &PathBuf) -> PathBuf {
diff --git a/src/inventory.rs b/src/inventory.rs
index 4639372..a4e1d2e 100644
--- a/src/inventory.rs
+++ b/src/inventory.rs
@@ -100,14 +100,9 @@ mod inventory_tests {
 
     #[test]
     fn test_render() {
-        let r = Reclass::new(
-            "./tests/inventory",
-            "nodes",
-            "classes",
-            // n18 includes a nonexistent class
-            true,
-        )
-        .unwrap();
+        let mut c = crate::Config::new(Some("./tests/inventory"), 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();
 
         // Check that all nodes are in `inv.nodes`. We do not verify the NodeInfos here, since we
diff --git a/src/lib.rs b/src/lib.rs
index 0e726c4..bf9de52 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -218,6 +218,16 @@ impl Reclass {
         Ok(r)
     }
 
+    #[classmethod]
+    fn from_config(_cls: &PyType, inventory_path: &str, config_file: &str) -> PyResult<Self> {
+        let mut c = Config::new(Some(inventory_path), None, None, None)
+            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
+        c.load_from_file(config_file)
+            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
+        let r = Self::new_from_config(c).map_err(|e| PyValueError::new_err(format!("{e}")))?;
+        Ok(r)
+    }
+
     fn __repr__(&self) -> String {
         format!("{self:#?}")
     }


Test patch:
diff --git a/tests/test_inventory.py b/tests/test_inventory.py
index 7ef3584..8de5949 100644
--- a/tests/test_inventory.py
+++ b/tests/test_inventory.py
@@ -66,10 +66,7 @@
 
 
 def test_inventory():
-    r = reclass_rs.Reclass(
-        inventory_path="./tests/inventory",
-        ignore_class_notfound=True,
-    )
+    r = reclass_rs.Reclass.from_config("./tests/inventory", "reclass-config.yml")
     inv = r.inventory()
 
     assert set(inv.nodes.keys()) == expected_nodes