mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Feat: CUE generator with type conversion (#5585)
* Feat: CUE generator with type conversion Signed-off-by: iyear <ljyngup@gmail.com> * Fix: golangci-lint error Signed-off-by: iyear <ljyngup@gmail.com> * Fix: add licenses Signed-off-by: iyear <ljyngup@gmail.com> * Fix: go mod tidy Signed-off-by: iyear <ljyngup@gmail.com> * Tests: add util unit tests Signed-off-by: iyear <ljyngup@gmail.com> * Tests: add registry unit tests Signed-off-by: iyear <ljyngup@gmail.com> * Fix: add util_test.go header license Signed-off-by: iyear <ljyngup@gmail.com> * Tests: add tag unit tests Signed-off-by: iyear <ljyngup@gmail.com> * Fix: small bugs Signed-off-by: iyear <ljyngup@gmail.com> * Tests: add testdata Signed-off-by: iyear <ljyngup@gmail.com> * Fix: add licenses to testdata go files Signed-off-by: iyear <ljyngup@gmail.com> * Chore: modify according to FogDong Signed-off-by: iyear <ljyngup@gmail.com> * Chore: remove ident '_' addition Signed-off-by: iyear <ljyngup@gmail.com> * Chore: add tests and refactor files Signed-off-by: iyear <ljyngup@gmail.com> * Chore: remove useless escape test and ident placeholder Signed-off-by: iyear <ljyngup@gmail.com> * Chore: add more escape tests Signed-off-by: iyear <ljyngup@gmail.com> * Fix: index out of range and add edge case tests Signed-off-by: iyear <ljyngup@gmail.com> * Feat: support escaping separators ';' ':' ',' Signed-off-by: iyear <ljyngup@gmail.com> --------- Signed-off-by: iyear <ljyngup@gmail.com>
This commit is contained in:
@@ -113,6 +113,7 @@ require (
|
||||
require (
|
||||
github.com/dave/jennifer v1.6.0
|
||||
github.com/ettle/strcase v0.1.1
|
||||
golang.org/x/tools v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -290,7 +291,6 @@ require (
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/grpc v1.48.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## CUE Generator
|
||||
|
||||
Auto generation of CUE schema and docs from Go struct
|
||||
|
||||
## Type Conversion
|
||||
|
||||
- All comments will be copied to CUE schema
|
||||
|
||||
### Basic Types
|
||||
|
||||
| Go Type | CUE Type |
|
||||
|:------------------:|:---------:|
|
||||
| `int` | `int` |
|
||||
| `int8` | `int8` |
|
||||
| `int16` | `int16` |
|
||||
| `int32` | `int32` |
|
||||
| `int64` | `int64` |
|
||||
| `uint` | `uint` |
|
||||
| `uint8` | `uint8` |
|
||||
| `uint16` | `uint16` |
|
||||
| `uint32` | `uint32` |
|
||||
| `uint64` | `uint64` |
|
||||
| `float32` | `float32` |
|
||||
| `float64` | `float64` |
|
||||
| `string` | `string` |
|
||||
| `bool` | `bool` |
|
||||
| `nil` | `null` |
|
||||
| `byte` | `uint8` |
|
||||
| `uintptr` | `uint64` |
|
||||
| `[]byte` | `bytes` |
|
||||
| `interface{}/any` | `{...}` |
|
||||
| `interface{ ... }` | `_` |
|
||||
|
||||
### Map Type
|
||||
|
||||
- CUE only supports `map[string]T` type, which is converted to `[string]: T` in CUE schema
|
||||
- All `map[string]any/map[string]interface{}` are converted to `{...}` in CUE schema
|
||||
|
||||
### Struct Type
|
||||
|
||||
- Fields will be expanded recursively in CUE schema
|
||||
- All unexported fields will be ignored
|
||||
- Do not support recursive struct type, which will cause infinite loop
|
||||
|
||||
`json` Tag:
|
||||
|
||||
- Fields with `json:"FIELD_NAME"` tag will be renamed to `FIELD_NAME` in CUE schema, otherwise the field name will be
|
||||
used
|
||||
- Fields with `json:"-"` tag will be ignored in generation
|
||||
- Anonymous fields with `json:",inline"` tag will be expanded inlined in CUE schema
|
||||
- Fields with `json:",omitempty"` tag will be marked as optional in CUE schema
|
||||
|
||||
`cue` Tag:
|
||||
|
||||
- Format: `cue:"key1:value1;key2:value2;boolValue1;boolValue2"`
|
||||
- Fields with `cue:"enum:VALUE1,VALUE2"` tag will be set with enum values `VALUE1` and `VALUE2` in CUE schema
|
||||
- Fields with `cue:"default:VALUE"` tag will be set with default value `VALUE` in CUE schema, and `VALUE` must be one of
|
||||
go basic types, including `int`, `float`, `string`, `bool`
|
||||
- Separators `';'`, `':'` and `','` can be escaped with `'\'`, e.g. `cue:"default:va\\;lue\\:;enum:e\\;num1,e\\:num2\\,enum3"` will
|
||||
be parsed as `Default: "va;lue:", Enum: []string{"e;num1", "e:num2,enum3"}}`
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goast "go/ast"
|
||||
gotoken "go/token"
|
||||
gotypes "go/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cueast "cuelang.org/go/cue/ast"
|
||||
cuetoken "cuelang.org/go/cue/token"
|
||||
)
|
||||
|
||||
func (g *Generator) convertDecls(x *goast.GenDecl) (decls []cueast.Decl, _ error) {
|
||||
// TODO(iyear): currently only support 'type'
|
||||
if x.Tok != gotoken.TYPE {
|
||||
return
|
||||
}
|
||||
|
||||
for _, spec := range x.Specs {
|
||||
typeSpec, ok := spec.(*goast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// only process struct
|
||||
typ := g.pkg.TypesInfo.TypeOf(typeSpec.Name)
|
||||
|
||||
if err := supportedType(nil, typ); err != nil {
|
||||
return nil, fmt.Errorf("unsupported type %s: %w", typeSpec.Name.Name, err)
|
||||
}
|
||||
|
||||
named, ok := typ.(*gotypes.Named)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
st, ok := named.Underlying().(*gotypes.Struct)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
lit, err := g.convert(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := &cueast.Field{
|
||||
Label: cueast.NewString(typeSpec.Name.Name),
|
||||
Value: lit,
|
||||
}
|
||||
// there is no doc for typeSpec, so we only add x.Doc
|
||||
makeComments(field, &commentUnion{comment: nil, doc: x.Doc})
|
||||
|
||||
cueast.SetRelPos(field, cuetoken.Blank)
|
||||
decls = append(decls, field)
|
||||
}
|
||||
|
||||
return decls, nil
|
||||
}
|
||||
|
||||
func (g *Generator) convert(typ gotypes.Type) (cueast.Expr, error) {
|
||||
// if type is registered as any, return {...}
|
||||
if _, ok := g.anyTypes[typ.String()]; ok {
|
||||
return anyLit(), nil
|
||||
}
|
||||
|
||||
switch t := typ.(type) {
|
||||
case *gotypes.Basic:
|
||||
return basicType(t), nil
|
||||
case *gotypes.Named:
|
||||
return g.convert(t.Underlying())
|
||||
case *gotypes.Struct:
|
||||
return g.makeStructLit(t)
|
||||
case *gotypes.Pointer:
|
||||
expr, err := g.convert(t.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cueast.BinaryExpr{
|
||||
X: cueast.NewNull(),
|
||||
Op: cuetoken.OR,
|
||||
Y: expr,
|
||||
}, nil
|
||||
case *gotypes.Slice:
|
||||
if t.Elem().String() == "byte" {
|
||||
return ident("bytes", false), nil
|
||||
}
|
||||
expr, err := g.convert(t.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cueast.NewList(&cueast.Ellipsis{Type: expr}), nil
|
||||
case *gotypes.Array:
|
||||
if t.Elem().String() == "byte" {
|
||||
// TODO: no way to constraint lengths of bytes for now, as regexps
|
||||
// operate on Unicode, not bytes. So we need
|
||||
// fmt.Fprint(e.w, fmt.Sprintf("=~ '^\C{%d}$'", x.Len())),
|
||||
// but regexp does not support that.
|
||||
// But translate to bytes, instead of [...byte] to be consistent.
|
||||
return ident("bytes", false), nil
|
||||
}
|
||||
|
||||
expr, err := g.convert(t.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cueast.BinaryExpr{
|
||||
X: &cueast.BasicLit{
|
||||
Kind: cuetoken.INT,
|
||||
Value: strconv.Itoa(int(t.Len())),
|
||||
},
|
||||
Op: cuetoken.MUL,
|
||||
Y: cueast.NewList(expr),
|
||||
}, nil
|
||||
case *gotypes.Map:
|
||||
// cue map only support string as key
|
||||
if b, ok := t.Key().Underlying().(*gotypes.Basic); !ok || b.Kind() != gotypes.String {
|
||||
return nil, fmt.Errorf("unsupported map key type %s of %s", t.Key(), t)
|
||||
}
|
||||
|
||||
expr, err := g.convert(t.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &cueast.Field{
|
||||
Label: cueast.NewList(ident("string", false)),
|
||||
Value: expr,
|
||||
}
|
||||
return &cueast.StructLit{
|
||||
Elts: []cueast.Decl{f},
|
||||
}, nil
|
||||
case *gotypes.Interface:
|
||||
// we don't process interface
|
||||
return ident("_", false), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported type %s", typ)
|
||||
}
|
||||
|
||||
func (g *Generator) makeStructLit(x *gotypes.Struct) (*cueast.StructLit, error) {
|
||||
st := &cueast.StructLit{
|
||||
Elts: make([]cueast.Decl, 0),
|
||||
}
|
||||
|
||||
// if num of fields is 1, we don't need braces. Keep it simple.
|
||||
if x.NumFields() > 1 {
|
||||
st.Lbrace = cuetoken.Blank.Pos()
|
||||
st.Rbrace = cuetoken.Newline.Pos()
|
||||
}
|
||||
|
||||
err := g.addFields(st, x, map[string]struct{}{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// addFields converts fields of go struct to CUE fields and add them to cue StructLit.
|
||||
func (g *Generator) addFields(st *cueast.StructLit, x *gotypes.Struct, names map[string]struct{}) error {
|
||||
comments := g.fieldComments(x)
|
||||
|
||||
for i := 0; i < x.NumFields(); i++ {
|
||||
field := x.Field(i)
|
||||
|
||||
// skip unexported fields
|
||||
if !field.Exported() {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(iyear): support more complex tags and usages
|
||||
opts := g.parseTag(x.Tag(i))
|
||||
|
||||
// skip fields with "-" tag
|
||||
if opts.Name == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// if field name tag is empty, use Go field name
|
||||
if opts.Name == "" {
|
||||
opts.Name = field.Name()
|
||||
}
|
||||
|
||||
// can't decl same field in the same scope
|
||||
if _, ok := names[opts.Name]; ok {
|
||||
return fmt.Errorf("field '%s' already exists, can not declare duplicate field name", opts.Name)
|
||||
}
|
||||
names[opts.Name] = struct{}{}
|
||||
|
||||
// process anonymous field with inline tag
|
||||
if field.Anonymous() && opts.Inline {
|
||||
if t, ok := field.Type().Underlying().(*gotypes.Struct); ok {
|
||||
if err := g.addFields(st, t, names); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
expr cueast.Expr
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
// process field with enum tag
|
||||
case opts.Enum != nil && len(opts.Enum) > 0:
|
||||
expr, err = g.enumField(field.Type(), opts)
|
||||
// process normal field
|
||||
default:
|
||||
expr, err = g.normalField(field.Type(), opts)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("field '%s': %w", opts.Name, err)
|
||||
}
|
||||
|
||||
f := &cueast.Field{
|
||||
Label: cueast.NewString(opts.Name),
|
||||
Value: expr,
|
||||
}
|
||||
|
||||
// process field with optional tag(omitempty in json tag)
|
||||
if opts.Optional {
|
||||
f.Token = cuetoken.COLON
|
||||
f.Optional = cuetoken.Blank.Pos()
|
||||
}
|
||||
|
||||
makeComments(f, comments[i])
|
||||
|
||||
st.Elts = append(st.Elts, f)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Generator) enumField(typ gotypes.Type, opts *tagOptions) (cueast.Expr, error) {
|
||||
tt, ok := typ.(*gotypes.Basic)
|
||||
if !ok {
|
||||
// TODO(iyear): support more types
|
||||
return nil, fmt.Errorf("enum value only support [int, float, string, bool]")
|
||||
}
|
||||
|
||||
expr, err := basicLabel(tt, opts.Enum[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, v := range opts.Enum[1:] {
|
||||
enumExpr, err := basicLabel(tt, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// default value should be marked with *
|
||||
if opts.Default != nil && *opts.Default == v {
|
||||
enumExpr = &cueast.UnaryExpr{Op: cuetoken.MUL, X: enumExpr}
|
||||
}
|
||||
|
||||
expr = &cueast.BinaryExpr{
|
||||
X: expr,
|
||||
Op: cuetoken.OR,
|
||||
Y: enumExpr,
|
||||
}
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func (g *Generator) normalField(typ gotypes.Type, opts *tagOptions) (cueast.Expr, error) {
|
||||
expr, err := g.convert(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// process field with default tag
|
||||
if opts.Default != nil {
|
||||
tt, ok := typ.(*gotypes.Basic)
|
||||
if !ok {
|
||||
// TODO(iyear): support more types
|
||||
return nil, fmt.Errorf("default value only support [int, float, string, bool]")
|
||||
}
|
||||
|
||||
defaultExpr, err := basicLabel(tt, *opts.Default)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expr = &cueast.BinaryExpr{
|
||||
// default value should be marked with *
|
||||
X: &cueast.UnaryExpr{Op: cuetoken.MUL, X: defaultExpr},
|
||||
Op: cuetoken.OR,
|
||||
Y: expr,
|
||||
}
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func supportedType(stack []gotypes.Type, t gotypes.Type) error {
|
||||
// we expand structures recursively, so we can't support recursive types
|
||||
for _, t0 := range stack {
|
||||
if t0 == t {
|
||||
return fmt.Errorf("recursive type %s", t)
|
||||
}
|
||||
}
|
||||
stack = append(stack, t)
|
||||
|
||||
t = t.Underlying()
|
||||
switch x := t.(type) {
|
||||
case *gotypes.Basic:
|
||||
if x.String() != "invalid type" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported type %s", t)
|
||||
case *gotypes.Named:
|
||||
return nil
|
||||
case *gotypes.Pointer:
|
||||
return supportedType(stack, x.Elem())
|
||||
case *gotypes.Slice:
|
||||
return supportedType(stack, x.Elem())
|
||||
case *gotypes.Array:
|
||||
return supportedType(stack, x.Elem())
|
||||
case *gotypes.Map:
|
||||
if b, ok := x.Key().Underlying().(*gotypes.Basic); !ok || b.Kind() != gotypes.String {
|
||||
return fmt.Errorf("unsupported map key type %s of %s", x.Key(), t)
|
||||
}
|
||||
return supportedType(stack, x.Elem())
|
||||
case *gotypes.Struct:
|
||||
// Eliminate structs with fields for which all fields are filtered.
|
||||
if x.NumFields() == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < x.NumFields(); i++ {
|
||||
f := x.Field(i)
|
||||
if f.Exported() {
|
||||
if err := supportedType(stack, f.Type()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case *gotypes.Interface:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported type %s", t)
|
||||
}
|
||||
|
||||
// ----------comment----------
|
||||
|
||||
type commentUnion struct {
|
||||
comment *goast.CommentGroup
|
||||
doc *goast.CommentGroup
|
||||
}
|
||||
|
||||
// fieldComments returns the comments for each field in a go struct.
|
||||
//
|
||||
// The comments are same order as the fields.
|
||||
func (g *Generator) fieldComments(x *gotypes.Struct) []*commentUnion {
|
||||
comments := make([]*commentUnion, x.NumFields())
|
||||
|
||||
st, ok := g.types[x]
|
||||
if !ok {
|
||||
return comments
|
||||
}
|
||||
|
||||
for i, field := range st.Fields.List {
|
||||
comments[i] = &commentUnion{comment: field.Comment, doc: field.Doc}
|
||||
}
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
// makeComments adds comments to a cue node.
|
||||
//
|
||||
// go docs/comments are converted to cue comments.
|
||||
func makeComments(node cueast.Node, c *commentUnion) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cg := make([]*cueast.Comment, 0)
|
||||
|
||||
if comment := makeComment(c.comment); comment != nil && len(comment.List) > 0 {
|
||||
cg = append(cg, comment.List...)
|
||||
}
|
||||
if doc := makeComment(c.doc); doc != nil && len(doc.List) > 0 {
|
||||
cg = append(cg, doc.List...)
|
||||
}
|
||||
|
||||
// avoid nil comment groups which will cause panics
|
||||
if len(cg) > 0 {
|
||||
cueast.AddComment(node, &cueast.CommentGroup{List: cg})
|
||||
}
|
||||
}
|
||||
|
||||
// makeComment converts a go CommentGroup to a cue CommentGroup.
|
||||
//
|
||||
// All /*-style comments are converted to //-style comments.
|
||||
func makeComment(cg *goast.CommentGroup) *cueast.CommentGroup {
|
||||
if cg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var comments []*cueast.Comment
|
||||
|
||||
for _, comment := range cg.List {
|
||||
c := comment.Text
|
||||
|
||||
if len(c) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove comment markers.
|
||||
// The parser has given us exactly the comment text.
|
||||
switch c[1] {
|
||||
case '/':
|
||||
// -style comment (no newline at the end)
|
||||
comments = append(comments, &cueast.Comment{Text: c})
|
||||
|
||||
case '*':
|
||||
/*-style comment */
|
||||
c = c[2 : len(c)-2]
|
||||
if len(c) > 0 && c[0] == '\n' {
|
||||
c = c[1:]
|
||||
}
|
||||
|
||||
lines := strings.Split(c, "\n")
|
||||
|
||||
// Find common space prefix
|
||||
i := 0
|
||||
line := lines[0]
|
||||
for ; i < len(line); i++ {
|
||||
if c := line[i]; c != ' ' && c != '\t' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range lines {
|
||||
for j := 0; j < i && j < len(l); j++ {
|
||||
if line[j] != l[j] {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip last line if empty.
|
||||
if n := len(lines); n > 1 && len(lines[n-1]) < i {
|
||||
lines = lines[:n-1]
|
||||
}
|
||||
|
||||
// Print lines.
|
||||
for _, l := range lines {
|
||||
if i >= len(l) {
|
||||
comments = append(comments, &cueast.Comment{Text: "//"})
|
||||
continue
|
||||
}
|
||||
comments = append(comments, &cueast.Comment{Text: "// " + l[i:]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &cueast.CommentGroup{List: comments}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
g, err := NewGenerator("testdata/valid.go")
|
||||
assert.NoError(t, err)
|
||||
g.RegisterAny(
|
||||
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured",
|
||||
)
|
||||
|
||||
got := &bytes.Buffer{}
|
||||
assert.NoError(t, g.Generate(got))
|
||||
|
||||
want, err := os.ReadFile("testdata/valid.cue")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, got.String(), string(want))
|
||||
}
|
||||
|
||||
func TestConvertInvalid(t *testing.T) {
|
||||
if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
g, err := NewGenerator(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Error(t, g.Generate(io.Discard), path)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goast "go/ast"
|
||||
gotypes "go/types"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
cueast "cuelang.org/go/cue/ast"
|
||||
"cuelang.org/go/cue/ast/astutil"
|
||||
cueformat "cuelang.org/go/cue/format"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// Generator generates CUE schema from Go struct.
|
||||
type Generator struct {
|
||||
// immutable
|
||||
pkg *packages.Package
|
||||
types typeInfo
|
||||
|
||||
anyTypes map[string]struct{}
|
||||
}
|
||||
|
||||
var defaultAnyTypes = []string{
|
||||
"map[string]interface{}",
|
||||
"map[string]any",
|
||||
"interface{}",
|
||||
"any",
|
||||
}
|
||||
|
||||
// NewGenerator creates a new generator with given file or package path.
|
||||
func NewGenerator(f string) (*Generator, error) {
|
||||
pkg, err := loadPackage(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types := getTypeInfo(pkg)
|
||||
|
||||
g := &Generator{
|
||||
pkg: pkg,
|
||||
types: types,
|
||||
anyTypes: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
g.RegisterAny(defaultAnyTypes...)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Generate generates CUE schema from Go struct and writes to w.
|
||||
func (g *Generator) Generate(w io.Writer) error {
|
||||
var decls []cueast.Decl
|
||||
|
||||
for _, syntax := range g.pkg.Syntax {
|
||||
for _, decl := range syntax.Decls {
|
||||
if d, ok := decl.(*goast.GenDecl); ok {
|
||||
t, err := g.convertDecls(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decls = append(decls, t...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pkg := &cueast.Package{Name: ident(g.pkg.Name, false)}
|
||||
|
||||
f := &cueast.File{Decls: []cueast.Decl{pkg}}
|
||||
f.Decls = append(f.Decls, decls...)
|
||||
|
||||
return g.write(w, f)
|
||||
}
|
||||
|
||||
func (g *Generator) write(w io.Writer, f *cueast.File) error {
|
||||
if err := astutil.Sanitize(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := cueformat.Node(f, cueformat.Simplify())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// loadPackage loads a package from given path.
|
||||
func loadPackage(p string) (*packages.Package, error) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
|
||||
packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes |
|
||||
packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
|
||||
packages.NeedModule,
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(cfg, []string{p}...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
return nil, fmt.Errorf("expected one package, got %d", len(pkgs))
|
||||
}
|
||||
|
||||
// only need to check the first package
|
||||
pkg := pkgs[0]
|
||||
if pkg.Errors != nil {
|
||||
errs := make([]string, 0, len(pkg.Errors))
|
||||
for _, e := range pkg.Errors {
|
||||
errs = append(errs, fmt.Sprintf("\t%s: %v", pkg.PkgPath, e))
|
||||
}
|
||||
return nil, fmt.Errorf("could not load Go packages:\n%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
type typeInfo map[gotypes.Type]*goast.StructType
|
||||
|
||||
func getTypeInfo(p *packages.Package) typeInfo {
|
||||
m := make(typeInfo)
|
||||
|
||||
for _, f := range p.Syntax {
|
||||
goast.Inspect(f, func(n goast.Node) bool {
|
||||
// record all struct types
|
||||
if t, ok := n.(*goast.StructType); ok {
|
||||
m[p.TypesInfo.TypeOf(t)] = t
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testGenerator(t *testing.T) *Generator {
|
||||
g, err := NewGenerator("testdata/valid.go")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, g)
|
||||
require.Len(t, g.pkg.Errors, 0)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func TestNewGenerator(t *testing.T) {
|
||||
g := testGenerator(t)
|
||||
|
||||
assert.NotNil(t, g.pkg)
|
||||
assert.NotNil(t, g.types)
|
||||
assert.NotNil(t, g.anyTypes)
|
||||
|
||||
assert.Equal(t, len(defaultAnyTypes), len(g.anyTypes))
|
||||
assert.Greater(t, len(g.types), 0)
|
||||
}
|
||||
|
||||
func TestGeneratorGenerate(t *testing.T) {
|
||||
g := testGenerator(t)
|
||||
|
||||
require.NoError(t, g.Generate(io.Discard))
|
||||
}
|
||||
|
||||
func TestLoadPackage(t *testing.T) {
|
||||
pkg, err := loadPackage("testdata/valid.go")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pkg)
|
||||
require.Len(t, pkg.Errors, 0)
|
||||
}
|
||||
|
||||
func TestGetTypeInfo(t *testing.T) {
|
||||
pkg, err := loadPackage("testdata/valid.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Greater(t, len(getTypeInfo(pkg)), 0)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
// RegisterAny registers go types' package+name as any type({...} in CUE)
|
||||
//
|
||||
// Example:RegisterAny("*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured")
|
||||
//
|
||||
// Default any types are: map[string]interface{}, map[string]any, interface{}, any
|
||||
func (g *Generator) RegisterAny(types ...string) {
|
||||
for _, t := range types {
|
||||
g.anyTypes[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegisterAny(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typ []string
|
||||
want map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "builtin",
|
||||
typ: []string{"int", "string", "bool"},
|
||||
want: map[string]struct{}{"int": {}, "string": {}, "bool": {}},
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
typ: []string{"map[string]interface{}"},
|
||||
want: map[string]struct{}{"map[string]interface{}": {}},
|
||||
},
|
||||
{
|
||||
name: "external",
|
||||
typ: []string{"http.Header"},
|
||||
want: map[string]struct{}{"http.Header": {}},
|
||||
},
|
||||
{
|
||||
name: "external2",
|
||||
typ: []string{"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured", "http.Header"},
|
||||
want: map[string]struct{}{
|
||||
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": {},
|
||||
"http.Header": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
g := Generator{anyTypes: map[string]struct{}{}}
|
||||
g.RegisterAny(tt.typ...)
|
||||
assert.Equal(t, tt.want, g.anyTypes, tt.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type tagOptions struct {
|
||||
// basic
|
||||
Name string
|
||||
Inline bool
|
||||
Optional bool
|
||||
|
||||
// extended
|
||||
Default *string // nil means no default value
|
||||
Enum []string
|
||||
}
|
||||
|
||||
// TODO(iyear): be customizable
|
||||
const (
|
||||
basicTag = "json" // same as json tag
|
||||
extTag = "cue" // format: cue:"key1:value1;key2:value2;boolValue1;boolValue2"
|
||||
)
|
||||
|
||||
func (g *Generator) parseTag(tag string) *tagOptions {
|
||||
if tag == "" {
|
||||
return &tagOptions{}
|
||||
}
|
||||
|
||||
name, opts := parseTag(reflect.StructTag(tag).Get(basicTag))
|
||||
ext := parseExtTag(reflect.StructTag(tag).Get(extTag))
|
||||
|
||||
return &tagOptions{
|
||||
Name: name,
|
||||
Inline: opts.Has("inline"),
|
||||
Optional: opts.Has("omitempty"),
|
||||
|
||||
Default: ext.GetX("default"),
|
||||
Enum: unescapeSplit(ext.Get("enum"), ","),
|
||||
}
|
||||
}
|
||||
|
||||
type basicTagOptions string
|
||||
|
||||
func parseTag(tag string) (string, basicTagOptions) {
|
||||
tag, opt, _ := strings.Cut(tag, ",")
|
||||
return tag, basicTagOptions(opt)
|
||||
}
|
||||
|
||||
func (o basicTagOptions) Has(opt string) bool {
|
||||
if len(o) == 0 {
|
||||
return false
|
||||
}
|
||||
s := string(o)
|
||||
for s != "" {
|
||||
var name string
|
||||
name, s, _ = strings.Cut(s, ",")
|
||||
if name == opt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseExtTag(str string) extTagOptions {
|
||||
settings := map[string]string{}
|
||||
if str == "" {
|
||||
return settings
|
||||
}
|
||||
|
||||
pairs := unescapeSplit(str, ";")
|
||||
for _, pair := range pairs {
|
||||
switch kv := unescapeSplit(pair, ":"); len(kv) {
|
||||
case 1:
|
||||
settings[kv[0]] = ""
|
||||
case 2:
|
||||
settings[kv[0]] = kv[1]
|
||||
default:
|
||||
// ignore invalid pair
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
func unescapeSplit(str string, sep string) []string {
|
||||
if str == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
ss := strings.Split(str, sep)
|
||||
for i := 0; i < len(ss); i++ {
|
||||
j := i
|
||||
if len(ss[j]) > 0 {
|
||||
for {
|
||||
if ss[j][len(ss[j])-1] == '\\' && i+1 < len(ss) {
|
||||
i++
|
||||
ss[j] = ss[j][0:len(ss[j])-1] + sep + ss[i]
|
||||
ss[i] = ""
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter empty strings
|
||||
res := make([]string, 0, len(ss))
|
||||
for _, s := range ss {
|
||||
if s != "" {
|
||||
res = append(res, s)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type extTagOptions map[string]string
|
||||
|
||||
// GetX returns the value of the key if it exists, otherwise nil.
|
||||
func (e extTagOptions) GetX(key string) *string {
|
||||
if v, ok := e[key]; ok {
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the value of the key if it exists, otherwise "".
|
||||
func (e extTagOptions) Get(key string) string {
|
||||
return e[key]
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func str(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestGeneratorParseTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
opts *tagOptions
|
||||
}{
|
||||
{"empty", "", &tagOptions{}},
|
||||
{"only_name", `json:"name"`, &tagOptions{Name: "name", Enum: []string{}}},
|
||||
{"only_name_2", `json:""`, &tagOptions{Name: "", Enum: []string{}}},
|
||||
{"only_name_3", `json:"-"`, &tagOptions{Name: "-", Enum: []string{}}},
|
||||
{"json_omitempty", `json:"name,omitempty"`, &tagOptions{Name: "name", Optional: true, Enum: []string{}}},
|
||||
{"json_omitempty_2", `json:"name,omitempty,omitempty"`, &tagOptions{Name: "name", Optional: true, Enum: []string{}}},
|
||||
{"json_omitempty_3", `json:",omitempty"`, &tagOptions{Name: "", Optional: true, Enum: []string{}}},
|
||||
{"json_inline", `json:",inline"`, &tagOptions{Name: "", Inline: true, Enum: []string{}}},
|
||||
{"json_inline_2", `json:"name,inline"`, &tagOptions{Name: "name", Inline: true, Enum: []string{}}},
|
||||
{"json_inline_3", `json:"name,inline,inline"`, &tagOptions{Name: "name", Inline: true, Enum: []string{}}},
|
||||
{"json_omitempty_inline", `json:"name,omitempty,inline"`, &tagOptions{Name: "name", Optional: true, Inline: true, Enum: []string{}}},
|
||||
{"json_omitempty_inline_2", `json:",omitempty,inline"`, &tagOptions{Name: "", Optional: true, Inline: true, Enum: []string{}}},
|
||||
{"cue_default", `cue:"default:default_value"`, &tagOptions{Default: str("default_value"), Enum: []string{}}},
|
||||
{"cue_default_2", `cue:"default:default_value;default:default_value2"`, &tagOptions{Default: str("default_value2"), Enum: []string{}}},
|
||||
{"cue_default_3", `cue:"default:1.11"`, &tagOptions{Default: str("1.11"), Enum: []string{}}},
|
||||
{"cue_default_4", `cue:"default:va,lue"`, &tagOptions{Default: str(`va,lue`), Enum: []string{}}},
|
||||
{"cue_enum", `cue:"enum:enum1,enum2"`, &tagOptions{Enum: []string{"enum1", "enum2"}}},
|
||||
{"cue_enum_2", `cue:"enum:enum1,enum2;enum:enum3,enum4"`, &tagOptions{Enum: []string{"enum3", "enum4"}}},
|
||||
{"cue_enum_empty", `cue:""`, &tagOptions{Enum: []string{}}},
|
||||
{"cue_enum_empty_2", `cue:"enum:"`, &tagOptions{Enum: []string{}}},
|
||||
{"cue_escape", `cue:"default:\"default_value\""`, &tagOptions{Default: str(`"default_value"`), Enum: []string{}}},
|
||||
{"cue_escape_2", `cue:"default:\"default_value\\\"\""`, &tagOptions{Default: str(`"default_value\""`), Enum: []string{}}},
|
||||
{"cue_escape_3", `cue:"default:value\\;vv;enum:enum1,enum2"`, &tagOptions{Default: str(`value;vv`), Enum: []string{"enum1", "enum2"}}},
|
||||
{"cue_escape_default_semicolon", `cue:"default:va\\;lue\\"`, &tagOptions{Default: str(`va;lue\`), Enum: []string{}}},
|
||||
{"cue_escape_default_colon", `cue:"default:va\\:lue\\"`, &tagOptions{Default: str(`va:lue\`), Enum: []string{}}},
|
||||
{"cue_escape_enum_semicolon", `cue:"enum:e\\;num1,enum2"`, &tagOptions{Enum: []string{`e;num1`, "enum2"}}},
|
||||
{"cue_escape_enum_colon", `cue:"enum:e\\:num1,enum2"`, &tagOptions{Enum: []string{`e:num1`, "enum2"}}},
|
||||
{"cue_escape_enum_colon_2", `cue:"enum:enum1\\,enum2"`, &tagOptions{Enum: []string{"enum1,enum2"}}},
|
||||
{"cue_escape_all", `cue:"default:va\\;lue\\:;enum:e\\;num1,e\\:num2\\,enum3"`, &tagOptions{Default: str(`va;lue:`), Enum: []string{`e;num1`, `e:num2,enum3`}}},
|
||||
{"json_cue", `json:"name" cue:"default:default_value"`, &tagOptions{Name: "name", Default: str("default_value"), Enum: []string{}}},
|
||||
{"json_cue_2", `json:"name" cue:"default:default_value;enum:enum1,enum2"`, &tagOptions{Name: "name", Default: str("default_value"), Enum: []string{"enum1", "enum2"}}},
|
||||
{"json_cue_3", `json:",omitempty" cue:"default:default_value"`, &tagOptions{Name: "", Optional: true, Default: str("default_value"), Enum: []string{}}},
|
||||
{"json_cue_4", `json:",inline" cue:"default:default_value"`, &tagOptions{Name: "", Inline: true, Default: str("default_value"), Enum: []string{}}},
|
||||
{"json_cue_5", `json:"name,omitempty,inline" cue:"default:default_value"`, &tagOptions{Name: "name", Optional: true, Inline: true, Default: str("default_value"), Enum: []string{}}},
|
||||
{"json_cue_6", `json:",omitempty,inline" cue:"default:default_value;enum:enum1,enum2"`, &tagOptions{Name: "", Optional: true, Inline: true, Default: str("default_value"), Enum: []string{"enum1", "enum2"}}},
|
||||
}
|
||||
|
||||
g := &Generator{}
|
||||
for _, tt := range tests {
|
||||
got := g.parseTag(tt.tag)
|
||||
assert.Equal(t, tt.opts, got, tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBasicTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
wantName string
|
||||
wantOpt string
|
||||
}{
|
||||
{"empty", "", "", ""},
|
||||
{"only_name", "name", "name", ""},
|
||||
{"name_and_opt", "name,opt,opt2", "name", "opt,opt2"},
|
||||
{"name_and_opt_2", "name,opt1;opt2.opt3", "name", "opt1;opt2.opt3"},
|
||||
{"name_and_opt_with_space", "name, opt", "name", " opt"},
|
||||
{"only_opt", ",opt,opt2", "", "opt,opt2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotName, gotOpt := parseTag(tt.tag)
|
||||
assert.EqualValues(t, tt.wantName, gotName, tt.name)
|
||||
assert.EqualValues(t, tt.wantOpt, gotOpt, tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicTagOptHas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
opt string
|
||||
want bool
|
||||
}{
|
||||
{"empty", "", "", false},
|
||||
{"empty_opt", "name", "", false},
|
||||
{"empty_tag", "", "opt", false},
|
||||
{"single_opt", "name,opt", "opt", true},
|
||||
{"multi_opt", "name,opt1,opt2,opt3", "opt1", true},
|
||||
{"multi_opt_2", "name,opt1,opt2,opt3", "opt2", true},
|
||||
{"multi_opt_3", "name,opt1,opt2,opt3", "opt3", true},
|
||||
{"only_multi_opt", ",opt1,opt2,opt3", "opt1", true},
|
||||
{"only_multi_opt_2", ",opt1,opt2,opt3", "opt2", true},
|
||||
{"only_multi_opt_3", ",opt1,opt2,opt3", "opt3", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, opt := parseTag(tt.tag)
|
||||
assert.Equal(t, tt.want, opt.Has(tt.opt), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
want map[string]string
|
||||
}{
|
||||
{"empty", "", map[string]string{}},
|
||||
{"only_key", "key", map[string]string{"key": ""}},
|
||||
{"one_kv", "key:value", map[string]string{"key": "value"}},
|
||||
{"multi_kv", "key1:value1;key2:value2", map[string]string{"key1": "value1", "key2": "value2"}},
|
||||
{"bool", "key1;key2", map[string]string{"key1": "", "key2": ""}},
|
||||
{"bool_and_kv", "key1;key2:value2", map[string]string{"key1": "", "key2": "value2"}},
|
||||
{"kv_and_bool", "key1:value1;key2", map[string]string{"key1": "value1", "key2": ""}},
|
||||
{"multi_kv_and_bool", "key1:value1;key2:value2;key3", map[string]string{"key1": "value1", "key2": "value2", "key3": ""}},
|
||||
{"escape_semicolon_value", `key1:value\;1`, map[string]string{"key1": "value;1"}},
|
||||
{"escape_semicolon_key", `key\;1:value1`, map[string]string{"key;1": "value1"}},
|
||||
{"escape_semicolon_pairs", `key\;1:value\;1;key3:va\lue3\`, map[string]string{"key;1": "value;1", "key3": `va\lue3\`}},
|
||||
{"escape_semicolon_last", `key\1:value1\`, map[string]string{`key\1`: `value1\`}},
|
||||
{"escape_semicolon_last_2", `k\ey1:va\lue1\1`, map[string]string{`k\ey1`: `va\lue1\1`}},
|
||||
{"escape_semicolon_last_3", `key1:value1\;`, map[string]string{"key1": `value1;`}},
|
||||
{"escape_colon_value", `key1:value\:1`, map[string]string{"key1": "value:1"}},
|
||||
{"escape_colon_key", `key\:1:value1`, map[string]string{"key:1": "value1"}},
|
||||
{"escape_colon_pairs", `key\:1:value\:1;key3:va\lue3\`, map[string]string{"key:1": "value:1", "key3": `va\lue3\`}},
|
||||
{"escape_colon_last", `key\1:value1\:`, map[string]string{`key\1`: `value1:`}},
|
||||
{"escape_colon_last_2", `k\ey1:va\lue1\:1`, map[string]string{`k\ey1`: `va\lue1:1`}},
|
||||
{"escape_colon_last_3", `key1:value1\:`, map[string]string{"key1": `value1:`}},
|
||||
{"invalid_pair", `key1:value1:invalid;key2:value2`, map[string]string{"key2": "value2"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := parseExtTag(tt.tag)
|
||||
assert.EqualValues(t, tt.want, got, tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtTagGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{"empty", "", "", ""},
|
||||
{"empty_key", "key", "", ""},
|
||||
{"empty_tag", "", "key", ""},
|
||||
{"one_kv", "key:value", "key", "value"},
|
||||
{"multi_kv", "key1:value1;key2:value2", "key1", "value1"},
|
||||
{"multi_kv_2", "key1:value1;key2:value2", "key2", "value2"},
|
||||
{"bool", "key1;key2", "key1", ""},
|
||||
{"bool2", "key1;key2", "key2", ""},
|
||||
{"escape", "key1:value1\\;vv", "key1", "value1;vv"},
|
||||
{"escape_2", "key1:\"value2\"", "key1", `"value2"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := parseExtTag(tt.tag)
|
||||
assert.EqualValues(t, tt.want, got.Get(tt.key), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtTagGetX(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
key string
|
||||
want *string
|
||||
}{
|
||||
{"empty", "", "", nil},
|
||||
{"empty_key", "key", "", nil},
|
||||
{"empty_tag", "", "key", nil},
|
||||
{"one_kv", "key:value", "key", str("value")},
|
||||
{"multi_kv", "key1:value1;key2:value2", "key1", str("value1")},
|
||||
{"multi_kv_2", "key1:value1;key2:value2", "key2", str("value2")},
|
||||
{"bool", "key1;key2", "key1", str("")},
|
||||
{"bool2", "key1;key2", "key2", str("")},
|
||||
{"escape", "key1:value1\\;vv", "key1", str("value1;vv")},
|
||||
{"escape_2", "key1:\"value2\"", "key1", str(`"value2"`)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := parseExtTag(tt.tag)
|
||||
assert.EqualValues(t, tt.want, got.GetX(tt.key), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnescapeSplit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s string
|
||||
sep string
|
||||
want []string
|
||||
}{
|
||||
{"empty", "", "", []string{}},
|
||||
{"empty_sep", "key:value", "", []string{"k", "e", "y", ":", "v", "a", "l", "u", "e"}},
|
||||
{"one", "key:value", ":", []string{"key", "value"}},
|
||||
{"escape_sep", `key\:value`, ":", []string{"key:value"}},
|
||||
{"escape_sep_2", `key\:value\:`, ":", []string{"key:value:"}},
|
||||
{"escape_last", `key\:value\`, ":", []string{`key:value\`}},
|
||||
{"escape_multi", `key\:value\:;key2:value2`, ":", []string{"key:value:;key2", "value2"}},
|
||||
{"escape_multi_2", `key\:value\:;key2:value2`, ";", []string{`key\:value\:`, "key2:value2"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := unescapeSplit(tt.s, tt.sep)
|
||||
assert.EqualValues(t, tt.want, got, tt.name)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package invalid
|
||||
|
||||
type Default struct {
|
||||
Field1 chan int `json:"field1"`
|
||||
Field2 int `json:"field2" cue:"default:a"`
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package invalid
|
||||
|
||||
type Enum struct {
|
||||
Field1 map[string]string `json:"field1" cue:"enum:1,2,3"`
|
||||
Field2 int `json:"field2" cue:"enum:a,b,c"`
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package invalid
|
||||
|
||||
type InlineStruct struct {
|
||||
Field1 string `json:"field1"`
|
||||
}
|
||||
|
||||
type SameNameInlined struct {
|
||||
Field1 string `json:"field1"`
|
||||
InlineStruct `json:",inline"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package invalid
|
||||
|
||||
type NonStringMapKey struct {
|
||||
Field1 map[int]string `json:"field1"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package invalid
|
||||
|
||||
type RecursiveStruct struct {
|
||||
Field1 *RecursiveStruct `json:"field1"`
|
||||
}
|
||||
Vendored
+321
@@ -0,0 +1,321 @@
|
||||
package testdata
|
||||
|
||||
BasicType: {
|
||||
field1: string
|
||||
field2: int
|
||||
field3: bool
|
||||
field4: float32
|
||||
field5: float64
|
||||
field6: int8
|
||||
field7: int16
|
||||
field8: int32
|
||||
field9: int64
|
||||
field10: uint
|
||||
field11: uint8
|
||||
field12: uint16
|
||||
field13: uint32
|
||||
field14: uint64
|
||||
field15: uint64
|
||||
field16: uint8
|
||||
field17: rune
|
||||
field18: {
|
||||
...
|
||||
}
|
||||
field19: {
|
||||
...
|
||||
}
|
||||
}
|
||||
TagName: {
|
||||
f1: string
|
||||
f2: string
|
||||
f3: string
|
||||
}
|
||||
SliceAndArray: {
|
||||
field1: [...string]
|
||||
field2: 3 * [string]
|
||||
field3: [...int]
|
||||
field4: 3 * [int]
|
||||
field5: [...bool]
|
||||
field6: 3 * [bool]
|
||||
field7: [...float32]
|
||||
field8: 3 * [float32]
|
||||
field9: [...float64]
|
||||
field10: 3 * [float64]
|
||||
field11: bytes
|
||||
field12: bytes
|
||||
}
|
||||
SmallStruct: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
AnonymousField: SmallStruct: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
ReferenceField: field1: null | {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
StructField: {
|
||||
field1: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
field2: null | {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
}
|
||||
EmbedStruct: {
|
||||
field1: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
field2: {
|
||||
field1: string
|
||||
field2: string
|
||||
field3: {
|
||||
field1: string
|
||||
field2: string
|
||||
field3: {
|
||||
field1: string
|
||||
field2: string
|
||||
field3: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
field3: [string]: [...string]
|
||||
field4: uint
|
||||
}
|
||||
MapField: {
|
||||
field1: [string]: string
|
||||
field2: [string]: int
|
||||
field3: {
|
||||
...
|
||||
}
|
||||
field4: [string]: {
|
||||
field1: string
|
||||
field2: string
|
||||
}
|
||||
field5: {
|
||||
...
|
||||
}
|
||||
}
|
||||
EmptyStruct: {}
|
||||
// Comment is a test struct1
|
||||
// Struct is a test struct2
|
||||
// Struct is a test struct3
|
||||
// Struct is a test struct4
|
||||
Comment: {
|
||||
// Field1 comment
|
||||
// Field1 doc
|
||||
field1: string
|
||||
// Field2 comment
|
||||
// Field2 doc
|
||||
field2: string
|
||||
// Field3 comment
|
||||
// Field3 doc
|
||||
field3: string
|
||||
// Field4 comment
|
||||
field4: string
|
||||
// Field5 doc
|
||||
field5: {
|
||||
// Field5.Field1 comment
|
||||
field1: string
|
||||
// Field5.Field2 doc
|
||||
field2: string
|
||||
// Field5.Field3 doc
|
||||
field3: string
|
||||
// Field5.Field4 doc
|
||||
field4: string
|
||||
// Field5.Field5 doc
|
||||
field5: {
|
||||
// Field5.Field5.Field1 comment
|
||||
field1: string
|
||||
// Field5.Field5.Field2 doc
|
||||
field2: string
|
||||
// Field5.Field5.Field3 doc
|
||||
field3: string
|
||||
// Field5.Field5.Field4 doc
|
||||
field4: string
|
||||
}
|
||||
}
|
||||
// Field6 doc
|
||||
field6: [string]: [...string]
|
||||
// Field7 comment
|
||||
field7: [string]: [...string]
|
||||
// Field8 comment
|
||||
// Field8 doc
|
||||
field8: [string]: [...string]
|
||||
field9: [string]: [...string]
|
||||
// Field10 comment
|
||||
// Field10 doc1
|
||||
// Field10 doc2
|
||||
// Field10 doc3
|
||||
field10: [string]: [...string]
|
||||
// Field11 doc
|
||||
field11: {
|
||||
// Field11.Field1 comment
|
||||
field1: string
|
||||
}
|
||||
// Field12 doc
|
||||
field12: {
|
||||
// Field12.Field1 doc
|
||||
field1: string
|
||||
}
|
||||
// Field13 doc
|
||||
field13: field1: string
|
||||
// Field14 doc
|
||||
field14: [string]: string
|
||||
}
|
||||
Default: {
|
||||
a1: *"abc" | string
|
||||
// empty string
|
||||
a2: *"" | string
|
||||
b1: *true | bool
|
||||
b2: *false | bool
|
||||
c1: *123 | int
|
||||
c2: *123 | int8
|
||||
c3: *123 | int16
|
||||
c4: *123 | int32
|
||||
c5: *123 | int64
|
||||
d1: *123 | uint
|
||||
d2: *123 | uint8
|
||||
d3: *123 | uint16
|
||||
d4: *123 | uint32
|
||||
d5: *123 | uint64
|
||||
e1: *123.456 | float32
|
||||
e2: *123.456 | float64
|
||||
}
|
||||
Enum: {
|
||||
a: "abc" | "def" | "ghi"
|
||||
b: 1 | 2 | 3
|
||||
c: true | false
|
||||
d: 1.1 | 2.2 | 3.3
|
||||
e: "abc" | "def" | *"ghi"
|
||||
f: 1 | *2 | 3
|
||||
g: true | *false
|
||||
// if default value is first enum, '*' will not be added
|
||||
h: 1.1 | 2.2 | 3.3
|
||||
i: "abc"
|
||||
}
|
||||
Unexported: {
|
||||
field1: string
|
||||
field2: {
|
||||
field1: string
|
||||
field3: {
|
||||
field1: string
|
||||
field3: string
|
||||
}
|
||||
}
|
||||
}
|
||||
// RequestVars is the vars for http request
|
||||
// TODO: support timeout & tls
|
||||
RequestVars: {
|
||||
method: string
|
||||
url: string
|
||||
request: {
|
||||
body: string
|
||||
header: [string]: [...string]
|
||||
trailer: [string]: [...string]
|
||||
}
|
||||
}
|
||||
// ResponseVars is the vars for http response
|
||||
ResponseVars: {
|
||||
body: string
|
||||
header: [string]: [...string]
|
||||
trailer: [string]: [...string]
|
||||
statusCode: int
|
||||
}
|
||||
// DoParams is the params for http request
|
||||
DoParams: $params: {
|
||||
method: string
|
||||
url: string
|
||||
request: {
|
||||
body: string
|
||||
header: [string]: [...string]
|
||||
trailer: [string]: [...string]
|
||||
}
|
||||
}
|
||||
// DoReturns returned struct for http response
|
||||
DoReturns: $returns: {
|
||||
body: string
|
||||
header: [string]: [...string]
|
||||
trailer: [string]: [...string]
|
||||
statusCode: int
|
||||
}
|
||||
ResourceReturns: $returns: {
|
||||
...
|
||||
}
|
||||
InlineStruct1: {
|
||||
// Field1 comment
|
||||
// Field1 doc
|
||||
field11: string
|
||||
field12: string
|
||||
// Field3 doc
|
||||
field13: {
|
||||
// Field3.Field1 comment
|
||||
// Field3.Field1 doc
|
||||
field11: string
|
||||
// Field3.Field2 comment
|
||||
field12: string
|
||||
}
|
||||
}
|
||||
InlineStruct2: {
|
||||
// Field1 doc
|
||||
field21: string
|
||||
// Field1 comment
|
||||
// Field1 doc
|
||||
field11: string
|
||||
field12: string
|
||||
// Field3 doc
|
||||
field13: {
|
||||
// Field3.Field1 comment
|
||||
// Field3.Field1 doc
|
||||
field11: string
|
||||
// Field3.Field2 comment
|
||||
field12: string
|
||||
}
|
||||
}
|
||||
InlineStruct3: {
|
||||
field31: string
|
||||
// Field1 doc
|
||||
field21: string
|
||||
// Field1 comment
|
||||
// Field1 doc
|
||||
field11: string
|
||||
field12: string
|
||||
// Field3 doc
|
||||
field13: {
|
||||
// Field3.Field1 comment
|
||||
// Field3.Field1 doc
|
||||
field11: string
|
||||
// Field3.Field2 comment
|
||||
field12: string
|
||||
}
|
||||
}
|
||||
Optional: {
|
||||
field1?: string
|
||||
field2: string
|
||||
field3?: string
|
||||
field4: string
|
||||
field5: {
|
||||
field1?: string
|
||||
field2: string
|
||||
}
|
||||
field6?: {
|
||||
field1?: string
|
||||
field2: string
|
||||
field3?: {
|
||||
field1?: string
|
||||
field2: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Skip: {
|
||||
field2: string
|
||||
field4: string
|
||||
}
|
||||
Vendored
+323
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"net/http"
|
||||
|
||||
"github.com/kubevela/pkg/cue/cuex/providers"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
type BasicType struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 int `json:"field2"`
|
||||
Field3 bool `json:"field3"`
|
||||
Field4 float32 `json:"field4"`
|
||||
Field5 float64 `json:"field5"`
|
||||
Field6 int8 `json:"field6"`
|
||||
Field7 int16 `json:"field7"`
|
||||
Field8 int32 `json:"field8"`
|
||||
Field9 int64 `json:"field9"`
|
||||
Field10 uint `json:"field10"`
|
||||
Field11 uint8 `json:"field11"`
|
||||
Field12 uint16 `json:"field12"`
|
||||
Field13 uint32 `json:"field13"`
|
||||
Field14 uint64 `json:"field14"`
|
||||
Field15 uintptr `json:"field15"`
|
||||
Field16 byte `json:"field16"`
|
||||
Field17 rune `json:"field17"`
|
||||
Field18 interface{} `json:"field18"`
|
||||
Field19 any `json:"field19"`
|
||||
}
|
||||
|
||||
type TagName struct {
|
||||
Field1 string `json:"f1"`
|
||||
Field2 string `json:"f2"`
|
||||
Field3 string `json:"f3"`
|
||||
}
|
||||
|
||||
type SliceAndArray struct {
|
||||
Field1 []string `json:"field1"`
|
||||
Field2 [3]string `json:"field2"`
|
||||
Field3 []int `json:"field3"`
|
||||
Field4 [3]int `json:"field4"`
|
||||
Field5 []bool `json:"field5"`
|
||||
Field6 [3]bool `json:"field6"`
|
||||
Field7 []float32 `json:"field7"`
|
||||
Field8 [3]float32 `json:"field8"`
|
||||
Field9 []float64 `json:"field9"`
|
||||
Field10 [3]float64 `json:"field10"`
|
||||
Field11 [3]byte `json:"field11"`
|
||||
Field12 []byte `json:"field12"`
|
||||
}
|
||||
|
||||
type SmallStruct struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
}
|
||||
|
||||
type AnonymousField struct {
|
||||
SmallStruct
|
||||
}
|
||||
|
||||
type ReferenceField struct {
|
||||
Field1 *SmallStruct `json:"field1"`
|
||||
}
|
||||
|
||||
type StructField struct {
|
||||
Field1 SmallStruct `json:"field1"`
|
||||
Field2 *SmallStruct `json:"field2"`
|
||||
}
|
||||
|
||||
type EmbedStruct struct {
|
||||
Field1 struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
} `json:"field1"`
|
||||
Field2 struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 string `json:"field2"`
|
||||
} `json:"field3"`
|
||||
} `json:"field3"`
|
||||
} `json:"field3"`
|
||||
} `json:"field2"`
|
||||
Field3 http.Header `json:"field3"`
|
||||
Field4 crypto.Hash `json:"field4"`
|
||||
}
|
||||
|
||||
type MapField struct {
|
||||
Field1 map[string]string `json:"field1"`
|
||||
Field2 map[string]int `json:"field2"`
|
||||
Field3 map[string]interface{} `json:"field3"`
|
||||
Field4 map[string]SmallStruct `json:"field4"`
|
||||
Field5 map[string]any `json:"field5"`
|
||||
}
|
||||
|
||||
type EmptyStruct struct{}
|
||||
|
||||
type Interface interface {
|
||||
Foo()
|
||||
}
|
||||
|
||||
// Comment is a test struct1
|
||||
/* Struct is a test struct2 */
|
||||
/*
|
||||
Struct is a test struct3
|
||||
Struct is a test struct4
|
||||
*/
|
||||
type Comment struct {
|
||||
// Field1 doc
|
||||
Field1 string `json:"field1"` // Field1 comment
|
||||
/* Field2 doc */
|
||||
Field2 string `json:"field2"` // Field2 comment
|
||||
/*
|
||||
Field3 doc
|
||||
*/
|
||||
Field3 string `json:"field3"` // Field3 comment
|
||||
Field4 string `json:"field4"` // Field4 comment
|
||||
|
||||
// Field5 doc
|
||||
Field5 struct {
|
||||
Field1 string `json:"field1"` // Field5.Field1 comment
|
||||
// Field5.Field2 doc
|
||||
Field2 string `json:"field2"`
|
||||
/* Field5.Field3 doc */
|
||||
Field3 string `json:"field3"`
|
||||
/*
|
||||
Field5.Field4 doc
|
||||
*/
|
||||
Field4 string `json:"field4"`
|
||||
// Field5.Field5 doc
|
||||
Field5 struct {
|
||||
Field1 string `json:"field1"` // Field5.Field5.Field1 comment
|
||||
// Field5.Field5.Field2 doc
|
||||
Field2 string `json:"field2"`
|
||||
/* Field5.Field5.Field3 doc */
|
||||
Field3 string `json:"field3"`
|
||||
/*
|
||||
Field5.Field5.Field4 doc
|
||||
*/
|
||||
Field4 string `json:"field4"`
|
||||
} `json:"field5"`
|
||||
} `json:"field5"`
|
||||
|
||||
// Field6 doc
|
||||
Field6 http.Header `json:"field6"`
|
||||
Field7 http.Header `json:"field7"` // Field7 comment
|
||||
// Field8 doc
|
||||
Field8 http.Header `json:"field8"` // Field8 comment
|
||||
Field9 http.Header `json:"field9"`
|
||||
|
||||
/*
|
||||
Field10 doc1
|
||||
Field10 doc2
|
||||
Field10 doc3
|
||||
*/
|
||||
Field10 http.Header `json:"field10"` // Field10 comment
|
||||
|
||||
// Field11 doc
|
||||
Field11 struct {
|
||||
Field1 string `json:"field1"` // Field11.Field1 comment
|
||||
} `json:"field11"`
|
||||
|
||||
// Field12 doc
|
||||
Field12 struct {
|
||||
// Field12.Field1 doc
|
||||
Field1 string `json:"field1"`
|
||||
} `json:"field12"`
|
||||
|
||||
// Field13 doc
|
||||
Field13 struct {
|
||||
Field1 string `json:"field1"`
|
||||
} `json:"field13"`
|
||||
|
||||
// Field14 doc
|
||||
Field14 map[string]string `json:"field14"`
|
||||
}
|
||||
|
||||
type Default struct {
|
||||
A1 string `json:"a1" cue:"default:abc"`
|
||||
A2 string `json:"a2" cue:"default:"` // empty string
|
||||
B1 bool `json:"b1" cue:"default:true"`
|
||||
B2 bool `json:"b2" cue:"default:false"`
|
||||
C1 int `json:"c1" cue:"default:123"`
|
||||
C2 int8 `json:"c2" cue:"default:123"`
|
||||
C3 int16 `json:"c3" cue:"default:123"`
|
||||
C4 int32 `json:"c4" cue:"default:123"`
|
||||
C5 int64 `json:"c5" cue:"default:123"`
|
||||
D1 uint `json:"d1" cue:"default:123"`
|
||||
D2 uint8 `json:"d2" cue:"default:123"`
|
||||
D3 uint16 `json:"d3" cue:"default:123"`
|
||||
D4 uint32 `json:"d4" cue:"default:123"`
|
||||
D5 uint64 `json:"d5" cue:"default:123"`
|
||||
E1 float32 `json:"e1" cue:"default:123.456"`
|
||||
E2 float64 `json:"e2" cue:"default:123.456"`
|
||||
}
|
||||
|
||||
type Enum struct {
|
||||
A string `json:"a" cue:"enum:abc,def,ghi"`
|
||||
B int `json:"b" cue:"enum:1,2,3"`
|
||||
C bool `json:"c" cue:"enum:true,false"`
|
||||
D float64 `json:"d" cue:"enum:1.1,2.2,3.3"`
|
||||
E string `json:"e" cue:"enum:abc,def,ghi;default:ghi"`
|
||||
F int `json:"f" cue:"enum:1,2,3;default:2"`
|
||||
G bool `json:"g" cue:"enum:true,false;default:false"`
|
||||
H float64 `json:"h" cue:"enum:1.1,2.2,3.3;default:1.1"` // if default value is first enum, '*' will not be added
|
||||
I string `json:"i" cue:"enum:abc"`
|
||||
}
|
||||
|
||||
type Unexported struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 struct {
|
||||
Field1 string `json:"field1"`
|
||||
field2 string // unexported field will be ignored
|
||||
Field3 struct {
|
||||
Field1 string `json:"field1"`
|
||||
field2 string // unexported field will be ignored
|
||||
Field3 string `json:"field3"`
|
||||
} `json:"field3"`
|
||||
} `json:"field2"`
|
||||
field3 string // unexported field will be ignored
|
||||
}
|
||||
|
||||
// RequestVars is the vars for http request
|
||||
// TODO: support timeout & tls
|
||||
type RequestVars struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Request struct {
|
||||
Body string `json:"body"`
|
||||
Header http.Header `json:"header"`
|
||||
Trailer http.Header `json:"trailer"`
|
||||
} `json:"request"`
|
||||
}
|
||||
|
||||
// ResponseVars is the vars for http response
|
||||
type ResponseVars struct {
|
||||
Body string `json:"body"`
|
||||
Header http.Header `json:"header"`
|
||||
Trailer http.Header `json:"trailer"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
}
|
||||
|
||||
// DoParams is the params for http request
|
||||
type DoParams providers.Params[RequestVars]
|
||||
|
||||
// DoReturns returned struct for http response
|
||||
type DoReturns providers.Returns[ResponseVars]
|
||||
|
||||
type ResourceReturns providers.Returns[*unstructured.Unstructured]
|
||||
|
||||
type InlineStruct1 struct {
|
||||
// Field1 doc
|
||||
Field1 string `json:"field11"` // Field1 comment
|
||||
Field2 string `json:"field12"`
|
||||
// Field3 doc
|
||||
Field3 struct {
|
||||
// Field3.Field1 doc
|
||||
Field1 string `json:"field11"` // Field3.Field1 comment
|
||||
Field2 string `json:"field12"` // Field3.Field2 comment
|
||||
} `json:"field13"`
|
||||
}
|
||||
|
||||
type InlineStruct2 struct {
|
||||
// Field1 doc
|
||||
Field1 string `json:"field21"`
|
||||
InlineStruct1 `json:",inline"` // Field1 comment
|
||||
}
|
||||
|
||||
type InlineStruct3 struct {
|
||||
Field1 string `json:"field31"`
|
||||
InlineStruct2 `json:",inline"`
|
||||
}
|
||||
|
||||
type Optional struct {
|
||||
Field1 string `json:"field1,omitempty"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 string `json:"field3,omitempty"`
|
||||
Field4 string `json:"field4"`
|
||||
Field5 struct {
|
||||
Field1 string `json:"field1,omitempty"`
|
||||
Field2 string `json:"field2"`
|
||||
} `json:"field5"`
|
||||
Field6 struct {
|
||||
Field1 string `json:"field1,omitempty"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 struct {
|
||||
Field1 string `json:"field1,omitempty"`
|
||||
Field2 string `json:"field2"`
|
||||
} `json:"field3,omitempty"`
|
||||
} `json:"field6,omitempty"`
|
||||
}
|
||||
|
||||
type Skip struct {
|
||||
Field1 string `json:"-"`
|
||||
Field2 string `json:"field2"`
|
||||
Field3 string `json:"-"`
|
||||
Field4 string `json:"field4"`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
gotypes "go/types"
|
||||
"strconv"
|
||||
|
||||
cueast "cuelang.org/go/cue/ast"
|
||||
cuetoken "cuelang.org/go/cue/token"
|
||||
)
|
||||
|
||||
func ident(name string, isDef bool) *cueast.Ident {
|
||||
if isDef {
|
||||
name = "#" + name
|
||||
}
|
||||
return cueast.NewIdent(name)
|
||||
}
|
||||
|
||||
func basicType(x *gotypes.Basic) cueast.Expr {
|
||||
// byte is an alias for uint8 in go/types
|
||||
|
||||
switch t := x.String(); t {
|
||||
case "uintptr":
|
||||
return ident("uint64", false)
|
||||
case "byte":
|
||||
return ident("uint8", false)
|
||||
default:
|
||||
return ident(t, false)
|
||||
}
|
||||
}
|
||||
|
||||
func anyLit() cueast.Expr {
|
||||
return &cueast.StructLit{Elts: []cueast.Decl{&cueast.Ellipsis{}}}
|
||||
}
|
||||
|
||||
func basicLabel(t *gotypes.Basic, v string) (cueast.Expr, error) {
|
||||
switch {
|
||||
case t.Info()&gotypes.IsInteger != 0:
|
||||
if _, err := strconv.ParseInt(v, 10, 64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cueast.BasicLit{Kind: cuetoken.INT, Value: v}, nil
|
||||
case t.Info()&gotypes.IsFloat != 0:
|
||||
if _, err := strconv.ParseFloat(v, 64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cueast.BasicLit{Kind: cuetoken.FLOAT, Value: v}, nil
|
||||
case t.Info()&gotypes.IsBoolean != 0:
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cueast.NewBool(b), nil
|
||||
case t.Info()&gotypes.IsString != 0:
|
||||
return cueast.NewString(v), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported basic type %s", t)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cuegen
|
||||
|
||||
import (
|
||||
gotypes "go/types"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
cueast "cuelang.org/go/cue/ast"
|
||||
cuetoken "cuelang.org/go/cue/token"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIdent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isDef bool
|
||||
want string
|
||||
}{
|
||||
{name: "test", isDef: true, want: "#test"},
|
||||
{name: "test", isDef: false, want: "test"},
|
||||
{name: "Test", isDef: true, want: "#Test"},
|
||||
{name: "Test", isDef: false, want: "Test"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.want, ident(tt.name, tt.isDef).String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typ *gotypes.Basic
|
||||
want string
|
||||
}{
|
||||
{name: "uintptr", typ: gotypes.Typ[gotypes.Uintptr], want: "uint64"},
|
||||
{name: "byte", typ: gotypes.Typ[gotypes.Byte], want: "uint8"},
|
||||
{name: "int", typ: gotypes.Typ[gotypes.Int], want: "int"},
|
||||
{name: "int8", typ: gotypes.Typ[gotypes.Int8], want: "int8"},
|
||||
{name: "int16", typ: gotypes.Typ[gotypes.Int16], want: "int16"},
|
||||
{name: "int32", typ: gotypes.Typ[gotypes.Int32], want: "int32"},
|
||||
{name: "int64", typ: gotypes.Typ[gotypes.Int64], want: "int64"},
|
||||
{name: "uint", typ: gotypes.Typ[gotypes.Uint], want: "uint"},
|
||||
{name: "uint8", typ: gotypes.Typ[gotypes.Uint8], want: "uint8"},
|
||||
{name: "uint16", typ: gotypes.Typ[gotypes.Uint16], want: "uint16"},
|
||||
{name: "uint32", typ: gotypes.Typ[gotypes.Uint32], want: "uint32"},
|
||||
{name: "uint64", typ: gotypes.Typ[gotypes.Uint64], want: "uint64"},
|
||||
{name: "float32", typ: gotypes.Typ[gotypes.Float32], want: "float32"},
|
||||
{name: "float64", typ: gotypes.Typ[gotypes.Float64], want: "float64"},
|
||||
{name: "string", typ: gotypes.Typ[gotypes.String], want: "string"},
|
||||
{name: "bool", typ: gotypes.Typ[gotypes.Bool], want: "bool"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.want, basicType(tt.typ).(*cueast.Ident).String(), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnyLit(t *testing.T) {
|
||||
assert.Equal(t, anyLit(), &cueast.StructLit{Elts: []cueast.Decl{&cueast.Ellipsis{}}})
|
||||
}
|
||||
|
||||
func TestBasicLabel(t *testing.T) {
|
||||
overflowInt64 := strconv.FormatInt(math.MaxInt64, 10) + "0"
|
||||
overflowUint64 := strconv.FormatUint(math.MaxUint64, 10) + "0"
|
||||
overflowFloat64 := strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64) + "0"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
typ *gotypes.Basic
|
||||
v string
|
||||
wantErr bool
|
||||
want *cueast.BasicLit
|
||||
}{
|
||||
{name: "int", typ: gotypes.Typ[gotypes.Int], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "int8", typ: gotypes.Typ[gotypes.Int8], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "int16", typ: gotypes.Typ[gotypes.Int16], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "int32", typ: gotypes.Typ[gotypes.Int32], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "int64", typ: gotypes.Typ[gotypes.Int64], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "uint", typ: gotypes.Typ[gotypes.Uint], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "uint8", typ: gotypes.Typ[gotypes.Uint8], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "uint16", typ: gotypes.Typ[gotypes.Uint16], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "uint32", typ: gotypes.Typ[gotypes.Uint32], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "uint64", typ: gotypes.Typ[gotypes.Uint64], v: "123", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.INT, Value: "123"}},
|
||||
{name: "float32", typ: gotypes.Typ[gotypes.Float32], v: "123.456", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.FLOAT, Value: "123.456"}},
|
||||
{name: "float64", typ: gotypes.Typ[gotypes.Float64], v: "123.456", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.FLOAT, Value: "123.456"}},
|
||||
{name: "string", typ: gotypes.Typ[gotypes.String], v: "abc", wantErr: false, want: &cueast.BasicLit{Kind: cuetoken.STRING, Value: `"abc"`}},
|
||||
{name: "bool", typ: gotypes.Typ[gotypes.Bool], v: "true", wantErr: false, want: cueast.NewBool(true)},
|
||||
{name: "bool", typ: gotypes.Typ[gotypes.Bool], v: "false", wantErr: false, want: cueast.NewBool(false)},
|
||||
{name: "int_error", typ: gotypes.Typ[gotypes.Int], v: "abc", wantErr: true},
|
||||
{name: "uint_error", typ: gotypes.Typ[gotypes.Uint], v: "abc", wantErr: true},
|
||||
{name: "float_error", typ: gotypes.Typ[gotypes.Float64], v: "abc", wantErr: true},
|
||||
{name: "bool_error", typ: gotypes.Typ[gotypes.Bool], v: "abc", wantErr: true},
|
||||
{name: "type_error", typ: gotypes.Typ[gotypes.Complex64], v: "abc", wantErr: true},
|
||||
{name: "int_overflow", typ: gotypes.Typ[gotypes.Int], v: overflowInt64, wantErr: true},
|
||||
{name: "uint_overflow", typ: gotypes.Typ[gotypes.Uint], v: overflowUint64, wantErr: true},
|
||||
{name: "int64_overflow", typ: gotypes.Typ[gotypes.Int64], v: overflowInt64, wantErr: true},
|
||||
{name: "uint64_overflow", typ: gotypes.Typ[gotypes.Uint64], v: overflowUint64, wantErr: true},
|
||||
{name: "float64_overflow", typ: gotypes.Typ[gotypes.Float64], v: overflowFloat64, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := basicLabel(tt.typ, tt.v)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err, tt.name)
|
||||
} else {
|
||||
assert.NoError(t, err, tt.name)
|
||||
assert.Equal(t, tt.want, got, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user