Skip to content

Commit d4bb9d4

Browse files
cty: Various new mark-inspecting helpers
I've seen variants of each of these hand-written many times in callers, so it's well past time to bring them upstream to better support the common marks-related usage patterns that have emerged downstream.
1 parent 31572cf commit d4bb9d4

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
This new function allows inspecting and transforming marks with far less overhead, by creating new values only for parts of a structure that actually need to change and by reusing (rather than recreating) the "payloads" of the values being modified when we know that only the marks have changed.
1212

13+
- `cty.ValueMarksOfType` and `cty.ValueMarksOfTypeDeep` make it easier to use type-based rather than value-based mark schemes, where different values of a common type are used to track a specific kind of relationship with multiple external values.
14+
- `cty.Value.HasMarkDeep` provides a "deep" version of the existing `cty.Value.HasMark`, searching throughout a possibly-nested structure for any values that have the given mark.
15+
1316
# 1.16.4 (August 20, 2025)
1417

1518
* `cty.UnknownAsNull` now accepts marked values and preserves the given marks in its result. Previously it had no direct support for marks and so would either panic or return incorrect results when given marked values.

cty/marks.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cty
22

33
import (
44
"fmt"
5+
"io"
6+
"iter"
57
"strings"
68
)
79

@@ -119,6 +121,60 @@ func (val Value) HasMark(mark interface{}) bool {
119121
return false
120122
}
121123

124+
// HasMarkDeep is like [HasMark] but also searches any values nested inside
125+
// the given value.
126+
func (val Value) HasMarkDeep(mark interface{}) bool {
127+
found := false
128+
Walk(val, func(p Path, v Value) (bool, error) {
129+
if v.HasMark(mark) {
130+
found = true
131+
return false, io.EOF // arbitrary error just to stop the Walk early
132+
}
133+
return true, nil
134+
})
135+
return found
136+
}
137+
138+
// ValueMarksOfType returns an iterable sequence of any marks directly
139+
// associated with the given value that can be type-asserted to the given
140+
// type.
141+
func ValueMarksOfType[T any](v Value) iter.Seq[T] {
142+
return func(yield func(T) bool) {
143+
yieldValueMarksOfType(v, yield)
144+
}
145+
}
146+
147+
// ValueMarksOfTypeDeep is like [ValueMarksOfType] but also visits any values
148+
// nested inside the given value.
149+
//
150+
// The same value may be produced multiple times if multiple nested values are
151+
// marked with it.
152+
func ValueMarksOfTypeDeep[T any](v Value) iter.Seq[T] {
153+
return func(yield func(T) bool) {
154+
Walk(v, func(p Path, v Value) (bool, error) {
155+
if !yieldValueMarksOfType(v, yield) {
156+
return false, io.EOF // arbitrary error just to stop the Walk early
157+
}
158+
return true, nil
159+
})
160+
}
161+
}
162+
163+
func yieldValueMarksOfType[T any](v Value, yield func(T) bool) bool {
164+
mr, ok := v.v.(marker)
165+
if !ok {
166+
return true
167+
}
168+
for mark := range mr.marks {
169+
if v, ok := mark.(T); ok {
170+
if !yield(v) {
171+
return false
172+
}
173+
}
174+
}
175+
return true
176+
}
177+
122178
// ContainsMarked returns true if the receiving value or any value within it
123179
// is marked.
124180
//

cty/marks_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package cty
22

33
import (
44
"fmt"
5+
"slices"
56
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
69
)
710

811
func TestContainsMarked(t *testing.T) {
@@ -507,3 +510,57 @@ func TestReapplyMarks(t *testing.T) {
507510
t.Fatalf("Value changed re-applying marks\n1st: %#v\n2nd: %#v\n", first, second)
508511
}
509512
}
513+
514+
func TestHasMarkDeep(t *testing.T) {
515+
obj := ObjectVal(map[string]Value{
516+
"nested": ObjectVal(map[string]Value{
517+
"marked": True.Mark("boop"),
518+
}),
519+
})
520+
if !obj.HasMarkDeep("boop") {
521+
t.Error("did not find nested mark")
522+
}
523+
}
524+
525+
func TestValueMarksOfType(t *testing.T) {
526+
t.Run("shallow", func(t *testing.T) {
527+
obj := ObjectVal(map[string]Value{
528+
"nested": ObjectVal(map[string]Value{
529+
"marked 1": True.Mark("nested"),
530+
"marked 2": True.Mark(2),
531+
}),
532+
}).Mark("shallow").Mark(2)
533+
got := slices.Collect(ValueMarksOfType[string](obj))
534+
want := []string{"shallow"}
535+
if diff := cmp.Diff(want, got); diff != "" {
536+
t.Error("wrong result\n" + diff)
537+
}
538+
})
539+
t.Run("only nested", func(t *testing.T) {
540+
obj := ObjectVal(map[string]Value{
541+
"nested": ObjectVal(map[string]Value{
542+
"marked 1": True.Mark("nested"),
543+
"marked 2": True.Mark(2),
544+
}),
545+
})
546+
got := slices.Collect(ValueMarksOfType[string](obj))
547+
want := []string(nil)
548+
if diff := cmp.Diff(want, got); diff != "" {
549+
t.Error("wrong result\n" + diff)
550+
}
551+
})
552+
}
553+
554+
func TestValueMarksOfTypeDeep(t *testing.T) {
555+
obj := ObjectVal(map[string]Value{
556+
"nested": ObjectVal(map[string]Value{
557+
"marked 1": True.Mark("boop"),
558+
"marked 2": True.Mark(2),
559+
}),
560+
})
561+
got := slices.Collect(ValueMarksOfTypeDeep[string](obj))
562+
want := []string{"boop"}
563+
if diff := cmp.Diff(want, got); diff != "" {
564+
t.Error("wrong result\n" + diff)
565+
}
566+
}

0 commit comments

Comments
 (0)