Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save sini/775d6c7fdf4d50721da33cdcf4bcf947 to your computer and use it in GitHub Desktop.

Select an option

Save sini/775d6c7fdf4d50721da33cdcf4bcf947 to your computer and use it in GitHub Desktop.
gen-schema: listOf ref element-level coercion

gen-schema: listOf ref element-level coercion

Problem

schemaLib.ref "kind" supports string-to-instance coercion for scalar fields. Assigning host = "igloo" on a ref "host" field resolves to config.hosts.igloo via an apply function injected by mkRefBindingModules.

This breaks for listOf (ref "kind"). The apply function receives the entire list, checks builtins.isString val, gets false (it's a list), and passes it through unchanged. Individual string elements never get coerced. Same gap exists for nullOr (ref "kind") in the deferred path (the direct-mode nullOr test uses nullOr (ref instances), not nullOr (ref "kind")).

Consumers hit this in practice: nest-traits declares needs = listOf (ref "trait") and wants needs = [ "nginx" "firewall" ] to resolve each string to a trait instance.

Current flow

schema kind declares:  options.host = mkOption { type = ref "host"; };
                        options.needs = mkOption { type = listOf (ref "trait"); };

findRefFields scans evaluated options via refsFromOptions
  -> getRefKind traverses nestedTypes.elemType to find refKind
  -> returns { host = "host"; needs = "trait"; }   # correct detection

mkRefBindingModules builds per-field apply:
  apply = val:
    if builtins.isString val then registry.${val}   # works for scalar
    else val;                                        # list falls through!

getRefKind already traverses listOf/nullOr wrappers correctly. The only gap is the apply function in mkRefBindingModules assuming scalar shape.

Fix

Make the apply function polymorphic over value shape. Extract a coerce helper and dispatch on the value's runtime type:

# In mkRefBindingModules, replace the apply lambda:
apply =
  val:
  let
    coerce = v:
      if builtins.isString v then
        if registry ? ${v} then
          registry.${v}
        else
          throw "ref field '${field}' on kind '${kind}': reference '${v}' not found in instance registry (available: ${builtins.concatStringsSep ", " (builtins.attrNames registry)})"
      else
        v;
  in
  if builtins.isList val then map coerce val
  else if val == null then null
  else coerce val;

This works because:

  • apply runs post-merge, after the module system has type-checked the structure
  • For ref "kind" (bare): val is string or attrset, coerce handles both
  • For listOf (ref "kind"): val is a list, map coerce handles each element
  • For nullOr (ref "kind"): val is null or string/attrset, both branches handled
  • For nested wrappers like nullOr (listOf (ref "kind")): null passes through, list gets mapped
  • Instance values (attrsets) pass through coerce unchanged

No changes needed to getRefKind, refsFromOptions, or findRefFields. The type detection already works; only the coercion apply was too narrow.

Error message improvement

Current error doesn't list available keys. The fix adds (available: ...) to the error message, matching the pattern in mkCoercingRefType's error. This helps users diagnose typos.

Files changed

  • nix/lib/instance.nixmkRefBindingModules apply function: ~10 lines changed

Test plan

New test file templates/ci/tests/ref-listof.nix:

  1. test-listof-ref-string-coercionlistOf (ref "kind") with string elements resolves each to instance
  2. test-listof-ref-mixed-coercion — mix of string keys and direct instance values in same list
  3. test-listof-ref-empty — empty list passes through
  4. test-nullor-deferred-ref-nullnullOr (ref "kind") deferred with null value
  5. test-nullor-deferred-ref-stringnullOr (ref "kind") deferred with string key

Existing tests must continue passing (scalar ref, nullable direct-mode ref, instance coercion, etc).

Scope

gen-schema only. No consumer changes. nest-traits can switch needs from listOf raw to listOf (ref "trait") after this lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment