Skip to content

Commit 00cbf64

Browse files
feat: update the options generation to be more general
1 parent d8d2579 commit 00cbf64

File tree

2 files changed

+264
-324
lines changed

2 files changed

+264
-324
lines changed

scripts/generate-options.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import sys
5+
import urllib.request
6+
7+
# Get schema URL from argument or default
8+
schema_url = sys.argv[1] if len(sys.argv) > 1 else "https://charm.land/crush.json"
9+
10+
try:
11+
with urllib.request.urlopen(schema_url, timeout=10) as response:
12+
schema = json.loads(response.read().decode())
13+
except Exception as e:
14+
print(f"Error fetching schema: {e}", file=sys.stderr)
15+
sys.exit(1)
16+
17+
18+
def nix_type(json_type):
19+
"""Convert JSON schema type to Nix type."""
20+
if json_type == "string":
21+
return "lib.types.str"
22+
elif json_type == "number":
23+
return "lib.types.number"
24+
elif json_type == "integer":
25+
return "lib.types.int"
26+
elif json_type == "boolean":
27+
return "lib.types.bool"
28+
elif json_type == "array":
29+
return "lib.types.listOf lib.types.str"
30+
elif json_type == "object":
31+
return "lib.types.attrsOf lib.types.anything"
32+
else:
33+
return "lib.types.anything"
34+
35+
36+
def nix_default(value):
37+
"""Convert a default value to Nix syntax."""
38+
if value is None:
39+
return "null"
40+
elif isinstance(value, bool):
41+
return "true" if value else "false"
42+
elif isinstance(value, (int, float)):
43+
return str(value)
44+
elif isinstance(value, str):
45+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
46+
return f'"{escaped}"'
47+
elif isinstance(value, list):
48+
return "[]"
49+
elif isinstance(value, dict):
50+
return "{}"
51+
else:
52+
return str(value)
53+
54+
55+
def escape_description(desc):
56+
"""Escape description for Nix."""
57+
if not desc:
58+
return ""
59+
return desc.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
60+
61+
62+
def resolve_ref(ref):
63+
"""Resolve $ref to actual schema."""
64+
if not ref.startswith("#/$defs/"):
65+
return {}
66+
def_name = ref.split("/")[-1]
67+
return schema.get("$defs", {}).get(def_name, {})
68+
69+
70+
def is_valid_nix_identifier(name):
71+
"""Check if name is a valid Nix identifier."""
72+
if not name:
73+
return False
74+
if name[0].isdigit() or name[0] in ("$",):
75+
return False
76+
return all(c.isalnum() or c in ("_", "-") for c in name)
77+
78+
79+
def generate_options(properties, indent=" "):
80+
"""Generate Nix option declarations from schema properties."""
81+
if not properties:
82+
return ""
83+
84+
lines = []
85+
for name, schema_prop in properties.items():
86+
# Skip invalid Nix identifiers like $schema
87+
if not is_valid_nix_identifier(name):
88+
continue
89+
90+
# Resolve $ref if present
91+
if "$ref" in schema_prop:
92+
schema_prop = resolve_ref(schema_prop["$ref"])
93+
94+
desc = schema_prop.get("description", "")
95+
default = schema_prop.get("default")
96+
json_type = schema_prop.get("type", "string")
97+
98+
# Handle enums
99+
if "enum" in schema_prop and schema_prop["enum"]:
100+
lines.append(f"{indent}{name} = lib.mkOption {{")
101+
lines.append(f"{indent} type = lib.types.enum [")
102+
for val in schema_prop["enum"]:
103+
lines.append(f'{indent} "{val}"')
104+
lines.append(f"{indent} ];")
105+
if default is not None:
106+
lines.append(f'{indent} default = "{default}";')
107+
if desc:
108+
lines.append(f'{indent} description = "{escape_description(desc)}";')
109+
lines.append(f"{indent}}};")
110+
111+
# Handle nested objects with properties
112+
elif json_type == "object" and "properties" in schema_prop:
113+
lines.append(f"{indent}{name} = lib.mkOption {{")
114+
lines.append(f"{indent} type = lib.types.submodule {{")
115+
lines.append(f"{indent} options = {{")
116+
lines.append(generate_options(schema_prop["properties"], indent + " "))
117+
lines.append(f"{indent} }};")
118+
lines.append(f"{indent} }};")
119+
if default is not None:
120+
lines.append(f"{indent} default = {{}};")
121+
if desc:
122+
lines.append(f'{indent} description = "{escape_description(desc)}";')
123+
lines.append(f"{indent}}};")
124+
125+
# Handle arrays of objects
126+
elif json_type == "array" and "items" in schema_prop:
127+
items = schema_prop["items"]
128+
if "$ref" in items:
129+
items = resolve_ref(items["$ref"])
130+
131+
if (
132+
isinstance(items, dict)
133+
and items.get("type") == "object"
134+
and "properties" in items
135+
):
136+
lines.append(f"{indent}{name} = lib.mkOption {{")
137+
lines.append(
138+
f"{indent} type = lib.types.listOf (lib.types.submodule {{"
139+
)
140+
lines.append(f"{indent} options = {{")
141+
lines.append(generate_options(items["properties"], indent + " "))
142+
lines.append(f"{indent} }};")
143+
lines.append(f"{indent} }});")
144+
if default is not None:
145+
lines.append(f"{indent} default = [];")
146+
if desc:
147+
lines.append(
148+
f'{indent} description = "{escape_description(desc)}";'
149+
)
150+
lines.append(f"{indent}}};")
151+
else:
152+
# Simple array
153+
lines.append(f"{indent}{name} = lib.mkOption {{")
154+
lines.append(f"{indent} type = lib.types.listOf lib.types.str;")
155+
if default is not None:
156+
lines.append(f"{indent} default = [];")
157+
if desc:
158+
lines.append(
159+
f'{indent} description = "{escape_description(desc)}";'
160+
)
161+
lines.append(f"{indent}}};")
162+
163+
# Handle additionalProperties (attrs of objects)
164+
elif json_type == "object" and "additionalProperties" in schema_prop:
165+
additional_props = schema_prop["additionalProperties"]
166+
if "$ref" in additional_props:
167+
additional_props = resolve_ref(additional_props["$ref"])
168+
169+
if (
170+
isinstance(additional_props, dict)
171+
and additional_props.get("type") == "object"
172+
):
173+
if "properties" in additional_props:
174+
lines.append(f"{indent}{name} = lib.mkOption {{")
175+
lines.append(
176+
f"{indent} type = lib.types.attrsOf (lib.types.submodule {{"
177+
)
178+
lines.append(f"{indent} options = {{")
179+
lines.append(
180+
generate_options(
181+
additional_props["properties"], indent + " "
182+
)
183+
)
184+
lines.append(f"{indent} }};")
185+
lines.append(f"{indent} }});")
186+
if default is not None:
187+
lines.append(f"{indent} default = {{}};")
188+
if desc:
189+
lines.append(
190+
f'{indent} description = "{escape_description(desc)}";'
191+
)
192+
lines.append(f"{indent}}};")
193+
else:
194+
# Simple attrsOf
195+
lines.append(f"{indent}{name} = lib.mkOption {{")
196+
lines.append(
197+
f"{indent} type = lib.types.attrsOf lib.types.anything;"
198+
)
199+
if default is not None:
200+
lines.append(f"{indent} default = {{}};")
201+
if desc:
202+
lines.append(
203+
f'{indent} description = "{escape_description(desc)}";'
204+
)
205+
lines.append(f"{indent}}};")
206+
else:
207+
# Simple attrsOf
208+
lines.append(f"{indent}{name} = lib.mkOption {{")
209+
lines.append(f"{indent} type = lib.types.attrsOf lib.types.anything;")
210+
if default is not None:
211+
lines.append(f"{indent} default = {{}};")
212+
if desc:
213+
lines.append(
214+
f'{indent} description = "{escape_description(desc)}";'
215+
)
216+
lines.append(f"{indent}}};")
217+
218+
# Simple types
219+
else:
220+
lines.append(f"{indent}{name} = lib.mkOption {{")
221+
lines.append(f"{indent} type = {nix_type(json_type)};")
222+
if default is not None:
223+
lines.append(f"{indent} default = {nix_default(default)};")
224+
if desc:
225+
lines.append(f'{indent} description = "{escape_description(desc)}";')
226+
lines.append(f"{indent}}};")
227+
228+
return "\n".join(lines)
229+
230+
231+
# Generate the Nix module
232+
output = []
233+
output.append("{lib}:")
234+
output.append("lib.mkOption {")
235+
output.append(" type = lib.types.submodule {")
236+
output.append(" options = {")
237+
238+
# Get the root Config definition
239+
if "$ref" in schema:
240+
root_def = resolve_ref(schema["$ref"])
241+
else:
242+
root_def = schema
243+
244+
if "properties" in root_def:
245+
output.append(generate_options(root_def["properties"], " "))
246+
247+
output.append(" };")
248+
output.append(" };")
249+
output.append(" default = {};")
250+
output.append("}")
251+
252+
print("\n".join(output))

0 commit comments

Comments
 (0)