github.com/solo-io/cue@v0.4.7/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/solo-io/cue/cue" 23 "github.com/solo-io/cue/cue/ast" 24 "github.com/solo-io/cue/cue/errors" 25 "github.com/solo-io/cue/cue/token" 26 cuejson "github.com/solo-io/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 // UnstructuredFields is map of: 71 // go package to type name to the list of fields on the type which should be treated as unstructured 72 // for the purpose of openapi schema generation 73 UnstructuredFields map[string][][]string 74 } 75 76 type Generator = Config 77 78 // Gen generates the set OpenAPI schema for all top-level types of the 79 // given instance. 80 func Gen(inst *cue.Instance, c *Config) ([]byte, error) { 81 if c == nil { 82 c = defaultConfig 83 } 84 all, err := c.All(inst) 85 if err != nil { 86 return nil, err 87 } 88 return json.Marshal(all) 89 } 90 91 // Generate generates the set of OpenAPI schema for all top-level types of the 92 // given instance. 93 // 94 // Note: only a limited number of top-level types are supported so far. 95 func Generate(inst *cue.Instance, c *Config) (*ast.File, error) { 96 all, err := schemas(c, inst) 97 if err != nil { 98 return nil, err 99 } 100 top, err := c.compose(inst, all) 101 if err != nil { 102 return nil, err 103 } 104 return &ast.File{Decls: top.Elts}, nil 105 } 106 107 // All generates an OpenAPI definition from the given instance. 108 // 109 // Note: only a limited number of top-level types are supported so far. 110 // Deprecated: use Generate 111 func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) { 112 all, err := schemas(g, inst) 113 if err != nil { 114 return nil, err 115 } 116 top, err := g.compose(inst, all) 117 return (*OrderedMap)(top), err 118 } 119 120 func toCUE(name string, x interface{}) (v ast.Expr, err error) { 121 b, err := json.Marshal(x) 122 if err == nil { 123 v, err = cuejson.Extract(name, b) 124 } 125 if err != nil { 126 return nil, errors.Wrapf(err, token.NoPos, 127 "openapi: could not encode %s", name) 128 } 129 return v, nil 130 131 } 132 133 func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) { 134 135 var errs errors.Error 136 137 var title, version string 138 var info *ast.StructLit 139 140 for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); { 141 if i.IsDefinition() { 142 continue 143 } 144 label := i.Label() 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().Lookup("title").String() 159 version, _ = i.Value().Lookup("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 // Support of OrderedMap is mostly for backwards compatibility. 168 switch x := c.Info.(type) { 169 case nil: 170 if title == "" { 171 title = "Generated by cue." 172 for _, d := range inst.Doc() { 173 title = strings.TrimSpace(d.Text()) 174 break 175 } 176 if p := inst.ImportPath; title == "" && p != "" { 177 title = fmt.Sprintf("Generated by cue from package %q", p) 178 } 179 } 180 181 if version == "" { 182 version, _ = inst.Lookup("$version").String() 183 if version == "" { 184 version = "no version" 185 } 186 } 187 188 if info == nil { 189 info = ast.NewStruct( 190 "title", ast.NewString(title), 191 "version", ast.NewString(version), 192 ) 193 } else { 194 m := (*OrderedMap)(info) 195 m.Set("title", ast.NewString(title)) 196 m.Set("version", ast.NewString(version)) 197 } 198 199 case *ast.StructLit: 200 info = x 201 case *OrderedMap: 202 info = (*ast.StructLit)(x) 203 case OrderedMap: 204 info = (*ast.StructLit)(&x) 205 default: 206 x, err := toCUE("info section", x) 207 if err != nil { 208 return nil, err 209 } 210 info, _ = x.(*ast.StructLit) 211 errs = errors.Append(errs, errors.Newf(token.NoPos, 212 "Info field supplied must be an *ast.StructLit")) 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 // Schemas extracts component/schemas from the CUE top-level types. 224 func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) { 225 comps, err := schemas(g, inst) 226 if err != nil { 227 return nil, err 228 } 229 return (*OrderedMap)(comps), err 230 } 231 232 var defaultConfig = &Config{} 233 234 // TODO 235 // The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows: 236 // 237 // readOnly sets the readOnly flag for a property in the schema 238 // only one of readOnly and writeOnly may be set. 239 // writeOnly sets the writeOnly flag for a property in the schema 240 // only one of readOnly and writeOnly may be set. 241 // discriminator explicitly sets a field as the discriminator field 242 //