cuelang.org/go@v0.13.0/encoding/openapi/openapi.go (about) 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 15 package openapi 16 17 import ( 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. 30 type 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 86 type Generator = Config 87 88 // Gen generates the set OpenAPI schema for all top-level types of the 89 // given instance. 90 func Gen(inst cue.InstanceOrValue, c *Config) ([]byte, error) { 91 if c == nil { 92 c = defaultConfig 93 } 94 all, err := schemas(c, inst) 95 if err != nil { 96 return nil, err 97 } 98 top, err := c.compose(inst, all) 99 if err != nil { 100 return nil, err 101 } 102 topValue := inst.Value().Context().BuildExpr(top) 103 if err := topValue.Err(); err != nil { 104 return nil, err 105 } 106 return internaljson.Marshal(topValue) 107 } 108 109 // Generate generates the set of OpenAPI schema for all top-level types of the 110 // given instance. 111 // 112 // Note: only a limited number of top-level types are supported so far. 113 func Generate(inst cue.InstanceOrValue, c *Config) (*ast.File, error) { 114 if c == nil { 115 c = defaultConfig 116 } 117 all, err := schemas(c, inst) 118 if err != nil { 119 return nil, err 120 } 121 top, err := c.compose(inst, all) 122 if err != nil { 123 return nil, err 124 } 125 return &ast.File{Decls: top.Elts}, nil 126 } 127 128 func toCUE(name string, x interface{}) (v ast.Expr, err error) { 129 b, err := internaljson.Marshal(x) 130 if err == nil { 131 v, err = cuejson.Extract(name, b) 132 } 133 if err != nil { 134 return nil, errors.Wrapf(err, token.NoPos, 135 "openapi: could not encode %s", name) 136 } 137 return v, nil 138 139 } 140 141 func (c *Config) compose(inst cue.InstanceOrValue, schemas *ast.StructLit) (x *ast.StructLit, err error) { 142 val := inst.Value() 143 var errs errors.Error 144 145 var title, version string 146 var info *ast.StructLit 147 148 for i, _ := val.Fields(); i.Next(); { 149 label := i.Selector().Unquoted() 150 attr := i.Value().Attribute("openapi") 151 if s, _ := attr.String(0); s != "" { 152 label = s 153 } 154 switch label { 155 case "$version": 156 case "-": 157 case "info": 158 info, _ = i.Value().Syntax().(*ast.StructLit) 159 if info == nil { 160 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 161 "info must be a struct")) 162 } 163 title, _ = i.Value().LookupPath(cue.MakePath(cue.Str("title"))).String() 164 version, _ = i.Value().LookupPath(cue.MakePath(cue.Str("version"))).String() 165 166 default: 167 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 168 "openapi: unsupported top-level field %q", label)) 169 } 170 } 171 172 switch x := c.Info.(type) { 173 case nil: 174 if title == "" { 175 title = "Generated by cue." 176 for _, d := range val.Doc() { 177 title = strings.TrimSpace(d.Text()) 178 break 179 } 180 } 181 182 if version == "" { 183 version, _ = val.LookupPath(cue.MakePath(cue.Str("$version"))).String() 184 if version == "" { 185 version = "no version" 186 } 187 } 188 189 if info == nil { 190 info = ast.NewStruct( 191 "title", ast.NewString(title), 192 "version", ast.NewString(version), 193 ) 194 } else { 195 m := (*orderedMap)(info) 196 m.setExpr("title", ast.NewString(title)) 197 m.setExpr("version", ast.NewString(version)) 198 } 199 200 case *ast.StructLit: 201 info = x 202 default: 203 x, err := toCUE("info section", x) 204 if err != nil { 205 return nil, err 206 } 207 var ok bool 208 info, ok = x.(*ast.StructLit) 209 if !ok { 210 errs = errors.Append(errs, errors.Newf(token.NoPos, 211 "Info field supplied must marshal to a struct but got %s", fmt.Sprintf("%T", x))) 212 } 213 } 214 215 return ast.NewStruct( 216 "openapi", ast.NewString(c.Version), 217 "info", info, 218 "paths", ast.NewStruct(), 219 "components", ast.NewStruct("schemas", schemas), 220 ), errs 221 } 222 223 var defaultConfig = &Config{} 224 225 // TODO 226 // The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows: 227 // 228 // readOnly sets the readOnly flag for a property in the schema 229 // only one of readOnly and writeOnly may be set. 230 // writeOnly sets the writeOnly flag for a property in the schema 231 // only one of readOnly and writeOnly may be set. 232 // discriminator explicitly sets a field as the discriminator field 233 //