...

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

Documentation: cuelang.org/go/encoding/openapi

     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  	"strings"
    19  
    20  	"cuelang.org/go/cue"
    21  	"cuelang.org/go/cue/ast"
    22  	"cuelang.org/go/cue/errors"
    23  	"cuelang.org/go/cue/token"
    24  	"cuelang.org/go/encoding/jsonschema"
    25  	"cuelang.org/go/internal"
    26  )
    27  
    28  // Extract converts OpenAPI definitions to an equivalent CUE representation.
    29  //
    30  // It currently only converts entries in #/components/schema and extracts some
    31  // meta data.
    32  func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) {
    33  	// TODO: find a good OpenAPI validator. Both go-openapi and kin-openapi
    34  	// seem outdated. The k8s one might be good, but avoid pulling in massive
    35  	// amounts of dependencies.
    36  
    37  	f := &ast.File{}
    38  	add := func(d ast.Decl) {
    39  		if d != nil {
    40  			f.Decls = append(f.Decls, d)
    41  		}
    42  	}
    43  
    44  	js, err := jsonschema.Extract(data, &jsonschema.Config{
    45  		Root: oapiSchemas,
    46  		Map:  openAPIMapping,
    47  	})
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	v := data.Value()
    53  
    54  	doc, _ := v.Lookup("info", "title").String() // Required
    55  	if s, _ := v.Lookup("info", "description").String(); s != "" {
    56  		doc += "\n\n" + s
    57  	}
    58  	cg := internal.NewComment(true, doc)
    59  
    60  	if c.PkgName != "" {
    61  		p := &ast.Package{Name: ast.NewIdent(c.PkgName)}
    62  		p.AddComment(cg)
    63  		add(p)
    64  	} else {
    65  		add(cg)
    66  	}
    67  
    68  	preamble := js.Preamble()
    69  	body := js.Decls[len(preamble):]
    70  	for _, d := range preamble {
    71  		switch x := d.(type) {
    72  		case *ast.Package:
    73  			return nil, errors.Newf(x.Pos(), "unexpected package %q", x.Name.Name)
    74  
    75  		default:
    76  			add(x)
    77  		}
    78  	}
    79  
    80  	// TODO: allow attributes before imports? Would be easier.
    81  
    82  	// TODO: do we want to store the OpenAPI version?
    83  	// if version, _ := v.Lookup("openapi").String(); version != "" {
    84  	// 	add(internal.NewAttr("openapi", "version="+ version))
    85  	// }
    86  
    87  	if info := v.Lookup("info"); info.Exists() {
    88  		decls := []interface{}{}
    89  		if st, ok := info.Syntax().(*ast.StructLit); ok {
    90  			// Remove title.
    91  			for _, d := range st.Elts {
    92  				if f, ok := d.(*ast.Field); ok {
    93  					switch name, _, _ := ast.LabelName(f.Label); name {
    94  					case "title", "version":
    95  						// title: *"title" | string
    96  						decls = append(decls, &ast.Field{
    97  							Label: f.Label,
    98  							Value: ast.NewBinExpr(token.OR,
    99  								&ast.UnaryExpr{Op: token.MUL, X: f.Value},
   100  								ast.NewIdent("string")),
   101  						})
   102  						continue
   103  					}
   104  				}
   105  				decls = append(decls, d)
   106  			}
   107  			add(&ast.Field{
   108  				Label: ast.NewIdent("info"),
   109  				Value: ast.NewStruct(decls...),
   110  			})
   111  		}
   112  	}
   113  
   114  	if len(body) > 0 {
   115  		ast.SetRelPos(body[0], token.NewSection)
   116  		f.Decls = append(f.Decls, body...)
   117  	}
   118  
   119  	return f, nil
   120  }
   121  
   122  const oapiSchemas = "#/components/schemas/"
   123  
   124  // rootDefs is the fallback for schemas that are not valid identifiers.
   125  // TODO: find something more principled.
   126  const rootDefs = "#SchemaMap"
   127  
   128  func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) {
   129  	if len(a) != 3 || a[0] != "components" || a[1] != "schemas" {
   130  		return nil, errors.Newf(pos,
   131  			`openapi: reference must be of the form %q; found "#/%s"`,
   132  			oapiSchemas, strings.Join(a, "/"))
   133  	}
   134  	name := a[2]
   135  	if ast.IsValidIdent(name) &&
   136  		name != rootDefs[1:] &&
   137  		!internal.IsDefOrHidden(name) {
   138  		return []ast.Label{ast.NewIdent("#" + name)}, nil
   139  	}
   140  	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
   141  }
   142  

View as plain text