Skip to content

Commit 7e37d42

Browse files
committed
refactor: use named field policies in codegen overrides
1 parent e5f3b93 commit 7e37d42

File tree

4 files changed

+130
-42
lines changed

4 files changed

+130
-42
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ Terraform state reflects what the user cares about, not the entire API response.
195195

196196
For generated XML -> model conversions, see `internal/codegen/README.md`, especially the "Preserve User Intent For Optional Nested Objects" section. That generator-level rule is authoritative for optional nested object readback behavior.
197197

198+
For codegen overrides, prefer the policy layer in `internal/codegen/policy/field_policy.go`. Add exact field overrides there using named policy functions rather than adding new struct/field special cases in generator templates. Template branches should stay generic unless the behavior is truly structural and cannot be expressed as field policy.
199+
198200
**Computed** (not Optional) - Always read from API
199201
- Examples: `id`, `key`, `allocation`, `physical`
200202
- `model.Key = types.StringValue(xml.Key)`

internal/codegen/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,29 @@ Examples:
100100

101101
### Override strategy
102102

103-
When the default rules are not enough, use explicit overrides keyed by full Terraform or XML path rather than helper functions like `isUserManagedFooStruct`.
103+
When the default rules are not enough, use explicit overrides keyed by exact field identity rather than helper functions like `isUserManagedFooStruct`.
104+
105+
Prefer declaring overrides in the policy layer as a registry of named policy functions, for example:
106+
107+
- `StoragePool.capacity``policyComputedReportedField`, `policyUseStateForUnknown`
108+
- `DomainCPU.mode``policyPreservePlannedValueOnReadbackOmit`
109+
110+
This keeps policy declarative and reviewable. Avoid encoding overrides as ad hoc conditionals in converter templates.
104111

105112
Good override targets:
106113

107114
- `storage_pool.capacity`
108115
- `storage_pool.allocation`
109116
- `storage_volume.physical`
117+
- `DomainCPU.mode`
118+
- `DomainGraphicSpice.listen`
110119

111120
Avoid:
112121

113122
- Struct-name allowlists for one field
114123
- Field-name heuristics that ignore nesting scope
115124
- Mixing reflection logic with provider semantics in the same function
125+
- Adding new `if struct == ... && field == ...` branches in generator templates when a policy override can express the behavior
116126

117127
This keeps the generator predictable, makes exceptions easy to audit, and prevents the parser from turning into a collection of special cases.
118128

internal/codegen/policy/field_policy.go

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@ package policy
22

33
import "github.com/dmacvicar/terraform-provider-libvirt/v2/internal/codegen/generator"
44

5+
type fieldPolicy func(*generator.FieldIR)
6+
7+
var fieldPolicies = map[string][]fieldPolicy{
8+
"StoragePool.capacity": {
9+
policyComputedReportedField,
10+
policyUseStateForUnknown,
11+
policyDisablePreserveUserIntent,
12+
},
13+
"StoragePool.allocation": {
14+
policyComputedReportedField,
15+
policyDisablePreserveUserIntent,
16+
},
17+
"StoragePool.available": {
18+
policyComputedReportedField,
19+
policyDisablePreserveUserIntent,
20+
},
21+
"StorageVolume.capacity": {
22+
policyComputedReportedField,
23+
},
24+
"StorageVolume.allocation": {
25+
policyComputedReportedField,
26+
policyDisablePreserveUserIntent,
27+
},
28+
"StorageVolume.physical": {
29+
policyComputedReportedField,
30+
policyDisablePreserveUserIntent,
31+
},
32+
"DomainCPU.mode": {
33+
policyPreservePlannedValueOnReadbackOmit,
34+
},
35+
"DomainGraphicSpice.listen": {
36+
policyPreservePlannedValueOnReadbackOmit,
37+
},
38+
}
39+
540
// ApplyFieldPolicies mutates the IR with Terraform-specific schema/conversion
641
// semantics after structural reflection is complete.
742
func ApplyFieldPolicies(structs []*generator.StructIR) {
@@ -18,8 +53,7 @@ func applyStructPolicies(s *generator.StructIR) {
1853

1954
applyTopLevelIdentityPolicy(s, field)
2055
applyTopLevelImmutabilityPolicy(s, field)
21-
applyReportedFieldPolicy(s, field)
22-
applyReadbackPreservationPolicy(s, field)
56+
applyExplicitFieldPolicies(s, field)
2357
}
2458
}
2559

@@ -58,50 +92,39 @@ func applyTopLevelImmutabilityPolicy(s *generator.StructIR, field *generator.Fie
5892
}
5993
}
6094

61-
func applyReportedFieldPolicy(s *generator.StructIR, field *generator.FieldIR) {
95+
func applyExplicitFieldPolicies(s *generator.StructIR, field *generator.FieldIR) {
6296
if field.IsFlattenedUnit {
6397
return
6498
}
6599

66-
switch s.Name {
67-
case "StoragePool":
68-
switch field.TFName {
69-
case "capacity":
70-
field.IsComputed = true
71-
field.IsOptional = false
72-
field.IsRequired = false
73-
field.PlanModifier = "UseStateForUnknown"
74-
field.PreserveUserIntent = false
75-
case "allocation", "available":
76-
field.IsComputed = true
77-
field.IsOptional = false
78-
field.IsRequired = false
79-
field.PreserveUserIntent = false
80-
}
81-
case "StorageVolume":
82-
switch field.TFName {
83-
case "capacity":
84-
field.IsComputed = true
85-
field.IsOptional = false
86-
field.IsRequired = false
87-
case "allocation", "physical":
88-
field.IsComputed = true
89-
field.IsOptional = false
90-
field.IsRequired = false
91-
field.PreserveUserIntent = false
92-
}
93-
}
100+
key := fieldOverrideKey(s, field)
101+
applyPolicies(field, fieldPolicies[key])
94102
}
95103

96-
func applyReadbackPreservationPolicy(s *generator.StructIR, field *generator.FieldIR) {
97-
switch s.Name {
98-
case "DomainCPU":
99-
if field.TFName == "mode" {
100-
field.PreservePlannedValueOnReadbackOmit = true
101-
}
102-
case "DomainGraphicSpice":
103-
if field.TFName == "listen" {
104-
field.PreservePlannedValueOnReadbackOmit = true
105-
}
104+
func fieldOverrideKey(s *generator.StructIR, field *generator.FieldIR) string {
105+
return s.Name + "." + field.TFName
106+
}
107+
108+
func applyPolicies(field *generator.FieldIR, policies []fieldPolicy) {
109+
for _, policy := range policies {
110+
policy(field)
106111
}
107112
}
113+
114+
func policyComputedReportedField(field *generator.FieldIR) {
115+
field.IsComputed = true
116+
field.IsOptional = false
117+
field.IsRequired = false
118+
}
119+
120+
func policyUseStateForUnknown(field *generator.FieldIR) {
121+
field.PlanModifier = "UseStateForUnknown"
122+
}
123+
124+
func policyDisablePreserveUserIntent(field *generator.FieldIR) {
125+
field.PreserveUserIntent = false
126+
}
127+
128+
func policyPreservePlannedValueOnReadbackOmit(field *generator.FieldIR) {
129+
field.PreservePlannedValueOnReadbackOmit = true
130+
}

internal/codegen/policy/field_policy_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,56 @@ func TestApplyFieldPoliciesMarksReadbackPreservationOverrides(t *testing.T) {
7676
t.Fatal("expected DomainGraphicSpice.listen to preserve planned value on omitted readback")
7777
}
7878
}
79+
80+
func TestApplyFieldPoliciesMarksReportedFieldOverrides(t *testing.T) {
81+
structs := []*generator.StructIR{
82+
{
83+
Name: "StoragePool",
84+
Fields: []*generator.FieldIR{
85+
{TFName: "capacity", IsOptional: true, IsRequired: true, PreserveUserIntent: true},
86+
{TFName: "allocation", IsOptional: true, IsRequired: true, PreserveUserIntent: true},
87+
},
88+
},
89+
{
90+
Name: "StorageVolume",
91+
Fields: []*generator.FieldIR{
92+
{TFName: "capacity", IsOptional: true, IsRequired: true},
93+
{TFName: "physical", IsOptional: true, IsRequired: true, PreserveUserIntent: true},
94+
},
95+
},
96+
}
97+
98+
ApplyFieldPolicies(structs)
99+
100+
poolCapacity := structs[0].Fields[0]
101+
if !poolCapacity.IsComputed || poolCapacity.IsOptional || poolCapacity.IsRequired {
102+
t.Fatal("expected StoragePool.capacity to be computed-only after override")
103+
}
104+
if poolCapacity.PlanModifier != "UseStateForUnknown" {
105+
t.Fatalf("expected StoragePool.capacity plan modifier UseStateForUnknown, got %q", poolCapacity.PlanModifier)
106+
}
107+
if poolCapacity.PreserveUserIntent {
108+
t.Fatal("expected StoragePool.capacity to disable PreserveUserIntent")
109+
}
110+
111+
poolAllocation := structs[0].Fields[1]
112+
if !poolAllocation.IsComputed || poolAllocation.IsOptional || poolAllocation.IsRequired {
113+
t.Fatal("expected StoragePool.allocation to be computed-only after override")
114+
}
115+
if poolAllocation.PreserveUserIntent {
116+
t.Fatal("expected StoragePool.allocation to disable PreserveUserIntent")
117+
}
118+
119+
volumeCapacity := structs[1].Fields[0]
120+
if !volumeCapacity.IsComputed || volumeCapacity.IsOptional || volumeCapacity.IsRequired {
121+
t.Fatal("expected StorageVolume.capacity to be computed-only after override")
122+
}
123+
124+
volumePhysical := structs[1].Fields[1]
125+
if !volumePhysical.IsComputed || volumePhysical.IsOptional || volumePhysical.IsRequired {
126+
t.Fatal("expected StorageVolume.physical to be computed-only after override")
127+
}
128+
if volumePhysical.PreserveUserIntent {
129+
t.Fatal("expected StorageVolume.physical to disable PreserveUserIntent")
130+
}
131+
}

0 commit comments

Comments
 (0)