From 75f8209a4cd3693162908269f3db33e141a2fd1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Mar 2023 11:34:16 +0800 Subject: [PATCH] [Backport release-1.8] Feat: add sub-module to Golang SDK (#5731) * wait to deal with go.mod Signed-off-by: Qiaozp (cherry picked from commit 91f9e49d21db2d3a899422632dfe878ea181aee3) * seperate def and module modifier Signed-off-by: Qiaozp (cherry picked from commit 8f4ef2f62afdda8bec61de423ec3150b160c165f) * fix module import Signed-off-by: Qiaozp (cherry picked from commit fa02a0f8cd4bb4fbdf1123d29c0435220314f079) * refine code Signed-off-by: Qiaozp (cherry picked from commit 3a56d8c8292cf27c778777a01ff46a6fec1f2270) * remove the pointer reference in loop generalize the language-specific argument parsing amend tests Signed-off-by: Qiaozp (cherry picked from commit 38b593d6f95b6bc66ef730c5bc8831d3e82923b1) * remove focused test Signed-off-by: Qiaozp (cherry picked from commit 8aa74df69ff318b346803aaaf1143346133d4c1f) * fix test Signed-off-by: Qiaozp (cherry picked from commit 6d40d257e148c6008ed8e6f09d3af459cc01af31) * update command usage Signed-off-by: Qiaozp (cherry picked from commit 78bb040039f673e7ef8dd5d795b53442c3eebca5) --------- Co-authored-by: Qiaozp --- .github/workflows/sdk-test.yml | 4 +- design/vela-cli/sdk_generating.md | 4 +- hack/sdk/sync.sh | 13 +- pkg/definition/gen_sdk/_scaffold/go/go.mod_ | 5 + .../go/pkg/apis/common/application.go | 24 +- .../gen_sdk/_scaffold/go/pkg/client/client.go | 4 +- pkg/definition/gen_sdk/gen_sdk.go | 204 +++++++++--- pkg/definition/gen_sdk/gen_sdk_test.go | 169 ++++++++-- pkg/definition/gen_sdk/go.go | 310 +++++++++++++----- references/cli/def.go | 28 +- 10 files changed, 592 insertions(+), 173 deletions(-) diff --git a/.github/workflows/sdk-test.yml b/.github/workflows/sdk-test.yml index 4a97bcf5e..9cc005554 100644 --- a/.github/workflows/sdk-test.yml +++ b/.github/workflows/sdk-test.yml @@ -42,10 +42,10 @@ jobs: run: make vela-cli - name: Build SDK - run: bin/vela def gen-api -f vela-templates/definitions/internal/ -o ./kubevela-go-sdk --package=github.com/kubevela-contrib/kubevela-go-sdk + run: bin/vela def gen-api -f vela-templates/definitions/internal/ -o ./kubevela-go-sdk --package=github.com/kubevela-contrib/kubevela-go-sdk --init - name: Validate SDK run: | cd kubevela-go-sdk go mod tidy - golangci-lint run --timeout 5m ./... + golangci-lint run --timeout 5m -e "exported:" -e "dot-imports" ./... diff --git a/design/vela-cli/sdk_generating.md b/design/vela-cli/sdk_generating.md index bd16268a6..50e3db390 100644 --- a/design/vela-cli/sdk_generating.md +++ b/design/vela-cli/sdk_generating.md @@ -96,10 +96,10 @@ This command requires `docker` in PATH as we'll run openapi-generator in docker. ```shell # Initialize the SDK, generate API from all definitions, -vela def gen-api --init -f /path/to/def/dir -o /path/to/sdk --lang go +vela def gen-api --init -f /path/to/def/dir -o /path/to/sdk --language go # Incrementally generate API from definitions -vela def gen-api -f /path/to/def/dir -o /path/to/sdk --lang go +vela def gen-api -f /path/to/def/dir -o /path/to/sdk --language go ``` ## Future work diff --git a/hack/sdk/sync.sh b/hack/sdk/sync.sh index 8a2ef835b..d27a0386f 100644 --- a/hack/sdk/sync.sh +++ b/hack/sdk/sync.sh @@ -33,7 +33,7 @@ config() { git config --global user.name "kubevela-bot" } -cloneAndClearRepo() { +cloneAndClearCoreAPI() { echo "git clone" if [[ -n "$SSH_DEPLOY_KEY" ]]; then @@ -42,8 +42,13 @@ cloneAndClearRepo() { git clone --single-branch --depth 1 https://github.com/$VELA_GO_SDK.git kubevela-go-sdk fi - echo "Clear kubevela-go-sdk pkg/*" - rm -r kubevela-go-sdk/pkg/* + echo "Clear kubevela-go-sdk pkg/apis/common, pkg/apis/component, pkg/apis/policy, pkg/apis/trait, pkg/apis/workflow-step, pkg/apis/utils, pkg/apis/types.go " + rm -rf kubevela-go-sdk/pkg/apis/common + rm -rf kubevela-go-sdk/pkg/apis/component + rm -rf kubevela-go-sdk/pkg/apis/policy + rm -rf kubevela-go-sdk/pkg/apis/trait + rm -rf kubevela-go-sdk/pkg/apis/workflow-step + rm -rf kubevela-go-sdk/pkg/apis/utils } updateRepo() { @@ -79,7 +84,7 @@ syncRepo() { main() { config - cloneAndClearRepo + cloneAndClearCoreAPI updateRepo syncRepo } diff --git a/pkg/definition/gen_sdk/_scaffold/go/go.mod_ b/pkg/definition/gen_sdk/_scaffold/go/go.mod_ index 99468113b..bc02f2797 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/go.mod_ +++ b/pkg/definition/gen_sdk/_scaffold/go/go.mod_ @@ -4,6 +4,8 @@ go 1.19 require ( github.com/oam-dev/kubevela-core-api v1.5.8 + + // for main module github.com/pkg/errors v0.9.1 k8s.io/apimachinery v0.23.6 k8s.io/client-go v0.23.6 @@ -11,6 +13,9 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +// for sub-module +// require github.com/kubevela/vela-go-sdk v0.0.0-20230309022604-cd431bb25a9a + require ( cuelang.org/go v0.5.0-alpha.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect diff --git a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go index 64ce15719..ea21135de 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go +++ b/pkg/definition/gen_sdk/_scaffold/go/pkg/apis/common/application.go @@ -285,14 +285,14 @@ func (a *ApplicationBuilder) Validate() error { return nil } -func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) { +func FromK8sObject(app v1beta1.Application) (TypedApplication, error) { a := &ApplicationBuilder{} a.Name(app.Name) a.Namespace(app.Namespace) a.resourceVersion = app.ResourceVersion for _, comp := range app.Spec.Components { - c, err := FromComponent(&comp) + c, err := FromComponent(comp) if err != nil { return nil, errors.Wrap(err, "convert component from k8s object") } @@ -300,7 +300,7 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) { } if app.Spec.Workflow != nil { for _, step := range app.Spec.Workflow.Steps { - s, err := FromWorkflowStep(&step) + s, err := FromWorkflowStep(step) if err != nil { return nil, errors.Wrap(err, "convert workflow step from k8s object") } @@ -308,7 +308,7 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) { } } for _, policy := range app.Spec.Policies { - p, err := FromPolicy(&policy) + p, err := FromPolicy(policy) if err != nil { return nil, errors.Wrap(err, "convert policy from k8s object") } @@ -317,34 +317,34 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) { return a, nil } -func FromComponent(component *common.ApplicationComponent) (Component, error) { +func FromComponent(component common.ApplicationComponent) (Component, error) { build, ok := ComponentsBuilders[component.Type] if !ok { return nil, errors.Errorf("no component type %s registered", component.Type) } - return build(*component) + return build(component) } -func FromWorkflowStep(step *v1beta1.WorkflowStep) (WorkflowStep, error) { +func FromWorkflowStep(step v1beta1.WorkflowStep) (WorkflowStep, error) { build, ok := WorkflowStepsBuilders[step.Type] if !ok { return nil, errors.Errorf("no workflow step type %s registered", step.Type) } - return build(*step) + return build(step) } -func FromPolicy(policy *v1beta1.AppPolicy) (Policy, error) { +func FromPolicy(policy v1beta1.AppPolicy) (Policy, error) { build, ok := PoliciesBuilders[policy.Type] if !ok { return nil, errors.Errorf("no policy type %s registered", policy.Type) } - return build(*policy) + return build(policy) } -func FromTrait(trait *common.ApplicationTrait) (Trait, error) { +func FromTrait(trait common.ApplicationTrait) (Trait, error) { build, ok := TraitBuilders[trait.Type] if !ok { return nil, errors.Errorf("no trait type %s registered", trait.Type) } - return build(*trait) + return build(trait) } diff --git a/pkg/definition/gen_sdk/_scaffold/go/pkg/client/client.go b/pkg/definition/gen_sdk/_scaffold/go/pkg/client/client.go index 5438d4a88..b0a38d034 100644 --- a/pkg/definition/gen_sdk/_scaffold/go/pkg/client/client.go +++ b/pkg/definition/gen_sdk/_scaffold/go/pkg/client/client.go @@ -97,7 +97,7 @@ func (c clientImpl) Get(ctx context.Context, key client.ObjectKey) (apis.TypedAp if err != nil { return nil, err } - return sdkcommon.FromK8sObject(&_app) + return sdkcommon.FromK8sObject(_app) } func (c clientImpl) List(ctx context.Context, opts client.ListOption) ([]apis.TypedApplication, error) { @@ -108,7 +108,7 @@ func (c clientImpl) List(ctx context.Context, opts client.ListOption) ([]apis.Ty } var apps []apis.TypedApplication for _, app := range _appList.Items { - _app, err := sdkcommon.FromK8sObject(&app) + _app, err := sdkcommon.FromK8sObject(app) if err != nil { return nil, err } diff --git a/pkg/definition/gen_sdk/gen_sdk.go b/pkg/definition/gen_sdk/gen_sdk.go index c9d5a889e..dbfdf1065 100644 --- a/pkg/definition/gen_sdk/gen_sdk.go +++ b/pkg/definition/gen_sdk/gen_sdk.go @@ -25,6 +25,8 @@ import ( "os/exec" "path" "path/filepath" + "reflect" + "runtime" "runtime/debug" "strings" @@ -47,17 +49,31 @@ import ( type byteHandler func([]byte) []byte +var ( + defaultAPIDir = map[string]string{ + "go": "pkg/apis", + } + // LangArgsRegistry is used to store the argument info + LangArgsRegistry = map[string]map[langArgKey]LangArg{} +) + // GenMeta stores the metadata for generator. type GenMeta struct { config *rest.Config + name string + kind string - Output string - Lang string - Package string - Template string - File []string - InitSDK bool - Verbose bool + Output string + APIDirectory string + IsSubModule bool + Lang string + Package string + Template string + File []string + InitSDK bool + Verbose bool + + LangArgs LanguageArgs cuePaths []string templatePath string @@ -67,11 +83,70 @@ type GenMeta struct { // Generator is used to generate SDK code from CUE template for one language. type Generator struct { meta *GenMeta - name string - kind string def definition.Definition openapiSchema []byte - modifiers []Modifier + // defModifiers are the modifiers for each definition. + defModifiers []Modifier + // moduleModifiers are the modifiers for the whole module. It will be executed after generating all definitions. + moduleModifiers []Modifier +} + +// LanguageArgs is used to store the arguments for the language. +type LanguageArgs interface { + Get(key langArgKey) string + Set(key langArgKey, value string) +} + +// langArgKey is language argument key. +type langArgKey string + +// LangArg is language-specific argument. +type LangArg struct { + Name langArgKey + Desc string + Default string +} + +// registerLangArg should be called in init() function of each language. +func registerLangArg(lang string, arg ...LangArg) { + if _, ok := LangArgsRegistry[lang]; !ok { + LangArgsRegistry[lang] = map[langArgKey]LangArg{} + } + for _, a := range arg { + LangArgsRegistry[lang][a.Name] = a + } +} + +// NewLanguageArgs parses the language arguments and returns a LanguageArgs. +func NewLanguageArgs(lang string, langArgs []string) (LanguageArgs, error) { + availableArgs := LangArgsRegistry[lang] + res := languageArgs{} + for _, arg := range langArgs { + parts := strings.Split(arg, "=") + if len(parts) != 2 { + return nil, errors.Errorf("argument %s is not in the format of key=value", arg) + } + if _, ok := availableArgs[langArgKey(parts[0])]; !ok { + return nil, errors.Errorf("argument %s is not supported for language %s", parts[0], lang) + } + res.Set(langArgKey(parts[0]), parts[1]) + } + for k, v := range availableArgs { + if res.Get(k) == "" { + res.Set(k, v.Default) + } + } + return res, nil +} + +type languageArgs map[string]string + +func (l languageArgs) Get(key langArgKey) string { + return l[string(key)] +} + +func (l languageArgs) Set(key langArgKey, value string) { + l[string(key)] = value } // Modifier is used to modify the generated code. @@ -82,7 +157,7 @@ type Modifier interface { // Init initializes the generator. // It will validate the param, analyze the CUE files, read them to memory, mkdir for output. -func (meta *GenMeta) Init(c common.Args) (err error) { +func (meta *GenMeta) Init(c common.Args, langArgs []string) (err error) { meta.config, err = c.GetConfig() if err != nil { klog.Info("No kubeconfig found, skipping") @@ -95,9 +170,18 @@ func (meta *GenMeta) Init(c common.Args) (err error) { return fmt.Errorf("language %s is not supported", meta.Lang) } + // Init arguments + if meta.APIDirectory == "" { + meta.APIDirectory = defaultAPIDir[meta.Lang] + } + + meta.LangArgs, err = NewLanguageArgs(meta.Lang, langArgs) + if err != nil { + return err + } packageFuncs := map[string]byteHandler{ "go": func(b []byte) []byte { - return bytes.ReplaceAll(b, []byte("github.com/kubevela/vela-go-sdk"), []byte(meta.Package)) + return bytes.ReplaceAll(b, []byte(PackagePlaceHolder), []byte(meta.Package)) }, } @@ -137,7 +221,7 @@ func (meta *GenMeta) CreateScaffold() error { if !meta.InitSDK { return nil } - fmt.Println("Flag --init is set, creating scaffold...") + klog.Info("Flag --init is set, creating scaffold...") langDirPrefix := fmt.Sprintf("%s/%s", ScaffoldDir, meta.Lang) err := fs.WalkDir(Scaffold, ScaffoldDir, func(_path string, d fs.DirEntry, err error) error { if err != nil { @@ -155,7 +239,7 @@ func (meta *GenMeta) CreateScaffold() error { } fileContent = meta.packageFunc(fileContent) fileName := path.Join(meta.Output, strings.TrimPrefix(_path, langDirPrefix)) - // go.mod_ is a special file name, it will be renamed to go.mod. Go will exclude directory go.mod located from the build process. + // go.mod_ is a special file name, it will be renamed to go.mod. Go will ignore directory containing go.mod during the build process. fileName = strings.ReplaceAll(fileName, "go.mod_", "go.mod") fileDir := path.Dir(fileName) if err = os.MkdirAll(fileDir, 0750); err != nil { @@ -236,18 +320,24 @@ func (meta *GenMeta) PrepareGeneratorAndTemplate() error { // 1. Generate OpenAPI schema from cue files // 2. Generate code from OpenAPI schema func (meta *GenMeta) Run() error { + g := NewModifiableGenerator(meta) + if len(meta.cuePaths) == 0 { + return nil + } + APIGenerated := false for _, cuePath := range meta.cuePaths { - klog.Infof("Generating SDK for %s", cuePath) - g := NewModifiableGenerator(meta) + klog.Infof("Generating API for %s", cuePath) // nolint:gosec cueBytes, err := os.ReadFile(cuePath) if err != nil { return errors.Wrapf(err, "failed to read %s", cuePath) } - template, err := g.GetDefinitionValue(cueBytes) + template, defName, defKind, err := g.GetDefinitionValue(cueBytes) if err != nil { return err } + g.meta.SetDefinition(defName, defKind) + err = g.GenOpenAPISchema(template) if err != nil { if strings.Contains(err.Error(), "unsupported node string (*ast.Ident)") { @@ -257,32 +347,51 @@ func (meta *GenMeta) Run() error { } return errors.Wrapf(err, "generate OpenAPI schema") } + err = g.GenerateCode() if err != nil { return err } + APIGenerated = true } + if !APIGenerated { + return nil + } + for _, m := range g.moduleModifiers { + err := m.Modify() + if err != nil { + return err + } + } + return nil } -// GetDefinitionValue returns a value.Value from cue bytes -func (g *Generator) GetDefinitionValue(cueBytes []byte) (*value.Value, error) { +// SetDefinition sets definition name and kind +func (meta *GenMeta) SetDefinition(defName, defKind string) { + meta.name = defName + meta.kind = defKind +} + +// GetDefinitionValue returns a value.Value definition name, definition kind from cue bytes +func (g *Generator) GetDefinitionValue(cueBytes []byte) (*value.Value, string, string, error) { g.def = definition.Definition{Unstructured: unstructured.Unstructured{}} if err := g.def.FromCUEString(string(cueBytes), g.meta.config); err != nil { - return nil, errors.Wrapf(err, "failed to parse CUE") + return nil, "", "", errors.Wrapf(err, "failed to parse CUE") } - g.name = g.def.GetName() - g.kind = g.def.GetKind() templateString, _, err := unstructured.NestedString(g.def.Object, definition.DefinitionTemplateKeys...) if err != nil { - return nil, err + return nil, "", "", err + } + if templateString == "" { + return nil, "", "", errors.New("definition doesn't include cue schematic") } template, err := value.NewValue(templateString+velacue.BaseTemplate, nil, "") if err != nil { - return nil, err + return nil, "", "", err } - return template, nil + return template, g.def.GetName(), g.def.GetKind(), nil } // GenOpenAPISchema generates OpenAPI json schema from cue.Instance @@ -327,8 +436,8 @@ func (g *Generator) GenOpenAPISchema(val *value.Value) error { openapiSchema, err := doc.MarshalJSON() g.openapiSchema = openapiSchema if g.meta.Verbose { - fmt.Println("OpenAPI schema:") - fmt.Println(string(g.openapiSchema)) + klog.Info("OpenAPI schema:") + klog.Info(string(g.openapiSchema)) } return err } @@ -337,13 +446,13 @@ func (g *Generator) completeOpenAPISchema(doc *openapi3.T) { for key, schema := range doc.Components.Schemas { switch key { case "parameter": - spec := g.name + "-spec" + spec := g.meta.name + "-spec" schema.Value.Title = spec completeFreeFormSchema(schema) completeSchema(key, schema) doc.Components.Schemas[spec] = schema delete(doc.Components.Schemas, key) - case g.name + "-spec": + case g.meta.name + "-spec": continue default: completeSchema(key, schema) @@ -353,7 +462,7 @@ func (g *Generator) completeOpenAPISchema(doc *openapi3.T) { // GenerateCode will call openapi-generator to generate code and modify it func (g *Generator) GenerateCode() (err error) { - tmpFile, err := os.CreateTemp("", g.name+"-*.json") + tmpFile, err := os.CreateTemp("", g.meta.name+"-*.json") _, err = tmpFile.Write(g.openapiSchema) if err != nil { return errors.Wrap(err, "write openapi schema to temporary file") @@ -364,11 +473,11 @@ func (g *Generator) GenerateCode() (err error) { _ = os.Remove(tmpFile.Name()) } }() - apiDir, err := filepath.Abs(path.Join(g.meta.Output, "pkg", "apis")) + apiDir, err := filepath.Abs(path.Join(g.meta.Output, g.meta.APIDirectory)) if err != nil { return errors.Wrapf(err, "get absolute path of %s", apiDir) } - err = os.MkdirAll(path.Join(apiDir, definition.DefinitionKindToType[g.kind]), 0750) + err = os.MkdirAll(path.Join(apiDir, definition.DefinitionKindToType[g.meta.kind]), 0750) if err != nil { return errors.Wrapf(err, "create directory %s", apiDir) } @@ -384,28 +493,28 @@ func (g *Generator) GenerateCode() (err error) { "generate", "-i", "/local/input/"+filepath.Base(tmpFile.Name()), "-g", g.meta.Lang, - "-o", fmt.Sprintf("/local/output/%s/%s", definition.DefinitionKindToType[g.kind], g.name), + "-o", fmt.Sprintf("/local/output/%s/%s", definition.DefinitionKindToType[g.meta.kind], g.meta.name), "-t", "/local/template", "--skip-validate-spec", "--enable-post-process-file", "--generate-alias-as-model", "--inline-schema-name-defaults", "arrayItemSuffix=,mapItemSuffix=", - "--additional-properties", fmt.Sprintf("isGoSubmodule=true,packageName=%s", strings.ReplaceAll(g.name, "-", "_")), + "--additional-properties", fmt.Sprintf("packageName=%s", strings.ReplaceAll(g.meta.name, "-", "_")), "--global-property", "modelDocs=false,models,supportingFiles=utils.go", ) if g.meta.Verbose { - fmt.Println(cmd.String()) + klog.Info(cmd.String()) } output, err := cmd.CombinedOutput() if err != nil { return errors.Wrap(err, string(output)) } if g.meta.Verbose { - fmt.Println(string(output)) + klog.Info(string(output)) } // Adjust the generated files and code - for _, m := range g.modifiers { + for _, m := range g.defModifiers { err := m.Modify() if err != nil { return errors.Wrapf(err, "modify fail by %s", m.Name()) @@ -531,20 +640,21 @@ func completeSchemas(schemas openapi3.Schemas) { // NewModifiableGenerator returns a new Generator with modifiers func NewModifiableGenerator(meta *GenMeta) *Generator { g := &Generator{ - meta: meta, - modifiers: []Modifier{}, + meta: meta, + defModifiers: []Modifier{}, + moduleModifiers: []Modifier{}, } - mo := newModifierOnLanguage(meta.Lang, g) - g.modifiers = append(g.modifiers, mo) + appendModifiersByLanguage(g, meta) return g } -func newModifierOnLanguage(lang string, generator *Generator) Modifier { - switch lang { +func appendModifiersByLanguage(g *Generator, meta *GenMeta) { + switch meta.Lang { case "go": - return &GoModifier{g: generator} + g.defModifiers = append(g.defModifiers, &GoDefModifier{GenMeta: meta}) + g.moduleModifiers = append(g.moduleModifiers, &GoModuleModifier{GenMeta: meta}) default: - panic("unsupported language: " + lang) + panic(fmt.Sprintf("unsupported language: %s", meta.Lang)) } } @@ -607,3 +717,7 @@ func defaultValueMatchOneOfItem(item *openapi3.Schema, defaultValue interface{}) } return false } + +func fnName(fn interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() +} diff --git a/pkg/definition/gen_sdk/gen_sdk_test.go b/pkg/definition/gen_sdk/gen_sdk_test.go index f9cb9e152..1a8120849 100644 --- a/pkg/definition/gen_sdk/gen_sdk_test.go +++ b/pkg/definition/gen_sdk/gen_sdk_test.go @@ -30,19 +30,28 @@ import ( var _ = Describe("Test Generating SDK", func() { var err error outputDir := filepath.Join("testdata", "output") + lang := "go" meta := GenMeta{ - Output: outputDir, - Lang: "go", - Package: "github.com/kubevela/test-gen-sdk", - File: []string{filepath.Join("testdata", "cron-task.cue")}, - InitSDK: true, + Output: outputDir, + Lang: lang, + Package: "github.com/kubevela-contrib/kubevela-go-sdk", + APIDirectory: defaultAPIDir[lang], + Verbose: true, } + var langArgs []string + + BeforeEach(func() { + meta.InitSDK = false + meta.File = []string{filepath.Join("testdata", "cron-task.cue")} + meta.cuePaths = []string{} + }) + checkDirNotEmpty := func(dir string) { _, err = os.Stat(dir) Expect(err).Should(BeNil()) } - genWithMeta := func(meta GenMeta) { - err = meta.Init(common.Args{}) + genWithMeta := func() { + err = meta.Init(common.Args{}, langArgs) Expect(err).Should(BeNil()) err = meta.CreateScaffold() Expect(err).Should(BeNil()) @@ -52,43 +61,40 @@ var _ = Describe("Test Generating SDK", func() { Expect(err).Should(BeNil()) } It("Test generating SDK and init the scaffold", func() { - genWithMeta(meta) + meta.InitSDK = true + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis")) checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "component", "cron-task")) }) It("Test generating SDK, append apis", func() { - meta.InitSDK = false meta.File = append(meta.File, "testdata/shared-resource.cue") - genWithMeta(meta) + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "policy", "shared-resource")) }) It("Test free form parameter {...}", func() { - meta.InitSDK = false meta.File = []string{"testdata/json-merge-patch.cue"} meta.Verbose = true - genWithMeta(meta) + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "trait", "json-merge-patch")) }) It("Test workflow step", func() { - meta.InitSDK = false meta.File = []string{"testdata/deploy.cue"} meta.Verbose = true - genWithMeta(meta) + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "deploy")) }) It("Test step-group", func() { - meta.InitSDK = false meta.File = []string{"testdata/step-group.cue"} meta.Verbose = true - genWithMeta(meta) + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "step-group")) By("check if AddSubStep is generated") content, err := os.ReadFile(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "step-group", "step_group.go")) @@ -97,20 +103,27 @@ var _ = Describe("Test Generating SDK", func() { }) It("Test oneOf", func() { - meta.InitSDK = false meta.File = []string{"testdata/one_of.cue"} meta.Verbose = true - genWithMeta(meta) + genWithMeta() checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "one_of")) - By("check if ") }) It("Test known issue: apply-terraform-provider", func() { - meta.InitSDK = false meta.Verbose = true meta.File = []string{"testdata/apply-terraform-provider.cue"} - genWithMeta(meta) + genWithMeta() + }) + + It("Test generate sub-module", func() { + meta.APIDirectory = "pkg/apis/addons/test_addon" + langArgs = []string{ + string(mainModuleVersionKey) + "=" + mainModuleVersion.Default, + } + meta.IsSubModule = true + genWithMeta() + checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "addons", "test_addon", "component", "cron-task")) }) AfterSuite(func() { @@ -207,3 +220,117 @@ var _ = Describe("FixSchemaWithOneAnyAllOf", func() { Expect(schema.Value.OneOf[0].Value.Enum).To(Equal([]interface{}{"go", "java", "python", "node", "ruby"})) }) }) + +var _ = Describe("TestNewLanguageArgs", func() { + type args struct { + lang string + langArgs []string + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + { + name: "should create a languageArgs struct with the correct values", + args: args{ + lang: "go", + langArgs: []string{"flag1=value1", "flag2=value2"}, + }, + want: map[string]string{"flag1": "value1", "flag2": "value2"}, + wantErr: false, + }, + { + name: "should not set a value for an unknown flag", + args: args{ + lang: "go", + langArgs: []string{"unknownFlag=value"}, + }, + want: map[string]string{}, + wantErr: true, + }, + { + name: "should warn if an argument is not in the key=value format", + args: args{ + lang: "go", + langArgs: []string{"invalidArgument"}, + }, + want: map[string]string{}, + wantErr: true, + }, + } + for _, tt := range tests { + It(tt.name, func() { + got, err := NewLanguageArgs(tt.args.lang, tt.args.langArgs) + if tt.wantErr { + Expect(err).To(HaveOccurred()) + return + } + for k, v := range tt.want { + Expect(got.Get(langArgKey(k))).To(Equal(v)) + } + }) + } + +}) + +var _ = Describe("getValueType", func() { + + type valueTypeTest struct { + input interface{} + expected CUEType + } + + tests := []valueTypeTest{ + {nil, ""}, + {"hello", "string"}, + {42, "integer"}, + {float32(3.14), "number"}, + {3.14159265358979323846, "number"}, + {true, "boolean"}, + {map[string]interface{}{"key": "value"}, "object"}, + {[]interface{}{1, 2, 3}, "array"}, + } + + for _, tt := range tests { + tt := tt // capture range variable + It("should return the correct CUEType for the input", func() { + Expect(getValueType(tt.input)).To(Equal(tt.expected)) + }) + } +}) + +var _ = Describe("type fit", func() { + + var schema *openapi3.Schema + + BeforeEach(func() { + schema = &openapi3.Schema{Type: "string"} + }) + + var testCases = []struct { + name string + cueType CUEType + expectedFit bool + schemaType string + }{ + {"string can be oas string", CUEType("string"), true, "string"}, + {"string not oas integer", CUEType("string"), false, "integer"}, + {"integer can be oas integer", CUEType("integer"), true, "integer"}, + {"integer can be oas number", CUEType("integer"), true, "number"}, + {"number can be oas number", CUEType("number"), true, "number"}, + {"number not oas integer", CUEType("number"), false, "integer"}, + {"boolean can be oas boolean", CUEType("boolean"), true, "boolean"}, + {"array can be oas array", CUEType("array"), true, "array"}, + {"invalid type and any schema", CUEType(""), false, "anyschema"}, + } + + It("should return whether the CUEType fits the schema type or not", func() { + for _, tc := range testCases { + schema.Type = tc.schemaType + result := tc.cueType.fit(schema) + Expect(result).To(Equal(tc.expectedFit), tc.name) + } + }) +}) diff --git a/pkg/definition/gen_sdk/go.go b/pkg/definition/gen_sdk/go.go index ed2e441f8..7b0124662 100644 --- a/pkg/definition/gen_sdk/go.go +++ b/pkg/definition/gen_sdk/go.go @@ -23,11 +23,10 @@ import ( "os" "os/exec" "path" + "path/filepath" "regexp" "strings" - // we need dot import here to make the complex go code generating simpler - // nolint:revive j "github.com/dave/jennifer/jen" "github.com/ettle/strcase" "github.com/pkg/errors" @@ -36,6 +35,32 @@ import ( pkgdef "github.com/oam-dev/kubevela/pkg/definition" ) +var ( + mainModuleVersionKey langArgKey = "MainModuleVersion" + goProxyKey langArgKey = "GoProxy" + + mainModuleVersion = LangArg{ + Name: mainModuleVersionKey, + Desc: "The version of main module, it will be used in go get command. For example, tag, commit id, branch name", + // default hash of main module. This is a commit hash of kubevela-contrib/kubvela-go-sdk. It will be used in go get command. + Default: "cd431bb25a9a", + } + goProxy = LangArg{ + Name: goProxyKey, + Desc: "The proxy for go get/go mod tidy command", + Default: "https://goproxy.cn,direct", + } +) + +func init() { + registerLangArg("go", mainModuleVersion, goProxy) +} + +const ( + // PackagePlaceHolder is the package name placeholder + PackagePlaceHolder = "github.com/kubevela/vela-go-sdk" +) + var ( // DefinitionKindToPascal is the map of definition kind to pascal case DefinitionKindToPascal = map[string]string{ @@ -60,14 +85,22 @@ var ( } ) -// GoModifier is the Modifier for golang -type GoModifier struct { - g *Generator +// GoDefModifier is the Modifier for golang, modify code for each definition +type GoDefModifier struct { + *GenMeta + *goArgs - defName string - defKind string - verbose bool + defStructPointer *j.Statement +} +// GoModuleModifier is the Modifier for golang, modify code for each module which contains multiple definitions +type GoModuleModifier struct { + *GenMeta + *goArgs +} + +type goArgs struct { + apiDir string defDir string utilsDir string // def name of different cases @@ -77,16 +110,57 @@ type GoModifier struct { typeVarName string defStructName string defFuncReceiver string - defStructPointer *j.Statement +} + +func (a *goArgs) init(m *GenMeta) error { + var err error + a.apiDir, err = filepath.Abs(path.Join(m.Output, m.APIDirectory)) + if err != nil { + return err + } + a.defDir = path.Join(a.apiDir, pkgdef.DefinitionKindToType[m.kind], m.name) + a.utilsDir = path.Join(m.Output, "pkg", "apis", "utils") + a.nameInSnakeCase = strcase.ToSnake(m.name) + a.nameInPascalCase = strcase.ToPascal(m.name) + a.typeVarName = a.nameInPascalCase + "Type" + a.specNameInPascalCase = a.nameInPascalCase + "Spec" + a.defStructName = strcase.ToGoPascal(m.name + "-" + pkgdef.DefinitionKindToType[m.kind]) + a.defFuncReceiver = m.name[:1] + return nil +} + +// Modify implements Modifier +func (m *GoModuleModifier) Modify() error { + for _, fn := range []func() error{ + m.init, + m.format, + m.addSubGoMod, + m.tidyMainMod, + } { + if err := fn(); err != nil { + return errors.Wrap(err, fnName(fn)) + } + } + return nil +} + +func (m *GoModuleModifier) init() error { + m.goArgs = &goArgs{} + return m.goArgs.init(m.GenMeta) } // Name the name of modifier -func (m *GoModifier) Name() string { - return "GoModifier" +func (m *GoModuleModifier) Name() string { + return "goModuleModifier" +} + +// Name the name of modifier +func (m *GoDefModifier) Name() string { + return "GoDefModifier" } // Modify the modification of generated code -func (m *GoModifier) Modify() error { +func (m *GoDefModifier) Modify() error { for _, fn := range []func() error{ m.init, m.clean, @@ -95,35 +169,28 @@ func (m *GoModifier) Modify() error { m.addDefAPI, m.addValidateTraits, m.exportMethods, - m.format, } { if err := fn(); err != nil { - return err + return errors.Wrap(err, fnName(fn)) } } return nil } -func (m *GoModifier) init() error { - m.defName = m.g.name - m.defKind = m.g.kind - m.verbose = m.g.meta.Verbose +func (m *GoDefModifier) init() error { + m.goArgs = &goArgs{} + err := m.goArgs.init(m.GenMeta) + if err != nil { + return err + } - pkgAPIDir := path.Join(m.g.meta.Output, "pkg", "apis") - m.defDir = path.Join(pkgAPIDir, pkgdef.DefinitionKindToType[m.defKind], m.defName) - m.utilsDir = path.Join(pkgAPIDir, "utils") - m.nameInSnakeCase = strcase.ToSnake(m.defName) - m.nameInPascalCase = strcase.ToPascal(m.defName) - m.typeVarName = m.nameInPascalCase + "Type" - m.specNameInPascalCase = m.nameInPascalCase + "Spec" - m.defStructName = strcase.ToGoPascal(m.defName + "-" + pkgdef.DefinitionKindToType[m.defKind]) m.defStructPointer = j.Op("*").Id(m.defStructName) - m.defFuncReceiver = m.defName[:1] - err := os.MkdirAll(m.utilsDir, 0750) + + err = os.MkdirAll(m.utilsDir, 0750) return err } -func (m *GoModifier) clean() error { +func (m *GoDefModifier) clean() error { err := os.RemoveAll(path.Join(m.defDir, ".openapi-generator")) if err != nil { return err @@ -148,17 +215,101 @@ func (m *GoModifier) clean() error { } +// addSubGoMod will add a go.mod and go.sum in the api directory if user mark that the api is a submodule +func (m *GoModuleModifier) addSubGoMod() error { + if !m.IsSubModule { + return nil + } + files := map[string]string{ + "go.mod_": "go.mod", + "go.sum": "go.sum", + } + for src, dst := range files { + srcContent, err := Scaffold.ReadFile(path.Join(ScaffoldDir, "go", src)) + if err != nil { + return errors.Wrap(err, "read "+src) + } + subModuleName := strings.TrimSuffix(fmt.Sprintf("%s/%s", m.Package, m.APIDirectory), "/") + srcContent = bytes.ReplaceAll(srcContent, []byte("module "+PackagePlaceHolder), []byte("module "+subModuleName)) + srcContent = bytes.ReplaceAll(srcContent, []byte("// require "+PackagePlaceHolder), []byte("require "+m.Package)) + + err = os.WriteFile(path.Join(m.apiDir, dst), srcContent, 0600) + if err != nil { + return errors.Wrap(err, "write "+dst) + } + } + + cmds := make([]*exec.Cmd, 0) + if m.LangArgs.Get(mainModuleVersionKey) != mainModuleVersion.Default { + // nolint:gosec + cmds = append(cmds, exec.Command("docker", "run", + "--rm", + "-v", m.apiDir+":/api", + "-w", "/api", + "golang:1.19-alpine", + "go", "get", fmt.Sprintf("%s@%s", m.Package, m.LangArgs.Get(mainModuleVersionKey)), + )) + } + // nolint:gosec + cmds = append(cmds, exec.Command("docker", "run", + "--rm", + "-v", m.apiDir+":/api", + "-w", "/api", + "--env", "GOPROXY="+m.LangArgs.Get(goProxyKey), + "golang:1.19-alpine", + "go", "mod", "tidy", + )) + for _, cmd := range cmds { + if m.Verbose { + fmt.Println(cmd.String()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + err := cmd.Run() + if err != nil { + return errors.Wrapf(err, "fail to run command %s", cmd.String()) + } + } + return nil +} + +// tidyMainMod will run go mod tidy in the main module +func (m *GoModuleModifier) tidyMainMod() error { + if !m.InitSDK { + return nil + } + outDir, err := filepath.Abs(m.GenMeta.Output) + if err != nil { + return err + } + // nolint:gosec + cmd := exec.Command("docker", "run", + "--rm", + "-v", outDir+":/api", + "-w", "/api", + "golang:1.19-alpine", + "go", "mod", "tidy", + ) + if m.Verbose { + fmt.Println(cmd.String()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + return cmd.Run() +} + // read all files in definition directory, // 1. replace the Nullable* Struct // 2. replace the package name -func (m *GoModifier) modifyDefs() error { +func (m *GoDefModifier) modifyDefs() error { changeNullableType := func(b []byte) []byte { return regexp.MustCompile("Nullable(String|(Float|Int)(32|64)|Bool)").ReplaceAll(b, []byte("utils.Nullable$1")) } files, err := os.ReadDir(m.defDir) defHandleFunc := []byteHandler{ - m.g.meta.packageFunc, + m.packageFunc, changeNullableType, } if err != nil { @@ -180,7 +331,7 @@ func (m *GoModifier) modifyDefs() error { return nil } -func (m *GoModifier) moveUtils() error { +func (m *GoDefModifier) moveUtils() error { // Adjust the generated files and code err := os.Rename(path.Join(m.defDir, "utils.go"), path.Join(m.utilsDir, "utils.go")) if err != nil { @@ -193,7 +344,7 @@ func (m *GoModifier) moveUtils() error { if err != nil { return err } - utilsBytes = bytes.Replace(utilsBytes, []byte(fmt.Sprintf("package %s", strcase.ToSnake(m.defName))), []byte("package utils"), 1) + utilsBytes = bytes.Replace(utilsBytes, []byte(fmt.Sprintf("package %s", strcase.ToSnake(m.name))), []byte("package utils"), 1) utilsBytes = bytes.ReplaceAll(utilsBytes, []byte("isNil"), []byte("IsNil")) err = os.WriteFile(utilsFile, utilsBytes, 0600) if err != nil { @@ -203,7 +354,7 @@ func (m *GoModifier) moveUtils() error { } // addDefAPI will add component/trait/workflowstep/policy Object to the api -func (m *GoModifier) addDefAPI() error { +func (m *GoDefModifier) addDefAPI() error { file, err := os.OpenFile(path.Join(m.defDir, m.nameInSnakeCase+".go"), os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return err @@ -236,11 +387,11 @@ func (m *GoModifier) addDefAPI() error { return nil } -func (m *GoModifier) genCommonFunc() []*j.Statement { - kind := m.defKind +func (m *GoDefModifier) genCommonFunc() []*j.Statement { + kind := m.kind typeName := j.Id(m.nameInPascalCase + "Type") - typeConst := j.Const().Add(typeName).Op("=").Lit(m.defName) - j.Op("=").Lit(m.defName) + typeConst := j.Const().Add(typeName).Op("=").Lit(m.name) + j.Op("=").Lit(m.name) defStruct := j.Type().Id(m.defStructName).Struct( j.Id("Base").Id("apis").Dot(DefinitionKindToBaseType[kind]), j.Id("Properties").Id(m.specNameInPascalCase), @@ -328,8 +479,8 @@ func (m *GoModifier) genCommonFunc() []*j.Statement { return []*j.Statement{typeConst, initFunc, defStruct, defStructConstructor, buildFunc} } -func (m *GoModifier) genFromFunc() []*j.Statement { - kind := m.g.kind +func (m *GoDefModifier) genFromFunc() []*j.Statement { + kind := m.kind kindBaseProperties := map[string][]string{ v1beta1.ComponentDefinitionKind: {"Name", "DependsOn", "Inputs", "Outputs"}, v1beta1.WorkflowStepDefinitionKind: {"Name", "DependsOn", "Inputs", "Outputs", "If", "Timeout", "Meta"}, @@ -340,7 +491,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement { // fromFuncRsv means build from a part of K8s Object (e.g. v1beta1.Application.spec.component[*] to internal presentation (e.g. Component) // fromFuncRsv will have a function receiver getSubSteps := func(sub bool) func(g *j.Group) { - if m.defKind != v1beta1.WorkflowStepDefinitionKind || sub { + if m.kind != v1beta1.WorkflowStepDefinitionKind || sub { return func(g *j.Group) {} } return func(g *j.Group) { @@ -358,7 +509,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement { } } assignSubSteps := func(sub bool) func(g *j.Group) { - if m.defKind != v1beta1.WorkflowStepDefinitionKind || sub { + if m.kind != v1beta1.WorkflowStepDefinitionKind || sub { return func(g *j.Group) {} } return func(g *j.Group) { @@ -379,7 +530,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement { BlockFunc(func(g *j.Group) { if kind == v1beta1.ComponentDefinitionKind { g.Add(j.For(j.List(j.Id("_"), j.Id("trait")).Op(":=").Range().Id("from").Dot("Traits")).Block( - j.List(j.Id("_t"), j.Err()).Op(":=").Qual("sdkcommon", "FromTrait").Call(j.Op("&").Id("trait")), + j.List(j.Id("_t"), j.Err()).Op(":=").Qual("sdkcommon", "FromTrait").Call(j.Id("trait")), j.If(j.Err().Op("!=").Nil()).Block( j.Return(j.Nil(), j.Err()), ), @@ -425,15 +576,15 @@ func (m *GoModifier) genFromFunc() []*j.Statement { ) res := []*j.Statement{fromFuncRsv(false), fromFunc} - if m.defKind == v1beta1.WorkflowStepDefinitionKind { + if m.kind == v1beta1.WorkflowStepDefinitionKind { res = append(res, fromFuncRsv(true), fromSubFunc) } return res } // genDedicatedFunc generate functions for definition kinds -func (m *GoModifier) genDedicatedFunc() []*j.Statement { - switch m.defKind { +func (m *GoDefModifier) genDedicatedFunc() []*j.Statement { + switch m.kind { case v1beta1.ComponentDefinitionKind: setTraitFunc := j.Func(). Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)). @@ -484,14 +635,14 @@ func (m *GoModifier) genDedicatedFunc() []*j.Statement { return nil } -func (m *GoModifier) genNameTypeFunc() []*j.Statement { - nameFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id(DefinitionKindToPascal[m.defKind] + "Name").Params().String().Block( +func (m *GoDefModifier) genNameTypeFunc() []*j.Statement { + nameFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id(DefinitionKindToPascal[m.kind] + "Name").Params().String().Block( j.Return(j.Id(m.defFuncReceiver).Dot("Base").Dot("Name")), ) typeFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id("DefType").Params().String().Block( j.Return(j.Id(m.typeVarName)), ) - switch m.defKind { + switch m.kind { case v1beta1.ComponentDefinitionKind, v1beta1.WorkflowStepDefinitionKind, v1beta1.PolicyDefinitionKind: return []*j.Statement{nameFunc, typeFunc} case v1beta1.TraitDefinitionKind: @@ -500,11 +651,11 @@ func (m *GoModifier) genNameTypeFunc() []*j.Statement { return nil } -func (m *GoModifier) genUnmarshalFunc() []*j.Statement { +func (m *GoDefModifier) genUnmarshalFunc() []*j.Statement { return []*j.Statement{j.Null()} } -func (m *GoModifier) genBaseSetterFunc() []*j.Statement { +func (m *GoDefModifier) genBaseSetterFunc() []*j.Statement { baseFuncArgs := map[string][]struct { funcName string argName string @@ -533,7 +684,7 @@ func (m *GoModifier) genBaseSetterFunc() []*j.Statement { }, } baseFuncs := make([]*j.Statement, 0) - for _, fn := range baseFuncArgs[m.defKind] { + for _, fn := range baseFuncArgs[m.kind] { if fn.dst == nil { fn.dst = j.Dot(fn.funcName) } @@ -556,8 +707,8 @@ func (m *GoModifier) genBaseSetterFunc() []*j.Statement { return baseFuncs } -func (m *GoModifier) genAddSubStepFunc() *j.Statement { - if m.defName != "step-group" || m.defKind != v1beta1.WorkflowStepDefinitionKind { +func (m *GoDefModifier) genAddSubStepFunc() *j.Statement { + if m.name != "step-group" || m.kind != v1beta1.WorkflowStepDefinitionKind { return j.Null() } subList := j.Id(m.defFuncReceiver).Dot("Base").Dot("SubSteps") @@ -573,7 +724,7 @@ func (m *GoModifier) genAddSubStepFunc() *j.Statement { } // exportMethods will export methods from definition spec struct to definition struct -func (m *GoModifier) exportMethods() error { +func (m *GoDefModifier) exportMethods() error { fileLoc := path.Join(m.defDir, m.nameInSnakeCase+".go") // nolint:gosec file, err := os.ReadFile(fileLoc) @@ -604,8 +755,8 @@ func (m *GoModifier) exportMethods() error { return os.WriteFile(fileLoc, []byte(fileStr), 0600) } -func (m *GoModifier) addValidateTraits() error { - if m.defKind != v1beta1.ComponentDefinitionKind { +func (m *GoDefModifier) addValidateTraits() error { + if m.kind != v1beta1.ComponentDefinitionKind { return nil } fileLoc := path.Join(m.defDir, m.nameInSnakeCase+".go") @@ -632,11 +783,12 @@ func (m *GoModifier) addValidateTraits() error { return os.WriteFile(fileLoc, []byte(fileStr), 0600) } -func (m *GoModifier) format() error { +func (m *GoModuleModifier) format() error { // check if gofmt is installed + // todo (chivalryq): support go mod tidy for sub-module formatters := []string{"gofmt", "goimports"} - formatterPaths := []string{} + var formatterPaths []string allFormattersInstalled := true for _, formatter := range formatters { p, err := exec.LookPath(formatter) @@ -648,11 +800,11 @@ func (m *GoModifier) format() error { } if allFormattersInstalled { for _, fmter := range formatterPaths { - if m.verbose { + if m.Verbose { fmt.Printf("Use %s to format code\n", fmter) } // nolint:gosec - cmd := exec.Command(fmter, "-w", m.defDir) + cmd := exec.Command(fmter, "-w", m.apiDir) output, err := cmd.CombinedOutput() if err != nil { return errors.Wrap(err, string(output)) @@ -661,31 +813,31 @@ func (m *GoModifier) format() error { return nil } // fallback to use go lib - if m.verbose { + if m.Verbose { fmt.Println("At least one of linters is not installed, use go/format lib to format code") } - files, err := os.ReadDir(m.defDir) - if err != nil { - return errors.Wrap(err, "read dir") - } - for _, f := range files { - if !strings.HasSuffix(f.Name(), ".go") { - continue - } - filePath := path.Join(m.defDir, f.Name()) - // nolint:gosec - content, err := os.ReadFile(filePath) + + // format all .go files + return filepath.Walk(m.apiDir, func(path string, info os.FileInfo, err error) error { if err != nil { - return errors.Wrapf(err, "read file %s", filePath) + return err + } + if !strings.HasSuffix(path, ".go") { + return nil + } + // nolint:gosec + content, err := os.ReadFile(path) + if err != nil { + return errors.Wrapf(err, "read file %s", path) } formatted, err := format.Source(content) if err != nil { - return errors.Wrapf(err, "format file %s", filePath) + return errors.Wrapf(err, "format file %s", path) } - err = os.WriteFile(filePath, formatted, 0600) + err = os.WriteFile(path, formatted, 0600) if err != nil { - return errors.Wrapf(err, "write file %s", filePath) + return errors.Wrapf(err, "write file %s", path) } - } - return nil + return nil + }) } diff --git a/references/cli/def.go b/references/cli/def.go index f34365b37..065684c65 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -1065,19 +1065,22 @@ func NewDefinitionValidateCommand(c common.Args) *cobra.Command { // NewDefinitionGenAPICommand create the `vela def gen-api` command to help user generate Go code from the definition func NewDefinitionGenAPICommand(c common.Args) *cobra.Command { meta := gen_sdk.GenMeta{} + var languageArgs []string cmd := &cobra.Command{ Use: "gen-api DEFINITION.cue", Short: "Generate SDK from X-Definition.", Long: "Generate SDK from X-definition file.\n" + - "* This command leverage openapi-generator project. Therefore demands \"docker\" exist in PATH" + + "* This command leverage openapi-generator project. Therefore demands \"docker\" exist in PATH\n" + "* Currently, this function is still working in progress and not all formats of parameter in X-definition are supported yet.", Example: "# Generate SDK for golang with scaffold initialized\n" + - "> vela def gen-api --init --lang go -f /path/to/def -o /path/to/sdk\n" + + "> vela def gen-api --init --language go -f /path/to/def -o /path/to/sdk\n" + "# Generate incremental definition files to existing sdk directory\n" + - "> vela def gen-api --lang go -f /path/to/def -o /path/to/sdk", + "> vela def gen-api --language go -f /path/to/def -o /path/to/sdk\n" + + "# Generate definitions to a sub-module\n" + + "> vela def gen-api --language go -f /path/to/def -o /path/to/sdk --submodule --api-dir path/relative/to/output --language-args arg1=val1,arg2=val2\n", RunE: func(cmd *cobra.Command, args []string) error { - err := meta.Init(c) + err := meta.Init(c, languageArgs) if err != nil { return err } @@ -1097,13 +1100,26 @@ func NewDefinitionGenAPICommand(c common.Args) *cobra.Command { return nil }, } + cmd.Flags().StringVarP(&meta.Output, "output", "o", "./apis", "Output directory path") - cmd.Flags().StringVarP(&meta.Package, "package", "p", "github.com/kubevela/vela-go-sdk", "Package name of generated code") - cmd.Flags().StringVarP(&meta.Lang, "lang", "g", "go", "Language to generate code. Valid languages: go") + cmd.Flags().StringVar(&meta.APIDirectory, "api-dir", "", "API directory path to put definition API files, relative to output directory. Default value: go: pkg/apis") + cmd.Flags().BoolVar(&meta.IsSubModule, "submodule", false, "Whether the generated code is a submodule of the project. If set, the directory specified by `api-dir` will be treated as a submodule of the project") + cmd.Flags().StringVarP(&meta.Package, "package", "p", gen_sdk.PackagePlaceHolder, "Package name of generated code") + cmd.Flags().StringVarP(&meta.Lang, "language", "g", "go", "Language to generate code. Valid languages: go") cmd.Flags().StringVarP(&meta.Template, "template", "t", "", "Template file path, if not specified, the default template will be used") cmd.Flags().StringSliceVarP(&meta.File, "file", "f", nil, "File name of definitions, can be specified multiple times, or use comma to separate multiple files. If directory specified, all files found recursively in the directory will be used") cmd.Flags().BoolVar(&meta.InitSDK, "init", false, "Init the whole SDK project, if not set, only the API file will be generated") cmd.Flags().BoolVarP(&meta.Verbose, "verbose", "v", false, "Print verbose logs") + var langArgsDescStr string + for lang, args := range gen_sdk.LangArgsRegistry { + langArgsDescStr += lang + ": \n" + for key, arg := range args { + langArgsDescStr += fmt.Sprintf("\t%s: %s(default: %s)\n", key, arg.Name, arg.Default) + } + } + cmd.Flags().StringSliceVar(&languageArgs, "language-args", []string{}, + fmt.Sprintf("language-specific arguments to pass to the go generator, available options: \n"+langArgsDescStr), + ) return cmd }