this repo has no description
at master 228 lines 6.9 kB view raw
1// Copyright 2019 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 cuejson "cuelang.org/go/encoding/json" 26 internaljson "cuelang.org/go/internal/encoding/json" 27) 28 29// A Config defines options for converting CUE to and from OpenAPI. 30type Config struct { 31 // PkgName defines to package name for a generated CUE package. 32 PkgName string 33 34 // Info specifies the info section of the OpenAPI document. To be a valid 35 // OpenAPI document, it must include at least the title and version fields. 36 // Info may be a *ast.StructLit or any type that marshals to JSON. 37 Info interface{} 38 39 // NameFunc allows users to specify an alternative representation 40 // for references. It is called with the value passed to the top level 41 // method or function and the path to the entity being generated. 42 // If it returns an empty string the generator will expand the type 43 // in place and, if applicable, not generate a schema for that entity. 44 // 45 // Note: this only returns the final element of the /-separated 46 // reference. 47 NameFunc func(val cue.Value, path cue.Path) string 48 49 // DescriptionFunc allows rewriting a description associated with a certain 50 // field. A typical implementation compiles the description from the 51 // comments obtains from the Doc method. No description field is added if 52 // the empty string is returned. 53 DescriptionFunc func(v cue.Value) string 54 55 // SelfContained causes all non-expanded external references to be included 56 // in this document. 57 SelfContained bool 58 59 // OpenAPI version to use. Supported as of v3.0.0. 60 Version string 61 62 // FieldFilter defines a regular expression of all fields to omit from the 63 // output. It is only allowed to filter fields that add additional 64 // constraints. Fields that indicate basic types cannot be removed. It is 65 // an error for such fields to be excluded by this filter. 66 // Fields are qualified by their Object type. For instance, the 67 // minimum field of the schema object is qualified as Schema/minimum. 68 FieldFilter string 69 70 // ExpandReferences replaces references with actual objects when generating 71 // OpenAPI Schema. It is an error for an CUE value to refer to itself 72 // if this option is used. 73 ExpandReferences bool 74 75 // StrictFeatures reports an error for features that are known 76 // to be unsupported. 77 StrictFeatures bool 78 79 // StrictKeywords reports an error when unknown keywords 80 // are encountered. For OpenAPI 3.0, this is implicitly always 81 // true, as that specification explicitly prohibits unknown keywords 82 // other than "x-" prefixed keywords. 83 StrictKeywords bool 84} 85 86type Generator = Config 87 88// Gen generates the set OpenAPI schema for all top-level types of the 89// given instance. 90// 91// Deprecated: use [Generate]. 92func Gen(inst cue.InstanceOrValue, c *Config) ([]byte, error) { 93 f, err := Generate(inst, c) 94 if err != nil { 95 return nil, err 96 } 97 topValue := inst.Value().Context().BuildFile(f) 98 if err := topValue.Err(); err != nil { 99 return nil, err 100 } 101 return internaljson.Marshal(topValue) 102} 103 104// Generate generates the set of OpenAPI schema for all top-level types of the 105// given instance. 106// 107// Note: only a limited number of top-level types are supported so far. 108func Generate(inst cue.InstanceOrValue, c *Config) (*ast.File, error) { 109 if c == nil { 110 c = defaultConfig 111 } 112 all, err := schemas(c, inst) 113 if err != nil { 114 return nil, err 115 } 116 top, err := c.compose(inst, all) 117 if err != nil { 118 return nil, err 119 } 120 return &ast.File{Decls: top.Elts}, nil 121} 122 123func toCUE(name string, x interface{}) (v ast.Expr, err error) { 124 b, err := internaljson.Marshal(x) 125 if err == nil { 126 v, err = cuejson.Extract(name, b) 127 } 128 if err != nil { 129 return nil, errors.Wrapf(err, token.NoPos, 130 "openapi: could not encode %s", name) 131 } 132 return v, nil 133 134} 135 136func (c *Config) compose(inst cue.InstanceOrValue, schemas *ast.StructLit) (x *ast.StructLit, err error) { 137 val := inst.Value() 138 var errs errors.Error 139 140 var title, version string 141 var info *ast.StructLit 142 143 for i, _ := val.Fields(); i.Next(); { 144 label := i.Selector().Unquoted() 145 attr := i.Value().Attribute("openapi") 146 if s, _ := attr.String(0); s != "" { 147 label = s 148 } 149 switch label { 150 case "$version": 151 case "-": 152 case "info": 153 info, _ = i.Value().Syntax().(*ast.StructLit) 154 if info == nil { 155 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 156 "info must be a struct")) 157 } 158 title, _ = i.Value().LookupPath(cue.MakePath(cue.Str("title"))).String() 159 version, _ = i.Value().LookupPath(cue.MakePath(cue.Str("version"))).String() 160 161 default: 162 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 163 "openapi: unsupported top-level field %q", label)) 164 } 165 } 166 167 switch x := c.Info.(type) { 168 case nil: 169 if title == "" { 170 title = "Generated by cue." 171 for _, d := range val.Doc() { 172 title = strings.TrimSpace(d.Text()) 173 break 174 } 175 } 176 177 if version == "" { 178 version, _ = val.LookupPath(cue.MakePath(cue.Str("$version"))).String() 179 if version == "" { 180 version = "no version" 181 } 182 } 183 184 if info == nil { 185 info = ast.NewStruct( 186 "title", ast.NewString(title), 187 "version", ast.NewString(version), 188 ) 189 } else { 190 m := (*orderedMap)(info) 191 m.setExpr("title", ast.NewString(title)) 192 m.setExpr("version", ast.NewString(version)) 193 } 194 195 case *ast.StructLit: 196 info = x 197 default: 198 x, err := toCUE("info section", x) 199 if err != nil { 200 return nil, err 201 } 202 var ok bool 203 info, ok = x.(*ast.StructLit) 204 if !ok { 205 errs = errors.Append(errs, errors.Newf(token.NoPos, 206 "Info field supplied must marshal to a struct but got %s", fmt.Sprintf("%T", x))) 207 } 208 } 209 210 return ast.NewStruct( 211 "openapi", ast.NewString(c.Version), 212 "info", info, 213 "paths", ast.NewStruct(), 214 "components", ast.NewStruct("schemas", schemas), 215 ), errs 216} 217 218var defaultConfig = &Config{} 219 220// TODO 221// The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows: 222// 223// readOnly sets the readOnly flag for a property in the schema 224// only one of readOnly and writeOnly may be set. 225// writeOnly sets the writeOnly flag for a property in the schema 226// only one of readOnly and writeOnly may be set. 227// discriminator explicitly sets a field as the discriminator field 228//