github.com/solo-io/cue@v0.4.7/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  	"encoding/json"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/solo-io/cue/cue"
    23  	"github.com/solo-io/cue/cue/ast"
    24  	"github.com/solo-io/cue/cue/errors"
    25  	"github.com/solo-io/cue/cue/token"
    26  	cuejson "github.com/solo-io/cue/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  	ReferenceFunc func(inst *cue.Instance, path []string) string
    43  
    44  	// 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  
    50  	// SelfContained causes all non-expanded external references to be included
    51  	// in this document.
    52  	SelfContained bool
    53  
    54  	// OpenAPI version to use. Supported as of v3.0.0.
    55  	Version string
    56  
    57  	// 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  
    65  	// ExpandReferences replaces references with actual objects when generating
    66  	// OpenAPI Schema. It is an error for an CUE value to refer to itself
    67  	// if this option is used.
    68  	ExpandReferences bool
    69  
    70  	// UnstructuredFields is map of:
    71  	// go package to type name to the list of fields on the type which should be treated as unstructured
    72  	// for the purpose of openapi schema generation
    73  	UnstructuredFields map[string][][]string
    74  }
    75  
    76  type Generator = Config
    77  
    78  // Gen generates the set OpenAPI schema for all top-level types of the
    79  // given instance.
    80  func Gen(inst *cue.Instance, c *Config) ([]byte, error) {
    81  	if c == nil {
    82  		c = defaultConfig
    83  	}
    84  	all, err := c.All(inst)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	return json.Marshal(all)
    89  }
    90  
    91  // Generate generates the set of OpenAPI schema for all top-level types of the
    92  // given instance.
    93  //
    94  // Note: only a limited number of top-level types are supported so far.
    95  func Generate(inst *cue.Instance, c *Config) (*ast.File, error) {
    96  	all, err := schemas(c, inst)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	top, err := c.compose(inst, all)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return &ast.File{Decls: top.Elts}, nil
   105  }
   106  
   107  // All generates an OpenAPI definition from the given instance.
   108  //
   109  // Note: only a limited number of top-level types are supported so far.
   110  // Deprecated: use Generate
   111  func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
   112  	all, err := schemas(g, inst)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	top, err := g.compose(inst, all)
   117  	return (*OrderedMap)(top), err
   118  }
   119  
   120  func toCUE(name string, x interface{}) (v ast.Expr, err error) {
   121  	b, err := json.Marshal(x)
   122  	if err == nil {
   123  		v, err = cuejson.Extract(name, b)
   124  	}
   125  	if err != nil {
   126  		return nil, errors.Wrapf(err, token.NoPos,
   127  			"openapi: could not encode %s", name)
   128  	}
   129  	return v, nil
   130  
   131  }
   132  
   133  func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) {
   134  
   135  	var errs errors.Error
   136  
   137  	var title, version string
   138  	var info *ast.StructLit
   139  
   140  	for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); {
   141  		if i.IsDefinition() {
   142  			continue
   143  		}
   144  		label := i.Label()
   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().Lookup("title").String()
   159  			version, _ = i.Value().Lookup("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  	// Support of OrderedMap is mostly for backwards compatibility.
   168  	switch x := c.Info.(type) {
   169  	case nil:
   170  		if title == "" {
   171  			title = "Generated by cue."
   172  			for _, d := range inst.Doc() {
   173  				title = strings.TrimSpace(d.Text())
   174  				break
   175  			}
   176  			if p := inst.ImportPath; title == "" && p != "" {
   177  				title = fmt.Sprintf("Generated by cue from package %q", p)
   178  			}
   179  		}
   180  
   181  		if version == "" {
   182  			version, _ = inst.Lookup("$version").String()
   183  			if version == "" {
   184  				version = "no version"
   185  			}
   186  		}
   187  
   188  		if info == nil {
   189  			info = ast.NewStruct(
   190  				"title", ast.NewString(title),
   191  				"version", ast.NewString(version),
   192  			)
   193  		} else {
   194  			m := (*OrderedMap)(info)
   195  			m.Set("title", ast.NewString(title))
   196  			m.Set("version", ast.NewString(version))
   197  		}
   198  
   199  	case *ast.StructLit:
   200  		info = x
   201  	case *OrderedMap:
   202  		info = (*ast.StructLit)(x)
   203  	case OrderedMap:
   204  		info = (*ast.StructLit)(&x)
   205  	default:
   206  		x, err := toCUE("info section", x)
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		info, _ = x.(*ast.StructLit)
   211  		errs = errors.Append(errs, errors.Newf(token.NoPos,
   212  			"Info field supplied must be an *ast.StructLit"))
   213  	}
   214  
   215  	return ast.NewStruct(
   216  		"openapi", ast.NewString(c.Version),
   217  		"info", info,
   218  		"paths", ast.NewStruct(),
   219  		"components", ast.NewStruct("schemas", schemas),
   220  	), errs
   221  }
   222  
   223  // Schemas extracts component/schemas from the CUE top-level types.
   224  func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) {
   225  	comps, err := schemas(g, inst)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	return (*OrderedMap)(comps), err
   230  }
   231  
   232  var defaultConfig = &Config{}
   233  
   234  // TODO
   235  // The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows:
   236  //
   237  //      readOnly        sets the readOnly flag for a property in the schema
   238  //                      only one of readOnly and writeOnly may be set.
   239  //      writeOnly       sets the writeOnly flag for a property in the schema
   240  //                      only one of readOnly and writeOnly may be set.
   241  //      discriminator   explicitly sets a field as the discriminator field
   242  //