...

Source file src/cuelang.org/go/encoding/openapi/openapi.go

Documentation: cuelang.org/go/encoding/openapi

     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  	// 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  	//
    43  	// If this field is non-nil and a cue.Value is passed as the InstanceOrValue,
    44  	// there will be a panic.
    45  	//
    46  	// Deprecated: use NameFunc instead.
    47  	ReferenceFunc func(inst *cue.Instance, path []string) string
    48  
    49  	// NameFunc allows users to specify an alternative representation
    50  	// for references. It is called with the value passed to the top level
    51  	// method or function and the path to the entity being generated.
    52  	// If it returns an empty string the generator will  expand the type
    53  	// in place and, if applicable, not generate a schema for that entity.
    54  	//
    55  	// Note: this only returns the final element of the /-separated
    56  	// reference.
    57  	NameFunc func(val cue.Value, path cue.Path) string
    58  
    59  	// DescriptionFunc allows rewriting a description associated with a certain
    60  	// field. A typical implementation compiles the description from the
    61  	// comments obtains from the Doc method. No description field is added if
    62  	// the empty string is returned.
    63  	DescriptionFunc func(v cue.Value) string
    64  
    65  	// SelfContained causes all non-expanded external references to be included
    66  	// in this document.
    67  	SelfContained bool
    68  
    69  	// OpenAPI version to use. Supported as of v3.0.0.
    70  	Version string
    71  
    72  	// FieldFilter defines a regular expression of all fields to omit from the
    73  	// output. It is only allowed to filter fields that add additional
    74  	// constraints. Fields that indicate basic types cannot be removed. It is
    75  	// an error for such fields to be excluded by this filter.
    76  	// Fields are qualified by their Object type. For instance, the
    77  	// minimum field of the schema object is qualified as Schema/minimum.
    78  	FieldFilter string
    79  
    80  	// ExpandReferences replaces references with actual objects when generating
    81  	// OpenAPI Schema. It is an error for an CUE value to refer to itself
    82  	// if this option is used.
    83  	ExpandReferences 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 := c.All(inst)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	return internaljson.Marshal(all)
    99  }
   100  
   101  // Generate generates the set of OpenAPI schema for all top-level types of the
   102  // given instance.
   103  //
   104  // Note: only a limited number of top-level types are supported so far.
   105  func Generate(inst cue.InstanceOrValue, c *Config) (*ast.File, error) {
   106  	all, err := schemas(c, inst)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	top, err := c.compose(inst, all)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	return &ast.File{Decls: top.Elts}, nil
   115  }
   116  
   117  // All generates an OpenAPI definition from the given instance.
   118  //
   119  // Note: only a limited number of top-level types are supported so far.
   120  // Deprecated: use Generate
   121  func (g *Generator) All(inst cue.InstanceOrValue) (*OrderedMap, error) {
   122  	all, err := schemas(g, inst)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	top, err := g.compose(inst, all)
   127  	return (*OrderedMap)(top), err
   128  }
   129  
   130  func toCUE(name string, x interface{}) (v ast.Expr, err error) {
   131  	b, err := internaljson.Marshal(x)
   132  	if err == nil {
   133  		v, err = cuejson.Extract(name, b)
   134  	}
   135  	if err != nil {
   136  		return nil, errors.Wrapf(err, token.NoPos,
   137  			"openapi: could not encode %s", name)
   138  	}
   139  	return v, nil
   140  
   141  }
   142  
   143  func (c *Config) compose(inst cue.InstanceOrValue, schemas *ast.StructLit) (x *ast.StructLit, err error) {
   144  	val := inst.Value()
   145  	var errs errors.Error
   146  
   147  	var title, version string
   148  	var info *ast.StructLit
   149  
   150  	for i, _ := val.Fields(cue.Definitions(true)); i.Next(); {
   151  		if i.IsDefinition() {
   152  			continue
   153  		}
   154  		label := i.Label()
   155  		attr := i.Value().Attribute("openapi")
   156  		if s, _ := attr.String(0); s != "" {
   157  			label = s
   158  		}
   159  		switch label {
   160  		case "$version":
   161  		case "-":
   162  		case "info":
   163  			info, _ = i.Value().Syntax().(*ast.StructLit)
   164  			if info == nil {
   165  				errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
   166  					"info must be a struct"))
   167  			}
   168  			title, _ = i.Value().Lookup("title").String()
   169  			version, _ = i.Value().Lookup("version").String()
   170  
   171  		default:
   172  			errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
   173  				"openapi: unsupported top-level field %q", label))
   174  		}
   175  	}
   176  
   177  	// Support of OrderedMap is mostly for backwards compatibility.
   178  	switch x := c.Info.(type) {
   179  	case nil:
   180  		if title == "" {
   181  			title = "Generated by cue."
   182  			for _, d := range val.Doc() {
   183  				title = strings.TrimSpace(d.Text())
   184  				break
   185  			}
   186  		}
   187  
   188  		if version == "" {
   189  			version, _ = val.Lookup("$version").String()
   190  			if version == "" {
   191  				version = "no version"
   192  			}
   193  		}
   194  
   195  		if info == nil {
   196  			info = ast.NewStruct(
   197  				"title", ast.NewString(title),
   198  				"version", ast.NewString(version),
   199  			)
   200  		} else {
   201  			m := (*OrderedMap)(info)
   202  			m.Set("title", ast.NewString(title))
   203  			m.Set("version", ast.NewString(version))
   204  		}
   205  
   206  	case *ast.StructLit:
   207  		info = x
   208  	case *OrderedMap:
   209  		info = (*ast.StructLit)(x)
   210  	case OrderedMap:
   211  		info = (*ast.StructLit)(&x)
   212  	default:
   213  		x, err := toCUE("info section", x)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		var ok bool
   218  		info, ok = x.(*ast.StructLit)
   219  		if !ok {
   220  			errs = errors.Append(errs, errors.Newf(token.NoPos,
   221  				"Info field supplied must marshal to a struct but got %s", fmt.Sprintf("%T", x)))
   222  		}
   223  	}
   224  
   225  	return ast.NewStruct(
   226  		"openapi", ast.NewString(c.Version),
   227  		"info", info,
   228  		"paths", ast.NewStruct(),
   229  		"components", ast.NewStruct("schemas", schemas),
   230  	), errs
   231  }
   232  
   233  // Schemas extracts component/schemas from the CUE top-level types.
   234  func (g *Generator) Schemas(inst cue.InstanceOrValue) (*OrderedMap, error) {
   235  	comps, err := schemas(g, inst)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	return (*OrderedMap)(comps), err
   240  }
   241  
   242  var defaultConfig = &Config{}
   243  
   244  // TODO
   245  // The conversion interprets @openapi(<entry> {, <entry>}) attributes as follows:
   246  //
   247  //      readOnly        sets the readOnly flag for a property in the schema
   248  //                      only one of readOnly and writeOnly may be set.
   249  //      writeOnly       sets the writeOnly flag for a property in the schema
   250  //                      only one of readOnly and writeOnly may be set.
   251  //      discriminator   explicitly sets a field as the discriminator field
   252  //
   253  

View as plain text