# schemas/lib/integration/cabling.ncl # # Cabling format: per-workspace, per-mode binding file that resolves # domain context fields to their concrete sources. # # Location: infra//integrations/.ncl # # Each entry in `bindings` maps a dotted domain field path # (e.g. "secret-delivery.registry_password") to a Resolver record. # # Resolver kinds (discriminated by the `kind` field): # "sops" — decrypt field from a SOPS-encrypted file # "component" — read field from a component's output record # "literal" — static hardcoded value # "env" — read from an environment variable at assembly time # # Usage: # "secret-delivery.registry_password" = { kind = "sops", path = "secrets/zot.sops.yaml", key = "ZOT_HTPASSWD" }, # "secret-delivery.registry_url" = { kind = "component", name = "zot", field = "registry_url" }, # "event-emission.subject_prefix" = { kind = "literal", value = "ws.libre-wuji.build.lian-build" }, # "compute.api_key" = { kind = "env", env_var = "HETZNER_API_KEY" }, let _valid_kinds = [| 'sops, 'component, 'literal, 'env |] in let _Resolver = std.contract.custom (fun label value => if !std.is_record value then std.contract.blame_with_message "Resolver must be a record with a 'kind' field" label else if !std.record.has_field "kind" value then std.contract.blame_with_message "Resolver missing required field 'kind'" label else match { "sops" => if std.record.has_field "path" value && std.record.has_field "key" value then 'Ok value else std.contract.blame_with_message "Resolver kind='sops' requires fields: path (String), key (String)" label, "component" => if std.record.has_field "name" value && std.record.has_field "field" value then 'Ok value else std.contract.blame_with_message "Resolver kind='component' requires fields: name (String), field (String)" label, "literal" => if std.record.has_field "value" value then 'Ok value else std.contract.blame_with_message "Resolver kind='literal' requires field: value (any)" label, "env" => if std.record.has_field "env_var" value then 'Ok value else std.contract.blame_with_message "Resolver kind='env' requires field: env_var (String)" label, _ => std.contract.blame_with_message "Unknown Resolver kind '%{value.kind}'. Valid: sops, component, literal, env" label, } value.kind ) in # Base shape for structural validation before cross-field checks. let _CablingBase = { mode_id | String | doc "Integration mode id — e.g. 'lian-build-provisioning'", workspace | String | doc "Workspace identifier — e.g. 'libre-wuji'", bindings | { _ | _Resolver } | doc "Map of '.' to a Resolver", } in # Full Cabling contract: structural + non-empty bindings. let _Cabling = std.contract.custom (fun label value => let validated = value | _CablingBase in if std.record.length validated.bindings == 0 then std.contract.blame_with_message "Cabling '%{validated.mode_id}': bindings must be non-empty" label else 'Ok validated ) in { Resolver = _Resolver, Cabling = _Cabling, }