this repo has no description
0
fork

Configure Feed

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

encoding/openapi: detect cycles when expanding references

Fixes #915

Change-Id: Ie781ee316e8675da66f7ca3bea4c841acaaa8a5b
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9603
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>

+185 -64
+16 -3
encoding/openapi/build.go
··· 36 36 instExt *cue.Instance 37 37 refPrefix string 38 38 path []string 39 + errs errors.Error 39 40 40 41 expandRefs bool 41 42 structural bool ··· 49 50 50 51 // Track external schemas. 51 52 externalRefs map[string]*externalType 53 + 54 + // Used for cycle detection in case of using ExpandReferences. At the 55 + // moment, CUE does not detect cycles when a user forcefully steps into a 56 + // pattern constraint. 57 + // 58 + // TODO: consider an option in the CUE API where optional fields are 59 + // recursively evaluated. 60 + cycleNodes []*adt.Vertex 52 61 } 53 62 54 63 type externalType struct { ··· 168 177 return x < y 169 178 }) 170 179 171 - return (*ast.StructLit)(c.schemas), nil 180 + return (*ast.StructLit)(c.schemas), c.errs 172 181 } 173 182 174 183 func (c *buildContext) build(name string, v cue.Value) *ast.StructLit { ··· 314 323 } 315 324 316 325 func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) { 326 + b.pushNode(v) 327 + defer b.popNode() 328 + 317 329 count := 0 318 330 disallowDefault := false 319 331 var values cue.Value ··· 770 782 b.setSingle("properties", (*ast.StructLit)(properties), false) 771 783 } 772 784 773 - if t, ok := v.Elem(); ok && (b.core == nil || b.core.items == nil) { 785 + if t, ok := v.Elem(); ok && 786 + (b.core == nil || b.core.items == nil) && b.checkCycle(t) { 774 787 schema := b.schema(nil, "*", t) 775 788 if len(schema.Elts) > 0 { 776 789 b.setSingle("additionalProperties", schema, true) // Not allowed in structural. ··· 871 884 } 872 885 873 886 if !hasMax || int64(len(items)) < maxLength { 874 - if typ, ok := v.Elem(); ok { 887 + if typ, ok := v.Elem(); ok && b.checkCycle(typ) { 875 888 var core *builder 876 889 if b.core != nil { 877 890 core = b.core.items
+9
encoding/openapi/crd.go
··· 109 109 // To this extent, all fields of both conjunctions and disjunctions are 110 110 // collected in a single properties map. 111 111 func (b *builder) buildCore(v cue.Value) { 112 + b.pushNode(v) 113 + defer b.popNode() 114 + 112 115 if !b.ctx.expandRefs { 113 116 _, r := v.Reference() 114 117 if len(r) > 0 { ··· 126 129 switch b.kind { 127 130 case cue.StructKind: 128 131 if typ, ok := v.Elem(); ok { 132 + if !b.checkCycle(typ) { 133 + return 134 + } 129 135 if b.items == nil { 130 136 b.items = newCoreBuilder(b.ctx) 131 137 } ··· 135 141 136 142 case cue.ListKind: 137 143 if typ, ok := v.Elem(); ok { 144 + if !b.checkCycle(typ) { 145 + return 146 + } 138 147 if b.items == nil { 139 148 b.items = newCoreBuilder(b.ctx) 140 149 }
+59
encoding/openapi/cycle.go
··· 1 + // Copyright 2021 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package openapi 16 + 17 + import ( 18 + "cuelang.org/go/cue" 19 + "cuelang.org/go/cue/errors" 20 + "cuelang.org/go/cue/token" 21 + "cuelang.org/go/internal/core/dep" 22 + "cuelang.org/go/internal/core/eval" 23 + internalvalue "cuelang.org/go/internal/value" 24 + ) 25 + 26 + func (b *builder) pushNode(v cue.Value) { 27 + _, n := internalvalue.ToInternal(v) 28 + b.ctx.cycleNodes = append(b.ctx.cycleNodes, n) 29 + } 30 + 31 + func (b *builder) popNode() { 32 + b.ctx.cycleNodes = b.ctx.cycleNodes[:len(b.ctx.cycleNodes)-1] 33 + } 34 + 35 + func (b *builder) checkCycle(v cue.Value) bool { 36 + if !b.ctx.expandRefs { 37 + return true 38 + } 39 + r, n := internalvalue.ToInternal(v) 40 + ctx := eval.NewContext(r, n) 41 + 42 + err := dep.Visit(ctx, n, func(d dep.Dependency) error { 43 + for _, m := range b.ctx.cycleNodes { 44 + if m == d.Node { 45 + var p token.Pos 46 + if src := d.Node.Source(); src != nil { 47 + p = src.Pos() 48 + } 49 + err := errors.Newf(p, 50 + "cycle in reference at %v: cyclic structures not allowed when reference expansion is requested", v.Path()) 51 + b.ctx.errs = errors.Append(b.ctx.errs, err) 52 + return err 53 + } 54 + } 55 + return nil 56 + }) 57 + 58 + return err == nil 59 + }
+80 -61
encoding/openapi/openapi_test.go
··· 43 43 testCases := []struct { 44 44 in, out string 45 45 config *openapi.Config 46 + err string 46 47 }{{ 47 - "structural.cue", 48 - "structural.json", 49 - resolveRefs, 48 + in: "structural.cue", 49 + out: "structural.json", 50 + config: resolveRefs, 50 51 }, { 51 - "nested.cue", 52 - "nested.json", 53 - defaultConfig, 52 + in: "nested.cue", 53 + out: "nested.json", 54 + config: defaultConfig, 54 55 }, { 55 - "simple.cue", 56 - "simple.json", 57 - resolveRefs, 56 + in: "simple.cue", 57 + out: "simple.json", 58 + config: resolveRefs, 58 59 }, { 59 - "simple.cue", 60 - "simple-filter.json", 61 - &openapi.Config{Info: info, FieldFilter: "min.*|max.*"}, 60 + in: "simple.cue", 61 + out: "simple-filter.json", 62 + config: &openapi.Config{Info: info, FieldFilter: "min.*|max.*"}, 62 63 }, { 63 - "array.cue", 64 - "array.json", 65 - defaultConfig, 64 + in: "array.cue", 65 + out: "array.json", 66 + config: defaultConfig, 66 67 }, { 67 - "enum.cue", 68 - "enum.json", 69 - defaultConfig, 68 + in: "enum.cue", 69 + out: "enum.json", 70 + config: defaultConfig, 70 71 }, { 71 - "struct.cue", 72 - "struct.json", 73 - defaultConfig, 72 + in: "struct.cue", 73 + out: "struct.json", 74 + config: defaultConfig, 74 75 }, { 75 - "strings.cue", 76 - "strings.json", 77 - defaultConfig, 76 + in: "strings.cue", 77 + out: "strings.json", 78 + config: defaultConfig, 78 79 }, { 79 - "nums.cue", 80 - "nums.json", 81 - defaultConfig, 80 + in: "nums.cue", 81 + out: "nums.json", 82 + config: defaultConfig, 82 83 }, { 83 - "nums.cue", 84 - "nums-v3.1.0.json", 85 - &openapi.Config{Info: info, Version: "3.1.0"}, 84 + in: "nums.cue", 85 + out: "nums-v3.1.0.json", 86 + config: &openapi.Config{Info: info, Version: "3.1.0"}, 86 87 }, { 87 - "builtins.cue", 88 - "builtins.json", 89 - defaultConfig, 88 + in: "builtins.cue", 89 + out: "builtins.json", 90 + config: defaultConfig, 90 91 }, { 91 - "oneof.cue", 92 - "oneof.json", 93 - defaultConfig, 92 + in: "oneof.cue", 93 + out: "oneof.json", 94 + config: defaultConfig, 94 95 }, { 95 - "oneof.cue", 96 - "oneof-resolve.json", 97 - resolveRefs, 96 + in: "oneof.cue", 97 + out: "oneof-resolve.json", 98 + config: resolveRefs, 98 99 }, { 99 - "openapi.cue", 100 - "openapi.json", 101 - defaultConfig, 100 + in: "openapi.cue", 101 + out: "openapi.json", 102 + config: defaultConfig, 102 103 }, { 103 - "openapi.cue", 104 - "openapi-norefs.json", 105 - resolveRefs, 104 + in: "openapi.cue", 105 + out: "openapi-norefs.json", 106 + config: resolveRefs, 106 107 }, { 107 - "oneof.cue", 108 - "oneof-funcs.json", 109 - &openapi.Config{ 108 + in: "oneof.cue", 109 + out: "oneof-funcs.json", 110 + config: &openapi.Config{ 110 111 Info: info, 111 112 ReferenceFunc: func(inst *cue.Instance, path []string) string { 112 113 return strings.ToUpper(strings.Join(path, "_")) ··· 116 117 }, 117 118 }, 118 119 }, { 119 - "refs.cue", 120 - "refs.json", 121 - &openapi.Config{ 120 + in: "refs.cue", 121 + out: "refs.json", 122 + config: &openapi.Config{ 122 123 Info: info, 123 124 ReferenceFunc: func(inst *cue.Instance, path []string) string { 124 125 switch { ··· 129 130 }, 130 131 }, 131 132 }, { 132 - "issue131.cue", 133 - "issue131.json", 134 - &openapi.Config{Info: info, SelfContained: true}, 133 + in: "issue131.cue", 134 + out: "issue131.json", 135 + config: &openapi.Config{Info: info, SelfContained: true}, 136 + }, { 137 + // Issue #915 138 + in: "cycle.cue", 139 + out: "cycle.json", 140 + config: &openapi.Config{Info: info}, 141 + }, { 142 + // Issue #915 143 + in: "cycle.cue", 144 + config: &openapi.Config{Info: info, ExpandReferences: true}, 145 + err: "cycle", 135 146 }} 136 147 for _, tc := range testCases { 137 148 t.Run(tc.out, func(t *testing.T) { ··· 144 155 t.Fatal(errors.Details(inst.Err, nil)) 145 156 } 146 157 147 - all, err := tc.config.All(inst) 158 + b, err := openapi.Gen(inst, tc.config) 148 159 if err != nil { 149 - t.Fatal(err) 160 + if tc.err == "" { 161 + t.Fatal("unexpected error:", errors.Details(inst.Err, nil)) 162 + } 163 + return 150 164 } 151 - walk(all) 152 165 153 - b, err := openapi.Gen(inst, tc.config) 154 - if err != nil { 155 - t.Fatal(err) 166 + if tc.err != "" { 167 + t.Fatal("unexpected success:", tc.err) 168 + } else { 169 + all, err := tc.config.All(inst) 170 + if err != nil { 171 + t.Fatal(err) 172 + } 173 + walk(all) 156 174 } 175 + 157 176 var out = &bytes.Buffer{} 158 177 _ = json.Indent(out, b, "", " ") 159 178 ··· 206 225 ExpandReferences: true, 207 226 }) 208 227 if err != nil { 209 - t.Fatal(err) 228 + t.Fatal(errors.Details(err, nil)) 210 229 } 211 230 212 231 var out = &bytes.Buffer{}
+2
encoding/openapi/testdata/cycle.cue
··· 1 + // Issue #915 2 + #Foo: [string]: #Foo
+19
encoding/openapi/testdata/cycle.json
··· 1 + { 2 + "openapi": "3.0.0", 3 + "info": { 4 + "title": "test", 5 + "version": "v1" 6 + }, 7 + "paths": {}, 8 + "components": { 9 + "schemas": { 10 + "Foo": { 11 + "description": "Issue #915", 12 + "type": "object", 13 + "additionalProperties": { 14 + "$ref": "#/components/schemas/Foo" 15 + } 16 + } 17 + } 18 + } 19 + }