clan config: match dynamic options containing <name>
All checks were successful
checks-impure / test (pull_request) Successful in 8s
checks / test (pull_request) Successful in 23s

This commit is contained in:
DavHau 2023-09-24 13:04:18 +01:00
parent 765f982d11
commit ec70b34470
3 changed files with 91 additions and 18 deletions

View File

@ -25,7 +25,9 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
subparsers = parser.add_subparsers()
parser_create = subparsers.add_parser("create", help="create a clan flake inside the current directory")
parser_create = subparsers.add_parser(
"create", help="create a clan flake inside the current directory"
)
create.register_parser(parser_create)
parser_config = subparsers.add_parser("config", help="set nixos configuration")

View File

@ -2,11 +2,12 @@
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Any, Optional, Type
from typing import Any, Optional, Tuple, Type
from clan_cli.dirs import get_clan_flake_toplevel
from clan_cli.errors import ClanError
@ -181,6 +182,57 @@ def get_or_set_option(args: argparse.Namespace) -> None:
print(new_value)
def find_option(
option: str, value: Any, options: dict, option_description: Optional[str] = None
) -> Tuple[str, Any]:
"""
The option path specified by the user doesn't have to match exactly to an
entry in the options.json file. Examples
Example 1:
$ clan config services.openssh.settings.SomeSetting 42
This is a freeform option that does not appear in the options.json
The actual option is `services.openssh.settings`
And the value must be wrapped: {"SomeSettings": 42}
Example 2:
$ clan config users.users.my-user.name my-name
The actual option is `users.users.<name>.name`
"""
# option description is used for error messages
if option_description is None:
option_description = option
option_path = option.split(".")
# fuzzy search the option paths, so when
# specified option path: "foo.bar.baz.bum"
# available option path: "foo.<name>.baz.<name>"
# we can still find the option
first = option_path[0]
regex = rf"({first}|<name>)"
for elem in option_path[1:]:
regex += rf"\.({elem}|<name>)"
for opt in options.keys():
if re.match(regex, opt):
return opt, value
# if the regex search did not find the option, start stripping the last
# element of the option path and find matching parent option
# (see examples above for why this is needed)
if len(option_path) == 1:
raise ClanError(f"Option {option_description} not found")
option_path_parent = option_path[:-1]
attr_prefix = option_path[-1]
return find_option(
option=".".join(option_path_parent),
value={attr_prefix: value},
options=options,
option_description=option_description,
)
def set_option(
option: str,
value: Any,
@ -189,24 +241,14 @@ def set_option(
option_description: str = "",
show_trace: bool = False,
) -> None:
option, value = find_option(
option=option,
value=value,
options=options,
option_description=option_description,
)
option_path = option.split(".")
# if the option cannot be found, then likely the type is attrs and we need to
# find the parent option.
if option not in options:
if len(option_path) == 1:
raise ClanError(f"Option {option_description} not found")
option_parent = option_path[:-1]
attr = option_path[-1]
return set_option(
option=".".join(option_parent),
value={attr: value},
options=options,
settings_file=settings_file,
option_description=option,
show_trace=show_trace,
)
target_type = map_type(options[option]["type"])
casted = cast(value, target_type, option)

View File

@ -177,3 +177,32 @@ def test_type_from_schema_path_dynamic_attrs() -> None:
# test the cast function with simple types
def test_cast_simple() -> None:
assert config.cast(["true"], bool, "foo-option") is True
@pytest.mark.parametrize(
"option,value,options,expected",
[
("foo.bar", ["baz"], {"foo.bar": {"type": "str"}}, ("foo.bar", ["baz"])),
("foo.bar", ["baz"], {"foo": {"type": "attrs"}}, ("foo", {"bar": ["baz"]})),
(
"users.users.my-user.name",
["my-name"],
{"users.users.<name>.name": {"type": "str"}},
("users.users.<name>.name", ["my-name"]),
),
(
"foo.bar.baz.bum",
["val"],
{"foo.<name>.baz": {"type": "attrs"}},
("foo.<name>.baz", {"bum": ["val"]}),
),
(
"userIds.DavHau",
["42"],
{"userIds": {"type": "attrs"}},
("userIds", {"DavHau": ["42"]}),
),
],
)
def test_find_option(option: str, value: list, options: dict, expected: tuple) -> None:
assert config.find_option(option, value, options) == expected