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