github.com/hashicorp/hcl/v2@v2.20.0/hcldec/spec_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hcldec 5 6 import ( 7 "fmt" 8 "reflect" 9 "testing" 10 11 "github.com/apparentlymart/go-dump/dump" 12 "github.com/google/go-cmp/cmp" 13 "github.com/zclconf/go-cty-debug/ctydebug" 14 "github.com/zclconf/go-cty/cty" 15 "github.com/zclconf/go-cty/cty/function" 16 17 "github.com/hashicorp/hcl/v2" 18 "github.com/hashicorp/hcl/v2/hclsyntax" 19 ) 20 21 // Verify that all of our spec types implement the necessary interfaces 22 var _ Spec = ObjectSpec(nil) 23 var _ Spec = TupleSpec(nil) 24 var _ Spec = (*AttrSpec)(nil) 25 var _ Spec = (*LiteralSpec)(nil) 26 var _ Spec = (*ExprSpec)(nil) 27 var _ Spec = (*BlockSpec)(nil) 28 var _ Spec = (*BlockListSpec)(nil) 29 var _ Spec = (*BlockSetSpec)(nil) 30 var _ Spec = (*BlockMapSpec)(nil) 31 var _ Spec = (*BlockAttrsSpec)(nil) 32 var _ Spec = (*BlockLabelSpec)(nil) 33 var _ Spec = (*DefaultSpec)(nil) 34 var _ Spec = (*TransformExprSpec)(nil) 35 var _ Spec = (*TransformFuncSpec)(nil) 36 var _ Spec = (*ValidateSpec)(nil) 37 38 var _ attrSpec = (*AttrSpec)(nil) 39 var _ attrSpec = (*DefaultSpec)(nil) 40 41 var _ blockSpec = (*BlockSpec)(nil) 42 var _ blockSpec = (*BlockListSpec)(nil) 43 var _ blockSpec = (*BlockSetSpec)(nil) 44 var _ blockSpec = (*BlockMapSpec)(nil) 45 var _ blockSpec = (*BlockAttrsSpec)(nil) 46 var _ blockSpec = (*DefaultSpec)(nil) 47 48 var _ specNeedingVariables = (*AttrSpec)(nil) 49 var _ specNeedingVariables = (*BlockSpec)(nil) 50 var _ specNeedingVariables = (*BlockListSpec)(nil) 51 var _ specNeedingVariables = (*BlockSetSpec)(nil) 52 var _ specNeedingVariables = (*BlockMapSpec)(nil) 53 var _ specNeedingVariables = (*BlockAttrsSpec)(nil) 54 55 func TestDefaultSpec(t *testing.T) { 56 config := ` 57 foo = fooval 58 bar = barval 59 ` 60 f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1}) 61 if diags.HasErrors() { 62 t.Fatal(diags.Error()) 63 } 64 65 t.Run("primary set", func(t *testing.T) { 66 spec := &DefaultSpec{ 67 Primary: &AttrSpec{ 68 Name: "foo", 69 Type: cty.String, 70 }, 71 Default: &AttrSpec{ 72 Name: "bar", 73 Type: cty.String, 74 }, 75 } 76 77 gotVars := Variables(f.Body, spec) 78 wantVars := []hcl.Traversal{ 79 { 80 hcl.TraverseRoot{ 81 Name: "fooval", 82 SrcRange: hcl.Range{ 83 Filename: "", 84 Start: hcl.Pos{Line: 2, Column: 7, Byte: 7}, 85 End: hcl.Pos{Line: 2, Column: 13, Byte: 13}, 86 }, 87 }, 88 }, 89 { 90 hcl.TraverseRoot{ 91 Name: "barval", 92 SrcRange: hcl.Range{ 93 Filename: "", 94 Start: hcl.Pos{Line: 3, Column: 7, Byte: 20}, 95 End: hcl.Pos{Line: 3, Column: 13, Byte: 26}, 96 }, 97 }, 98 }, 99 } 100 if !reflect.DeepEqual(gotVars, wantVars) { 101 t.Errorf("wrong Variables result\ngot: %s\nwant: %s", dump.Value(gotVars), dump.Value(wantVars)) 102 } 103 104 ctx := &hcl.EvalContext{ 105 Variables: map[string]cty.Value{ 106 "fooval": cty.StringVal("foo value"), 107 "barval": cty.StringVal("bar value"), 108 }, 109 } 110 111 got, err := Decode(f.Body, spec, ctx) 112 if err != nil { 113 t.Fatal(err) 114 } 115 want := cty.StringVal("foo value") 116 if !got.RawEquals(want) { 117 t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want) 118 } 119 }) 120 121 t.Run("primary not set", func(t *testing.T) { 122 spec := &DefaultSpec{ 123 Primary: &AttrSpec{ 124 Name: "foo", 125 Type: cty.String, 126 }, 127 Default: &AttrSpec{ 128 Name: "bar", 129 Type: cty.String, 130 }, 131 } 132 133 ctx := &hcl.EvalContext{ 134 Variables: map[string]cty.Value{ 135 "fooval": cty.NullVal(cty.String), 136 "barval": cty.StringVal("bar value"), 137 }, 138 } 139 140 got, err := Decode(f.Body, spec, ctx) 141 if err != nil { 142 t.Fatal(err) 143 } 144 want := cty.StringVal("bar value") 145 if !got.RawEquals(want) { 146 t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want) 147 } 148 }) 149 } 150 151 func TestValidateFuncSpec(t *testing.T) { 152 config := ` 153 foo = "invalid" 154 ` 155 f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1}) 156 if diags.HasErrors() { 157 t.Fatal(diags.Error()) 158 } 159 160 expectRange := map[string]*hcl.Range{ 161 "without_range": nil, 162 "with_range": &hcl.Range{ 163 Filename: "foobar", 164 Start: hcl.Pos{Line: 99, Column: 99}, 165 End: hcl.Pos{Line: 999, Column: 999}, 166 }, 167 } 168 169 for name := range expectRange { 170 t.Run(name, func(t *testing.T) { 171 spec := &ValidateSpec{ 172 Wrapped: &AttrSpec{ 173 Name: "foo", 174 Type: cty.String, 175 }, 176 Func: func(value cty.Value) hcl.Diagnostics { 177 if value.AsString() != "invalid" { 178 return hcl.Diagnostics{ 179 &hcl.Diagnostic{ 180 Severity: hcl.DiagError, 181 Summary: "incorrect value", 182 Detail: fmt.Sprintf("invalid value passed in: %s", value.GoString()), 183 }, 184 } 185 } 186 187 return hcl.Diagnostics{ 188 &hcl.Diagnostic{ 189 Severity: hcl.DiagWarning, 190 Summary: "OK", 191 Detail: "validation called correctly", 192 Subject: expectRange[name], 193 }, 194 } 195 }, 196 } 197 198 _, diags = Decode(f.Body, spec, nil) 199 if len(diags) != 1 || 200 diags[0].Severity != hcl.DiagWarning || 201 diags[0].Summary != "OK" || 202 diags[0].Detail != "validation called correctly" { 203 t.Fatalf("unexpected diagnostics: %s", diags.Error()) 204 } 205 206 if expectRange[name] == nil && diags[0].Subject == nil { 207 t.Fatal("returned diagnostic subject missing") 208 } 209 210 if expectRange[name] != nil && !reflect.DeepEqual(expectRange[name], diags[0].Subject) { 211 t.Fatalf("expected range %s, got range %s", expectRange[name], diags[0].Subject) 212 } 213 }) 214 } 215 } 216 217 func TestRefineValueSpec(t *testing.T) { 218 config := ` 219 foo = "hello" 220 bar = unk 221 dyn = dyn 222 marked = mark(unk) 223 ` 224 225 f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.InitialPos) 226 if diags.HasErrors() { 227 t.Fatal(diags.Error()) 228 } 229 230 attrSpec := func(name string) Spec { 231 return &RefineValueSpec{ 232 // RefineValueSpec should typically have a ValidateSpec wrapped 233 // inside it to catch any values that are outside of the required 234 // range and return a helpful error message about it. In this 235 // case our refinement is .NotNull so the validation function 236 // must reject null values. 237 Wrapped: &ValidateSpec{ 238 Wrapped: &AttrSpec{ 239 Name: name, 240 Required: true, 241 Type: cty.String, 242 }, 243 Func: func(value cty.Value) hcl.Diagnostics { 244 var diags hcl.Diagnostics 245 if value.IsNull() { 246 diags = diags.Append(&hcl.Diagnostic{ 247 Severity: hcl.DiagError, 248 Summary: "Cannot be null", 249 Detail: "Argument is required.", 250 }) 251 } 252 return diags 253 }, 254 }, 255 Refine: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { 256 return rb.NotNull() 257 }, 258 } 259 } 260 spec := &ObjectSpec{ 261 "foo": attrSpec("foo"), 262 "bar": attrSpec("bar"), 263 "dyn": attrSpec("dyn"), 264 "marked": attrSpec("marked"), 265 } 266 267 got, diags := Decode(f.Body, spec, &hcl.EvalContext{ 268 Variables: map[string]cty.Value{ 269 "unk": cty.UnknownVal(cty.String), 270 "dyn": cty.DynamicVal, 271 }, 272 Functions: map[string]function.Function{ 273 "mark": function.New(&function.Spec{ 274 Params: []function.Parameter{ 275 { 276 Name: "v", 277 Type: cty.DynamicPseudoType, 278 AllowMarked: true, 279 AllowNull: true, 280 AllowUnknown: true, 281 AllowDynamicType: true, 282 }, 283 }, 284 Type: func(args []cty.Value) (cty.Type, error) { 285 return args[0].Type(), nil 286 }, 287 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 288 return args[0].Mark("boop"), nil 289 }, 290 }), 291 }, 292 }) 293 if diags.HasErrors() { 294 t.Fatal(diags.Error()) 295 } 296 297 want := cty.ObjectVal(map[string]cty.Value{ 298 // This argument had a known value, so it's unchanged but the 299 // RefineValueSpec still checks that it isn't null to catch 300 // bugs in the application's validation function. 301 "foo": cty.StringVal("hello"), 302 303 // The final value of bar is unknown but refined as non-null. 304 "bar": cty.UnknownVal(cty.String).RefineNotNull(), 305 306 // The final value of dyn is unknown but refined as non-null. 307 // Correct behavior here requires that we convert the DynamicVal 308 // to an unknown string first and then refine it. 309 "dyn": cty.UnknownVal(cty.String).RefineNotNull(), 310 311 // This argument had a mark applied, which should be preserved 312 // despite the refinement. 313 "marked": cty.UnknownVal(cty.String).RefineNotNull().Mark("boop"), 314 }) 315 if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { 316 t.Errorf("wrong result\n%s", diff) 317 } 318 }