From 1f13ce27321949f4efec5bb6779d04d522d2d1a5 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 3 Oct 2025 11:36:23 +0200 Subject: [PATCH] WIP: dont merge --- lib/introspection/test.nix | 52 +++++---- pkgs/clan-cli/clan_lib/persist/modules.nix | 53 +++++++++ pkgs/clan-cli/clan_lib/persist/write_rules.py | 72 ++++++++++++- .../clan_lib/persist/write_rules_test.py | 101 +++++++++++++++++- 4 files changed, 254 insertions(+), 24 deletions(-) create mode 100644 pkgs/clan-cli/clan_lib/persist/modules.nix diff --git a/lib/introspection/test.nix b/lib/introspection/test.nix index 73c4c5a53..b8900e4e3 100644 --- a/lib/introspection/test.nix +++ b/lib/introspection/test.nix @@ -1,7 +1,8 @@ # tests for the nixos options to jsonschema converter # run these tests via `nix-unit ./test.nix` { - lib ? (import { }).lib, + lib ? import /home/johannes/git/nixpkgs/lib, + # lib ? (import { }).lib, slib ? (import ./. { inherit lib; }), }: let @@ -67,31 +68,38 @@ in }; }; }; - test_no_default = { - expr = stableView ( - slib.getPrios { - options = - (eval [ - { - options.foo.bar = lib.mkOption { - type = lib.types.bool; - }; - } - ]).options; - } - ); - expected = { - foo = { - bar = { - __this = { - files = [ ]; - prio = 9999; - total = false; + test_no_default = + let + + configuration = ( + eval [ + { + options.foo.bar = lib.mkOption { + type = lib.types.bool; + }; + } + ] + ); + in + { + inherit configuration; + expr = stableView ( + slib.getPrios { + options = configuration.options; + } + ); + expected = { + foo = { + bar = { + __this = { + files = [ ]; + prio = 9999; + total = false; + }; }; }; }; }; - }; test_submodule = { expr = stableView ( diff --git a/pkgs/clan-cli/clan_lib/persist/modules.nix b/pkgs/clan-cli/clan_lib/persist/modules.nix new file mode 100644 index 000000000..e09b167d3 --- /dev/null +++ b/pkgs/clan-cli/clan_lib/persist/modules.nix @@ -0,0 +1,53 @@ +let + lib = import /home/johannes/git/nixpkgs/lib; + + clanLib = import ../../../../lib { inherit lib; }; + + inherit (lib) evalModules mkOption types; + + eval = evalModules { + modules = [ + { + options.foos = mkOption { + type = types.attrsOf ( + types.submodule { + options.bar = mkOption { }; + } + ); + }; + # config.foos = lib.mkForce { this.bar = 42; }; + config.instances.a = { }; + # config.foo = lib.mkForce { + # bar = 42; + # }; + } + { + _file = "inventory.json"; + # instances.a = { setting = }; + } + + # { + # options.foo = mkOption { + # type = types.attrsOf (types.attrsOf (types.submoduleWith { modules = [ + # { + # options.bar = mkOption {}; + # } + # ]; })); + # default = { bar = { }; }; + # }; + # } + # { + # _file = "static.nix"; + # foo.static.thing = { bar = 1; }; # <- Can: Op.Modify + # } + # { + # _file = "inventory.json"; + # foo.managed.thing = { bar = 1; }; # <- Can: Op.Delete, Op.Modify + # # + # } + ]; + }; +in +{ + inherit clanLib eval; +} diff --git a/pkgs/clan-cli/clan_lib/persist/write_rules.py b/pkgs/clan-cli/clan_lib/persist/write_rules.py index 67307fe91..8163248a3 100644 --- a/pkgs/clan-cli/clan_lib/persist/write_rules.py +++ b/pkgs/clan-cli/clan_lib/persist/write_rules.py @@ -1,7 +1,11 @@ +from enum import Enum from typing import Any, TypedDict from clan_lib.errors import ClanError -from clan_lib.persist.path_utils import PathTuple, path_to_string +from clan_lib.persist.path_utils import ( + PathTuple, + path_to_string, +) WRITABLE_PRIORITY_THRESHOLD = 100 # Values below this are not writeable @@ -189,3 +193,69 @@ def compute_write_map( """ return _determine_writeability_recursive(priorities, all_values, persisted) + + +class RawAttributes(TypedDict): + headType: str + nullable: bool + prio: int + total: bool + files: list[str] + + +class OpType(Enum): + MODIFY = "modify" + DELETE = "delete" + + +def transform_attribute_properties( + introspection: dict[str, Any], + all_values: dict[str, Any], + persisted: dict[str, Any], + # Passthrough for recursion + curr_path: PathTuple = (), + parent_attributes: RawAttributes | None = None, +) -> dict[PathTuple, set[OpType]]: + """Transform attribute properties to ensure correct types and defaults.""" + results: dict[PathTuple, set[OpType]] = {} + + for key, key_meta in introspection.items(): + if key in {"__this", "__list"}: + continue + + path = (*curr_path, key) + results[path] = set() + + local_attributes: RawAttributes = key_meta.get("__this") + + key_priority = local_attributes["prio"] or None + + effective_priority = key_priority or ( + parent_attributes["prio"] if parent_attributes else None + ) + if effective_priority is None: + msg = f"Priority for path '{path_to_string(path)}' is not defined and no parent to inherit from. Cannot determine effective priority." + raise ClanError(msg) + + if isinstance(key_meta, dict): + subattrs = transform_attribute_properties( + key_meta, + all_values.get(key, {}), + persisted.get(key, {}), + curr_path=path, + parent_attributes=local_attributes, + ) + results.update(dict(subattrs.items())) + + return results + + # Only defined in inventory.json -> We might be able to delete it, because we defined it. + # But we could also have some option default somewhere else, so we cannot be sure. + # if all(f.endswith("inventory.json") for f in raw_attributes["files"]): + # operations.add(OpType.DELETE) + + # if ( + # raw_attributes["prio"] >= WRITABLE_PRIORITY_THRESHOLD + # or ".json" in raw_attributes["files"] + # ): + # operations.add(OpType.MODIFY) diff --git a/pkgs/clan-cli/clan_lib/persist/write_rules_test.py b/pkgs/clan-cli/clan_lib/persist/write_rules_test.py index af0516956..82a5521fa 100644 --- a/pkgs/clan-cli/clan_lib/persist/write_rules_test.py +++ b/pkgs/clan-cli/clan_lib/persist/write_rules_test.py @@ -5,11 +5,110 @@ import pytest from clan_lib.flake.flake import Flake from clan_lib.persist.inventory_store import InventoryStore -from clan_lib.persist.write_rules import compute_write_map +from clan_lib.persist.write_rules import ( + compute_write_map, + transform_attribute_properties, +) if TYPE_CHECKING: from clan_lib.nix_models.clan import Clan +# foos.this = lib.mkForce { bar = 42; }; +# -> +# { +# foos = { +# __this = { +# files = [ +# "inventory.json" +# "" +# ]; +# headType = "attrsOf"; +# nullable = false; +# prio = 100; +# total = false; +# }; +# this = { +# __this = { +# files = [ "" ]; +# headType = "submodule"; +# nullable = false; +# prio = 50; +# total = true; +# }; +# bar = { +# __this = { +# files = [ "" ]; +# headType = "unspecified"; +# nullable = false; +# prio = 100; +# total = false; +# }; +# }; +# }; +# }; +# } + + +def test_write_new() -> None: + all_data: dict = {"foo": {"bar": 42}} + persisted_data: dict = {} + introspection: dict = { + "foo": { + "__this": { + "files": ["/dir/file.nix"], + "headType": "unspecified", + "nullable": False, + "prio": 100, # <- default prio + "total": False, + }, + "bar": { + "__this": { + "files": ["/dir/file.nix"], + "headType": "int", + "nullable": False, + "prio": 100, # <- default prio + "total": False, + } + }, + } + } + + res = transform_attribute_properties(introspection, all_data, persisted_data) + + breakpoint() + + # No operations allowed, because mkForce + # We cannot modify this value in ANY possible way. + # inventory.json definitions and children definition are filtered out by the module system + # assert attributes == {"operations": set(), "path": ["foo", "bar"]} + + # normal_prio_attrs: RawAttributes = { + # "files": ["/dir/file.nix"], + # "headType": "attrsOf", + # "nullable": False, + # "prio": 100, # <- default prio + # "total": False, + # } + + # attributes = transform_attribute_properties(("foo", "bar"), normal_prio_attrs) + + # # We can modify this value, because its a normal prio + # # This means keys can be added/removed/changed respecting their individual local constraints + # assert attributes == {"operations": { OpType.MODIFY }, "path": ["foo", "bar"]} + + # default_prio_attrs: RawAttributes = { + # "files": ["/dir/file.nix"], + # "headType": "attrsOf", + # "nullable": False, + # "prio": 100, # <- default prio + # "total": False, + # } + # attributes = transform_attribute_properties(("foo", "bar"), default_prio_attrs) + + # # We can modify this value, because its a normal prio + # # This means keys can be added/removed/changed respecting their individual local constraints + # assert attributes == {"operations": { OpType.MODIFY, OpType.DELETE }, "path": ["foo", "bar"]} + # Integration test @pytest.mark.with_core