docs: move generated markdown into a package
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{ inputs, self, ... }:
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
@@ -7,74 +7,7 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Simply evaluated options (JSON)
|
||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||
inherit (self) clanModules;
|
||||
clan-core = self;
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
# clan service options
|
||||
clanModulesViaService = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesViaService);
|
||||
|
||||
# Simply evaluated options (JSON)
|
||||
renderOptions =
|
||||
pkgs.runCommand "render-options"
|
||||
{
|
||||
# TODO: ruff does not splice properly in nativeBuildInputs
|
||||
depsBuildBuild = [ pkgs.ruff ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.mypy
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
install -D -m755 ${./render_options}/__init__.py $out/bin/render-options
|
||||
patchShebangs --build $out/bin/render-options
|
||||
|
||||
ruff format --check --diff $out/bin/render-options
|
||||
ruff check --line-length 88 $out/bin/render-options
|
||||
mypy --strict $out/bin/render-options
|
||||
'';
|
||||
|
||||
module-docs =
|
||||
pkgs.runCommand "rendered"
|
||||
{
|
||||
buildInputs = [
|
||||
pkgs.python3
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
export CLAN_CORE_PATH=${
|
||||
inputs.nixpkgs.lib.fileset.toSource {
|
||||
root = ../..;
|
||||
fileset = ../../clanModules;
|
||||
}
|
||||
}
|
||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
|
||||
# A file that contains the links to all clanModule docs
|
||||
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
|
||||
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
|
||||
export CLAN_OPTIONS_PATH=${self'.legacyPackages.clan-options}/share/doc/nixos/options.json
|
||||
|
||||
mkdir $out
|
||||
|
||||
# The python script will place mkDocs files in the output directory
|
||||
exec python3 ${renderOptions}/bin/render-options
|
||||
'';
|
||||
in
|
||||
{
|
||||
legacyPackages = {
|
||||
inherit
|
||||
jsonDocs
|
||||
clanModulesViaService
|
||||
;
|
||||
};
|
||||
devShells.docs = self'.packages.docs.overrideAttrs (_old: {
|
||||
nativeBuildInputs = [
|
||||
# Run: htmlproofer --disable-external
|
||||
@@ -96,12 +29,11 @@
|
||||
option-search
|
||||
inventory-api-docs
|
||||
clan-lib-openapi
|
||||
module-docs
|
||||
;
|
||||
inherit (inputs) nixpkgs;
|
||||
inherit module-docs;
|
||||
};
|
||||
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
|
||||
inherit module-docs;
|
||||
};
|
||||
checks.docs-integrity =
|
||||
pkgs.runCommand "docs-integrity"
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
nixosOptionsDoc,
|
||||
lib,
|
||||
pkgs,
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (clan-core.clanLib.docs) stripStorePathsFromDeclarations;
|
||||
transformOptions = stripStorePathsFromDeclarations;
|
||||
|
||||
nixosConfigurationWithClan =
|
||||
let
|
||||
evaled = lib.evalModules {
|
||||
class = "nixos";
|
||||
modules = [
|
||||
# Basemodule
|
||||
(
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = (import (pkgs.path + "/nixos/modules/module-list.nix"));
|
||||
nixpkgs.pkgs = pkgs;
|
||||
clan.core.name = "dummy";
|
||||
system.stateVersion = config.system.nixos.release;
|
||||
# Set this to work around a bug where `clan.core.settings.machine.name`
|
||||
# is forced due to `networking.interfaces` being forced
|
||||
# somewhere in the nixpkgs options
|
||||
facter.detected.dhcp.enable = lib.mkForce false;
|
||||
}
|
||||
)
|
||||
{
|
||||
clan.core.settings.directory = clan-core;
|
||||
}
|
||||
clan-core.nixosModules.clanCore
|
||||
];
|
||||
};
|
||||
in
|
||||
evaled;
|
||||
in
|
||||
{
|
||||
# Test with:
|
||||
# nix build .\#legacyPackages.x86_64-linux.clanModulesViaService
|
||||
clanModulesViaService = lib.mapAttrs (
|
||||
_moduleName: moduleValue:
|
||||
let
|
||||
evaluatedService = clan-core.clanLib.evalService {
|
||||
modules = [ moduleValue ];
|
||||
prefix = [ ];
|
||||
};
|
||||
in
|
||||
{
|
||||
roles = lib.mapAttrs (
|
||||
_roleName: role:
|
||||
(nixosOptionsDoc {
|
||||
transformOptions =
|
||||
opt:
|
||||
let
|
||||
# Apply store path stripping first
|
||||
transformed = transformOptions opt;
|
||||
in
|
||||
if lib.strings.hasPrefix "_" transformed.name then
|
||||
transformed // { visible = false; }
|
||||
else
|
||||
transformed;
|
||||
options = (lib.evalModules { modules = [ role.interface ]; }).options;
|
||||
warningsAreErrors = true;
|
||||
}).optionsJSON
|
||||
) evaluatedService.config.roles;
|
||||
manifest = evaluatedService.config.manifest;
|
||||
}
|
||||
) clan-core.clan.modules;
|
||||
|
||||
clanCore =
|
||||
(nixosOptionsDoc {
|
||||
options = nixosConfigurationWithClan.options.clan.core;
|
||||
warningsAreErrors = true;
|
||||
inherit transformOptions;
|
||||
}).optionsJSON;
|
||||
}
|
||||
@@ -1,623 +0,0 @@
|
||||
"""Module for rendering NixOS options documentation from JSON format."""
|
||||
|
||||
# Options are available in the following format:
|
||||
# https://github.com/nixos/nixpkgs/blob/master/nixos/lib/make-options-doc/default.nix
|
||||
#
|
||||
# ```json
|
||||
# {
|
||||
# ...
|
||||
# "fileSystems.<name>.options": {
|
||||
# "declarations": ["nixos/modules/tasks/filesystems.nix"],
|
||||
# "default": {
|
||||
# "_type": "literalExpression",
|
||||
# "text": "[\n \"defaults\"\n]"
|
||||
# },
|
||||
# "description": "Options used to mount the file system.",
|
||||
# "example": {
|
||||
# "_type": "literalExpression",
|
||||
# "text": "[\n \"data=journal\"\n]"
|
||||
# },
|
||||
# "loc": ["fileSystems", "<name>", "options"],
|
||||
# "readOnly": false,
|
||||
# "type": "non-empty (list of string (with check: non-empty))"
|
||||
# "relatedPackages": "- [`pkgs.tmux`](\n https://search.nixos.org/packages?show=tmux&sort=relevance&query=tmux\n )\n",
|
||||
# }
|
||||
# }
|
||||
# ```
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.services.modules import (
|
||||
CategoryInfo,
|
||||
ModuleManifest,
|
||||
)
|
||||
|
||||
# Get environment variables
|
||||
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])
|
||||
CLAN_CORE_DOCS = Path(os.environ["CLAN_CORE_DOCS"])
|
||||
CLAN_OPTIONS_PATH = Path(os.environ["CLAN_OPTIONS_PATH"])
|
||||
|
||||
# Options how to author clan.modules
|
||||
# perInstance, perMachine, ...
|
||||
CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
|
||||
|
||||
CLAN_MODULES_VIA_SERVICE = os.environ.get("CLAN_MODULES_VIA_SERVICE")
|
||||
|
||||
OUT = os.environ.get("out") # noqa: SIM112
|
||||
|
||||
|
||||
def sanitize(text: str) -> str:
|
||||
return text.replace(">", "\\>")
|
||||
|
||||
|
||||
def replace_git_url(text: str) -> tuple[str, str]:
|
||||
res = text
|
||||
name = Path(res).name
|
||||
if text.startswith("https://git.clan.lol/clan/clan-core/src/branch/main/"):
|
||||
name = str(Path(*Path(text).parts[7:]))
|
||||
return (res, name)
|
||||
|
||||
|
||||
def render_option_header(name: str) -> str:
|
||||
return f"# {name}\n"
|
||||
|
||||
|
||||
def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str:
|
||||
"""Joins multiple lines with a specified number of whitespace characters as indentation.
|
||||
|
||||
Args:
|
||||
lines (list of str): The lines of text to join.
|
||||
indent (int): The number of whitespace characters to use as indentation for each line.
|
||||
|
||||
Returns:
|
||||
str: The indented and concatenated string.
|
||||
|
||||
"""
|
||||
# Create the indentation string (e.g., four spaces)
|
||||
indent_str = " " * indent
|
||||
# Join each line with the indentation added at the beginning
|
||||
return "\n".join(indent_str + line for line in lines)
|
||||
|
||||
|
||||
def sanitize_anchor(text: str) -> str:
|
||||
parts = text.split(".")
|
||||
res = []
|
||||
for part in parts:
|
||||
if "<" in part:
|
||||
continue
|
||||
res.append(part)
|
||||
|
||||
return ".".join(res)
|
||||
|
||||
|
||||
def render_option(
|
||||
name: str,
|
||||
option: dict[str, Any],
|
||||
level: int = 3,
|
||||
short_head: str | None = None,
|
||||
) -> str:
|
||||
read_only = option.get("readOnly")
|
||||
|
||||
res = f"""
|
||||
{"#" * level} {sanitize(name) if short_head is None else sanitize(short_head)} {"{: #" + sanitize_anchor(name) + "}" if level > 1 else ""}
|
||||
|
||||
"""
|
||||
|
||||
res += f"""
|
||||
{f"**Attribute: `{name}`**" if short_head is not None else ""}
|
||||
|
||||
{"**Readonly**" if read_only else ""}
|
||||
|
||||
{option.get("description", "")}
|
||||
"""
|
||||
if option.get("type"):
|
||||
res += f"""
|
||||
|
||||
**Type**: `{option["type"]}`
|
||||
|
||||
"""
|
||||
if option.get("default"):
|
||||
res += f"""
|
||||
**Default**:
|
||||
|
||||
```nix
|
||||
{option.get("default", {}).get("text") if option.get("default") else "No default set."}
|
||||
```
|
||||
"""
|
||||
example = option.get("example", {}).get("text")
|
||||
if example:
|
||||
example_indented = join_lines_with_indentation(example.split("\n"))
|
||||
res += f"""
|
||||
|
||||
???+ example
|
||||
|
||||
```nix
|
||||
{example_indented}
|
||||
```
|
||||
"""
|
||||
if option.get("relatedPackages"):
|
||||
res += f"""
|
||||
### Related Packages
|
||||
|
||||
{option["relatedPackages"]}
|
||||
"""
|
||||
|
||||
decls = option.get("declarations", [])
|
||||
if decls:
|
||||
source_path, name = replace_git_url(decls[0])
|
||||
|
||||
name = name.split(",")[0]
|
||||
source_path = source_path.split(",")[0]
|
||||
|
||||
res += f"""
|
||||
:simple-git: Declared in: [{name}]({source_path})
|
||||
"""
|
||||
res += "\n\n"
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def print_options(
|
||||
options_file: str,
|
||||
head: str,
|
||||
no_options: str,
|
||||
replace_prefix: str | None = None,
|
||||
) -> str:
|
||||
res = ""
|
||||
with (Path(options_file) / "share/doc/nixos/options.json").open() as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
|
||||
res += head if len(options.items()) else no_options
|
||||
for option_name, info in options.items():
|
||||
if replace_prefix:
|
||||
display_name = option_name.replace(replace_prefix + ".", "")
|
||||
else:
|
||||
display_name = option_name
|
||||
|
||||
res += render_option(display_name, info, 4)
|
||||
return res
|
||||
|
||||
|
||||
def module_header(module_name: str) -> str:
|
||||
return f"# {module_name}\n\n"
|
||||
|
||||
|
||||
clan_core_descr = """
|
||||
`clan.core` is always present in a clan machine
|
||||
|
||||
* It is a module of class **`nixos`**
|
||||
* Provides a set of common options for every machine (in addition to the NixOS options)
|
||||
|
||||
Your can customize your machines behavior with the configuration [options](#module-options) provided below.
|
||||
"""
|
||||
|
||||
options_head = """
|
||||
### Module Options
|
||||
|
||||
The following options are available for this module.
|
||||
"""
|
||||
|
||||
|
||||
def produce_clan_core_docs() -> None:
|
||||
if not CLAN_CORE_DOCS:
|
||||
msg = f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not OUT:
|
||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
||||
raise ClanError(msg)
|
||||
|
||||
# A mapping of output file to content
|
||||
core_outputs: dict[str, str] = {}
|
||||
with CLAN_CORE_DOCS.open() as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
module_name = "clan.core"
|
||||
|
||||
transform = {n.replace("clan.core.", ""): v for n, v in options.items()}
|
||||
split = split_options_by_root(transform)
|
||||
|
||||
# Prepopulate the index file header
|
||||
indexfile = f"{module_name}/index.md"
|
||||
core_outputs[indexfile] = module_header(module_name) + clan_core_descr
|
||||
|
||||
core_outputs[indexfile] += """!!! info "Submodules"\n"""
|
||||
|
||||
for submodule_name, split_options in split.items():
|
||||
root = options_to_tree(split_options)
|
||||
module = root.suboptions[0]
|
||||
module_type = module.info.get("type")
|
||||
if module_type is not None and "submodule" not in module_type:
|
||||
continue
|
||||
core_outputs[indexfile] += (
|
||||
f" - [{submodule_name}](../../reference/clan.core/{submodule_name}.md)\n"
|
||||
)
|
||||
|
||||
core_outputs[indexfile] += options_head
|
||||
|
||||
for submodule_name, split_options in split.items():
|
||||
outfile = f"{module_name}/{submodule_name}.md"
|
||||
print(
|
||||
f"[clan_core.{submodule_name}] Rendering option of: {submodule_name}... {outfile}",
|
||||
)
|
||||
init_level = 1
|
||||
root = options_to_tree(split_options, debug=True)
|
||||
|
||||
print(f"Submodule {submodule_name} - suboptions", len(root.suboptions))
|
||||
|
||||
module = root.suboptions[0]
|
||||
print("type", module.info.get("type"))
|
||||
|
||||
module_type = module.info.get("type")
|
||||
if module_type is not None and "submodule" not in module_type:
|
||||
outfile = indexfile
|
||||
init_level = 2
|
||||
|
||||
output = ""
|
||||
for option in root.suboptions:
|
||||
output += options_docs_from_tree(
|
||||
option,
|
||||
init_level=init_level,
|
||||
prefix=["clan", "core"],
|
||||
)
|
||||
|
||||
# Append the content
|
||||
if outfile not in core_outputs:
|
||||
core_outputs[outfile] = output
|
||||
else:
|
||||
core_outputs[outfile] += output
|
||||
|
||||
for outfile, output in core_outputs.items():
|
||||
(Path(OUT) / "reference" / outfile).parent.mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
with (Path(OUT) / "reference" / outfile).open("w") as of:
|
||||
of.write(output)
|
||||
|
||||
|
||||
def render_categories(
|
||||
categories: list[str],
|
||||
categories_info: dict[str, CategoryInfo],
|
||||
) -> str:
|
||||
res = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
|
||||
for cat in categories:
|
||||
color = categories_info[cat]["color"]
|
||||
res += f"""
|
||||
<div style="background-color: {color}; color: white; padding: 10px; border-radius: 20px; text-align: center;">
|
||||
{cat}
|
||||
</div>
|
||||
"""
|
||||
res += "</div>"
|
||||
return res
|
||||
|
||||
|
||||
def produce_clan_service_docs() -> None:
|
||||
if not CLAN_MODULES_VIA_SERVICE:
|
||||
msg = f"Environment variables are not set correctly: $CLAN_MODULES_VIA_SERVICE={CLAN_MODULES_VIA_SERVICE}"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not CLAN_CORE_PATH:
|
||||
msg = f"Environment variables are not set correctly: $CLAN_CORE_PATH={CLAN_CORE_PATH}"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not OUT:
|
||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
||||
raise ClanError(msg)
|
||||
|
||||
with Path(CLAN_MODULES_VIA_SERVICE).open() as f3:
|
||||
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
|
||||
|
||||
for module_name, module_info in service_links.items():
|
||||
# Skip specific modules that are not ready for documentation
|
||||
if module_name in ["internet", "tor"]:
|
||||
continue
|
||||
|
||||
output = f"# {module_name}\n\n"
|
||||
# output += f"`clan.modules.{module_name}`\n"
|
||||
output += f"*{module_info['manifest']['description']}*\n"
|
||||
|
||||
# output += "## Categories\n\n"
|
||||
output += render_categories(
|
||||
module_info["manifest"]["categories"],
|
||||
ModuleManifest.categories_info(),
|
||||
)
|
||||
|
||||
output += f"{module_info['manifest']['readme']}\n"
|
||||
|
||||
output += "\n---\n\n## Roles\n"
|
||||
|
||||
output += f"The {module_name} module has the following roles:\n\n"
|
||||
|
||||
for role_name in module_info["roles"]:
|
||||
output += f"- {role_name}\n"
|
||||
|
||||
for role_name, role_filename in module_info["roles"].items():
|
||||
output += print_options(
|
||||
role_filename,
|
||||
f"## Options for the `{role_name}` role",
|
||||
"This role has no configuration",
|
||||
replace_prefix=f"clan.{module_name}",
|
||||
)
|
||||
|
||||
outfile = Path(OUT) / "services/official" / f"{module_name}.md"
|
||||
outfile.parent.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
with outfile.open("w") as of:
|
||||
of.write(output)
|
||||
|
||||
|
||||
def split_options_by_root(options: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
|
||||
{
|
||||
"a": { Data }
|
||||
"a.b": { Data }
|
||||
"c": { Data }
|
||||
}
|
||||
->
|
||||
{
|
||||
"a": {
|
||||
"a": { Data },
|
||||
"a.b": { Data }
|
||||
}
|
||||
"c": {
|
||||
"c": { Data }
|
||||
}
|
||||
}
|
||||
"""
|
||||
res: dict[str, dict[str, Any]] = {}
|
||||
for key, value in options.items():
|
||||
parts = key.split(".")
|
||||
root = parts[0]
|
||||
if root not in res:
|
||||
res[root] = {}
|
||||
res[root][key] = value
|
||||
return res
|
||||
|
||||
|
||||
def produce_clan_service_author_docs() -> None:
|
||||
if not CLAN_SERVICE_INTERFACE:
|
||||
msg = f"Environment variables are not set correctly: CLAN_SERVICE_INTERFACE={CLAN_SERVICE_INTERFACE}. Expected a path to the optionsJSON"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not OUT:
|
||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
||||
raise ClanError(msg)
|
||||
|
||||
output = """
|
||||
This document describes the structure and configurable attributes of a `clan.service` module.
|
||||
|
||||
Typically needed by module authors to define roles, behavior and metadata for distributed services.
|
||||
|
||||
!!! Note
|
||||
This is not a user-facing documentation, but rather meant as a reference for *module authors*
|
||||
|
||||
See: [clanService Authoring Guide](../../guides/services/community.md)
|
||||
"""
|
||||
# Inventory options are already included under the clan attribute
|
||||
# We just omitted them in the clan docs, because we want a separate output for the inventory model
|
||||
with Path(CLAN_SERVICE_INTERFACE).open() as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
|
||||
options_tree = options_to_tree(options, debug=True)
|
||||
# Find the inventory options
|
||||
|
||||
# Render the inventory options
|
||||
# This for loop excludes the root node
|
||||
# for option in options_tree.suboptions:
|
||||
output += options_docs_from_tree(options_tree, init_level=2)
|
||||
|
||||
outfile = Path(OUT) / "reference/options" / "clan_service.md"
|
||||
outfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Path.open(outfile, "w") as of:
|
||||
of.write(output)
|
||||
|
||||
|
||||
def produce_inventory_docs() -> None:
|
||||
if not CLAN_OPTIONS_PATH:
|
||||
msg = f"Environment variables are not set correctly: CLAN_OPTIONS_PATH={CLAN_OPTIONS_PATH}. Expected a path to the optionsJSON"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not OUT:
|
||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
||||
raise ClanError(msg)
|
||||
|
||||
output = """# Inventory Submodule
|
||||
This provides an overview of the available options of the `inventory` model.
|
||||
|
||||
It can be set via the `inventory` attribute of the [`clan`](../../reference/options/clan_inventory.md) function, or via the [`clan.inventory`](../../reference/options/clan_inventory.md) attribute of flake-parts.
|
||||
|
||||
"""
|
||||
# Inventory options are already included under the clan attribute
|
||||
# We just omitted them in the clan docs, because we want a separate output for the inventory model
|
||||
with Path(CLAN_OPTIONS_PATH).open() as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
|
||||
clan_root_option = options_to_tree(options)
|
||||
# Find the inventory options
|
||||
inventory_opt: None | Option = None
|
||||
for opt in clan_root_option.suboptions:
|
||||
if opt.name == "inventory":
|
||||
inventory_opt = opt
|
||||
break
|
||||
|
||||
if not inventory_opt:
|
||||
print("No inventory options found.")
|
||||
sys.exit(1)
|
||||
# Render the inventory options
|
||||
# This for loop excludes the root node
|
||||
for option in inventory_opt.suboptions:
|
||||
output += options_docs_from_tree(option, init_level=2)
|
||||
|
||||
outfile = Path(OUT) / "reference/options" / "clan_inventory.md"
|
||||
outfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Path.open(outfile, "w") as of:
|
||||
of.write(output)
|
||||
|
||||
|
||||
def produce_clan_options_docs() -> None:
|
||||
if not CLAN_OPTIONS_PATH:
|
||||
msg = f"Environment variables are not set correctly: CLAN_OPTIONS_PATH={CLAN_OPTIONS_PATH}. Expected a path to the optionsJSON"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not OUT:
|
||||
msg = f"Environment variables are not set correctly: $out={OUT}"
|
||||
raise ClanError(msg)
|
||||
|
||||
output = """# Clan Options
|
||||
This provides an overview of the available options
|
||||
|
||||
Those can be set via [`clan-core.lib.clan`](../../reference/options/clan.md) function,
|
||||
or via the [`clan`](../../reference/options/clan.md) attribute of flake-parts.
|
||||
|
||||
"""
|
||||
# Inventory options are already included under the clan attribute
|
||||
# We just omitted them in the clan docs, because we want a separate output for the inventory model
|
||||
with Path(CLAN_OPTIONS_PATH).open() as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
|
||||
clan_root_option = options_to_tree(options)
|
||||
# Render the inventory options
|
||||
# This for loop excludes the root node
|
||||
# Exclude inventory options
|
||||
for option in clan_root_option.suboptions:
|
||||
if "inventory" in option.name:
|
||||
output += """## Inventory
|
||||
|
||||
Attribute: `inventory`
|
||||
|
||||
See: [Inventory Submodule](../../reference/options/clan_inventory.md)
|
||||
"""
|
||||
continue
|
||||
output += options_docs_from_tree(option, init_level=2)
|
||||
|
||||
outfile = Path(OUT) / "reference/options" / "clan.md"
|
||||
outfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Path.open(outfile, "w") as of:
|
||||
of.write(output)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Option:
|
||||
name: str
|
||||
path: list[str]
|
||||
info: dict[str, Any]
|
||||
suboptions: list["Option"] = field(default_factory=list)
|
||||
|
||||
|
||||
def option_short_name(option_name: str) -> str:
|
||||
parts = option_name.split(".")
|
||||
short_name = ""
|
||||
for part in parts[1:]:
|
||||
if "<" in part:
|
||||
continue
|
||||
short_name += ("." + part) if short_name else part
|
||||
return short_name
|
||||
|
||||
|
||||
def options_to_tree(options: dict[str, Any], debug: bool = False) -> Option:
|
||||
"""Convert the options dictionary to a tree structure."""
|
||||
|
||||
# Helper function to create nested structure
|
||||
def add_to_tree(path_parts: list[str], info: Any, current_node: Option) -> None:
|
||||
if not path_parts:
|
||||
return
|
||||
|
||||
name = path_parts[0]
|
||||
remaining_path = path_parts[1:]
|
||||
|
||||
# Look for an existing suboption
|
||||
for sub in current_node.suboptions:
|
||||
if sub.name == name:
|
||||
add_to_tree(remaining_path, info, sub)
|
||||
return
|
||||
|
||||
# If no existing suboption is found, create a new one
|
||||
new_option = Option(
|
||||
name=name,
|
||||
path=[*current_node.path, name],
|
||||
info={}, # Populate info only at the final leaf
|
||||
)
|
||||
current_node.suboptions.append(new_option)
|
||||
|
||||
# If it's a leaf node, populate info
|
||||
if not remaining_path:
|
||||
new_option.info = info
|
||||
else:
|
||||
add_to_tree(remaining_path, info, new_option)
|
||||
|
||||
# Create the root option
|
||||
root = Option(name="<root>", path=[], info={})
|
||||
|
||||
# Process each key-value pair in the dictionary
|
||||
for key, value in options.items():
|
||||
path_parts = key.split(".")
|
||||
add_to_tree(path_parts, value, root)
|
||||
|
||||
def print_tree(option: Option, level: int = 0) -> None:
|
||||
print(" " * level + option.name + ":", option.path)
|
||||
for sub in option.suboptions:
|
||||
print_tree(sub, level + 1)
|
||||
|
||||
# Example usage
|
||||
if debug:
|
||||
print("Options tree:")
|
||||
print_tree(root)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def options_docs_from_tree(
|
||||
root: Option,
|
||||
init_level: int = 1,
|
||||
prefix: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Eender the options from the tree structure.
|
||||
|
||||
Args:
|
||||
root (Option): The root option node.
|
||||
init_level (int): The initial level of indentation.
|
||||
prefix (list str): Will be printed as common prefix of all attribute names.
|
||||
|
||||
"""
|
||||
|
||||
def render_tree(option: Option, level: int = init_level) -> str:
|
||||
output = ""
|
||||
|
||||
should_render = not option.name.startswith("<") and not option.name.startswith(
|
||||
"_",
|
||||
)
|
||||
if should_render:
|
||||
# short_name = option_short_name(option.name)
|
||||
path = ".".join(prefix + option.path) if prefix else ".".join(option.path)
|
||||
output += render_option(
|
||||
path,
|
||||
option.info,
|
||||
level=level,
|
||||
short_head=option.name,
|
||||
)
|
||||
|
||||
for sub in option.suboptions:
|
||||
h_increment = 1 if should_render else 0
|
||||
|
||||
if "_module" in sub.path:
|
||||
continue
|
||||
output += render_tree(sub, level + h_increment)
|
||||
|
||||
return output
|
||||
|
||||
return render_tree(root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
produce_clan_core_docs()
|
||||
produce_inventory_docs()
|
||||
produce_clan_options_docs()
|
||||
produce_clan_service_author_docs()
|
||||
produce_clan_service_docs()
|
||||
Reference in New Issue
Block a user