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