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