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:
iyear
2023-03-21 15:24:29 +08:00
committed by GitHub
parent 4d714ed2c2
commit 6976b2ba0c
19 changed files with 2247 additions and 1 deletions
+1 -1
View File
@@ -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
+60
View File
@@ -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"}}`
+479
View File
@@ -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}
}
+64
View File
@@ -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)
}
}
+152
View File
@@ -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
}
+65
View File
@@ -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)
}
+28
View File
@@ -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{}{}
}
}
+61
View File
@@ -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)
}
}
+146
View File
@@ -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]
}
+232
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"`
}
+21
View File
@@ -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"`
}
+321
View File
@@ -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
}
+323
View File
@@ -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"`
}
+75
View File
@@ -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)
}
}
+128
View File
@@ -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)
}
}
}