Skip to content

fix(parser): apply variable type constraints so optional() attributes materialise - #54

Merged
andreacappelletti97 merged 1 commit into
mainfrom
fix/module-variable-type-constraints
Jul 28, 2026
Merged

fix(parser): apply variable type constraints so optional() attributes materialise#54
andreacappelletti97 merged 1 commit into
mainfrom
fix/module-variable-type-constraints

Conversation

@andreacappelletti97

Copy link
Copy Markdown
Contributor

Fixes #53.

Root cause

c3x evaluated a variable's value but ignored its declared type constraint, so optional() object attributes were never filled. For an input typed map(object({ instance_class = optional(string) })), a caller value of { one = {} } stayed an empty object. Inside the module:

instance_class = try(coalesce(each.value.instance_class, var.cluster_instance_class), null)

each.value.instance_class errored on the missing attribute, the coalesce errored, try() swallowed it to null, and the resource fell back to the catalogue default class. That is why changing cluster_instance_class through terraform-aws-modules/rds-aurora produced no delta.

Fix

Apply the declared type constraint once the var scope is fully layered, mirroring Terraform's own decoding (typeexpr.TypeConstraintWithDefaults, then Defaults.Apply + convert.Convert), for both the root module (parser.go) and child-module inputs (modules.go). Best-effort: unconvertible values pass through unchanged, and any/untyped variables are skipped so no useful structure is stripped.

Verification

  • db.r6g.large to db.r7g.2xlarge now moves the estimate correctly ($246.74 to $1,049.01), directly and through a module block.
  • Two regression tests added (top-level and module-input paths).
  • Full go test ./..., go vet, gofmt clean. No go.mod change (typeexpr ships with hcl/v2).

Boundary

This covers optional()-typed inputs, the rds-aurora case. An input typed any would still not materialise the attribute, but that also fails in real Terraform, so matching Terraform's behaviour is the correct boundary.

… materialise

The optional() support added in #46 (applyOptionalDefaults) only filled
object attributes that had an explicit default, and only for a top-level
object() type. It skipped bare optional(t) (no default) and did not descend
into map(object(...)) / list(object(...)) wrappers. That is exactly the
terraform-aws-modules/rds-aurora shape (issue #53): instances is typed
map(object({ instance_class = optional(string), ... })), so a caller value
of { one = {} } stayed an empty object, try(coalesce(each.value.instance_class,
var.cluster_instance_class), null) errored on the missing attribute, try()
swallowed it to null, and the resource fell back to the catalogue default class.

Replace the hand-rolled extraction with the canonical Terraform mechanism:
typeexpr.TypeConstraintWithDefaults, then Defaults.Apply + convert.Convert,
applied once the var scope is fully layered, for both the root module and
child-module inputs. This is a strict superset: it fills explicit optional
defaults (including nested), materialises bare optionals as null, and handles
map/list-of-object wrappers. Best-effort: unconvertible values pass through
unchanged; any/untyped variables are skipped.

Removes optional_defaults.go and its test; ports the explicit-default and
nested-object coverage plus adds regression tests for the root and
module-input rds-aurora paths.
@andreacappelletti97
andreacappelletti97 force-pushed the fix/module-variable-type-constraints branch from 13fa74a to 3ffe6b6 Compare July 28, 2026 18:06
@andreacappelletti97
andreacappelletti97 merged commit e28e52c into main Jul 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do not work with terraform-aws-modules/rds-aurora module

1 participant