this repo has no description
at master 164 lines 5.0 kB view raw
1// Copyright 2020 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 15package openapi 16 17import ( 18 "fmt" 19 "strings" 20 21 "cuelang.org/go/cue" 22 "cuelang.org/go/cue/ast" 23 "cuelang.org/go/cue/errors" 24 "cuelang.org/go/cue/token" 25 "cuelang.org/go/encoding/jsonschema" 26 "cuelang.org/go/internal" 27) 28 29// Extract converts OpenAPI definitions to an equivalent CUE representation. 30// 31// It currently only converts entries in #/components/schema and extracts some 32// meta data. 33func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) { 34 // TODO: find a good OpenAPI validator. Both go-openapi and kin-openapi 35 // seem outdated. The k8s one might be good, but avoid pulling in massive 36 // amounts of dependencies. 37 38 f := &ast.File{} 39 add := func(d ast.Decl) { 40 if d != nil { 41 f.Decls = append(f.Decls, d) 42 } 43 } 44 45 v := data.Value() 46 versionValue := v.LookupPath(cue.MakePath(cue.Str("openapi"))) 47 if versionValue.Err() != nil { 48 return nil, fmt.Errorf("openapi field is required but not found") 49 } 50 version, err := versionValue.String() 51 if err != nil { 52 return nil, fmt.Errorf("invalid openapi field (must be string): %v", err) 53 } 54 // A simple prefix match is probably OK for now, following 55 // the same logic used by internal/encoding.isOpenAPI. 56 // The specification says that the patch version should be disregarded: 57 // https://swagger.io/specification/v3/ 58 var schemaVersion jsonschema.Version 59 switch { 60 case strings.HasPrefix(version, "3.0."): 61 schemaVersion = jsonschema.VersionOpenAPI 62 case strings.HasPrefix(version, "3.1."): 63 schemaVersion = jsonschema.VersionDraft2020_12 64 default: 65 return nil, fmt.Errorf("unknown OpenAPI version %q", version) 66 } 67 68 doc, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("title"))).String() // Required 69 if s, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("description"))).String(); s != "" { 70 doc += "\n\n" + s 71 } 72 cg := internal.NewComment(true, doc) 73 74 if c.PkgName != "" { 75 p := &ast.Package{Name: ast.NewIdent(c.PkgName)} 76 ast.AddComment(p, cg) 77 add(p) 78 } else if cg != nil { 79 add(cg) 80 } 81 82 js, err := jsonschema.Extract(data, &jsonschema.Config{ 83 Root: oapiSchemas, 84 Map: openAPIMapping, 85 DefaultVersion: schemaVersion, 86 StrictFeatures: c.StrictFeatures, 87 // OpenAPI 3.0 is stricter than JSON Schema about allowed keywords. 88 StrictKeywords: schemaVersion == jsonschema.VersionOpenAPI || c.StrictKeywords, 89 }) 90 if err != nil { 91 return nil, err 92 } 93 preamble := js.Preamble() 94 body := js.Decls[len(preamble):] 95 for _, d := range preamble { 96 switch x := d.(type) { 97 case *ast.Package: 98 return nil, errors.Newf(x.Pos(), "unexpected package %q", x.Name.Name) 99 100 default: 101 add(x) 102 } 103 } 104 105 // TODO: allow attributes before imports? Would be easier. 106 107 // TODO: do we want to store the OpenAPI version? 108 // if version, _ := v.Lookup("openapi").String(); version != "" { 109 // add(&ast.Attribute{Text: fmt.Sprintf("@openapi(version=%s)", version)}) 110 // } 111 112 if info := v.LookupPath(cue.MakePath(cue.Str("info"))); info.Exists() { 113 decls := []interface{}{} 114 if st, ok := info.Syntax().(*ast.StructLit); ok { 115 // Remove title. 116 for _, d := range st.Elts { 117 if f, ok := d.(*ast.Field); ok { 118 switch name, _, _ := ast.LabelName(f.Label); name { 119 case "title", "version": 120 // title: *"title" | string 121 decls = append(decls, &ast.Field{ 122 Label: f.Label, 123 Value: ast.NewBinExpr(token.OR, 124 &ast.UnaryExpr{Op: token.MUL, X: f.Value}, 125 ast.NewIdent("string")), 126 }) 127 continue 128 } 129 } 130 decls = append(decls, d) 131 } 132 add(&ast.Field{ 133 Label: ast.NewIdent("info"), 134 Value: ast.NewStruct(decls...), 135 }) 136 } 137 } 138 139 if len(body) > 0 { 140 ast.SetRelPos(body[0], token.NewSection) 141 f.Decls = append(f.Decls, body...) 142 } 143 144 return f, nil 145} 146 147const oapiSchemas = "#/components/schemas/" 148 149// rootDefs is the fallback for schemas that are not valid identifiers. 150// TODO: find something more principled. 151const rootDefs = "#SchemaMap" 152 153func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) { 154 if len(a) != 3 || a[0] != "components" || a[1] != "schemas" { 155 return nil, errors.Newf(pos, 156 `openapi: reference must be of the form %q; found "#/%s"`, 157 oapiSchemas, strings.Join(a, "/")) 158 } 159 name := a[2] 160 if name != rootDefs[1:] && !ast.StringLabelNeedsQuoting(name) { 161 return []ast.Label{ast.NewIdent("#" + name)}, nil 162 } 163 return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil 164}