blob: a8a61b8ac968191a12b3c20c169558035e5e103c [file] [log] [blame]
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package openapi
import (
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"cuelang.org/go/encoding/jsonschema"
"cuelang.org/go/internal"
)
// Extract converts OpenAPI definitions to an equivalent CUE representation.
//
// It currently only converts entries in #/components/schema and extracts some
// meta data.
func Extract(data *cue.Instance, c *Config) (*ast.File, error) {
// TODO: find a good OpenAPI validator. Both go-openapi and kin-openapi
// seem outdated. The k8s one might be good, but avoid pulling in massive
// amounts of dependencies.
f := &ast.File{}
add := func(d ast.Decl) {
if d != nil {
f.Decls = append(f.Decls, d)
}
}
js, err := jsonschema.Extract(data, &jsonschema.Config{
Root: oapiSchemas,
Map: openAPIMapping,
})
if err != nil {
return nil, err
}
v := data.Value()
doc, _ := v.Lookup("info", "title").String() // Required
if s, _ := v.Lookup("info", "description").String(); s != "" {
doc += "\n\n" + s
}
cg := internal.NewComment(true, doc)
if c.PkgName != "" {
p := &ast.Package{Name: ast.NewIdent(c.PkgName)}
p.AddComment(cg)
add(p)
} else {
add(cg)
}
i := 0
for ; i < len(js.Decls); i++ {
switch x := js.Decls[i].(type) {
case *ast.Package:
return nil, errors.Newf(x.Pos(), "unexpected package %q", x.Name.Name)
case *ast.ImportDecl, *ast.CommentGroup:
add(x)
continue
}
break
}
// TODO: allow attributes before imports? Would be easier.
// TODO: do we want to store the OpenAPI version?
// if version, _ := v.Lookup("openapi").String(); version != "" {
// add(internal.NewAttr("openapi", "version="+ version))
// }
if info := v.Lookup("info"); info.Exists() {
decls := []interface{}{}
if st, ok := info.Syntax().(*ast.StructLit); ok {
// Remove title.
for _, d := range st.Elts {
if f, ok := d.(*ast.Field); ok {
switch name, _, _ := ast.LabelName(f.Label); name {
case "title", "version":
// title: *"title" | string
decls = append(decls, &ast.Field{
Label: f.Label,
Value: ast.NewBinExpr(token.OR,
&ast.UnaryExpr{Op: token.MUL, X: f.Value},
ast.NewIdent("string")),
})
continue
}
}
decls = append(decls, d)
}
add(&ast.Field{
Label: ast.NewIdent("info"),
Value: ast.NewStruct(decls...),
})
}
}
if i < len(js.Decls) {
ast.SetRelPos(js.Decls[i], token.NewSection)
f.Decls = append(f.Decls, js.Decls[i:]...)
}
return f, nil
}
const oapiSchemas = "#/components/schemas/"
// rootDefs is the fallback for schemas that are not valid identifiers.
// TODO: find something more principled.
const rootDefs = "#SchemaMap"
func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) {
if len(a) != 3 || a[0] != "components" || a[1] != "schemas" {
return nil, errors.Newf(pos,
`openapi: reference must be of the form %q; found "#/%s"`,
oapiSchemas, strings.Join(a, "/"))
}
name := a[2]
if ast.IsValidIdent(name) &&
name != rootDefs[1:] &&
!internal.IsDefOrHidden(name) {
return []ast.Label{ast.NewIdent("#" + name)}, nil
}
return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
}