clan config: match dynamic options containing <name>
This commit is contained in:
parent
765f982d11
commit
ec70b34470
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user