Skip to content

Commit 83dd2d2

Browse files
authored
Merge branch 'main' into main
2 parents 7820a11 + 70c5272 commit 83dd2d2

File tree

14 files changed

+581
-156
lines changed

14 files changed

+581
-156
lines changed

.github/workflows/push.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,24 @@ jobs:
2828
run: |
2929
git config --global core.autocrlf false
3030
- name: "Fetch source code"
31-
uses: actions/checkout@v2
31+
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # https://github.com/actions/checkout/releases/tag/v3.2.0
3232
- name: Install Go
33-
uses: actions/setup-go@v2
33+
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # https://github.com/actions/setup-go/releases/tag/v3.5.0
3434
with:
3535
go-version: 1.18
3636
- name: Go test
3737
run: |
38-
go test ./...
38+
go test ./... -race
3939
4040
fmt_and_vet:
4141
name: "fmt and lint"
4242
runs-on: ubuntu-latest
4343

4444
steps:
4545
- name: "Fetch source code"
46-
uses: actions/checkout@v2
46+
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # https://github.com/actions/checkout/releases/tag/v3.2.0
4747
- name: Install Go
48-
uses: actions/setup-go@v2
48+
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # https://github.com/actions/setup-go/releases/tag/v3.5.0
4949
with:
5050
go-version: 1.18
5151
- name: "Check vet"

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
# HCL Changelog
22

3-
## v2.15.0 (Unreleased)
3+
## v2.16.0 (Unreleased)
4+
5+
### Bugs Fixed
6+
7+
* ext/typeexpr: Modify the `Defaults` functionality to implement additional flexibility. HCL will now upcast lists and sets into tuples, and maps into objects, when applying default values if the applied defaults cause the elements within a target collection to have differing types. Previously, this would have resulted in a panic, now HCL will return a modified overall type. ([#574](https://github.com/hashicorp/hcl/pull/574))
8+
9+
### Enhancements
10+
11+
* ext/typeexpr: Users should return to the advice provided by v2.14.0, and apply the go-cty convert functionality *after* setting defaults on a given `cty.Value`, rather than before. ([#574](https://github.com/hashicorp/hcl/pull/574))
12+
13+
## v2.15.0 (November 10, 2022)
14+
15+
### Bugs Fixed
16+
17+
* ext/typeexpr: Skip null objects when applying defaults. This prevents crashes when null objects are creating inside collections, and stops incomplete objects being created with only optional attributes set. ([#567](https://github.com/hashicorp/hcl/pull/567))
18+
* ext/typeexpr: Ensure default values do not have optional metadata attached. This prevents crashes when default values are inserted into concrete go-cty values that have also been stripped of their optional metadata. ([#568](https://github.com/hashicorp/hcl/pull/568))
19+
20+
### Enhancements
21+
22+
* ext/typeexpr: With the [go-cty](https://github.com/zclconf/go-cty) upstream depenendency updated to v1.12.0, the `Defaults` struct and associated functions can apply additional and more flexible 'unsafe' conversions (examples include tuples into collections such as lists and sets, and additional safety around null and dynamic values). ([#564](https://github.com/hashicorp/hcl/pull/564))
23+
* ext/typeexpr: With the [go-cty](https://github.com/zclconf/go-cty) upstream depenendency updated to v1.12.0, users should now apply the go-cty convert functionality *before* setting defaults on a given `cty.Value`, rather than after, if they require a specific `cty.Type`. ([#564](https://github.com/hashicorp/hcl/pull/564))
424

525
## v2.14.1 (September 23, 2022)
626

LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
Copyright (c) 2014 HashiCorp, Inc.
2+
13
Mozilla Public License, version 2.0
24

35
1. Definitions

ext/typeexpr/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,27 @@ types with weird attributes generally show up only from arbitrary object
6666
constructors in configuration files, which are usually treated either as maps
6767
or as the dynamic pseudo-type.
6868

69+
### Optional Object Attributes
70+
71+
As part of object expressions attributes can be marked as optional. Missing
72+
object attributes would typically result in an error when type constraints are
73+
validated or used. Optional missing attributes, however, would not result in an
74+
error. The `cty` ["convert" function](#the-convert-cty-function) will populate
75+
missing optional attributes with null values.
76+
77+
For example:
78+
79+
* `object({name=string,age=optional(number)})`
80+
81+
Optional attributes can also be specified with default values. The
82+
`TypeConstraintWithDefaults` function will return a `Defaults` object that can
83+
be used to populate missing optional attributes with defaults in a given
84+
`cty.Value`.
85+
86+
For example:
87+
88+
* `object({name=string,age=optional(number, 0)})`
89+
6990
## Type Constraints as Values
7091

7192
Along with defining a convention for writing down types using HCL expression

ext/typeexpr/defaults.go

Lines changed: 143 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package typeexpr
22

33
import (
4+
"sort"
5+
"strconv"
6+
47
"github.com/zclconf/go-cty/cty"
8+
"github.com/zclconf/go-cty/cty/convert"
59
)
610

711
// Defaults represents a type tree which may contain default values for
812
// optional object attributes at any level. This is used to apply nested
9-
// defaults to an input value before converting it to the concrete type.
13+
// defaults to a given cty.Value before converting it to a concrete type.
1014
type Defaults struct {
1115
// Type of the node for which these defaults apply. This is necessary in
1216
// order to determine how to inspect the Defaults and Children collections.
@@ -35,123 +39,168 @@ type Defaults struct {
3539
// caller will have better context to report useful type conversion failure
3640
// diagnostics.
3741
func (d *Defaults) Apply(val cty.Value) cty.Value {
38-
val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d})
39-
40-
// The transformer should never return an error.
41-
if err != nil {
42-
panic(err)
43-
}
44-
45-
return val
42+
return d.apply(val)
4643
}
4744

48-
// defaultsTransformer implements cty.Transformer, as a pre-order traversal,
49-
// applying defaults as it goes. The pre-order traversal allows us to specify
50-
// defaults more loosely for structural types, as the defaults for the types
51-
// will be applied to the default value later in the walk.
52-
type defaultsTransformer struct {
53-
defaults *Defaults
54-
}
55-
56-
var _ cty.Transformer = (*defaultsTransformer)(nil)
57-
58-
func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) {
59-
// Cannot apply defaults to an unknown value
60-
if !v.IsKnown() {
61-
return v, nil
45+
func (d *Defaults) apply(v cty.Value) cty.Value {
46+
// We don't apply defaults to null values or unknown values. To be clear,
47+
// we will overwrite children values with defaults if they are null but not
48+
// if the actual value is null.
49+
if !v.IsKnown() || v.IsNull() {
50+
return v
6251
}
6352

64-
// Look up the defaults for this path.
65-
defaults := t.defaults.traverse(p)
66-
67-
// If we have no defaults, nothing to do.
68-
if len(defaults) == 0 {
69-
return v, nil
53+
// Also, do nothing if we have no defaults to apply.
54+
if len(d.DefaultValues) == 0 && len(d.Children) == 0 {
55+
return v
7056
}
7157

72-
// Ensure we are working with an object or map.
73-
vt := v.Type()
74-
if !vt.IsObjectType() && !vt.IsMapType() {
75-
// Cannot apply defaults because the value type is incompatible.
76-
// We'll ignore this and let the later conversion stage display a
77-
// more useful diagnostic.
78-
return v, nil
58+
v, marks := v.Unmark()
59+
60+
switch {
61+
case v.Type().IsSetType(), v.Type().IsListType(), v.Type().IsTupleType():
62+
values := d.applyAsSlice(v)
63+
64+
if v.Type().IsSetType() {
65+
if len(values) == 0 {
66+
v = cty.SetValEmpty(v.Type().ElementType())
67+
break
68+
}
69+
if converts := d.unifyAsSlice(values); len(converts) > 0 {
70+
v = cty.SetVal(converts).WithMarks(marks)
71+
break
72+
}
73+
} else if v.Type().IsListType() {
74+
if len(values) == 0 {
75+
v = cty.ListValEmpty(v.Type().ElementType())
76+
break
77+
}
78+
if converts := d.unifyAsSlice(values); len(converts) > 0 {
79+
v = cty.ListVal(converts)
80+
break
81+
}
82+
}
83+
v = cty.TupleVal(values)
84+
case v.Type().IsObjectType(), v.Type().IsMapType():
85+
values := d.applyAsMap(v)
86+
87+
for key, defaultValue := range d.DefaultValues {
88+
if value, ok := values[key]; !ok || value.IsNull() {
89+
if defaults, ok := d.Children[key]; ok {
90+
values[key] = defaults.apply(defaultValue)
91+
continue
92+
}
93+
values[key] = defaultValue
94+
}
95+
}
96+
97+
if v.Type().IsMapType() {
98+
if len(values) == 0 {
99+
v = cty.MapValEmpty(v.Type().ElementType())
100+
break
101+
}
102+
if converts := d.unifyAsMap(values); len(converts) > 0 {
103+
v = cty.MapVal(converts)
104+
break
105+
}
106+
}
107+
v = cty.ObjectVal(values)
79108
}
80109

81-
// Unmark the value and reapply the marks later.
82-
v, valMarks := v.Unmark()
110+
return v.WithMarks(marks)
111+
}
83112

84-
// Convert the given value into an attribute map (if it's non-null and
85-
// non-empty).
86-
attrs := make(map[string]cty.Value)
87-
if !v.IsNull() && v.LengthInt() > 0 {
88-
attrs = v.AsValueMap()
113+
func (d *Defaults) applyAsSlice(value cty.Value) []cty.Value {
114+
var elements []cty.Value
115+
for ix, element := range value.AsValueSlice() {
116+
if childDefaults := d.getChild(ix); childDefaults != nil {
117+
element = childDefaults.apply(element)
118+
elements = append(elements, element)
119+
continue
120+
}
121+
elements = append(elements, element)
89122
}
123+
return elements
124+
}
90125

91-
// Apply defaults where attributes are missing, constructing a new
92-
// value with the same marks.
93-
for attr, defaultValue := range defaults {
94-
if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() {
95-
attrs[attr] = defaultValue
126+
func (d *Defaults) applyAsMap(value cty.Value) map[string]cty.Value {
127+
elements := make(map[string]cty.Value)
128+
for key, element := range value.AsValueMap() {
129+
if childDefaults := d.getChild(key); childDefaults != nil {
130+
elements[key] = childDefaults.apply(element)
131+
continue
96132
}
133+
elements[key] = element
97134
}
98-
99-
// We construct an object even if the input value was a map, as the
100-
// type of an attribute's default value may be incompatible with the
101-
// map element type.
102-
return cty.ObjectVal(attrs).WithMarks(valMarks), nil
135+
return elements
103136
}
104137

105-
func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) {
106-
return v, nil
138+
func (d *Defaults) getChild(key interface{}) *Defaults {
139+
switch {
140+
case d.Type.IsMapType(), d.Type.IsSetType(), d.Type.IsListType():
141+
return d.Children[""]
142+
case d.Type.IsTupleType():
143+
return d.Children[strconv.Itoa(key.(int))]
144+
case d.Type.IsObjectType():
145+
return d.Children[key.(string)]
146+
default:
147+
return nil
148+
}
107149
}
108150

109-
// traverse walks the abstract defaults structure for a given path, returning
110-
// a set of default values (if any are present) or nil (if not). This operation
111-
// differs from applying a path to a value because we need to customize the
112-
// traversal steps for collection types, where a single set of defaults can be
113-
// applied to an arbitrary number of elements.
114-
func (d *Defaults) traverse(path cty.Path) map[string]cty.Value {
115-
if len(path) == 0 {
116-
return d.DefaultValues
151+
func (d *Defaults) unifyAsSlice(values []cty.Value) []cty.Value {
152+
var types []cty.Type
153+
for _, value := range values {
154+
types = append(types, value.Type())
155+
}
156+
unify, conversions := convert.UnifyUnsafe(types)
157+
if unify == cty.NilType {
158+
return nil
117159
}
118160

119-
switch s := path[0].(type) {
120-
case cty.GetAttrStep:
121-
if d.Type.IsObjectType() {
122-
// Attribute path steps are normally applied to objects, where each
123-
// attribute may have different defaults.
124-
return d.traverseChild(s.Name, path)
125-
} else if d.Type.IsMapType() {
126-
// Literal values for maps can result in attribute path steps, in which
127-
// case we need to disregard the attribute name, as maps can have only
128-
// one child.
129-
return d.traverseChild("", path)
161+
var converts []cty.Value
162+
for ix := 0; ix < len(conversions); ix++ {
163+
if conversions[ix] == nil {
164+
converts = append(converts, values[ix])
165+
continue
130166
}
131167

132-
return nil
133-
case cty.IndexStep:
134-
if d.Type.IsTupleType() {
135-
// Tuples can have different types for each element, so we look
136-
// up the defaults based on the index key.
137-
return d.traverseChild(s.Key.AsBigFloat().String(), path)
138-
} else if d.Type.IsCollectionType() {
139-
// Defaults for collection element types are stored with a blank
140-
// key, so we disregard the index key.
141-
return d.traverseChild("", path)
168+
converted, err := conversions[ix](values[ix])
169+
if err != nil {
170+
return nil
142171
}
143-
return nil
144-
default:
145-
// At time of writing there are no other path step types.
146-
return nil
172+
converts = append(converts, converted)
147173
}
174+
return converts
148175
}
149176

150-
// traverseChild continues the traversal for a given child key, and mutually
151-
// recurses with traverse.
152-
func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value {
153-
if child, ok := d.Children[name]; ok {
154-
return child.traverse(path[1:])
177+
func (d *Defaults) unifyAsMap(values map[string]cty.Value) map[string]cty.Value {
178+
var keys []string
179+
for key := range values {
180+
keys = append(keys, key)
181+
}
182+
sort.Strings(keys)
183+
184+
var types []cty.Type
185+
for _, key := range keys {
186+
types = append(types, values[key].Type())
187+
}
188+
unify, conversions := convert.UnifyUnsafe(types)
189+
if unify == cty.NilType {
190+
return nil
191+
}
192+
193+
converts := make(map[string]cty.Value)
194+
for ix, key := range keys {
195+
if conversions[ix] == nil {
196+
converts[key] = values[key]
197+
continue
198+
}
199+
200+
var err error
201+
if converts[key], err = conversions[ix](values[key]); err != nil {
202+
return nil
203+
}
155204
}
156-
return nil
205+
return converts
157206
}

0 commit comments

Comments
 (0)