blob: 6eaebb8cae0b29f4f9c85f4e6a76886e518cf7e3 [file] [log] [blame]
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -04001// 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 "encoding/json"
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +020019 "fmt"
20 "strings"
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040021
Marcel van Lohuizen845df052020-07-26 13:15:45 +020022 "cuelang.org/go/cue"
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010023 "cuelang.org/go/cue/ast"
24 "cuelang.org/go/cue/errors"
25 "cuelang.org/go/cue/token"
26 cuejson "cuelang.org/go/encoding/json"
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040027)
28
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010029// A Config defines options for converting CUE to and from OpenAPI.
30type Config struct {
Marcel van Lohuizen64b39e62020-03-10 13:46:27 +010031 // PkgName defines to package name for a generated CUE package.
32 PkgName string
33
Marcel van Lohuizen4de93e12019-07-02 20:04:10 +020034 // 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.
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010036 // Info may be a *ast.StructLit or any type that marshals to JSON.
37 Info interface{}
Marcel van Lohuizen4de93e12019-07-02 20:04:10 +020038
Marcel van Lohuizenef90d5c2019-06-29 14:05:25 +020039 // ReferenceFunc allows users to specify an alternative representation
Marcel van Lohuizenceb003d2019-08-08 13:45:10 +020040 // for references. An empty string tells the generator to expand the type
Marcel van Lohuizen35abfa72019-08-12 15:31:53 +020041 // in place and, if applicable, not generate a schema for that entity.
Marcel van Lohuizenef90d5c2019-06-29 14:05:25 +020042 ReferenceFunc func(inst *cue.Instance, path []string) string
43
Marcel van Lohuizen4de93e12019-07-02 20:04:10 +020044 // 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
Marcel van Lohuizenef90d5c2019-06-29 14:05:25 +020050 // SelfContained causes all non-expanded external references to be included
51 // in this document.
52 SelfContained bool
53
Marcel van Lohuizen2a8b4ed2020-06-16 09:21:44 +020054 // OpenAPI version to use. Supported as of v3.0.0.
55 Version string
56
Marcel van Lohuizen7b7ccdd2019-07-03 13:33:03 +020057 // 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
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040065 // ExpandReferences replaces references with actual objects when generating
Marcel van Lohuizen73e3f6b2019-08-01 16:10:57 +020066 // OpenAPI Schema. It is an error for an CUE value to refer to itself
67 // if this option is used.
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040068 ExpandReferences bool
69}
70
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010071type Generator = Config
Marcel van Lohuizen865f1592019-07-02 19:43:45 +020072
73// Gen generates the set OpenAPI schema for all top-level types of the
74// given instance.
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040075func Gen(inst *cue.Instance, c *Config) ([]byte, error) {
76 if c == nil {
77 c = defaultConfig
78 }
Marcel van Lohuizen865f1592019-07-02 19:43:45 +020079 all, err := c.All(inst)
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -040080 if err != nil {
81 return nil, err
82 }
Marcel van Lohuizen865f1592019-07-02 19:43:45 +020083 return json.Marshal(all)
84}
85
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010086// Generate generates the set of OpenAPI schema for all top-level types of the
87// given instance.
Marcel van Lohuizen865f1592019-07-02 19:43:45 +020088//
89// Note: only a limited number of top-level types are supported so far.
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010090func Generate(inst *cue.Instance, c *Config) (*ast.File, error) {
91 all, err := schemas(c, inst)
Marcel van Lohuizen865f1592019-07-02 19:43:45 +020092 if err != nil {
93 return nil, err
94 }
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +020095 top, err := c.compose(inst, all)
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +010096 if err != nil {
97 return nil, err
98 }
99 return &ast.File{Decls: top.Elts}, nil
100}
Marcel van Lohuizen865f1592019-07-02 19:43:45 +0200101
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100102// 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
106func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
107 all, err := schemas(g, inst)
108 if err != nil {
109 return nil, err
110 }
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200111 top, err := g.compose(inst, all)
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100112 return (*OrderedMap)(top), err
113}
Marcel van Lohuizen865f1592019-07-02 19:43:45 +0200114
Marcel van Lohuizen5c2b0862020-03-17 11:52:14 +0100115func 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
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200128func (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(); {
Daniel Martí4c106922020-08-18 21:49:52 +0200136 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 {
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200150 errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
Daniel Martí4c106922020-08-18 21:49:52 +0200151 "info must be a struct"))
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200152 }
Daniel Martí4c106922020-08-18 21:49:52 +0200153 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))
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200159 }
160 }
161
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100162 // Support of OrderedMap is mostly for backwards compatibility.
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100163 switch x := c.Info.(type) {
164 case nil:
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200165 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:
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100195 info = x
196 case *OrderedMap:
197 info = (*ast.StructLit)(x)
198 case OrderedMap:
199 info = (*ast.StructLit)(&x)
200 default:
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200201 x, err := toCUE("info section", x)
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100202 if err != nil {
Marcel van Lohuizen5c2b0862020-03-17 11:52:14 +0100203 return nil, err
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100204 }
Marcel van Lohuizen3c437bd2020-04-01 18:19:30 +0200205 info, _ = x.(*ast.StructLit)
206 errs = errors.Append(errs, errors.Newf(token.NoPos,
207 "Info field supplied must be an *ast.StructLit"))
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100208 }
Marcel van Lohuizen865f1592019-07-02 19:43:45 +0200209
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100210 return ast.NewStruct(
Marcel van Lohuizen2a8b4ed2020-06-16 09:21:44 +0200211 "openapi", ast.NewString(c.Version),
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100212 "info", info,
213 "paths", ast.NewStruct(),
214 "components", ast.NewStruct("schemas", schemas),
Daniel Martí4c106922020-08-18 21:49:52 +0200215 ), errs
Marcel van Lohuizen865f1592019-07-02 19:43:45 +0200216}
217
218// Schemas extracts component/schemas from the CUE top-level types.
Marcel van Lohuizenc89f5a62019-07-04 10:41:18 +0200219func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) {
Marcel van Lohuizen865f1592019-07-02 19:43:45 +0200220 comps, err := schemas(g, inst)
221 if err != nil {
222 return nil, err
223 }
Marcel van Lohuizenf51c8792020-03-05 17:53:29 +0100224 return (*OrderedMap)(comps), err
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -0400225}
226
227var 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
Marcel van Lohuizenf4d483e2019-05-20 08:03:10 -0400237//