Home

gen @main - refs - log -
-
https://git.jolheiser.com/gen.git
Generate Go flags and Nix module
gen / nix.go
- raw -
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"maps"
	"regexp"
	"slices"
	"strconv"
	"strings"
)

// genNix returns a generated Nix function of lib (and any imports) that returns the options for root
func genNix(root *structType, schema string) ([]byte, error) {
	imports := nixImports(root)
	slices.Sort(imports)
	args := append([]string{"lib"}, slices.Compact(imports)...)

	var w bytes.Buffer
	fmt.Fprintf(&w, `# Code generated by gen from %s; DO NOT EDIT.
{ %s }:
let
  inherit (lib) mkOption types;
in
`, schema, strings.Join(args, ", "))
	writeNixStruct(&w, root, "")
	w.WriteString("\n")

	return w.Bytes(), nil
}

// nixImports returns the names imported by every field under st, which may contain duplicates
func nixImports(st *structType) []string {
	var names []string
	for _, f := range st.fields {
		if f.nixSkip {
			continue
		}
		if f.nested != nil {
			names = append(names, nixImports(f.nested)...)
			continue
		}
		names = append(names, f.nixImports...)
	}
	return names
}

// inNix reports whether f is part of the Nix options.
// It's left out if it's skipped, or if it's a struct with nothing in it that isn't.
func inNix(f *field) bool {
	if f.nixSkip {
		return false
	}
	if f.nested == nil {
		return true
	}
	return slices.ContainsFunc(f.nested.fields, inNix)
}

// writeNixStruct writes st as an attribute set of options, where indent is the indent of the closing brace
func writeNixStruct(w *bytes.Buffer, st *structType, indent string) {
	w.WriteString("{\n")

	innerIndent := indent + "  "
	isRoot := indent == ""
	first := true
	for _, f := range st.fields {
		if !inNix(f) {
			continue
		}

		// A blank line goes between top-level entries, and before any nested attribute set
		if !first && (isRoot || f.nested != nil) {
			w.WriteString("\n")
		}
		first = false

		fmt.Fprintf(w, "%s%s = ", innerIndent, nixKey(f.label))
		if f.nested != nil {
			writeNixStruct(w, f.nested, innerIndent)
			w.WriteString(";\n")
			continue
		}
		writeNixOption(w, f, innerIndent)
	}

	w.WriteString(indent + "}")
}

// writeNixOption writes the mkOption for f, where indent is the indent of the closing brace
func writeNixOption(w *bytes.Buffer, f *field, indent string) {
	typ, note := nixFieldType(f)
	apply := nixFieldApply(f)
	if f.optional {
		typ = "types.nullOr " + paren(typ)
		if apply != "" {
			apply = "lib.mapNullable " + paren(apply)
		}
	}

	innerIndent := indent + "  "
	w.WriteString("mkOption {\n")
	if note != "" {
		fmt.Fprintf(w, "%s# %s\n", innerIndent, note)
	}
	fmt.Fprintf(w, "%stype = %s;\n", innerIndent, typ)
	if def, ok := nixFieldDefault(f); ok {
		fmt.Fprintf(w, "%sdefault = %s;\n", innerIndent, def)
	}
	if apply != "" {
		fmt.Fprintf(w, "%sapply = %s;\n", innerIndent, apply)
	}
	if f.doc != "" {
		fmt.Fprintf(w, "%sdescription = %s;\n", innerIndent, nixString(f.doc))
	}
	fmt.Fprintf(w, "%s};\n", indent)
}

// nixFieldDefault returns the default for f: @nix(default) if set, otherwise the schema default,
// otherwise null for an optional field
func nixFieldDefault(f *field) (string, bool) {
	switch {
	case f.hasNixDefault:
		return nixValue(f.nixDefault), true
	case f.hasDefault:
		return nixValue(f.defaultValue), true
	case f.optional:
		return "null", true
	default:
		return "", false
	}
}

// nixFieldType returns the Nix type of f, and a note to comment on the option if the type doesn't check everything.
// On a list, @nix(type) replaces the whole list type.
func nixFieldType(f *field) (typ, note string) {
	if !f.isList {
		return nixScalarType(f.scalar)
	}
	if f.nixListType != "" {
		return f.nixListType, ""
	}
	elemType, note := nixScalarType(f.scalar)
	return "types.listOf " + paren(elemType), note
}

// nixFieldApply returns the apply function for f.
// On a list, @nix(type) or @nix(apply) on the field replaces the element's apply, otherwise the element's is mapped over the list.
func nixFieldApply(f *field) string {
	if !f.isList {
		return f.scalar.nixApply
	}
	if f.nixListType != "" || f.nixListApply != "" {
		return f.nixListApply
	}
	if f.scalar.nixApply != "" {
		return "map " + paren(f.scalar.nixApply)
	}
	return ""
}

// nixScalarType returns the Nix type of s, and a note to comment on the option if the type doesn't check everything
func nixScalarType(s scalar) (typ, note string) {
	if s.nixType != "" {
		return s.nixType, ""
	}

	if len(s.enum) > 0 {
		values := make([]string, len(s.enum))
		for idx, e := range s.enum {
			values[idx] = nixValue(e)
		}
		return "types.enum [ " + strings.Join(values, " ") + " ]", ""
	}

	switch s.kind {
	case kindBool:
		return "types.bool", ""
	case kindInt:
		return nixIntType(s), ""
	case kindFloat:
		return withRangeCheck("types.float", s), ""
	case kindNumber:
		return withRangeCheck("types.number", s), ""
	default:
		return nixStringType(s)
	}
}

// nixIntType returns the Nix type of an int, using the built-in ranged types where possible
func nixIntType(s scalar) string {
	lower, upper := intBounds(s)
	switch {
	case lower != nil && upper != nil:
		return fmt.Sprintf("types.ints.between %s %s", nixInt(*lower), nixInt(*upper))
	case lower != nil && *lower == 0:
		return "types.ints.unsigned"
	case lower != nil && *lower == 1:
		return "types.ints.positive"
	default:
		return withRangeCheck("types.int", s)
	}
}

// nixStringType returns the Nix type of a string, and a note if its regexes can't all be checked in Nix
func nixStringType(s scalar) (typ, note string) {
	if len(s.regexes) == 0 {
		if s.notEmpty {
			return "types.nonEmptyStr", ""
		}
		return "types.str", ""
	}

	// types.strMatching only takes a single regex, so only the first is checked
	re, ok := ereFull(s.regexes[0].pattern)
	if !ok {
		return "types.str", fmt.Sprintf("%q can't be expressed as a POSIX regex; checked by the application only.", s.regexes[0].pattern)
	}
	if len(s.regexes) > 1 {
		note = "Only the first regex is checked here; all are checked by the application."
	}
	return "types.strMatching " + nixString(re), note
}

// withRangeCheck adds a check of the scalar's bounds to base, if it has any
func withRangeCheck(base string, s scalar) string {
	var conds []string
	if s.min != nil {
		op := ">"
		if s.min.inclusive {
			op = ">="
		}
		conds = append(conds, fmt.Sprintf("x %s %s", op, s.min.value))
	}
	if s.max != nil {
		op := "<"
		if s.max.inclusive {
			op = "<="
		}
		conds = append(conds, fmt.Sprintf("x %s %s", op, s.max.value))
	}

	if len(conds) == 0 {
		return base
	}
	return fmt.Sprintf("types.addCheck %s (x: %s)", base, strings.Join(conds, " && "))
}

// nixInt returns n as a Nix int, where negative numbers need parentheses to be a function argument
func nixInt(n int64) string {
	if n < 0 {
		return "(" + strconv.FormatInt(n, 10) + ")"
	}
	return strconv.FormatInt(n, 10)
}

// re2Only matches RE2 syntax that POSIX ERE doesn't have: class escapes like \d, groups with flags like (?i), and lazy quantifiers
var re2Only = regexp.MustCompile(`\\[dDwWsSbBpPQEAzZx]|\(\?|[*+?}]\?`)

// ereFull converts an RE2 regex, which matches anywhere in a string, to a POSIX ERE that must match the whole string, as types.strMatching expects.
// It returns false if the regex can't be converted.
func ereFull(re string) (string, bool) {
	if re2Only.MatchString(re) {
		return "", false
	}

	anchoredStart := strings.HasPrefix(re, "^")
	anchoredEnd := strings.HasSuffix(re, "$") && !strings.HasSuffix(re, `\$`)

	// With an alternation, an anchor only applies to one side, so it can't just be stripped
	if strings.Contains(re, "|") {
		if anchoredStart || anchoredEnd {
			return "", false
		}
		return ".*(" + re + ").*", true
	}

	if anchoredStart {
		re = strings.TrimPrefix(re, "^")
	} else {
		re = ".*" + re
	}
	if anchoredEnd {
		re = strings.TrimSuffix(re, "$")
	} else {
		re += ".*"
	}
	return re, true
}

// paren wraps a Nix expression in parentheses if it has spaces, so it can be used as a function argument
func paren(s string) string {
	alreadyWrapped := strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")")
	if strings.Contains(s, " ") && !alreadyWrapped {
		return "(" + s + ")"
	}
	return s
}

// nixIdent matches a valid Nix identifier
var nixIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_'-]*$`)

// nixReserved are bound by the generated file itself
var nixReserved = map[string]bool{
	"lib":      true,
	"mkOption": true,
	"types":    true,
}

var nixKeywords = map[string]bool{
	"assert":  true,
	"else":    true,
	"if":      true,
	"in":      true,
	"inherit": true,
	"let":     true,
	"or":      true,
	"rec":     true,
	"then":    true,
	"with":    true,
}

// nixKey returns k as an attribute name, quoting it if needed
func nixKey(k string) string {
	if nixIdent.MatchString(k) && !nixKeywords[k] {
		return k
	}
	return nixString(k)
}

var nixStringEscaper = strings.NewReplacer(
	`\`, `\\`,
	`"`, `\"`,
	"${", `\${`,
	"\n", `\n`,
	"\r", `\r`,
	"\t", `\t`,
)

// nixString returns s as a quoted Nix string
func nixString(s string) string {
	return `"` + nixStringEscaper.Replace(s) + `"`
}

// nixValue returns a decoded value as a Nix expression
func nixValue(v any) string {
	switch v := v.(type) {
	case nil:
		return "null"
	case string:
		return nixString(v)
	case bool:
		return strconv.FormatBool(v)
	case json.Number:
		return nixNumber(v)
	case []any:
		if len(v) == 0 {
			return "[ ]"
		}
		items := make([]string, len(v))
		for idx, item := range v {
			items[idx] = nixValue(item)
		}
		return "[ " + strings.Join(items, " ") + " ]"
	case map[string]any:
		var attrs strings.Builder
		attrs.WriteString("{ ")
		for _, key := range slices.Sorted(maps.Keys(v)) {
			fmt.Fprintf(&attrs, "%s = %s; ", nixKey(key), nixValue(v[key]))
		}
		attrs.WriteString("}")
		return attrs.String()
	default:
		return fmt.Sprint(v)
	}
}

// nixNumber returns n as a Nix number.
// Nix has no exponent notation, and a negative number needs parentheses to be a list item or function argument.
func nixNumber(n json.Number) string {
	s := n.String()
	if strings.ContainsAny(s, "eE") {
		f, _ := n.Float64()
		s = strconv.FormatFloat(f, 'f', -1, 64)
		// Keep it a float in Nix
		if !strings.Contains(s, ".") {
			s += ".0"
		}
	}
	if strings.HasPrefix(s, "-") {
		return "(" + s + ")"
	}
	return s
}