Skip to content

Commit ca64fed

Browse files
bbasataapparentlymartmildwonkeypsellealisdair
authored
Forward to 1.7.0 (#17)
* Prepare for a potential future 1.6.1 release * build: remove go 1.11 and 1.12 from travis config * cty: RawEquals correct behavior for sets with partially-unknown values Sets with the same type and same unknown values are equal enough for RawEqual's purposes. This commit takes advantage of the setRules's defined ordering to compare each element of the sets as lists. * v1.6.1 * Prepare for an assumed future v1.6.2 release * cty: Utilities to unmark with marks at specific paths This allows a caller to call a method like Unmark, but instead of getting a superset of marks, get an array of marks and their associated paths. This allows for a Value to be unmarked and then remarked later without losing so much detail about the marks. * Update CHANGELOG.md * cty: Fix deep marks functions with new transformer The UnmarkDeep, UnmarkDeepWithPaths, and MarkWithPaths functions could previously panic with nested complex values. This was due to the Transform callback function only being called as part of a postorder traversal, when it is not permitted to traverse a marked value. This commit introduces a new TransformWithTransformer function, requiring an implementation of a new Transformer interface. Using this interface, callers can implement either preorder or postorder traversals. In turn, this allows us to write deep transformation for marks which successfully cope with all nested structures. The commit includes significantly more tests for these functions, which should now cover all complex value types. * cty: Remove already-fixed FIXME for SetVal The immediately preceding code already unmarks the values and applies the marking to the set, so this comment and panic are out of date. * functions/stdlib: formatdate to correctly handle literals at the end of the format string * Update CHANGELOG.md * cty: Fix path array reuse with UnmarkDeepWithPaths When unmarking a complex value and retaining the marked paths, a sufficiently deep structure would result in incorrect duplicate path output. For example, this structure (in HCL syntax): { environment = [ { variables = { "x" = 1 "y" = 2 } } ] } If the 1 and 2 values are marked, the resulting path value marks from UnmarkDeepWithPaths would have two entries, as expected. However, both would have the same Path attribute, like so: [ { environment[0].variables["x"], "mark" }, { environment[0].variables["x"], "mark" }, ] This is caused by calling `append` in the walk transform function with the same path object repeatedly, which eventually does not result in array reallocation, and therefore the path object is modified in-place. * stdlib: Add tests for join function * function: Deeply unmark function arguments For functions which do not explicitly support marks, we now deeply unmark arguments before calling, and reapply those marks to the return value. This ensures that these functions do not panic with arguments which are collection values with marked descendants. This does result in overly conservatively applying marks in some cases, but that can be fixed with later work to add explicit support for marks to those functions. * Update CHANGELOG.md * v1.7.0 * Update CHANGELOG.md --------- Co-authored-by: Martin Atkins <[email protected]> Co-authored-by: Kristin Laemmert <[email protected]> Co-authored-by: Pam Selle <[email protected]> Co-authored-by: Alisdair McDiarmid <[email protected]> Co-authored-by: OwenTuz <[email protected]>
1 parent ce534d0 commit ca64fed

File tree

12 files changed

+762
-50
lines changed

12 files changed

+762
-50
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# 1.7.0 (Unreleased)
2+
3+
* `cty`: `Value.UnmarkDeepWithPaths` and `Value.MarkWithPaths` are like `Value.UnmarkDeep` and `Value.Mark` but they retain path information for each marked value, so that marks can be re-applied later without all the loss of detail that results from `Value.UnmarkDeep` aggregating together all of the nested marks.
4+
* `function`: Unless a parameter has `AllowMarks: true` explicitly set, the functions infrastructure will now guarantee that it never sees a marked value even if the mark is deep inside a data structure. Previously that guarantee was only shallow for the top-level value, similar to `AllowUnknown`, but because marks are a relatively new addition to `cty` and numerous existing functions are not written to deal with them this is the more conservative and robust default. ([#72](https://github.com/zclconf/go-cty/pull/72))
5+
* `function/stdlib`: The `formatdate` function was not correctly handling literal sequences at the end of the format string. It will now handle those as intended. ([#69](https://github.com/zclconf/go-cty/pull/69))
6+
17
# 1.6.1 (Unreleased)
28

3-
* Fix a regression from 1.6.0 where `Value.RawEqual` no longer returned the correct result given a pair of sets containing partially-unknown values. ([#64](https://github.com/zclconf/go-cty/pull/64))
9+
* `cty`:: Fix a regression from 1.6.0 where `Value.RawEqual` no longer returned the correct result given a pair of sets containing partially-unknown values. ([#64](https://github.com/zclconf/go-cty/pull/64))
410

511
# 1.6.0 (Unreleased)
612

cty/function/function.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -244,19 +244,21 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
244244
return cty.UnknownVal(expectedType), nil
245245
}
246246

247-
if val.IsMarked() && !spec.AllowMarked {
248-
unwrappedVal, marks := val.Unmark()
249-
// In order to avoid additional overhead on applications that
250-
// are not using marked values, we copy the given args only
251-
// if we encounter a marked value we need to unmark. However,
252-
// as a consequence we end up doing redundant copying if multiple
253-
// marked values need to be unwrapped. That seems okay because
254-
// argument lists are generally small.
255-
newArgs := make([]cty.Value, len(args))
256-
copy(newArgs, args)
257-
newArgs[i] = unwrappedVal
258-
resultMarks = append(resultMarks, marks)
259-
args = newArgs
247+
if !spec.AllowMarked {
248+
unwrappedVal, marks := val.UnmarkDeep()
249+
if len(marks) > 0 {
250+
// In order to avoid additional overhead on applications that
251+
// are not using marked values, we copy the given args only
252+
// if we encounter a marked value we need to unmark. However,
253+
// as a consequence we end up doing redundant copying if multiple
254+
// marked values need to be unwrapped. That seems okay because
255+
// argument lists are generally small.
256+
newArgs := make([]cty.Value, len(args))
257+
copy(newArgs, args)
258+
newArgs[i] = unwrappedVal
259+
resultMarks = append(resultMarks, marks)
260+
args = newArgs
261+
}
260262
}
261263
}
262264

@@ -266,13 +268,15 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
266268
if !val.IsKnown() && !spec.AllowUnknown {
267269
return cty.UnknownVal(expectedType), nil
268270
}
269-
if val.IsMarked() && !spec.AllowMarked {
270-
unwrappedVal, marks := val.Unmark()
271-
newArgs := make([]cty.Value, len(args))
272-
copy(newArgs, args)
273-
newArgs[len(posArgs)+i] = unwrappedVal
274-
resultMarks = append(resultMarks, marks)
275-
args = newArgs
271+
if !spec.AllowMarked {
272+
unwrappedVal, marks := val.UnmarkDeep()
273+
if len(marks) > 0 {
274+
newArgs := make([]cty.Value, len(args))
275+
copy(newArgs, args)
276+
newArgs[len(posArgs)+i] = unwrappedVal
277+
resultMarks = append(resultMarks, marks)
278+
args = newArgs
279+
}
276280
}
277281
}
278282
}

cty/function/stdlib/datetime.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,14 @@ func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err er
362362
for i := 1; i < len(data); i++ {
363363
if data[i] == esc {
364364
if (i + 1) == len(data) {
365-
// We need at least one more byte to decide if this is an
366-
// escape or a terminator.
367-
return 0, nil, nil
365+
if atEOF {
366+
// We have a closing quote and are at the end of our input
367+
return len(data), data, nil
368+
} else {
369+
// We need at least one more byte to decide if this is an
370+
// escape or a terminator.
371+
return 0, nil, nil
372+
}
368373
}
369374
if data[i+1] == esc {
370375
i++ // doubled-up quotes are an escape sequence

cty/function/stdlib/datetime_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ func TestFormatDate(t *testing.T) {
3939
cty.StringVal("3 o'clock PM"),
4040
``,
4141
},
42+
{
43+
cty.StringVal("H 'o''clock'"),
44+
cty.StringVal("3 o'clock"),
45+
``,
46+
},
4247
{
4348
cty.StringVal("hh:mm:ssZZZZ"),
4449
cty.StringVal("15:04:05+0000"),

cty/function/stdlib/string_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,83 @@ func TestSubstr(t *testing.T) {
393393
})
394394
}
395395
}
396+
397+
func TestJoin(t *testing.T) {
398+
tests := map[string]struct {
399+
Separator cty.Value
400+
Lists []cty.Value
401+
Want cty.Value
402+
}{
403+
"single two-element list": {
404+
cty.StringVal("-"),
405+
[]cty.Value{
406+
cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
407+
},
408+
cty.StringVal("hello-world"),
409+
},
410+
"multiple single-element lists": {
411+
cty.StringVal("-"),
412+
[]cty.Value{
413+
cty.ListVal([]cty.Value{cty.StringVal("chicken")}),
414+
cty.ListVal([]cty.Value{cty.StringVal("egg")}),
415+
},
416+
cty.StringVal("chicken-egg"),
417+
},
418+
"single single-element list": {
419+
cty.StringVal("-"),
420+
[]cty.Value{
421+
cty.ListVal([]cty.Value{cty.StringVal("chicken")}),
422+
},
423+
cty.StringVal("chicken"),
424+
},
425+
"blank separator": {
426+
cty.StringVal(""),
427+
[]cty.Value{
428+
cty.ListVal([]cty.Value{cty.StringVal("horse"), cty.StringVal("face")}),
429+
},
430+
cty.StringVal("horseface"),
431+
},
432+
"marked list": {
433+
cty.StringVal("-"),
434+
[]cty.Value{
435+
cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}).Mark("sensitive"),
436+
},
437+
cty.StringVal("hello-world").Mark("sensitive"),
438+
},
439+
"marked separator": {
440+
cty.StringVal("-").Mark("sensitive"),
441+
[]cty.Value{
442+
cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
443+
},
444+
cty.StringVal("hello-world").Mark("sensitive"),
445+
},
446+
"list with some marked elements": {
447+
cty.StringVal("-"),
448+
[]cty.Value{
449+
cty.ListVal([]cty.Value{cty.StringVal("hello").Mark("sensitive"), cty.StringVal("world")}),
450+
},
451+
cty.StringVal("hello-world").Mark("sensitive"),
452+
},
453+
"multiple marks": {
454+
cty.StringVal("-").Mark("a"),
455+
[]cty.Value{
456+
cty.ListVal([]cty.Value{cty.StringVal("hello").Mark("b"), cty.StringVal("world").Mark("c")}),
457+
},
458+
cty.StringVal("hello-world").WithMarks(cty.NewValueMarks("a", "b", "c")),
459+
},
460+
}
461+
462+
for name, test := range tests {
463+
t.Run(name, func(t *testing.T) {
464+
got, err := Join(test.Separator, test.Lists...)
465+
466+
if err != nil {
467+
t.Fatalf("unexpected error: %s", err)
468+
}
469+
470+
if !got.RawEquals(test.Want) {
471+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
472+
}
473+
})
474+
}
475+
}

cty/json/marshal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error {
1212
if val.IsMarked() {
13-
return path.NewErrorf("value has marks, so it cannot be seralized")
13+
return path.NewErrorf("value has marks, so it cannot be serialized as JSON")
1414
}
1515

1616
// If we're going to decode as DynamicPseudoType then we need to save

cty/marks.go

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ func (m ValueMarks) GoString() string {
6767
return s.String()
6868
}
6969

70+
// PathValueMarks is a structure that enables tracking marks
71+
// and the paths where they are located in one type
72+
type PathValueMarks struct {
73+
Path Path
74+
Marks ValueMarks
75+
}
76+
77+
func (p PathValueMarks) Equal(o PathValueMarks) bool {
78+
if !p.Path.Equals(o.Path) {
79+
return false
80+
}
81+
if !p.Marks.Equal(o.Marks) {
82+
return false
83+
}
84+
return true
85+
}
86+
7087
// IsMarked returns true if and only if the receiving value carries at least
7188
// one mark. A marked value cannot be used directly with integration methods
7289
// without explicitly unmarking it (and retrieving the markings) first.
@@ -174,6 +191,31 @@ func (val Value) Mark(mark interface{}) Value {
174191
}
175192
}
176193

194+
type applyPathValueMarksTransformer struct {
195+
pvm []PathValueMarks
196+
}
197+
198+
func (t *applyPathValueMarksTransformer) Enter(p Path, v Value) (Value, error) {
199+
return v, nil
200+
}
201+
202+
func (t *applyPathValueMarksTransformer) Exit(p Path, v Value) (Value, error) {
203+
for _, path := range t.pvm {
204+
if p.Equals(path.Path) {
205+
return v.WithMarks(path.Marks), nil
206+
}
207+
}
208+
return v, nil
209+
}
210+
211+
// MarkWithPaths accepts a slice of PathValueMarks to apply
212+
// markers to particular paths and returns the marked
213+
// Value.
214+
func (val Value) MarkWithPaths(pvm []PathValueMarks) Value {
215+
ret, _ := TransformWithTransformer(val, &applyPathValueMarksTransformer{pvm})
216+
return ret
217+
}
218+
177219
// Unmark separates the marks of the receiving value from the value itself,
178220
// removing a new unmarked value and a map (representing a set) of the marks.
179221
//
@@ -191,24 +233,54 @@ func (val Value) Unmark() (Value, ValueMarks) {
191233
}, marks
192234
}
193235

236+
type unmarkTransformer struct {
237+
pvm []PathValueMarks
238+
}
239+
240+
func (t *unmarkTransformer) Enter(p Path, v Value) (Value, error) {
241+
unmarkedVal, marks := v.Unmark()
242+
if len(marks) > 0 {
243+
path := make(Path, len(p), len(p)+1)
244+
copy(path, p)
245+
t.pvm = append(t.pvm, PathValueMarks{path, marks})
246+
}
247+
return unmarkedVal, nil
248+
}
249+
250+
func (t *unmarkTransformer) Exit(p Path, v Value) (Value, error) {
251+
return v, nil
252+
}
253+
194254
// UnmarkDeep is similar to Unmark, but it works with an entire nested structure
195255
// rather than just the given value directly.
196256
//
197257
// The result is guaranteed to contain no nested values that are marked, and
198258
// the returned marks set includes the superset of all of the marks encountered
199259
// during the operation.
200260
func (val Value) UnmarkDeep() (Value, ValueMarks) {
261+
t := unmarkTransformer{}
262+
ret, _ := TransformWithTransformer(val, &t)
263+
201264
marks := make(ValueMarks)
202-
ret, _ := Transform(val, func(_ Path, v Value) (Value, error) {
203-
unmarkedV, valueMarks := v.Unmark()
204-
for m, s := range valueMarks {
265+
for _, pvm := range t.pvm {
266+
for m, s := range pvm.Marks {
205267
marks[m] = s
206268
}
207-
return unmarkedV, nil
208-
})
269+
}
270+
209271
return ret, marks
210272
}
211273

274+
// UnmarkDeepWithPaths is like UnmarkDeep, except it returns a slice
275+
// of PathValueMarks rather than a superset of all marks. This allows
276+
// a caller to know which marks are associated with which paths
277+
// in the Value.
278+
func (val Value) UnmarkDeepWithPaths() (Value, []PathValueMarks) {
279+
t := unmarkTransformer{}
280+
ret, _ := TransformWithTransformer(val, &t)
281+
return ret, t.pvm
282+
}
283+
212284
func (val Value) unmarkForce() Value {
213285
unw, _ := val.Unmark()
214286
return unw

0 commit comments

Comments
 (0)