cuelang.org/go@v0.10.1/encoding/openapi/decode.go (about) 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 15 package openapi 16 17 import ( 18 "strings" 19 20 "cuelang.org/go/cue" 21 "cuelang.org/go/cue/ast" 22 "cuelang.org/go/cue/errors" 23 "cuelang.org/go/cue/token" 24 "cuelang.org/go/encoding/jsonschema" 25 "cuelang.org/go/internal" 26 ) 27 28 // Extract converts OpenAPI definitions to an equivalent CUE representation. 29 // 30 // It currently only converts entries in #/components/schema and extracts some 31 // meta data. 32 func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) { 33 // TODO: find a good OpenAPI validator. Both go-openapi and kin-openapi 34 // seem outdated. The k8s one might be good, but avoid pulling in massive 35 // amounts of dependencies. 36 37 f := &ast.File{} 38 add := func(d ast.Decl) { 39 if d != nil { 40 f.Decls = append(f.Decls, d) 41 } 42 } 43 44 js, err := jsonschema.Extract(data, &jsonschema.Config{ 45 Root: oapiSchemas, 46 Map: openAPIMapping, 47 }) 48 if err != nil { 49 return nil, err 50 } 51 52 v := data.Value() 53 54 doc, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("title"))).String() // Required 55 if s, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("description"))).String(); s != "" { 56 doc += "\n\n" + s 57 } 58 cg := internal.NewComment(true, doc) 59 60 if c.PkgName != "" { 61 p := &ast.Package{Name: ast.NewIdent(c.PkgName)} 62 p.AddComment(cg) 63 add(p) 64 } else { 65 add(cg) 66 } 67 68 preamble := js.Preamble() 69 body := js.Decls[len(preamble):] 70 for _, d := range preamble { 71 switch x := d.(type) { 72 case *ast.Package: 73 return nil, errors.Newf(x.Pos(), "unexpected package %q", x.Name.Name) 74 75 default: 76 add(x) 77 } 78 } 79 80 // TODO: allow attributes before imports? Would be easier. 81 82 // TODO: do we want to store the OpenAPI version? 83 // if version, _ := v.Lookup("openapi").String(); version != "" { 84 // add(internal.NewAttr("openapi", "version="+ version)) 85 // } 86 87 if info := v.LookupPath(cue.MakePath(cue.Str("info"))); info.Exists() { 88 decls := []interface{}{} 89 if st, ok := info.Syntax().(*ast.StructLit); ok { 90 // Remove title. 91 for _, d := range st.Elts { 92 if f, ok := d.(*ast.Field); ok { 93 switch name, _, _ := ast.LabelName(f.Label); name { 94 case "title", "version": 95 // title: *"title" | string 96 decls = append(decls, &ast.Field{ 97 Label: f.Label, 98 Value: ast.NewBinExpr(token.OR, 99 &ast.UnaryExpr{Op: token.MUL, X: f.Value}, 100 ast.NewIdent("string")), 101 }) 102 continue 103 } 104 } 105 decls = append(decls, d) 106 } 107 add(&ast.Field{ 108 Label: ast.NewIdent("info"), 109 Value: ast.NewStruct(decls...), 110 }) 111 } 112 } 113 114 if len(body) > 0 { 115 ast.SetRelPos(body[0], token.NewSection) 116 f.Decls = append(f.Decls, body...) 117 } 118 119 return f, nil 120 } 121 122 const oapiSchemas = "#/components/schemas/" 123 124 // rootDefs is the fallback for schemas that are not valid identifiers. 125 // TODO: find something more principled. 126 const rootDefs = "#SchemaMap" 127 128 func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) { 129 if len(a) != 3 || a[0] != "components" || a[1] != "schemas" { 130 return nil, errors.Newf(pos, 131 `openapi: reference must be of the form %q; found "#/%s"`, 132 oapiSchemas, strings.Join(a, "/")) 133 } 134 name := a[2] 135 if ast.IsValidIdent(name) && 136 name != rootDefs[1:] && 137 !internal.IsDefOrHidden(name) { 138 return []ast.Label{ast.NewIdent("#" + name)}, nil 139 } 140 return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil 141 }