Feature Specification: Nested Resource Generation
Created: 2026-09-22
Status: Implemented
Input: Reconstructed from the code at 3c0765d, the nix-unit suite, the commit and pull request history of nix-tf-schema, and the Claude Code logs.
User Scenarios & Testing (mandatory)
User Story 1 - Type a resource with nested blocks (Priority: P1)
A maintainer has a resource schema with nested objects, in the
terraform-plugin-sdk/v2 dialect (block_types), the terraform-plugin-framework
dialect (nested_type), or both. The maintainer gets one Nix option per
settable top-level attribute or block, with nested submodules.
Why this priority: most real resources have nested blocks. The flat
helpers of 001-flat-schema-conversion cannot type them.
Independent Test: convert an sdk/v2 resource with a plural block and a
singleton block (max_items = 1), and a framework resource with a single
and a list nested attribute. Cast a configuration against the options. The
plural block and the framework list take a list of objects; the singleton and
the framework single take one object.
Acceptance Scenarios:
- Given a block with
nestingsingleorgroup, When the maintainer converts it, Then the type is asubmodule. - Given a block with
nestinglistorset, When the maintainer converts it, Then the type islistOfa submodule; withmap, Then it isattrsOfa submodule. - Given an sdk/v2 block with
max_items = 1, When the maintainer converts it, Then the type is one submodule, not a list. - Given a block that is not required, When the maintainer converts it, Then the option is nullable with
default = null. - Given an attribute that is computed and not optional, at any depth, When the maintainer converts the resource, Then it is absent.
User Story 2 - Generate the variables and the resource body (Priority: P1)
A maintainer generates the Terraform module for a resource: one variable
per top-level node, and a resource body that reads each variable. Values come
in at run time (for example as TF_VAR_*).
Why this priority: Terraform cannot fill an sdk/v2 block from a variable
directly. Without generated dynamic blocks, the maintainer must write them
by hand for each resource.
Independent Test: apply a generated module with the hashicorp/tls
provider and a plural block, a singleton block, and an unset block, with the
values in TF_VAR_*. The plural value reaches the resource as a list, the
singleton as a one-element list, and the unset block is absent.
Acceptance Scenarios:
- Given a plain attribute, When the maintainer generates the body, Then the value is
${var.<name>}, and the variable has the HCLtypeconstraint of the attribute (dynamicasany), so that Terraform reads a collection in theTF_VAR_*string as the collection. - Given an sdk/v2 block, When the maintainer generates the body, Then the body has a
dynamicblock that iterates the variable, an empty list when the variable isnull, and a one-element list for a singleton. - Given a
mapblock, When the maintainer generates the body, Then thedynamicblock haslabelsfrom the map key. - Given a framework nested attribute, When the maintainer generates the body, Then the value is assigned directly as
${var.<name>}, without adynamicblock. - Given a nested block, When the maintainer generates the variables, Then the variable has an HCL
typeconstraint, withoptional(...)for the optional attributes of objects, so that Terraform reads theTF_VAR_*string as an object. - Given a schema without nested blocks, When the maintainer generates variables and body, Then the result is the same as from the flat helpers of
001-flat-schema-conversion.
User Story 3 - Read the settable surface as a tree or a flat list (Priority: P2)
A consumer (for example a form generator or a diff tool) wants the settable surface of a resource without the Nix types. It gets a tree of nodes, or a flat map keyed by dotted path.
Why this priority: other renderers than the module system can use the same walk.
Independent Test: walk an sdk/v2 resource with a singleton block
smtp_server with a required host. The flat map has smtp_server (a block
with singleBlock = true) and smtp_server.host (an attribute).
Acceptance Scenarios:
- Given any node, When a consumer reads it, Then it has the keys
path,kind,tfType,required,optional,sensitive,description,nesting,singleBlockandblockSyntax, in both dialects. - Given a sensitive attribute nested in a block, When a consumer reads the flat map, Then the attribute is present with
sensitive = true.
Edge Cases
- A schema
descriptionthat isnull(Terraform emits it for blocks without documentation) becomes the empty string. - A
setblock is typed as a list. The order is not normalized and duplicates are not rejected. - An sdk/v2 resource and a framework attribute in one resource (for example
incus_instance) use the same walk. - There is no way to say "explicitly
null" as different from "unset". The README records this as a known limitation that does not matter for the generated output. - A failure in the walk names the path in the schema through
[nix-tf-schema]error contexts (constitution principle V). On success the contexts do not change the output.
Requirements (mandatory)
Functional Requirements
- FR-001: The library MUST walk one resource schema into a tree of settable nodes that covers both schema dialects with one node shape.
- FR-002: The walk MUST drop attributes that are computed and not optional, at every depth.
- FR-003: The library MUST convert the tree to Nix options, typed per nesting mode as in User Story 1.
- FR-004: The library MUST generate one Terraform
variableper top-level node, withdefault = nullfor a node that is not required, and with atypefrom the schema for every node, so that a value that arrives as a string at run time (for example throughTF_VAR_*) gets its Terraform type. - FR-005: The library MUST render HCL type constraints for all Terraform types, including
objectwithoptional(...),tuple, anddynamicasany. - FR-006: The library MUST generate a resource body that reads each variable, with
dynamicblocks for sdk/v2 blocks and direct assignment for framework nested attributes. - FR-007: An unset optional block MUST render as no block.
- FR-008: The library MUST give the settable surface as a flat map keyed by dotted path.
- FR-009: Every recursion step MUST carry a
[nix-tf-schema]error context that names the path.
Key Entities
- Node: one settable attribute or block of a resource, with
path,kind(attrorblock),tfType, the flags,nesting,singleBlock(sdk/v2 singleton) andblockSyntax(sdk/v2 block, which needs adynamicblock); a block haschildren. - Resource body: the attribute set under
resource.<type>.<name>in Terraform JSON.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: The
settableTree,settablePaths,nodeTo*,resourceTo*andtfTypeConstraintcases of thenix-unitsuite pass in CI, including the case that applies a generated module withtofu. - SC-002: For a flat schema, the nested generators give output equal to the flat helpers (tested).
Known Deviations
None.
Assumptions
- Values reach Terraform as variables at run time. How they get there (environment variables, a
tfvarsfile) is the consumer's choice. - Terraform treats an argument set to
nullas omitted.