this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

encoding/jsonschema: use unique item values in Generate

We now store all items canonicalized by structural equality
so that we can compare items with simple equality.

This makes it straightforward to use a map in `generator.makeStructItem`
rather using an triply nested DeepEquals loop.

We're still using `reflect.DeepEquals` as the fundamental source
of truth for equality for now, but we're leaving that for a future
change (either by writing custom equality methods/functions
or by writing some reflect-based equality code that's appropriate
for this scenario).

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I8de5d0d36eea8ab26da48a0af5ebbb35b47f32c8
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1224574
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+573 -390
+121 -120
encoding/jsonschema/generate.go
··· 18 18 "fmt" 19 19 "iter" 20 20 "maps" 21 - "reflect" 22 21 "regexp" 23 22 "slices" 24 23 "strings" ··· 76 75 } 77 76 78 77 g := &generator{ 79 - cfg: cfg, 80 - defs: make(map[string]item), 78 + cfg: cfg, 79 + defs: make(map[string]internItem), 80 + unique: newUniqueItems(), 81 81 } 82 82 item := g.makeItem(v) 83 - item = mergeAllOf(item) 84 - item = enumFromConst(item) 85 - expr := item.generate(g) 83 + item = mergeAllOf(item, g.unique) 84 + item = enumFromConst(item, g.unique) 85 + expr := item.Value().generate(g) 86 86 87 87 // Check if the result is a boolean literal 88 88 if lit, ok := expr.(*ast.BasicLit); ok && (lit.Kind == token.TRUE || lit.Kind == token.FALSE) { ··· 108 108 if len(g.defs) != 0 { 109 109 defFields := make([]ast.Decl, 0, len(g.defs)) 110 110 for _, name := range slices.Sorted(maps.Keys(g.defs)) { 111 - defFields = append(defFields, makeField(name, g.defs[name].generate(g))) 111 + defFields = append(defFields, makeField(name, g.defs[name].Value().generate(g))) 112 112 } 113 113 fields = append(fields, makeField("$defs", &ast.StructLit{Elts: defFields})) 114 114 } ··· 123 123 // mergeAllOf returns the item with adjacent itemAllOf nodes 124 124 // all merged into a single itemAllOf node with all 125 125 // the conjuncts in. 126 - func mergeAllOf(it item) item { 127 - switch it := it.(type) { 126 + func mergeAllOf(it internItem, u *uniqueItems) internItem { 127 + switch it1 := it.Value().(type) { 128 128 case *itemAllOf: 129 - it1 := &itemAllOf{ 130 - elems: make([]item, 0, len(it.elems)), 129 + it2 := &itemAllOf{ 130 + elems: make([]internItem, 0, len(it1.elems)), 131 131 } 132 - loop: 133 - for e := range siblings(it) { 132 + for e := range siblings(it1) { 134 133 // Remove elements that are entirely redundant. 135 - // Note: DeepEqual seems reasonable here because values are generally 136 - // small and the data structures are well-defined. We could 137 - // reconsider if these assumptions change. 138 134 // TODO we could unify itemType elements here, for example: 139 135 // allOf(itemType(number), itemType(integer)) -> itemType(integer) 140 - for _, e1 := range it1.elems { 141 - if reflect.DeepEqual(e1, e) { 142 - continue loop 143 - } 136 + if !slices.Contains(it2.elems, e) { 137 + it2.elems = append(it2.elems, mergeAllOf(e, u)) 144 138 } 145 - it1.elems = append(it1.elems, e.apply(mergeAllOf)) 146 139 } 147 - if len(it1.elems) == 1 { 148 - return it1.elems[0] 140 + if len(it2.elems) == 1 { 141 + return it2.elems[0] 149 142 } 150 - return it1 143 + return u.intern(it2) 151 144 default: 152 - return it.apply(mergeAllOf) 145 + return u.apply(it, mergeAllOf) 153 146 } 154 147 } 155 148 156 - func conjuncts(it item) iter.Seq[item] { 157 - return func(yield func(item) bool) { 158 - it1, ok := it.(*itemAllOf) 149 + func itemConjuncts(it internItem) iter.Seq[internItem] { 150 + return func(yield func(internItem) bool) { 151 + it1, ok := it.Value().(*itemAllOf) 159 152 if !ok { 160 153 yield(it) 161 154 return ··· 165 158 } 166 159 167 160 type elementsItem interface { 168 - elements() []item 161 + elements() []internItem 169 162 } 170 163 171 - func siblings[T elementsItem](it T) iter.Seq[item] { 172 - return func(yield func(item) bool) { 164 + func siblings[T elementsItem](it T) iter.Seq[internItem] { 165 + return func(yield func(internItem) bool) { 173 166 yieldSiblings(it, yield) 174 167 } 175 168 } 176 169 177 - func yieldSiblings[T elementsItem](it T, yield func(item) bool) bool { 170 + func yieldSiblings[T elementsItem](it T, yield func(internItem) bool) bool { 178 171 for _, e := range it.elements() { 179 - if ae, ok := e.(T); ok { 172 + if ae, ok := e.Value().(T); ok { 180 173 if !yieldSiblings(ae, yield) { 181 174 return false 182 175 } ··· 196 189 // anyOf(const("a"), const("b"), const("c")) 197 190 // -> 198 191 // enum("a", "b", "c") 199 - func enumFromConst(it item) item { 200 - switch it := it.(type) { 192 + func enumFromConst(it0 internItem, u *uniqueItems) internItem { 193 + switch it := it0.Value().(type) { 201 194 case *itemAnyOf: 202 - if slices.ContainsFunc(it.elems, func(it item) bool { 203 - _, ok := it.(*itemConst) 195 + if slices.ContainsFunc(it.elems, func(it internItem) bool { 196 + _, ok := it.Value().(*itemConst) 204 197 return !ok 205 198 }) { 206 199 // They're not all consts, so return as-is. 207 - return it 200 + return it0 208 201 } 209 202 // All items are const. We can make an enum from this. 210 203 // TODO this doesn't cover cases where there are some ··· 213 206 values: make([]ast.Expr, 0, len(it.elems)), 214 207 } 215 208 for _, e := range it.elems { 216 - it1.values = append(it1.values, e.(*itemConst).value) 209 + it1.values = append(it1.values, e.Value().(*itemConst).value) 217 210 } 218 - return it1 211 + return u.intern(it1) 219 212 default: 220 - return it.apply(enumFromConst) 213 + return u.apply(it0, enumFromConst) 221 214 } 222 215 } 223 216 ··· 229 222 230 223 // defs holds any definitions made during the course of generation, 231 224 // indexed by the entry name within the `$defs` field. 232 - defs map[string]item 225 + defs map[string]internItem 226 + 227 + // unique ensures that all items are comparable with 228 + // simple equality. 229 + unique *uniqueItems 233 230 } 234 231 235 232 func (g *generator) addError(pos cue.Value, err error) { ··· 243 240 244 241 // makeItem returns an item representing the JSON Schema 245 242 // for v in naive form. 246 - func (g *generator) makeItem(v cue.Value) item { 243 + func (g *generator) makeItem(v cue.Value) internItem { 244 + return g.unique.intern(g.makeItem0(v)) 245 + } 246 + 247 + func (g *generator) makeItem0(v cue.Value) item { 247 248 op, args := v.Expr() 248 249 switch op { 249 250 case cue.NoOp, cue.SelectorOp: ··· 261 262 // Already defined. 262 263 return ref 263 264 } 264 - g.defs[name] = nil // Prevent infinite loops on cycles. 265 + g.defs[name] = internItem{} // Prevent infinite loops on cycles. 265 266 g.defs[name] = g.makeItem(v.Eval()) 266 267 return ref 267 268 } ··· 280 281 g.addError(args[0], err) 281 282 return &itemFalse{} 282 283 } 283 - var m item = &itemPattern{ 284 + m := g.unique.intern(&itemPattern{ 284 285 regexp: re, 285 - } 286 + }) 286 287 if op == cue.NotRegexMatchOp { 287 - m = &itemNot{ 288 + m = g.unique.intern(&itemNot{ 288 289 elem: m, 289 - } 290 + }) 290 291 } 291 292 return &itemAllOf{ 292 - elems: []item{ 293 - &itemType{ 293 + elems: []internItem{ 294 + g.unique.intern(&itemType{ 294 295 kinds: []string{"string"}, 295 - }, 296 + }), 296 297 m, 297 298 }, 298 299 } ··· 313 314 g.addError(args[0], fmt.Errorf("expected expression from Syntax, got %T", syntax)) 314 315 return &itemFalse{} 315 316 } 316 - it := &itemConst{ 317 + it := g.unique.intern(&itemConst{ 317 318 value: expr, 318 - } 319 + }) 319 320 if op == cue.EqualOp { 320 - return it 321 + return it.Value() 321 322 } 322 323 return &itemNot{ 323 324 elem: it, ··· 338 339 return &itemTrue{} 339 340 } 340 341 return &itemAllOf{ 341 - elems: []item{ 342 - &itemBounds{ 342 + elems: []internItem{ 343 + g.unique.intern(&itemBounds{ 343 344 constraint: op, 344 345 n: n, 345 - }, 346 - &itemType{ 346 + }), 347 + g.unique.intern(&itemType{ 347 348 kinds: []string{"number"}, 348 - }, 349 + }), 349 350 }, 350 351 } 351 352 case cue.StringKind: ··· 381 382 case cue.ListKind: 382 383 it = g.makeListItem(v) 383 384 } 384 - var elems []item 385 + var elems []internItem 385 386 if kinds := cueKindToJSONSchemaTypes(kind); len(kinds) > 0 { 386 - elems = append(elems, &itemType{ 387 + elems = append(elems, g.unique.intern(&itemType{ 387 388 kinds: kinds, 388 - }) 389 + })) 389 390 } 390 391 if it != nil { 391 - elems = append(elems, it) 392 + elems = append(elems, g.unique.intern(it)) 392 393 } 393 394 switch len(elems) { 394 395 case 0: 395 396 return &itemTrue{} 396 397 case 1: 397 - return elems[0] 398 + return elems[0].Value() 398 399 } 399 400 return &itemAllOf{ 400 401 elems: elems, ··· 503 504 return &itemFalse{} 504 505 case "close": 505 506 // We'll detect closedness with IsClosed further down. 506 - return g.makeItem(v.Eval()) 507 + return g.makeItem(v.Eval()).Value() 507 508 case "strings.MinRunes": 508 509 if len(args) != 2 { 509 510 g.addError(v, fmt.Errorf("strings.MinRunes expects 1 argument, got %d", len(args)-1)) ··· 515 516 return &itemFalse{} 516 517 } 517 518 return &itemAllOf{ 518 - elems: []item{ 519 - &itemType{kinds: []string{"string"}}, 520 - &itemLengthBounds{constraint: cue.GreaterThanEqualOp, n: int(n)}, 519 + elems: []internItem{ 520 + g.unique.intern(&itemType{kinds: []string{"string"}}), 521 + g.unique.intern(&itemLengthBounds{constraint: cue.GreaterThanEqualOp, n: int(n)}), 521 522 }, 522 523 } 523 524 ··· 532 533 return &itemFalse{} 533 534 } 534 535 return &itemAllOf{ 535 - elems: []item{ 536 - &itemType{kinds: []string{"string"}}, 537 - &itemLengthBounds{constraint: cue.LessThanEqualOp, n: int(n)}, 536 + elems: []internItem{ 537 + g.unique.intern(&itemType{kinds: []string{"string"}}), 538 + g.unique.intern(&itemLengthBounds{constraint: cue.LessThanEqualOp, n: int(n)}), 538 539 }, 539 540 } 540 541 ··· 549 550 return &itemFalse{} 550 551 } 551 552 return &itemAllOf{ 552 - elems: []item{ 553 - &itemType{kinds: []string{"number"}}, 554 - &itemMultipleOf{n: n}, 553 + elems: []internItem{ 554 + g.unique.intern(&itemType{kinds: []string{"number"}}), 555 + g.unique.intern(&itemMultipleOf{n: n}), 555 556 }, 556 557 } 557 558 ··· 582 583 return &itemType{kinds: []string{"string"}} 583 584 } 584 585 return &itemAllOf{ 585 - elems: []item{ 586 - &itemType{kinds: []string{"string"}}, 587 - &itemFormat{format: format}, 586 + elems: []internItem{ 587 + g.unique.intern(&itemType{kinds: []string{"string"}}), 588 + g.unique.intern(&itemFormat{format: format}), 588 589 }, 589 590 } 590 591 case "list.MinItems", "list.MaxItems": ··· 604 605 constraint = cue.LessThanEqualOp 605 606 } 606 607 return &itemAllOf{ 607 - elems: []item{ 608 - &itemType{kinds: []string{"array"}}, 609 - &itemItemsBounds{constraint: constraint, n: int(n)}, 608 + elems: []internItem{ 609 + g.unique.intern(&itemType{kinds: []string{"array"}}), 610 + g.unique.intern(&itemItemsBounds{constraint: constraint, n: int(n)}), 610 611 }, 611 612 } 612 613 ··· 679 680 // Get the schema element from the second argument 680 681 // Check if it's bottom first (which represents "contains: false") 681 682 // to avoid adding errors to the generator. 682 - var elem item 683 + var elem internItem 683 684 elemVal := args[2] 684 685 if err := elemVal.Err(); err != nil { 685 686 // Bottom value - represents "contains: false" 686 - elem = &itemFalse{} 687 + elem = g.unique.intern(&itemFalse{}) 687 688 } else { 688 689 elem = g.makeItem(elemVal) 689 690 } 690 691 691 692 return &itemAllOf{ 692 - elems: []item{ 693 - &itemType{kinds: []string{"array"}}, 694 - &itemContains{elem: elem, min: minVal, max: maxVal}, 693 + elems: []internItem{ 694 + g.unique.intern(&itemType{kinds: []string{"array"}}), 695 + g.unique.intern(&itemContains{elem: elem, min: minVal, max: maxVal}), 695 696 }, 696 697 } 697 698 ··· 708 709 709 710 constraintVal, listVal := args[1], args[2] 710 711 711 - var items []item 712 + var items []internItem 712 713 for i := 0; ; i++ { 713 714 // Unfortunately https://github.com/cue-lang/cue/issues/4132 means 714 715 // that we cannot iterate over elements of the list with listVal.List ··· 800 801 801 802 func (g *generator) makeStructItem(v cue.Value) item { 802 803 props := itemProperties{ 803 - properties: make(map[string]item), 804 + properties: make(map[string]internItem), 804 805 } 805 806 806 - explicitlyOpen := func(constraint item) { 807 + explicitlyOpen := func(constraint internItem) { 807 808 props.additionalProperties = constraint 808 - if _, ok := constraint.(*itemTrue); ok && !g.cfg.ExplicitOpen { 809 + if _, ok := constraint.Value().(*itemTrue); ok && !g.cfg.ExplicitOpen { 809 810 // additionalProperties: true is a no-op in JSON Schema in general 810 811 // so omit it unless we're explicitly opening up schemas. 811 - props.additionalProperties = nil 812 + props.additionalProperties = internItem{} 812 813 } 813 814 } 814 815 ··· 817 818 // All fields are explicitly allowed (either with `...` or `[_]: T`) 818 819 explicitlyOpen(g.makeItem(ellipsis)) 819 820 } else if v.IsClosed() && !g.cfg.ExplicitOpen { 820 - props.additionalProperties = &itemFalse{} 821 + props.additionalProperties = g.unique.intern(&itemFalse{}) 821 822 } 822 823 823 824 iter, err := v.Fields(cue.Optional(true), cue.Patterns(true)) ··· 826 827 return &itemFalse{} 827 828 } 828 829 type pat struct { 829 - pattern *regexp.Regexp 830 - constraint item 830 + pattern *regexp.Regexp 831 + constraints map[internItem]bool 831 832 } 832 833 var patternConstraints []pat 833 834 outer: ··· 838 839 re, ok := regexpForValue(sel.Pattern()) 839 840 if ok { 840 841 if props.patternProperties == nil { 841 - props.patternProperties = make(map[string]item) 842 + props.patternProperties = make(map[string]internItem) 842 843 } 843 844 constraint := g.makeItem(iter.Value()) 844 845 props.patternProperties[re.String()] = constraint 845 - patternConstraints = append(patternConstraints, pat{re, constraint}) 846 + p := pat{ 847 + pattern: re, 848 + constraints: make(map[internItem]bool), 849 + } 850 + for c := range itemConjuncts(constraint) { 851 + p.constraints[c] = true 852 + } 853 + patternConstraints = append(patternConstraints, p) 846 854 } else { 847 855 // We can't express the constraint in JSON Schema, and it 848 856 // might cover any number of possible labels, so the 849 857 // only thing we can do is treat the whole thing as explicitly 850 858 // open. 851 - explicitlyOpen(&itemTrue{}) 859 + explicitlyOpen(g.unique.intern(&itemTrue{})) 852 860 } 853 861 continue outer 854 862 case cue.OptionalConstraint: ··· 874 882 // This has the potential to remove explicit constraints on the fields 875 883 // themselves, but this will not change behavior, just result in a slightly 876 884 // smaller resulting schema. 877 - allof, ok := propItem.(*itemAllOf) 885 + allof, ok := propItem.Value().(*itemAllOf) 878 886 if !ok || len(allof.elems) <= 1 { 879 887 // No possibility of removing any conjuncts. 880 888 props.properties[fieldName] = propItem 881 889 continue 882 890 } 883 - var elems []item 891 + var elems []internItem 884 892 for _, c := range patternConstraints { 885 893 if !c.pattern.MatchString(fieldName) { 886 894 continue ··· 891 899 // We've found a pattern constraint that unifies with the field name. 892 900 // Its constraint will have been added to this property's constraints 893 901 // but are redundant, so remove them. 894 - elems = slices.DeleteFunc(elems, func(it item) bool { 895 - // TODO this is unacceptably inefficient. We should fix that 896 - // by making comparisons more efficient somehow. 897 - for itc := range conjuncts(c.constraint) { 898 - if reflect.DeepEqual(it, itc) { 899 - return true 900 - } 901 - } 902 - return false 902 + elems = slices.DeleteFunc(elems, func(it internItem) bool { 903 + return c.constraints[it] 903 904 }) 904 905 } 905 906 if len(elems) == 0 { 906 - propItem = &itemTrue{} 907 + propItem = g.unique.intern(&itemTrue{}) 907 908 } else { 908 - propItem = &itemAllOf{elems: elems} 909 + propItem = g.unique.intern(&itemAllOf{elems: elems}) 909 910 } 910 911 props.properties[fieldName] = propItem 911 912 } ··· 944 945 g.addErrorf(v, "cannot extract concrete list length from %v: %v", v, err) 945 946 } 946 947 } 947 - prefix := make([]item, n) 948 + prefix := make([]internItem, n) 948 949 for i := range n { 949 950 elem := v.LookupPath(cue.MakePath(cue.Index(i))) 950 951 if !elem.Exists() { ··· 954 955 prefix[i] = g.makeItem(elem) 955 956 } 956 957 a := &itemAllOf{ 957 - elems: []item{&itemType{kinds: []string{"array"}}}, 958 + elems: []internItem{g.unique.intern(&itemType{kinds: []string{"array"}})}, 958 959 } 959 960 items := &itemItems{} 960 961 if len(prefix) > 0 { 961 - a.elems = append(a.elems, &itemLengthBounds{ 962 + a.elems = append(a.elems, g.unique.intern(&itemLengthBounds{ 962 963 constraint: cue.GreaterThanEqualOp, 963 964 n: len(prefix), 964 - }) 965 + })) 965 966 items.prefix = prefix 966 967 } 967 968 if ellipsis.Exists() { 968 969 items.rest = trueAsNil(g.makeItem(ellipsis)) 969 970 } else { 970 - a.elems = append(a.elems, &itemLengthBounds{ 971 + a.elems = append(a.elems, g.unique.intern(&itemLengthBounds{ 971 972 constraint: cue.LessThanEqualOp, 972 973 n: len(prefix), 973 - }) 974 + })) 974 975 } 975 - if items.rest != nil || len(items.prefix) > 0 { 976 - a.elems = append(a.elems, items) 976 + if items.rest.Value() != nil || len(items.prefix) > 0 { 977 + a.elems = append(a.elems, g.unique.intern(items)) 977 978 } 978 979 return a 979 980 } ··· 1058 1059 1059 1060 // trueAsNil returns the nil item if the item 1060 1061 // is *itemTrue (top). 1061 - func trueAsNil(it item) item { 1062 - if _, ok := it.(*itemTrue); ok { 1063 - return nil 1062 + func trueAsNil(it internItem) internItem { 1063 + if _, ok := it.Value().(*itemTrue); ok { 1064 + return internItem{} 1064 1065 } 1065 1066 return it 1066 1067 }
+312 -120
encoding/jsonschema/generate_items.go
··· 17 17 import ( 18 18 "cmp" 19 19 "fmt" 20 + "hash/maphash" 20 21 "maps" 22 + "reflect" 21 23 "slices" 22 24 23 25 "cuelang.org/go/cue" 24 26 "cuelang.org/go/cue/ast" 27 + "cuelang.org/go/cue/format" 25 28 "cuelang.org/go/cue/token" 29 + "cuelang.org/go/internal/anyunique" 26 30 ) 27 31 28 32 // TODO use a defined order when keywords are marshaled ··· 34 38 // generate returns the AST representation of this item. 35 39 generate(g *generator) ast.Expr 36 40 37 - // apply invokes f on each sub-item, replacing each with the 38 - // item returned, and returns the new item (or the same if nothing has changed). 39 - // Note that it does not call f on the item itself. 40 - apply(f func(item) item) item 41 + // apply invokes f on each sub-item, replacing each with the item 42 + // returned, and returns the new item (or the same if nothing has 43 + // changed). Note that it does not call f on the item itself. It can 44 + // use u to create new unique items. 45 + apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item 46 + 47 + // hash writes the hash of the item to h; it should use u.writeHash 48 + // to write the hash value for any items it contains. 49 + hash(h *maphash.Hash, u *uniqueItems) 41 50 } 42 51 43 52 // itemTrue represents a schema that accepts any value (true schema) ··· 47 56 return ast.NewBool(true) 48 57 } 49 58 50 - func (i *itemTrue) apply(f func(item) item) item { 59 + func (it *itemTrue) hash(h *maphash.Hash, u *uniqueItems) { 60 + } 61 + 62 + func (i *itemTrue) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 51 63 return i 52 64 } 53 65 ··· 58 70 return ast.NewBool(false) 59 71 } 60 72 61 - func (i *itemFalse) apply(f func(item) item) item { 73 + func (it *itemFalse) hash(h *maphash.Hash, u *uniqueItems) { 74 + } 75 + 76 + func (i *itemFalse) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 62 77 return i 63 78 } 64 79 65 80 // itemAllOf represents an allOf combinator 66 81 type itemAllOf struct { 67 - elems []item 82 + elems []internItem 68 83 } 69 84 70 - func (i *itemAllOf) add(it item) { 85 + func (it *itemAllOf) hash(h *maphash.Hash, u *uniqueItems) { 86 + for _, it := range it.elems { 87 + u.writeHash(h, it) 88 + } 89 + } 90 + 91 + func (i *itemAllOf) add(it internItem) { 71 92 i.elems = append(i.elems, it) 72 93 } 73 94 74 95 var _ elementsItem = (*itemAllOf)(nil) 75 96 76 97 // elements implements [elementsItem]. 77 - func (i *itemAllOf) elements() []item { 98 + func (i *itemAllOf) elements() []internItem { 78 99 return i.elems 79 100 } 80 101 ··· 88 109 finalFieldNames := make(map[string]bool) 89 110 90 111 for _, e := range i.elems { 91 - expr := e.generate(g) 112 + expr := e.Value().generate(g) 92 113 if lit, ok := expr.(*ast.BasicLit); ok { 93 114 switch lit.Kind { 94 115 case token.TRUE: ··· 152 173 return singleKeyword("allOf", ast.NewList(unmerged...)) 153 174 } 154 175 155 - func (i *itemAllOf) apply(f func(item) item) item { 156 - elems, changed := applyElems(i.elems, f) 176 + func (i *itemAllOf) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 177 + elems, changed := applyElems(i.elems, f, u) 157 178 if !changed { 158 179 return i 159 180 } ··· 162 183 163 184 // itemOneOf represents a oneOf combinator 164 185 type itemOneOf struct { 165 - elems []item 186 + elems []internItem 187 + } 188 + 189 + func (it *itemOneOf) hash(h *maphash.Hash, u *uniqueItems) { 190 + for _, it := range it.elems { 191 + u.writeHash(h, it) 192 + } 166 193 } 167 194 168 195 func (i *itemOneOf) generate(g *generator) ast.Expr { 169 196 return singleKeyword("oneOf", generateList(g, i.elems)) 170 197 } 171 198 172 - func (i *itemOneOf) apply(f func(item) item) item { 173 - elems, changed := applyElems(i.elems, f) 199 + func (i *itemOneOf) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 200 + elems, changed := applyElems(i.elems, f, u) 174 201 if !changed { 175 202 return i 176 203 } ··· 180 207 var _ elementsItem = (*itemOneOf)(nil) 181 208 182 209 // elements implements [elementsItem]. 183 - func (i *itemOneOf) elements() []item { 210 + func (i *itemOneOf) elements() []internItem { 184 211 return i.elems 185 212 } 186 213 187 214 // itemAnyOf represents an anyOf combinator 188 215 type itemAnyOf struct { 189 - elems []item 216 + elems []internItem 217 + } 218 + 219 + func (it *itemAnyOf) hash(h *maphash.Hash, u *uniqueItems) { 220 + for _, it := range it.elems { 221 + u.writeHash(h, it) 222 + } 190 223 } 191 224 192 225 func (i *itemAnyOf) generate(g *generator) ast.Expr { 193 226 return singleKeyword("anyOf", generateList(g, i.elems)) 194 227 } 195 228 196 - func (i *itemAnyOf) apply(f func(item) item) item { 197 - elems, changed := applyElems(i.elems, f) 229 + func (i *itemAnyOf) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 230 + elems, changed := applyElems(i.elems, f, u) 198 231 if !changed { 199 232 return i 200 233 } 201 234 return &itemAnyOf{elems: elems} 202 235 } 203 236 204 - var _ elementsItem = (*itemOneOf)(nil) 237 + var _ elementsItem = (*itemAnyOf)(nil) 205 238 206 239 // elements implements [elementsItem]. 207 - func (i *itemAnyOf) elements() []item { 240 + func (i *itemAnyOf) elements() []internItem { 208 241 return i.elems 209 242 } 210 243 211 244 // itemNot represents a not combinator 212 245 type itemNot struct { 213 - elem item 246 + elem internItem 247 + } 248 + 249 + func (it *itemNot) hash(h *maphash.Hash, u *uniqueItems) { 250 + u.writeHash(h, it.elem) 214 251 } 215 252 216 253 func (i *itemNot) generate(g *generator) ast.Expr { 217 - return singleKeyword("not", i.elem.generate(g)) 254 + return singleKeyword("not", i.elem.Value().generate(g)) 218 255 } 219 256 220 - func (i *itemNot) apply(f func(item) item) item { 221 - elem := f(i.elem) 257 + func (i *itemNot) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 258 + elem := f(i.elem, u) 222 259 if elem == i.elem { 223 260 return i 224 261 } ··· 231 268 value ast.Expr 232 269 } 233 270 271 + func (it *itemConst) hash(h *maphash.Hash, u *uniqueItems) { 272 + writeExprHash(h, it.value) 273 + } 274 + 234 275 func (i *itemConst) generate(g *generator) ast.Expr { 235 276 return singleKeyword("const", i.value) 236 277 } 237 278 238 - func (i *itemConst) apply(f func(item) item) item { 279 + func (i *itemConst) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 239 280 return i 240 281 } 241 282 ··· 245 286 values []ast.Expr 246 287 } 247 288 289 + func (it *itemEnum) hash(h *maphash.Hash, u *uniqueItems) { 290 + for _, v := range it.values { 291 + writeExprHash(h, v) 292 + } 293 + } 294 + 248 295 func (i *itemEnum) generate(g *generator) ast.Expr { 249 296 return singleKeyword("enum", ast.NewList(i.values...)) 250 297 } 251 298 252 - func (i *itemEnum) apply(f func(item) item) item { 299 + func (i *itemEnum) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 253 300 return i 254 301 } 255 302 ··· 257 304 defName string 258 305 } 259 306 307 + func (it *itemRef) hash(h *maphash.Hash, u *uniqueItems) { 308 + h.WriteString(it.defName) 309 + } 310 + 260 311 func (i *itemRef) generate(g *generator) ast.Expr { 261 312 return singleKeyword("$ref", ast.NewString("#/$defs/"+i.defName)) 262 313 } 263 314 264 - func (i *itemRef) apply(f func(item) item) item { 315 + func (i *itemRef) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 265 316 return i 266 317 } 267 318 ··· 270 321 kinds []string 271 322 } 272 323 324 + func (it *itemType) hash(h *maphash.Hash, u *uniqueItems) { 325 + for _, k := range it.kinds { 326 + h.WriteString(k) 327 + } 328 + } 329 + 273 330 func (i *itemType) generate(g *generator) ast.Expr { 274 331 if len(i.kinds) == 1 { 275 332 return singleKeyword("type", ast.NewString(i.kinds[0])) ··· 281 338 return singleKeyword("type", ast.NewList(exprs...)) 282 339 } 283 340 284 - func (i *itemType) apply(f func(item) item) item { 341 + func (i *itemType) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 285 342 return i 286 343 } 287 344 ··· 290 347 format string 291 348 } 292 349 350 + func (it *itemFormat) hash(h *maphash.Hash, u *uniqueItems) { 351 + h.WriteString(it.format) 352 + } 353 + 293 354 func (i *itemFormat) generate(g *generator) ast.Expr { 294 355 return singleKeyword("format", ast.NewString(i.format)) 295 356 } 296 357 297 - func (i *itemFormat) apply(f func(item) item) item { 358 + func (i *itemFormat) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 298 359 return i 299 360 } 300 361 ··· 303 364 regexp string 304 365 } 305 366 367 + func (it *itemPattern) hash(h *maphash.Hash, u *uniqueItems) { 368 + h.WriteString(it.regexp) 369 + } 370 + 306 371 func (i *itemPattern) generate(g *generator) ast.Expr { 307 372 return singleKeyword("pattern", ast.NewString(i.regexp)) 308 373 } 309 374 310 - func (i *itemPattern) apply(f func(item) item) item { 375 + func (i *itemPattern) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 311 376 return i 312 377 } 313 378 ··· 319 384 n float64 320 385 } 321 386 387 + func (it *itemBounds) hash(h *maphash.Hash, u *uniqueItems) { 388 + maphash.WriteComparable(h, it.constraint) 389 + maphash.WriteComparable(h, it.n) 390 + } 391 + 322 392 func (i *itemBounds) generate(g *generator) ast.Expr { 323 393 var keyword string 324 394 switch i.constraint { ··· 336 406 return singleKeyword(keyword, ast.NewLit(token.FLOAT, fmt.Sprint(i.n))) 337 407 } 338 408 339 - func (i *itemBounds) apply(f func(item) item) item { 409 + func (i *itemBounds) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 340 410 return i 341 411 } 342 412 ··· 345 415 n float64 346 416 } 347 417 418 + func (it *itemMultipleOf) hash(h *maphash.Hash, u *uniqueItems) { 419 + maphash.WriteComparable(h, it.n) 420 + } 421 + 348 422 func (i *itemMultipleOf) generate(g *generator) ast.Expr { 349 423 return singleKeyword("multipleOf", ast.NewLit(token.FLOAT, fmt.Sprint(i.n))) 350 424 } 351 425 352 - func (i *itemMultipleOf) apply(f func(item) item) item { 426 + func (i *itemMultipleOf) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 353 427 return i 354 428 } 355 429 ··· 359 433 n int 360 434 } 361 435 436 + func (it *itemLengthBounds) hash(h *maphash.Hash, u *uniqueItems) { 437 + maphash.WriteComparable(h, it.constraint) 438 + maphash.WriteComparable(h, it.n) 439 + } 440 + 362 441 func (i *itemLengthBounds) generate(g *generator) ast.Expr { 363 442 var keyword string 364 443 switch i.constraint { ··· 373 452 return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) 374 453 } 375 454 376 - func (i *itemLengthBounds) apply(f func(item) item) item { 455 + func (i *itemLengthBounds) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 377 456 return i 378 457 } 379 458 ··· 383 462 n int 384 463 } 385 464 465 + func (it *itemItemsBounds) hash(h *maphash.Hash, u *uniqueItems) { 466 + maphash.WriteComparable(h, it.constraint) 467 + maphash.WriteComparable(h, it.n) 468 + } 469 + 386 470 func (i *itemItemsBounds) generate(g *generator) ast.Expr { 387 471 var keyword string 388 472 switch i.constraint { ··· 396 480 return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) 397 481 } 398 482 399 - func (i *itemItemsBounds) apply(f func(item) item) item { 483 + func (i *itemItemsBounds) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 400 484 return i 401 485 } 402 486 ··· 406 490 n int 407 491 } 408 492 493 + func (it *itemPropertyBounds) hash(h *maphash.Hash, u *uniqueItems) { 494 + maphash.WriteComparable(h, it.constraint) 495 + maphash.WriteComparable(h, it.n) 496 + } 497 + 409 498 func (i *itemPropertyBounds) generate(g *generator) ast.Expr { 410 499 var keyword string 411 500 switch i.constraint { ··· 419 508 return singleKeyword(keyword, ast.NewLit(token.INT, fmt.Sprint(i.n))) 420 509 } 421 510 422 - func (i *itemPropertyBounds) apply(f func(item) item) item { 511 + func (i *itemPropertyBounds) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 423 512 return i 424 513 } 425 514 426 515 // itemItems represents the items and prefixItems constraint for arrays. 427 516 type itemItems struct { 428 517 // known prefix. 429 - prefix []item 518 + prefix []internItem 430 519 // all elements beyond the prefix. 431 - rest item 520 + rest internItem 521 + } 522 + 523 + func (it *itemItems) hash(h *maphash.Hash, u *uniqueItems) { 524 + for _, p := range it.prefix { 525 + u.writeHash(h, p) 526 + } 527 + u.writeHash(h, it.rest) 432 528 } 433 529 434 530 func (i *itemItems) generate(g *generator) ast.Expr { ··· 436 532 if len(i.prefix) > 0 { 437 533 items := make([]ast.Expr, len(i.prefix)) 438 534 for i, e := range i.prefix { 439 - items[i] = e.generate(g) 535 + items[i] = e.Value().generate(g) 440 536 } 441 537 fields = append(fields, makeField("prefixItems", &ast.ListLit{ 442 538 Elts: items, 443 539 })) 444 540 } 445 - if i.rest != nil { 446 - fields = append(fields, makeField("items", i.rest.generate(g))) 541 + if i.rest.Value() != nil { 542 + fields = append(fields, makeField("items", i.rest.Value().generate(g))) 447 543 } 448 544 return makeSchemaStructLit(fields...) 449 545 } 450 546 451 - func (i *itemItems) apply(f func(item) item) item { 547 + func (i *itemItems) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 452 548 rest := i.rest 453 - if rest != nil { 454 - rest = f(rest) 549 + if rest.Value() != nil { 550 + rest = f(rest, u) 455 551 } 456 - prefix, changed := applyElems(i.prefix, f) 552 + prefix, changed := applyElems(i.prefix, f, u) 457 553 if !changed && rest == i.rest { 458 554 return i 459 555 } ··· 462 558 463 559 // itemContains represents a contains constraint for arrays 464 560 type itemContains struct { 465 - elem item 561 + elem internItem 466 562 min *int64 467 563 max *int64 468 564 } 469 565 566 + func (it *itemContains) hash(h *maphash.Hash, u *uniqueItems) { 567 + u.writeHash(h, it.elem) 568 + maphash.WriteComparable(h, it.min == nil) 569 + if it.min != nil { 570 + maphash.WriteComparable(h, *it.min) 571 + } 572 + maphash.WriteComparable(h, it.max == nil) 573 + if it.max != nil { 574 + maphash.WriteComparable(h, *it.max) 575 + } 576 + } 577 + 470 578 func (i *itemContains) generate(g *generator) ast.Expr { 471 - fields := []ast.Decl{makeField("contains", i.elem.generate(g))} 579 + fields := []ast.Decl{makeField("contains", i.elem.Value().generate(g))} 472 580 if i.min != nil { 473 581 fields = append(fields, makeField("minContains", ast.NewLit(token.INT, fmt.Sprint(*i.min)))) 474 582 } ··· 478 586 return makeSchemaStructLit(fields...) 479 587 } 480 588 481 - func (i *itemContains) apply(f func(item) item) item { 482 - elem := f(i.elem) 589 + func (i *itemContains) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 590 + elem := f(i.elem, u) 483 591 if elem == i.elem { 484 592 return i 485 593 } ··· 494 602 495 603 // itemProperties represents object properties and associated keywords. 496 604 type itemProperties struct { 497 - properties map[string]item 605 + properties map[string]internItem 498 606 required []string 499 - additionalProperties item 500 - patternProperties map[string]item 607 + additionalProperties internItem 608 + patternProperties map[string]internItem 609 + } 610 + 611 + func (it *itemProperties) hash(h *maphash.Hash, u *uniqueItems) { 612 + writeMapHash(h, it.properties, u) 613 + for _, name := range slices.Sorted(slices.Values(it.required)) { 614 + h.WriteString(name) 615 + } 616 + u.writeHash(h, it.additionalProperties) 617 + writeMapHash(h, it.patternProperties, u) 501 618 } 502 619 503 620 func (i *itemProperties) generate(g *generator) ast.Expr { 504 621 propFields := make([]ast.Decl, 0, len(i.properties)) 505 622 for name, it := range i.properties { 506 - propFields = append(propFields, makeField(name, it.generate(g))) 623 + propFields = append(propFields, makeField(name, it.Value().generate(g))) 507 624 } 508 625 slices.SortFunc(propFields, func(a, b ast.Decl) int { 509 626 return cmp.Compare(fieldLabel(a), fieldLabel(b)) ··· 517 634 } 518 635 fields = append(fields, makeField("required", ast.NewList(reqExprs...))) 519 636 } 520 - if i.additionalProperties != nil { 521 - fields = append(fields, makeField("additionalProperties", i.additionalProperties.generate(g))) 637 + if i.additionalProperties.Value() != nil { 638 + fields = append(fields, makeField("additionalProperties", i.additionalProperties.Value().generate(g))) 522 639 } 523 640 if len(i.patternProperties) > 0 { 524 641 pp := &ast.StructLit{} 525 642 for _, p := range slices.Sorted(maps.Keys(i.patternProperties)) { 526 - pp.Elts = append(pp.Elts, makeField(p, i.patternProperties[p].generate(g))) 643 + pp.Elts = append(pp.Elts, makeField(p, i.patternProperties[p].Value().generate(g))) 527 644 } 528 645 fields = append(fields, makeField("patternProperties", pp)) 529 646 } 530 647 return makeSchemaStructLit(fields...) 531 648 } 532 649 533 - func (i *itemProperties) apply(f func(item) item) item { 534 - properties, changed0 := applyMap(i.properties, f) 535 - patternProperties, changed1 := applyMap(i.patternProperties, f) 650 + func (i *itemProperties) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 651 + properties, changed0 := applyMap(i.properties, f, u) 652 + patternProperties, changed1 := applyMap(i.patternProperties, f, u) 536 653 changed := changed0 || changed1 537 654 additionalProperties := i.additionalProperties 538 - if additionalProperties != nil { 539 - if ap := f(additionalProperties); ap != additionalProperties { 655 + if additionalProperties.Value() != nil { 656 + if ap := f(additionalProperties, u); ap != additionalProperties { 540 657 additionalProperties = ap 541 658 changed = true 542 659 } ··· 552 669 } 553 670 } 554 671 555 - func applyMap(m map[string]item, f func(item) item) (map[string]item, bool) { 556 - var m1 map[string]item 557 - for key, e := range m { 558 - e1 := f(e) 559 - if e1 == e { 560 - continue 561 - } 562 - if m1 == nil { 563 - m1 = make(map[string]item) 564 - } 565 - m1[key] = e1 566 - } 567 - if m1 == nil { 568 - return m, false 569 - } 570 - if len(m1) == len(m) { 571 - return m1, true 572 - } 573 - for key, e := range m { 574 - if _, ok := m1[key]; !ok { 575 - m1[key] = e 576 - } 577 - } 578 - return m1, true 579 - } 580 - 581 - func applyElems(elems []item, f func(item) item) ([]item, bool) { 582 - changed := false 583 - for i, e := range elems { 584 - e1 := f(e) 585 - if e1 == e { 586 - continue 587 - } 588 - if !changed { 589 - elems = slices.Clone(elems) 590 - changed = true 591 - } 592 - elems[i] = e1 593 - } 594 - return elems, changed 595 - } 596 - 597 672 // itemIfThenElse represents if/then/else constraints 598 673 type itemIfThenElse struct { 599 - ifElem item 600 - thenElem item 601 - elseElem item 674 + ifElem internItem 675 + thenElem internItem 676 + elseElem internItem 677 + } 678 + 679 + func (it *itemIfThenElse) hash(h *maphash.Hash, u *uniqueItems) { 680 + u.writeHash(h, it.ifElem) 681 + u.writeHash(h, it.thenElem) 682 + u.writeHash(h, it.elseElem) 602 683 } 603 684 604 685 func (i *itemIfThenElse) generate(g *generator) ast.Expr { 605 - fields := []ast.Decl{makeField("if", i.ifElem.generate(g))} 606 - if i.thenElem != nil { 607 - fields = append(fields, makeField("then", i.thenElem.generate(g))) 686 + fields := []ast.Decl{makeField("if", i.ifElem.Value().generate(g))} 687 + if i.thenElem.Value() != nil { 688 + fields = append(fields, makeField("then", i.thenElem.Value().generate(g))) 608 689 } 609 - if i.elseElem != nil { 610 - fields = append(fields, makeField("else", i.elseElem.generate(g))) 690 + if i.elseElem.Value() != nil { 691 + fields = append(fields, makeField("else", i.elseElem.Value().generate(g))) 611 692 } 612 693 return makeSchemaStructLit(fields...) 613 694 } 614 695 615 - func (i *itemIfThenElse) apply(f func(item) item) item { 616 - ifElem := f(i.ifElem) 617 - var thenElem, elseElem item 618 - if i.thenElem != nil { 619 - thenElem = f(i.thenElem) 696 + func (i *itemIfThenElse) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 697 + ifElem := f(i.ifElem, u) 698 + var thenElem, elseElem internItem 699 + if i.thenElem.Value() != nil { 700 + thenElem = f(i.thenElem, u) 620 701 } 621 - if i.elseElem != nil { 622 - elseElem = f(i.elseElem) 702 + if i.elseElem.Value() != nil { 703 + elseElem = f(i.elseElem, u) 623 704 } 624 705 625 706 if ifElem == i.ifElem && thenElem == i.thenElem && elseElem == i.elseElem { ··· 628 709 return &itemIfThenElse{ifElem: ifElem, thenElem: thenElem, elseElem: elseElem} 629 710 } 630 711 631 - func generateList(g *generator, items []item) ast.Expr { 712 + func generateList(g *generator, items []internItem) ast.Expr { 632 713 exprs := make([]ast.Expr, len(items)) 633 714 for i, it := range items { 634 - exprs[i] = it.generate(g) 715 + exprs[i] = it.Value().generate(g) 635 716 } 636 717 return ast.NewList(exprs...) 637 718 } ··· 720 801 } 721 802 return 1000 722 803 } 804 + 805 + func writeMapHash[K cmp.Ordered](h *maphash.Hash, m map[K]internItem, u *uniqueItems) { 806 + for _, k := range slices.Sorted(maps.Keys(m)) { 807 + maphash.WriteComparable(h, k) 808 + u.writeHash(h, m[k]) 809 + } 810 + } 811 + 812 + // writeExprHash hashes an AST expression using its formatted representation. 813 + // This is a simple approach that ensures structurally equivalent expressions 814 + // hash to the same value. 815 + func writeExprHash(h *maphash.Hash, expr ast.Expr) { 816 + // Use the formatted string representation of the expression for hashing. 817 + // This ensures that expressions that format the same way will hash the same. 818 + data, err := format.Node(expr, format.Simplify()) 819 + if err != nil { 820 + panic(fmt.Errorf("invalid ast Expr: %v", err)) 821 + } 822 + h.Write(data) 823 + } 824 + 825 + type uniqueItems struct { 826 + items *anyunique.Store[item, *uniqueItems] 827 + } 828 + 829 + func newUniqueItems() *uniqueItems { 830 + u := &uniqueItems{} 831 + u.items = anyunique.New[item, *uniqueItems](u) 832 + return u 833 + } 834 + 835 + func (u *uniqueItems) writeHash(h *maphash.Hash, it internItem) { 836 + u.items.WriteHash(h, it) 837 + } 838 + 839 + func (u *uniqueItems) apply(it internItem, f func(internItem, *uniqueItems) internItem) internItem { 840 + it1 := it.Value().apply(f, u) 841 + if it1 == it.Value() { 842 + return it 843 + } 844 + return u.items.Make(it1) 845 + } 846 + 847 + type internItem = anyunique.Handle[item] 848 + 849 + func (u *uniqueItems) intern(it item) internItem { 850 + return u.items.Make(it) 851 + } 852 + 853 + // Hash implements [anyunique.Hasher.Hash]. 854 + func (u *uniqueItems) Hash(h *maphash.Hash, x item) { 855 + maphash.WriteComparable(h, reflect.TypeOf(x)) 856 + x.hash(h, u) 857 + } 858 + 859 + // Equal implements [anyunique.Hasher.Equal] for two 860 + // items x0 and x1. 861 + func (u *uniqueItems) Equal(x0, x1 item) bool { 862 + if x0 == x1 { 863 + return true 864 + } 865 + // TODO although this is typically only called when items are 866 + // identical (because hash collisions are rare), it could made more 867 + // efficient. It would be better to have custom equality methods for 868 + // each type, or at least a reflect-based equality checker that 869 + // avoids unexported fields and compares [anyunique.Handle] values 870 + // without descending into them. 871 + return reflect.DeepEqual(x0, x1) 872 + } 873 + 874 + func applyMap(m map[string]internItem, f func(internItem, *uniqueItems) internItem, u *uniqueItems) (map[string]internItem, bool) { 875 + var m1 map[string]internItem 876 + for key, e := range m { 877 + e1 := f(e, u) 878 + if e1 == e { 879 + continue 880 + } 881 + if m1 == nil { 882 + m1 = make(map[string]internItem) 883 + } 884 + m1[key] = e1 885 + } 886 + if m1 == nil { 887 + return m, false 888 + } 889 + if len(m1) == len(m) { 890 + return m1, true 891 + } 892 + for key, e := range m { 893 + if _, ok := m1[key]; !ok { 894 + m1[key] = e 895 + } 896 + } 897 + return m1, true 898 + } 899 + 900 + func applyElems(elems []internItem, f func(internItem, *uniqueItems) internItem, u *uniqueItems) ([]internItem, bool) { 901 + changed := false 902 + for i, e := range elems { 903 + e1 := f(e, u) 904 + if e1 == e { 905 + continue 906 + } 907 + if !changed { 908 + elems = slices.Clone(elems) 909 + changed = true 910 + } 911 + elems[i] = e1 912 + } 913 + return elems, changed 914 + }
+140 -150
encoding/jsonschema/generate_mergeallof_test.go
··· 18 18 "testing" 19 19 20 20 "github.com/go-quicktest/qt" 21 - "github.com/google/go-cmp/cmp" 22 21 ) 23 22 24 23 func TestMergeAllOf(t *testing.T) { 25 - itemString := &itemType{kinds: []string{"string"}} 26 - itemNumber := &itemType{kinds: []string{"number"}} 27 - itemBool := &itemType{kinds: []string{"boolean"}} 24 + u := newUniqueItems() 25 + itemString := u.intern(&itemType{kinds: []string{"string"}}) 26 + itemNumber := u.intern(&itemType{kinds: []string{"number"}}) 27 + itemBool := u.intern(&itemType{kinds: []string{"boolean"}}) 28 28 29 29 tests := []struct { 30 30 name string 31 - item item 32 - want item 31 + item internItem 32 + want internItem 33 33 }{ 34 34 { 35 35 name: "NonAllOfItemReturnsAsIs", ··· 38 38 }, 39 39 { 40 40 name: "AllOfWithSingleElementReturnsThatElement", 41 - item: &itemAllOf{ 42 - elems: []item{itemString}, 43 - }, 41 + item: u.intern(&itemAllOf{ 42 + elems: []internItem{itemString}, 43 + }), 44 44 want: itemString, 45 45 }, 46 46 { 47 47 name: "AllOfWithMultipleElementsStaysAsAllOf", 48 - item: &itemAllOf{ 49 - elems: []item{itemString, itemNumber}, 50 - }, 51 - want: &itemAllOf{ 52 - elems: []item{itemString, itemNumber}, 53 - }, 48 + item: u.intern(&itemAllOf{ 49 + elems: []internItem{itemString, itemNumber}, 50 + }), 51 + want: u.intern(&itemAllOf{ 52 + elems: []internItem{itemString, itemNumber}, 53 + }), 54 54 }, 55 55 { 56 56 name: "NestedAllOfIsFlattened", 57 - item: &itemAllOf{ 58 - elems: []item{ 57 + item: u.intern(&itemAllOf{ 58 + elems: []internItem{ 59 59 itemString, 60 - &itemAllOf{ 61 - elems: []item{itemNumber, itemBool}, 62 - }, 60 + u.intern(&itemAllOf{ 61 + elems: []internItem{itemNumber, itemBool}, 62 + }), 63 63 }, 64 - }, 65 - want: &itemAllOf{ 66 - elems: []item{itemString, itemNumber, itemBool}, 67 - }, 64 + }), 65 + want: u.intern(&itemAllOf{ 66 + elems: []internItem{itemString, itemNumber, itemBool}, 67 + }), 68 68 }, 69 69 { 70 70 name: "MultipleNestedAllOfAreAllFlattened", 71 - item: &itemAllOf{ 72 - elems: []item{ 73 - &itemAllOf{ 74 - elems: []item{itemString}, 75 - }, 76 - &itemAllOf{ 77 - elems: []item{itemNumber}, 78 - }, 79 - &itemAllOf{ 80 - elems: []item{itemBool}, 81 - }, 71 + item: u.intern(&itemAllOf{ 72 + elems: []internItem{ 73 + u.intern(&itemAllOf{ 74 + elems: []internItem{itemString}, 75 + }), 76 + u.intern(&itemAllOf{ 77 + elems: []internItem{itemNumber}, 78 + }), 79 + u.intern(&itemAllOf{ 80 + elems: []internItem{itemBool}, 81 + }), 82 82 }, 83 - }, 84 - want: &itemAllOf{ 85 - elems: []item{itemString, itemNumber, itemBool}, 86 - }, 83 + }), 84 + want: u.intern(&itemAllOf{ 85 + elems: []internItem{itemString, itemNumber, itemBool}, 86 + }), 87 87 }, 88 88 { 89 89 name: "DeeplyNestedAllOfIsFullyFlattened", 90 - item: &itemAllOf{ 91 - elems: []item{ 90 + item: u.intern(&itemAllOf{ 91 + elems: []internItem{ 92 92 itemString, 93 - &itemAllOf{ 94 - elems: []item{ 93 + u.intern(&itemAllOf{ 94 + elems: []internItem{ 95 95 itemNumber, 96 - &itemAllOf{ 97 - elems: []item{itemBool}, 98 - }, 96 + u.intern(&itemAllOf{ 97 + elems: []internItem{itemBool}, 98 + }), 99 99 }, 100 - }, 100 + }), 101 101 }, 102 - }, 103 - want: &itemAllOf{ 104 - elems: []item{itemString, itemNumber, itemBool}, 105 - }, 102 + }), 103 + want: u.intern(&itemAllOf{ 104 + elems: []internItem{itemString, itemNumber, itemBool}, 105 + }), 106 106 }, 107 107 { 108 108 name: "DuplicateItemsAreRemoved", 109 - item: &itemAllOf{ 110 - elems: []item{itemString, itemString, itemNumber, itemString}, 111 - }, 112 - want: &itemAllOf{ 113 - elems: []item{itemString, itemNumber}, 114 - }, 109 + item: u.intern(&itemAllOf{ 110 + elems: []internItem{itemString, itemString, itemNumber, itemString}, 111 + }), 112 + want: u.intern(&itemAllOf{ 113 + elems: []internItem{itemString, itemNumber}, 114 + }), 115 115 }, 116 116 { 117 117 name: "DuplicateItemsAfterFlatteningAreRemoved", 118 - item: &itemAllOf{ 119 - elems: []item{ 118 + item: u.intern(&itemAllOf{ 119 + elems: []internItem{ 120 120 itemString, 121 - &itemAllOf{ 122 - elems: []item{itemString, itemNumber}, 123 - }, 121 + u.intern(&itemAllOf{ 122 + elems: []internItem{itemString, itemNumber}, 123 + }), 124 124 itemString, 125 125 }, 126 - }, 127 - want: &itemAllOf{ 128 - elems: []item{itemString, itemNumber}, 129 - }, 126 + }), 127 + want: u.intern(&itemAllOf{ 128 + elems: []internItem{itemString, itemNumber}, 129 + }), 130 130 }, 131 131 { 132 132 name: "AllOfNestedInOtherItemTypesHasChildrenMerged", 133 - item: &itemNot{ 134 - elem: &itemAllOf{ 135 - elems: []item{ 136 - &itemAllOf{ 137 - elems: []item{ 138 - &itemAllOf{ 139 - elems: []item{itemString}, 140 - }, 133 + item: u.intern(&itemNot{ 134 + elem: u.intern(&itemAllOf{ 135 + elems: []internItem{ 136 + u.intern(&itemAllOf{ 137 + elems: []internItem{ 138 + u.intern(&itemAllOf{ 139 + elems: []internItem{itemString}, 140 + }), 141 141 itemNumber, 142 142 }, 143 - }, 143 + }), 144 144 }, 145 - }, 146 - }, 147 - want: &itemNot{ 148 - elem: &itemAllOf{ 149 - elems: []item{itemString, itemNumber}, 150 - }, 151 - }, 145 + }), 146 + }), 147 + want: u.intern(&itemNot{ 148 + elem: u.intern(&itemAllOf{ 149 + elems: []internItem{itemString, itemNumber}, 150 + }), 151 + }), 152 152 }, 153 153 { 154 154 name: "AllOfNestedInAnyOfIsRecursivelyMerged", 155 - item: &itemAnyOf{ 156 - elems: []item{ 157 - &itemAllOf{ 158 - elems: []item{ 159 - &itemAllOf{ 160 - elems: []item{itemString}, 161 - }, 155 + item: u.intern(&itemAnyOf{ 156 + elems: []internItem{ 157 + u.intern(&itemAllOf{ 158 + elems: []internItem{ 159 + u.intern(&itemAllOf{ 160 + elems: []internItem{itemString}, 161 + }), 162 162 itemNumber, 163 163 }, 164 - }, 164 + }), 165 165 itemBool, 166 166 }, 167 - }, 168 - want: &itemAnyOf{ 169 - elems: []item{ 170 - &itemAllOf{ 171 - elems: []item{itemString, itemNumber}, 172 - }, 167 + }), 168 + want: u.intern(&itemAnyOf{ 169 + elems: []internItem{ 170 + u.intern(&itemAllOf{ 171 + elems: []internItem{itemString, itemNumber}, 172 + }), 173 173 itemBool, 174 174 }, 175 - }, 175 + }), 176 176 }, 177 177 { 178 178 name: "SingleElementAfterFlatteningAndDeduplication", 179 - item: &itemAllOf{ 180 - elems: []item{ 181 - &itemAllOf{ 182 - elems: []item{itemString}, 183 - }, 184 - &itemAllOf{ 185 - elems: []item{itemString}, 186 - }, 179 + item: u.intern(&itemAllOf{ 180 + elems: []internItem{ 181 + u.intern(&itemAllOf{ 182 + elems: []internItem{itemString}, 183 + }), 184 + u.intern(&itemAllOf{ 185 + elems: []internItem{itemString}, 186 + }), 187 187 }, 188 - }, 188 + }), 189 189 want: itemString, 190 190 }, 191 191 { 192 192 name: "EmptyAllOfBecomesSingleElementAndIsUnwrapped", 193 - item: &itemAllOf{ 194 - elems: []item{ 195 - &itemAllOf{ 196 - elems: []item{itemString}, 197 - }, 193 + item: u.intern(&itemAllOf{ 194 + elems: []internItem{ 195 + u.intern(&itemAllOf{ 196 + elems: []internItem{itemString}, 197 + }), 198 198 }, 199 - }, 199 + }), 200 200 want: itemString, 201 201 }, 202 202 { 203 203 name: "ComplexNestedStructureWithMixedTypes", 204 - item: &itemAllOf{ 205 - elems: []item{ 206 - &itemAllOf{ 207 - elems: []item{ 204 + item: u.intern(&itemAllOf{ 205 + elems: []internItem{ 206 + u.intern(&itemAllOf{ 207 + elems: []internItem{ 208 208 itemString, 209 - &itemAllOf{ 210 - elems: []item{itemNumber}, 211 - }, 209 + u.intern(&itemAllOf{ 210 + elems: []internItem{itemNumber}, 211 + }), 212 212 }, 213 - }, 214 - &itemNot{ 215 - elem: &itemAllOf{ 216 - elems: []item{ 213 + }), 214 + u.intern(&itemNot{ 215 + elem: u.intern(&itemAllOf{ 216 + elems: []internItem{ 217 217 itemBool, 218 - &itemAllOf{ 219 - elems: []item{&itemFormat{format: "date"}}, 220 - }, 218 + u.intern(&itemAllOf{ 219 + elems: []internItem{u.intern(&itemFormat{format: "date"})}, 220 + }), 221 221 }, 222 - }, 223 - }, 222 + }), 223 + }), 224 224 itemString, // Duplicate, should be removed 225 225 }, 226 - }, 227 - want: &itemAllOf{ 228 - elems: []item{ 226 + }), 227 + want: u.intern(&itemAllOf{ 228 + elems: []internItem{ 229 229 itemString, 230 230 itemNumber, 231 - &itemNot{ 232 - elem: &itemAllOf{ 233 - elems: []item{ 231 + u.intern(&itemNot{ 232 + elem: u.intern(&itemAllOf{ 233 + elems: []internItem{ 234 234 itemBool, 235 - &itemFormat{format: "date"}, 235 + u.intern(&itemFormat{format: "date"}), 236 236 }, 237 - }, 238 - }, 237 + }), 238 + }), 239 239 }, 240 - }, 240 + }), 241 241 }, 242 242 } 243 243 244 - // Define comparison options for unexported fields 245 - cmpOpt := cmp.AllowUnexported( 246 - itemAllOf{}, 247 - itemAnyOf{}, 248 - itemFormat{}, 249 - itemNot{}, 250 - itemType{}, 251 - property{}, 252 - ) 253 - 254 244 for _, tt := range tests { 255 245 t.Run(tt.name, func(t *testing.T) { 256 - got := mergeAllOf(tt.item) 257 - qt.Assert(t, qt.CmpEquals(got, tt.want, cmpOpt)) 246 + got := mergeAllOf(tt.item, u) 247 + qt.Assert(t, qt.Equals(got, tt.want)) 258 248 }) 259 249 } 260 250 }