cuelang.org/go@v0.13.0/encoding/openapi/decode.go (about)

     1  // Copyright 2020 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  	"cuelang.org/go/encoding/jsonschema"
    26  	"cuelang.org/go/internal"
    27  )
    28  
    29  // Extract converts OpenAPI definitions to an equivalent CUE representation.
    30  //
    31  // It currently only converts entries in #/components/schema and extracts some
    32  // meta data.
    33  func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) {
    34  	// TODO: find a good OpenAPI validator. Both go-openapi and kin-openapi
    35  	// seem outdated. The k8s one might be good, but avoid pulling in massive
    36  	// amounts of dependencies.
    37  
    38  	f := &ast.File{}
    39  	add := func(d ast.Decl) {
    40  		if d != nil {
    41  			f.Decls = append(f.Decls, d)
    42  		}
    43  	}
    44  
    45  	v := data.Value()
    46  	versionValue := v.LookupPath(cue.MakePath(cue.Str("openapi")))
    47  	if versionValue.Err() != nil {
    48  		return nil, fmt.Errorf("openapi field is required but not found")
    49  	}
    50  	version, err := versionValue.String()
    51  	if err != nil {
    52  		return nil, fmt.Errorf("invalid openapi field (must be string): %v", err)
    53  	}
    54  	// A simple prefix match is probably OK for now, following
    55  	// the same logic used by internal/encoding.isOpenAPI.
    56  	// The specification says that the patch version should be disregarded:
    57  	// https://swagger.io/specification/v3/
    58  	var schemaVersion jsonschema.Version
    59  	switch {
    60  	case strings.HasPrefix(version, "3.0."):
    61  		schemaVersion = jsonschema.VersionOpenAPI
    62  	case strings.HasPrefix(version, "3.1."):
    63  		schemaVersion = jsonschema.VersionDraft2020_12
    64  	default:
    65  		return nil, fmt.Errorf("unknown OpenAPI version %q", version)
    66  	}
    67  
    68  	doc, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("title"))).String() // Required
    69  	if s, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("description"))).String(); s != "" {
    70  		doc += "\n\n" + s
    71  	}
    72  	cg := internal.NewComment(true, doc)
    73  
    74  	if c.PkgName != "" {
    75  		p := &ast.Package{Name: ast.NewIdent(c.PkgName)}
    76  		p.AddComment(cg)
    77  		add(p)
    78  	} else if cg != nil {
    79  		add(cg)
    80  	}
    81  
    82  	js, err := jsonschema.Extract(data, &jsonschema.Config{
    83  		Root:           oapiSchemas,
    84  		Map:            openAPIMapping,
    85  		DefaultVersion: schemaVersion,
    86  		StrictFeatures: c.StrictFeatures,
    87  		// OpenAPI 3.0 is stricter than JSON Schema about allowed keywords.
    88  		StrictKeywords: schemaVersion == jsonschema.VersionOpenAPI || c.StrictKeywords,
    89  	})
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	preamble := js.Preamble()
    94  	body := js.Decls[len(preamble):]
    95  	for _, d := range preamble {
    96  		switch x := d.(type) {
    97  		case *ast.Package:
    98  			return nil, errors.Newf(x.Pos(), "unexpected package %q", x.Name.Name)
    99  
   100  		default:
   101  			add(x)
   102  		}
   103  	}
   104  
   105  	// TODO: allow attributes before imports? Would be easier.
   106  
   107  	// TODO: do we want to store the OpenAPI version?
   108  	// if version, _ := v.Lookup("openapi").String(); version != "" {
   109  	// 	add(internal.NewAttr("openapi", "version="+ version))
   110  	// }
   111  
   112  	if info := v.LookupPath(cue.MakePath(cue.Str("info"))); info.Exists() {
   113  		decls := []interface{}{}
   114  		if st, ok := info.Syntax().(*ast.StructLit); ok {
   115  			// Remove title.
   116  			for _, d := range st.Elts {
   117  				if f, ok := d.(*ast.Field); ok {
   118  					switch name, _, _ := ast.LabelName(f.Label); name {
   119  					case "title", "version":
   120  						// title: *"title" | string
   121  						decls = append(decls, &ast.Field{
   122  							Label: f.Label,
   123  							Value: ast.NewBinExpr(token.OR,
   124  								&ast.UnaryExpr{Op: token.MUL, X: f.Value},
   125  								ast.NewIdent("string")),
   126  						})
   127  						continue
   128  					}
   129  				}
   130  				decls = append(decls, d)
   131  			}
   132  			add(&ast.Field{
   133  				Label: ast.NewIdent("info"),
   134  				Value: ast.NewStruct(decls...),
   135  			})
   136  		}
   137  	}
   138  
   139  	if len(body) > 0 {
   140  		ast.SetRelPos(body[0], token.NewSection)
   141  		f.Decls = append(f.Decls, body...)
   142  	}
   143  
   144  	return f, nil
   145  }
   146  
   147  const oapiSchemas = "#/components/schemas/"
   148  
   149  // rootDefs is the fallback for schemas that are not valid identifiers.
   150  // TODO: find something more principled.
   151  const rootDefs = "#SchemaMap"
   152  
   153  func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) {
   154  	if len(a) != 3 || a[0] != "components" || a[1] != "schemas" {
   155  		return nil, errors.Newf(pos,
   156  			`openapi: reference must be of the form %q; found "#/%s"`,
   157  			oapiSchemas, strings.Join(a, "/"))
   158  	}
   159  	name := a[2]
   160  	if ast.IsValidIdent(name) &&
   161  		name != rootDefs[1:] &&
   162  		!internal.IsDefOrHidden(name) {
   163  		return []ast.Label{ast.NewIdent("#" + name)}, nil
   164  	}
   165  	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
   166  }