github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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 "encoding/json" 19 "fmt" 20 "strings" 21 22 "github.com/joomcode/cue/cue" 23 "github.com/joomcode/cue/cue/ast" 24 "github.com/joomcode/cue/cue/errors" 25 "github.com/joomcode/cue/cue/token" 26 cuejson "github.com/joomcode/cue/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 // ReferenceFunc allows users to specify an alternative representation 40 // for references. An empty string tells the generator to expand the type 41 // in place and, if applicable, not generate a schema for that entity. 42 ReferenceFunc func(inst *cue.Instance, path []string) string 43 44 // DescriptionFunc allows rewriting a description associated with a certain 45 // field. A typical implementation compiles the description from the 46 // comments obtains from the Doc method. No description field is added if 47 // the empty string is returned. 48 DescriptionFunc func(v cue.Value) string 49 50 // SelfContained causes all non-expanded external references to be included 51 // in this document. 52 SelfContained bool 53 54 // OpenAPI version to use. Supported as of v3.0.0. 55 Version string 56 57 // FieldFilter defines a regular expression of all fields to omit from the 58 // output. It is only allowed to filter fields that add additional 59 // constraints. Fields that indicate basic types cannot be removed. It is 60 // an error for such fields to be excluded by this filter. 61 // Fields are qualified by their Object type. For instance, the 62 // minimum field of the schema object is qualified as Schema/minimum. 63 FieldFilter string 64 65 // ExpandReferences replaces references with actual objects when generating 66 // OpenAPI Schema. It is an error for an CUE value to refer to itself 67 // if this option is used. 68 ExpandReferences bool 69 } 70 71 type Generator = Config 72 73 // Gen generates the set OpenAPI schema for all top-level types of the 74 // given instance. 75 func Gen(inst *cue.Instance, c *Config) ([]byte, error) { 76 if c == nil { 77 c = defaultConfig 78 } 79 all, err := c.All(inst) 80 if err != nil { 81 return nil, err 82 } 83 return json.Marshal(all) 84 } 85 86 // Generate generates the set of OpenAPI schema for all top-level types of the 87 // given instance. 88 // 89 // Note: only a limited number of top-level types are supported so far. 90 func Generate(inst *cue.Instance, c *Config) (*ast.File, error) { 91 all, err := schemas(c, inst) 92 if err != nil { 93 return nil, err 94 } 95 top, err := c.compose(inst, all) 96 if err != nil { 97 return nil, err 98 } 99 return &ast.File{Decls: top.Elts}, nil 100 } 101 102 // All generates an OpenAPI definition from the given instance. 103 // 104 // Note: only a limited number of top-level types are supported so far. 105 // Deprecated: use Generate 106 func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) { 107 all, err := schemas(g, inst) 108 if err != nil { 109 return nil, err 110 } 111 top, err := g.compose(inst, all) 112 return (*OrderedMap)(top), err 113 } 114 115 func toCUE(name string, x interface{}) (v ast.Expr, err error) { 116 b, err := json.Marshal(x) 117 if err == nil { 118 v, err = cuejson.Extract(name, b) 119 } 120 if err != nil { 121 return nil, errors.Wrapf(err, token.NoPos, 122 "openapi: could not encode %s", name) 123 } 124 return v, nil 125 126 } 127 128 func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) { 129 130 var errs errors.Error 131 132 var title, version string 133 var info *ast.StructLit 134 135 for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { 136 if i.IsDefinition() { 137 continue 138 } 139 label := i.Label() 140 attr := i.Value().Attribute("openapi") 141 if s, _ := attr.String(0); s != "" { 142 label = s 143 } 144 switch label { 145 case "$version": 146 case "-": 147 case "info": 148 info, _ = i.Value().Syntax().(*ast.StructLit) 149 if info == nil { 150 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 151 "info must be a struct")) 152 } 153 title, _ = i.Value().Lookup("title").String() 154 version, _ = i.Value().Lookup("version").String() 155 156 default: 157 errs = errors.Append(errs, errors.Newf(i.Value().Pos(), 158 "openapi: unsupported top-level field %q", label)) 159 } 160 } 161 162 // Support of OrderedMap is mostly for backwards compatibility. 163 switch x := c.Info.(type) { 164 case nil: 165 if title == "" { 166 title = "Generated by cue." 167 for _, d := range inst.Doc() { 168 title = strings.TrimSpace(d.Text()) 169 break 170 } 171 if p := inst.ImportPath; title == "" && p != "" { 172 title = fmt.Sprintf("Generated by cue from package %q", p) 173 } 174 } 175 176 if version == "" { 177 version, _ = inst.Lookup("$version").String() 178 if version == "" { 179 version = "no version" 180 } 181 } 182 183 if info == nil { 184 info = ast.NewStruct( 185 "title", ast.NewString(title), 186 "version", ast.NewString(version), 187 ) 188 } else { 189 m := (*OrderedMap)(info) 190 m.Set("title", ast.NewString(title)) 191 m.Set("version", ast.NewString(version)) 192 } 193 194 case *ast.StructLit: 195 info = x 196 case *OrderedMap: 197 info = (*ast.StructLit)(x) 198 case OrderedMap: 199 info = (*ast.StructLit)(&x) 200 default: 201 x, err := toCUE("info section", x) 202 if err != nil { 203 return nil, err 204 } 205 info, _ = x.(*ast.StructLit) 206 errs = errors.Append(errs, errors.Newf(token.NoPos, 207 "Info field supplied must be an *ast.StructLit")) 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 218 // Schemas extracts component/schemas from the CUE top-level types. 219 func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) { 220 comps, err := schemas(g, inst) 221 if err != nil { 222 return nil, err 223 } 224 return (*OrderedMap)(comps), err 225 } 226 227 var defaultConfig = &Config{} 228 229 // TODO 230 // The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows: 231 // 232 // readOnly sets the readOnly flag for a property in the schema 233 // only one of readOnly and writeOnly may be set. 234 // writeOnly sets the writeOnly flag for a property in the schema 235 // only one of readOnly and writeOnly may be set. 236 // discriminator explicitly sets a field as the discriminator field 237 //