github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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/joomcode/cue/cue"
    23  	"github.com/joomcode/cue/cue/ast"
    24  	"github.com/joomcode/cue/cue/errors"
    25  	"github.com/joomcode/cue/cue/token"
    26  	cuejson "github.com/joomcode/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  
    71  type Generator = Config
    72  
    73  // Gen generates the set OpenAPI schema for all top-level types of the
    74  // given instance.
    75  func Gen(inst *cue.Instance, c *Config) ([]byte, error) {
    76  	if c == nil {
    77  		c = defaultConfig
    78  	}
    79  	all, err := c.All(inst)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	return json.Marshal(all)
    84  }
    85  
    86  // Generate generates the set of OpenAPI schema for all top-level types of the
    87  // given instance.
    88  //
    89  // Note: only a limited number of top-level types are supported so far.
    90  func Generate(inst *cue.Instance, c *Config) (*ast.File, error) {
    91  	all, err := schemas(c, inst)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	top, err := c.compose(inst, all)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	return &ast.File{Decls: top.Elts}, nil
   100  }
   101  
   102  // 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
   106  func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
   107  	all, err := schemas(g, inst)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	top, err := g.compose(inst, all)
   112  	return (*OrderedMap)(top), err
   113  }
   114  
   115  func 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  
   128  func (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(); {
   136  		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 {
   150  				errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
   151  					"info must be a struct"))
   152  			}
   153  			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))
   159  		}
   160  	}
   161  
   162  	// Support of OrderedMap is mostly for backwards compatibility.
   163  	switch x := c.Info.(type) {
   164  	case nil:
   165  		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:
   195  		info = x
   196  	case *OrderedMap:
   197  		info = (*ast.StructLit)(x)
   198  	case OrderedMap:
   199  		info = (*ast.StructLit)(&x)
   200  	default:
   201  		x, err := toCUE("info section", x)
   202  		if err != nil {
   203  			return nil, err
   204  		}
   205  		info, _ = x.(*ast.StructLit)
   206  		errs = errors.Append(errs, errors.Newf(token.NoPos,
   207  			"Info field supplied must be an *ast.StructLit"))
   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  
   218  // Schemas extracts component/schemas from the CUE top-level types.
   219  func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) {
   220  	comps, err := schemas(g, inst)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	return (*OrderedMap)(comps), err
   225  }
   226  
   227  var 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
   237  //