Feature Specification: Module Authoring Primitives
Created: 2026-09-22
Status: Implemented
Input: Reconstructed from the code at 3c0765d, the nix-unit suite, the README, the comparison page, the commit and pull request history of nix-tf-schema (in particular the tofunix cherry-picks), and the Claude Code logs.
User Scenarios & Testing (mandatory)
User Story 1 - Assemble a Terraform module as JSON (Priority: P1)
A maintainer has provider requirements, variable names, and hand-written
resource, data and output blocks. The maintainer gets one attribute set
that builtins.toJSON turns into a main.tf.json.
Why this priority: it is the last step between the generated parts and a
module that tofu can run.
Independent Test: assemble a module with one provider, the variables
name and slug, one resource and one output. The JSON has
terraform.required_providers, a variable entry per name, the resource and
the output. Assemble it again with a data block; the JSON has it.
Acceptance Scenarios:
- Given a list of variable names, When the maintainer assembles the module, Then each name has an empty
variableentry; Given an attribute set of variables (for example from002-nested-resource-generation), Then it is used as it is. - Given no
dataor an emptydata, When the maintainer assembles the module, Then the JSON has nodatakey. - Given no
backend, When the maintainer assembles the module, Then the JSON has noterraform.backend; Given abackendsuch as{ http = { }; }, Then it isterraform.backend.
User Story 2 - Refer to other objects in hand-written blocks (Priority: P1)
A maintainer writes a data block, an output, or an extra resource
attribute that points at another object. The maintainer builds the reference
with a function chain (ref "type" "name" "attr", ref.var "x",
ref.data ...), not with a hand-written ${...} string, and a module type
accepts either a literal or a reference where the maintainer wants that.
Why this priority: hand-written reference strings are easy to get wrong and a typo is found only at plan time.
Independent Test: render ref "a" "b" "c", ref.data "a" "b" "c", a
splat "*", an integer index and a ref.count "index" index. The strings are
${a.b.c}, ${data.a.b.c}, ${a.b[*].c}, ${a.b[0].c} and
${a.b[count.index].c}.
Acceptance Scenarios:
- Given any prefix of a reference chain, When the maintainer renders it, Then it is a valid reference; there is no terminator.
- Given the reference type, When a value is a bare string without
${...}or a string with only part of a splice, Then the type rejects it. - Given a reference chain as the value of the reference type, When the module system merges it, Then the value is the rendered string, so
builtins.toJSONdoes not see an attribute set. - Given
refOr int, When the value is3or a reference, Then the type accepts both. - Given a reference, When the maintainer removes the splice, Then the result is the bare expression; a literal stays as it is.
User Story 3 - Set resource meta-arguments (Priority: P2)
A maintainer sets count, for_each, depends_on, provider or
lifecycle (with create_before_destroy, prevent_destroy,
ignore_changes, replace_triggered_by, precondition, postcondition) on a
resource. The maintainer gets typed options for them and a body fragment to
merge over the schema-derived resource body.
Why this priority: Terraform resolves these before it knows variables, so
they cannot go through the variable path of 002-nested-resource-generation.
Independent Test: render count = 2, depends_on = [ (ref "a" "b") ]
and lifecycle.prevent_destroy = true. The fragment is count = 2,
depends_on = [ "a.b" ] and lifecycle.prevent_destroy = true, and nothing
else.
Acceptance Scenarios:
- Given no meta-argument values, When the maintainer renders them, Then the fragment is empty.
- Given a reference in
depends_onorreplace_triggered_by, When the maintainer renders it, Then it has no${...}, as Terraform requires. - Given the meta-argument options, When the maintainer generates the variables of a resource, Then no meta-argument is a variable.
- Given the fragment and a schema-derived body, When the maintainer merges them, Then no key collides.
Edge Cases
- A schema-derived option does not accept a reference (constitution principle III). A reference placed in a variable value reaches the plan as a literal string.
- A reference is not checked against a schema. Terraform checks it at plan time.
countalso accepts a reference, so a count can come from a variable or another resource.
Requirements (mandatory)
Functional Requirements
- FR-001: The library MUST assemble a Terraform module with
terraform.required_providers,variable, optionaldata,resourceandoutput. - FR-002: The library MUST build reference expressions by function application, with the Terraform scopes
var,local,data,module,each,count,self,pathandterraformas attributes. - FR-003: A string segment MUST render as a dotted attribute; an integer, a nested reference or
"*"MUST render as a bracket index. - FR-004: The library MUST give a module type for reference expressions that merges to a string, and a helper that widens any type to also accept a reference.
- FR-005: The library MUST give options for all resource meta-arguments, each with a
nulldefault that means "do not emit". - FR-006: The library MUST render meta-argument values into a resource body fragment and drop unset values.
- FR-007: The module assembly MUST take the state backend as an optional parameter. When the caller does not give a backend, the module MUST NOT have
terraform.backend. The choice of a backend belongs to the consumer (constitution principles I and II).
Key Entities
- Reference: a Terraform expression spliced as
${...}, built as a chain that is both callable and renderable. - Module JSON: the attribute set that becomes
main.tf.json. - Meta-arguments: the resource arguments that Terraform handles, not the provider.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: The
mkTfModuleJson,ref,referenceType,refOr,unbraceandmetaArg*cases of thenix-unitsuite pass in CI.
Known Deviations
None.
Assumptions
- The module JSON uses the Terraform JSON syntax, where references are strings with
${...}anddepends_ontakes bare references.