blob: 6eaebb8cae0b29f4f9c85f4e6a76886e518cf7e3 [file] [log] [blame]
// Copyright 2019 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 (
"encoding/json"
"fmt"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
cuejson "cuelang.org/go/encoding/json"
)
// A Config defines options for converting CUE to and from OpenAPI.
type Config struct {
// PkgName defines to package name for a generated CUE package.
PkgName string
// Info specifies the info section of the OpenAPI document. To be a valid
// OpenAPI document, it must include at least the title and version fields.
// Info may be a *ast.StructLit or any type that marshals to JSON.
Info interface{}
// ReferenceFunc allows users to specify an alternative representation
// for references. An empty string tells the generator to expand the type
// in place and, if applicable, not generate a schema for that entity.
ReferenceFunc func(inst *cue.Instance, path []string) string
// DescriptionFunc allows rewriting a description associated with a certain
// field. A typical implementation compiles the description from the
// comments obtains from the Doc method. No description field is added if
// the empty string is returned.
DescriptionFunc func(v cue.Value) string
// SelfContained causes all non-expanded external references to be included
// in this document.
SelfContained bool
// OpenAPI version to use. Supported as of v3.0.0.
Version string
// FieldFilter defines a regular expression of all fields to omit from the
// output. It is only allowed to filter fields that add additional
// constraints. Fields that indicate basic types cannot be removed. It is
// an error for such fields to be excluded by this filter.
// Fields are qualified by their Object type. For instance, the
// minimum field of the schema object is qualified as Schema/minimum.
FieldFilter string
// ExpandReferences replaces references with actual objects when generating
// OpenAPI Schema. It is an error for an CUE value to refer to itself
// if this option is used.
ExpandReferences bool
}
type Generator = Config
// Gen generates the set OpenAPI schema for all top-level types of the
// given instance.
func Gen(inst *cue.Instance, c *Config) ([]byte, error) {
if c == nil {
c = defaultConfig
}
all, err := c.All(inst)
if err != nil {
return nil, err
}
return json.Marshal(all)
}
// Generate generates the set of OpenAPI schema for all top-level types of the
// given instance.
//
// Note: only a limited number of top-level types are supported so far.
func Generate(inst *cue.Instance, c *Config) (*ast.File, error) {
all, err := schemas(c, inst)
if err != nil {
return nil, err
}
top, err := c.compose(inst, all)
if err != nil {
return nil, err
}
return &ast.File{Decls: top.Elts}, nil
}
// All generates an OpenAPI definition from the given instance.
//
// Note: only a limited number of top-level types are supported so far.
// Deprecated: use Generate
func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
all, err := schemas(g, inst)
if err != nil {
return nil, err
}
top, err := g.compose(inst, all)
return (*OrderedMap)(top), err
}
func toCUE(name string, x interface{}) (v ast.Expr, err error) {
b, err := json.Marshal(x)
if err == nil {
v, err = cuejson.Extract(name, b)
}
if err != nil {
return nil, errors.Wrapf(err, token.NoPos,
"openapi: could not encode %s", name)
}
return v, nil
}
func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) {
var errs errors.Error
var title, version string
var info *ast.StructLit
for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); {
if i.IsDefinition() {
continue
}
label := i.Label()
attr := i.Value().Attribute("openapi")
if s, _ := attr.String(0); s != "" {
label = s
}
switch label {
case "$version":
case "-":
case "info":
info, _ = i.Value().Syntax().(*ast.StructLit)
if info == nil {
errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
"info must be a struct"))
}
title, _ = i.Value().Lookup("title").String()
version, _ = i.Value().Lookup("version").String()
default:
errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
"openapi: unsupported top-level field %q", label))
}
}
// Support of OrderedMap is mostly for backwards compatibility.
switch x := c.Info.(type) {
case nil:
if title == "" {
title = "Generated by cue."
for _, d := range inst.Doc() {
title = strings.TrimSpace(d.Text())
break
}
if p := inst.ImportPath; title == "" && p != "" {
title = fmt.Sprintf("Generated by cue from package %q", p)
}
}
if version == "" {
version, _ = inst.Lookup("$version").String()
if version == "" {
version = "no version"
}
}
if info == nil {
info = ast.NewStruct(
"title", ast.NewString(title),
"version", ast.NewString(version),
)
} else {
m := (*OrderedMap)(info)
m.Set("title", ast.NewString(title))
m.Set("version", ast.NewString(version))
}
case *ast.StructLit:
info = x
case *OrderedMap:
info = (*ast.StructLit)(x)
case OrderedMap:
info = (*ast.StructLit)(&x)
default:
x, err := toCUE("info section", x)
if err != nil {
return nil, err
}
info, _ = x.(*ast.StructLit)
errs = errors.Append(errs, errors.Newf(token.NoPos,
"Info field supplied must be an *ast.StructLit"))
}
return ast.NewStruct(
"openapi", ast.NewString(c.Version),
"info", info,
"paths", ast.NewStruct(),
"components", ast.NewStruct("schemas", schemas),
), errs
}
// Schemas extracts component/schemas from the CUE top-level types.
func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) {
comps, err := schemas(g, inst)
if err != nil {
return nil, err
}
return (*OrderedMap)(comps), err
}
var defaultConfig = &Config{}
// TODO
// The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows:
//
// readOnly sets the readOnly flag for a property in the schema
// only one of readOnly and writeOnly may be set.
// writeOnly sets the writeOnly flag for a property in the schema
// only one of readOnly and writeOnly may be set.
// discriminator explicitly sets a field as the discriminator field
//