From e23d63ae297609377505a9354d401666257d258b Mon Sep 17 00:00:00 2001 From: Rodrigo Freitas Date: Thu, 26 Dec 2024 11:33:20 -0300 Subject: [PATCH 1/5] feat: initial changes to support rust services --- internal/assets/{templates => golang}/embed.go | 2 +- internal/assets/{templates => golang}/lifecycle.tmpl | 0 internal/assets/{templates => golang}/main.tmpl | 0 internal/assets/{templates => golang}/service.tmpl | 0 internal/cmd/service/init.go | 12 ++++++++++-- internal/cmd/service/init_golang.go | 1 + pkg/cmd/service_init.go | 11 +++++++++++ 7 files changed, 23 insertions(+), 3 deletions(-) rename internal/assets/{templates => golang}/embed.go (76%) rename internal/assets/{templates => golang}/lifecycle.tmpl (100%) rename internal/assets/{templates => golang}/main.tmpl (100%) rename internal/assets/{templates => golang}/service.tmpl (100%) create mode 100644 internal/cmd/service/init_golang.go diff --git a/internal/assets/templates/embed.go b/internal/assets/golang/embed.go similarity index 76% rename from internal/assets/templates/embed.go rename to internal/assets/golang/embed.go index 147ba9c..e966f8e 100644 --- a/internal/assets/templates/embed.go +++ b/internal/assets/golang/embed.go @@ -1,4 +1,4 @@ -package templates +package golang import ( "embed" diff --git a/internal/assets/templates/lifecycle.tmpl b/internal/assets/golang/lifecycle.tmpl similarity index 100% rename from internal/assets/templates/lifecycle.tmpl rename to internal/assets/golang/lifecycle.tmpl diff --git a/internal/assets/templates/main.tmpl b/internal/assets/golang/main.tmpl similarity index 100% rename from internal/assets/templates/main.tmpl rename to internal/assets/golang/main.tmpl diff --git a/internal/assets/templates/service.tmpl b/internal/assets/golang/service.tmpl similarity index 100% rename from internal/assets/templates/service.tmpl rename to internal/assets/golang/service.tmpl diff --git a/internal/cmd/service/init.go b/internal/cmd/service/init.go index 36b1c1d..9aed04a 100644 --- a/internal/cmd/service/init.go +++ b/internal/cmd/service/init.go @@ -17,7 +17,7 @@ import ( moptions "github.com/somatech1/mikros/components/options" "github.com/somatech1/mikros/components/plugin" - assets "github.com/somatech1/mikros-cli/internal/assets/templates" + golang_templates "github.com/somatech1/mikros-cli/internal/assets/golang" "github.com/somatech1/mikros-cli/internal/golang" "github.com/somatech1/mikros-cli/internal/protobuf" "github.com/somatech1/mikros-cli/internal/templates" @@ -28,6 +28,7 @@ import ( ) type InitOptions struct { + Kind Kind Path string ProtoFilename string FeatureNames []string @@ -36,6 +37,13 @@ type InitOptions struct { ExternalTemplates *TemplateFileOptions } +type Kind int + +const ( + KindGolang Kind = iota + KindRust +) + type TemplateFileOptions struct { Files embed.FS Templates []mtemplates.TemplateFile @@ -741,7 +749,7 @@ func generateImports(answers *initSurveyAnswers) map[string][]ImportContext { } func createServiceTemplates(options *InitOptions, filenames []mtemplates.TemplateFile, context interface{}) error { - if err := runTemplates(assets.Files, filenames, context, nil); err != nil { + if err := runTemplates(golang_templates.Files, filenames, context, nil); err != nil { return err } diff --git a/internal/cmd/service/init_golang.go b/internal/cmd/service/init_golang.go new file mode 100644 index 0000000..6d43c33 --- /dev/null +++ b/internal/cmd/service/init_golang.go @@ -0,0 +1 @@ +package service diff --git a/pkg/cmd/service_init.go b/pkg/cmd/service_init.go index edd9a6f..bbfa161 100644 --- a/pkg/cmd/service_init.go +++ b/pkg/cmd/service_init.go @@ -37,11 +37,22 @@ func serviceInitCmdInit(options *serviceInitCmdOptions) { serviceInitCmd.Flags().String("proto", "", "Uses an _api.proto file as source for the service API.") _ = viper.BindPFlag("init-proto", serviceInitCmd.Flags().Lookup("proto")) + // service kind option + serviceInitCmd.Flags().Bool("rust", false, "Creates rust service.") + _ = viper.BindPFlag("init-rust", serviceInitCmd.Flags().Lookup("rust")) + + serviceInitCmd.Flags().Bool("golang", true, "Creates golang service.") + _ = viper.BindPFlag("init-golang", serviceInitCmd.Flags().Lookup("golang")) + serviceInitCmd.Run = func(cmd *cobra.Command, args []string) { initOptions := &service.InitOptions{ + Kind: service.KindGolang, Path: viper.GetString("init-path"), ProtoFilename: viper.GetString("init-proto"), } + if viper.GetBool("init-rust") { + initOptions.Kind = service.KindRust + } if options != nil { initOptions.Features = options.Features From b23ba0a1babbb62da861f594292812cd6287c5c4 Mon Sep 17 00:00:00 2001 From: Rodrigo Freitas Date: Thu, 26 Dec 2024 14:02:45 -0300 Subject: [PATCH 2/5] feat: continue rust support implementation --- internal/cmd/service/init.go | 462 +----------------------- internal/cmd/service/survey.go | 474 +++++++++++++++++++++++++ internal/cmd/service/survey_answers.go | 7 + 3 files changed, 485 insertions(+), 458 deletions(-) create mode 100644 internal/cmd/service/survey.go diff --git a/internal/cmd/service/init.go b/internal/cmd/service/init.go index 9aed04a..107de72 100644 --- a/internal/cmd/service/init.go +++ b/internal/cmd/service/init.go @@ -2,19 +2,14 @@ package service import ( "embed" - "errors" "fmt" "os" "path/filepath" "slices" - "sort" "strings" - "github.com/AlecAivazis/survey/v2" - "github.com/go-playground/validator/v10" "github.com/iancoleman/strcase" "github.com/somatech1/mikros/components/definition" - moptions "github.com/somatech1/mikros/components/options" "github.com/somatech1/mikros/components/plugin" golang_templates "github.com/somatech1/mikros-cli/internal/assets/golang" @@ -23,7 +18,6 @@ import ( "github.com/somatech1/mikros-cli/internal/templates" "github.com/somatech1/mikros-cli/pkg/definitions" "github.com/somatech1/mikros-cli/pkg/path" - msurvey "github.com/somatech1/mikros-cli/pkg/survey" mtemplates "github.com/somatech1/mikros-cli/pkg/templates" ) @@ -55,466 +49,18 @@ type TemplateFileOptions struct { // Init initializes a new service locally. func Init(options *InitOptions) error { - answers := &initSurveyAnswers{} - if err := survey.Ask(baseQuestions(options), answers); err != nil { - return err - } - - if err := runServiceSurvey(answers, options); err != nil { - return err - } - - // Presents only questions from selected features - for _, name := range answers.Features { - defs, save, err := runFeatureSurvey(name, options) - if err != nil { - return err - } - if defs != nil { - answers.AddFeatureDefinitions(name, defs, save) - } - } - - if err := generateTemplates(options, answers); err != nil { - return err - } - - return nil -} - -func baseQuestions(options *InitOptions) []*survey.Question { - supportedTypes := []string{ - definition.ServiceType_gRPC.String(), - definition.ServiceType_HTTP.String(), - definition.ServiceType_Native.String(), - definition.ServiceType_Script.String(), - } - - if options.Services != nil { - for name := range options.Services.Services() { - supportedTypes = append(supportedTypes, name) - } - } - - sort.Strings(supportedTypes) - questions := []*survey.Question{ - // Service name - { - Name: "name", - Prompt: &survey.Input{ - Message: "Name. Can be a fully qualified service name (URL + name):", - }, - Validate: survey.ComposeValidators( - survey.Required, - survey.MinLength(0), - survey.MaxLength(512), - ), - }, - // Service type - { - Name: "type", - Prompt: &survey.Select{ - Message: "Select the type of service:", - Options: supportedTypes, - PageSize: len(supportedTypes), - }, - Validate: survey.Required, - }, - // Language - { - Name: "language", - Prompt: &survey.Select{ - Message: "Select the service main programming language:", - Options: definition.SupportedLanguages(), - PageSize: len(definition.SupportedLanguages()), - }, - Validate: survey.Required, - }, - // Version - { - Name: "version", - Prompt: &survey.Input{ - Message: "Version. A semver version string for the service, with 'v' as prefix (ex: v1.0.0):", - Default: "v0.1.0", - }, - Validate: func(val interface{}) error { - if str, ok := val.(string); ok { - if !definition.ValidateVersion(str) { - return errors.New("invalid version format") - } - - return nil - } - - return errors.New("version has an invalid value type") - }, - }, - // Product - { - Name: "product", - Prompt: &survey.Input{ - Message: "Product name. Enter the product name that the service belongs to:", - }, - Validate: survey.ComposeValidators( - survey.Required, - survey.MinLength(3), - survey.MaxLength(512), - ), - }, - // Lifecycle - { - Name: "lifecycle", - Prompt: &survey.MultiSelect{ - Message: "Select lifecycle events to handle in the service:", - Options: []string{"OnStart", "OnFinish"}, - }, - }, - } - - if options.Features != nil { - var ( - featureNames = options.FeatureNames - iter = options.Features.Iterator() - ) - - for f, next := iter.Next(); next; f, next = iter.Next() { - if api, ok := f.(msurvey.CLIFeature); ok { - if api.IsCLISupported() { - featureNames = append(featureNames, getFeatureUIName(f)) - } - } - } - - // Features - questions = append(questions, &survey.Question{ - Name: "features", - Prompt: &survey.MultiSelect{ - Message: "Select the features the service will have:", - Options: featureNames, - PageSize: len(featureNames), - }, - }) - } - - return questions -} - -func getFeatureUIName(feature plugin.Feature) string { - if api, ok := feature.(msurvey.FeatureSurveyUI); ok { - return api.UIName() - } - - return feature.Name() -} - -// runServiceSurvey executes the survey that a service may have implemented. -func runServiceSurvey(answers *initSurveyAnswers, options *InitOptions) error { - if options.Services == nil { - return nil - } - - s, ok := options.Services.Services()[answers.Type] - if !ok { - return nil - } - - cli, ok := s.(msurvey.CLIFeature) - if !ok || !cli.IsCLISupported() { - return nil - } - - api, ok := s.(msurvey.FeatureSurvey) - if !ok { - return nil - } - - svcSurvey := api.GetSurvey() - if s == nil { - return nil - } - - response, err := handleSurvey(answers.Type, svcSurvey) + answers, err := runInitSurvey(options) if err != nil { return err } - d, save, err := api.Answers(response) - if err != nil { + if err := generateTemplates(options, answers); err != nil { return err } - answers.SetServiceDefinitions(d, save) - return nil -} - -func handleSurvey(name string, featureSurvey *msurvey.Survey) (map[string]interface{}, error) { - if featureSurvey.ConfirmQuestion != nil { - var responses []map[string]interface{} - - loop: - for { - if !featureSurvey.ConfirmQuestion.ConfirmAfter { - res := msurvey.YesNo(featureSurvey.ConfirmQuestion.Message) - if !res { - break loop - } - } - - response, err := surveyFromQuestion(name, featureSurvey) - if err != nil { - return nil, err - } - responses = append(responses, response) - - if featureSurvey.ConfirmQuestion.ConfirmAfter { - res := msurvey.YesNo(featureSurvey.ConfirmQuestion.Message) - if !res { - break loop - } - } - } - - return map[string]interface{}{ - name: responses, - }, nil - } - - response, err := surveyFromQuestion(name, featureSurvey) - if err != nil { - return nil, err - } - - return response, nil -} - -func surveyFromQuestion(name string, entrySurvey *msurvey.Survey) (map[string]interface{}, error) { - var ( - s []*survey.Question - response = make(map[string]interface{}) - validate = validator.New() - ) - - for _, q := range entrySurvey.Questions { - if err := validate.Struct(q); err != nil { - return nil, err - } - - question := &survey.Question{ - Name: q.Name, - Validate: func() func(v interface{}) error { - if q.Validate != nil { - return q.Validate - } - - if q.Required { - return survey.Required - } - - return nil - }(), - } - - switch q.Prompt { - case msurvey.PromptSurvey: - if !validateInnerSurveyCondition(response, q.Condition) { - continue - } - - r, err := handleSurvey(q.Name, q.Survey) - if err != nil { - return nil, err - } - if r != nil { - r = sanitizeResponse(r) - response[q.Name] = r[q.Name] - } - - continue - - default: - question.Prompt = buildSurveyPrompt(name, q) - } - - if q.Prompt != msurvey.PromptSurvey { - s = append(s, question) - } - - if entrySurvey.AskOne { - if validateInnerSurveyCondition(response, q.Condition) { - r, err := askOne(question.Prompt, q) - if err != nil { - return nil, err - } - - response[question.Name] = r - response = sanitizeResponse(response) - } - } - } - - // If we don't have response we need to execute the survey entirely. - if len(response) == 0 { - if err := survey.Ask(s, &response); err != nil { - return nil, err - } - } - - return sanitizeResponse(response), nil -} - -func buildSurveyPrompt(name string, q *msurvey.Question) survey.Prompt { - switch q.Prompt { - case msurvey.PromptInput: - return &survey.Input{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Default: q.Default, - } - - case msurvey.PromptSelect: - return &survey.Select{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Options: q.Options, - PageSize: len(q.Options), - Default: q.Default, - } - - case msurvey.PromptMultiSelect: - return &survey.MultiSelect{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Options: q.Options, - } - - case msurvey.PromptMultiline: - return &survey.Multiline{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - } - - case msurvey.PromptConfirm: - return &survey.Confirm{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - } - - default: - } - return nil } -func validateInnerSurveyCondition(response map[string]interface{}, condition *msurvey.QuestionCondition) bool { - if condition != nil { - if r, ok := response[condition.Name]; ok { - switch value := condition.Value.(type) { - case []string: - if slices.Contains(value, r.(string)) { - return true - } - - case string: - if v, ok := r.(string); ok && v == value { - return true - } - } - } - - return false - } - - return true -} - -func askOne(prompt survey.Prompt, question *msurvey.Question) (interface{}, error) { - getOptions := func() survey.AskOpt { - if question.Validate != nil { - return survey.WithValidator(question.Validate) - } - if question.Required { - return survey.WithValidator(survey.Required) - } - - return nil - } - - if question.Prompt == msurvey.PromptSelect { - index := 0 - if err := survey.AskOne(prompt, &index, getOptions()); err != nil { - return nil, err - } - - return question.Options[index], nil - } - - if question.Prompt == msurvey.PromptMultiSelect { - var r []string - if err := survey.AskOne(prompt, &r, getOptions()); err != nil { - return nil, err - } - - return r, nil - } - - var r string - if err := survey.AskOne(prompt, &r, getOptions()); err != nil { - return nil, err - } - - return r, nil -} - -// sanitizeResponse sanitizes the response to avoid sending internal formats to -// the client. -func sanitizeResponse(response map[string]interface{}) map[string]interface{} { - for k, v := range response { - if s, ok := v.(survey.OptionAnswer); ok { - response[k] = s.Value - } - - if opts, ok := v.([]survey.OptionAnswer); ok { - var s []string - for _, o := range opts { - s = append(s, o.Value) - } - - response[k] = s - } - } - - return response -} - -func runFeatureSurvey(name string, options *InitOptions) (interface{}, bool, error) { - f, err := options.Features.Feature(name) - if err != nil { - // Search again using mikros feature prefix. Maybe it is an implementation - // of a mikros feature. - f, err = options.Features.Feature(moptions.FeatureNamePrefix + name) - if err != nil { - return nil, false, err - } - } - - api, ok := f.(msurvey.FeatureSurvey) - if !ok { - return nil, false, nil - } - - var response map[string]interface{} - - if s := api.GetSurvey(); s != nil { - res, err := handleSurvey(name, s) - if err != nil { - return nil, false, err - } - response = res - } - - defs, save, err := api.Answers(response) - if err != nil { - return nil, false, err - } - - return defs, save, nil -} - func generateTemplates(options *InitOptions, answers *initSurveyAnswers) error { var ( destinationPath = options.Path @@ -634,8 +180,8 @@ func generateTemplateContext(options *InitOptions, answers *initSurveyAnswers) ( context := TemplateContext{ featuresExtensions: len(answers.Features) > 0, servicesExtensions: externalService(), - onStartLifecycle: slices.Contains(answers.Lifecycle, "OnStart"), - onFinishLifecycle: slices.Contains(answers.Lifecycle, "OnFinish"), + onStartLifecycle: slices.Contains(answers.Lifecycle, "start"), + onFinishLifecycle: slices.Contains(answers.Lifecycle, "finish"), serviceType: answers.Type, NewServiceArgs: newServiceArgs, ServiceName: answers.Name, diff --git a/internal/cmd/service/survey.go b/internal/cmd/service/survey.go new file mode 100644 index 0000000..45720d6 --- /dev/null +++ b/internal/cmd/service/survey.go @@ -0,0 +1,474 @@ +package service + +import ( + "errors" + "fmt" + "slices" + "sort" + + "github.com/AlecAivazis/survey/v2" + "github.com/go-playground/validator/v10" + "github.com/somatech1/mikros/components/definition" + moptions "github.com/somatech1/mikros/components/options" + "github.com/somatech1/mikros/components/plugin" + + msurvey "github.com/somatech1/mikros-cli/pkg/survey" +) + +func runInitSurvey(options *InitOptions) (*initSurveyAnswers, error) { + answers := newInitSurveyAnswers(options.Kind) + + if err := survey.Ask(baseQuestions(options), answers); err != nil { + return nil, err + } + + if err := runServiceSurvey(answers, options); err != nil { + return nil, err + } + + // Presents only questions from selected features + for _, name := range answers.Features { + defs, save, err := runFeatureSurvey(name, options) + if err != nil { + return nil, err + } + if defs != nil { + answers.AddFeatureDefinitions(name, defs, save) + } + } + + return answers, nil +} + +func baseQuestions(options *InitOptions) []*survey.Question { + supportedTypes := []string{ + definition.ServiceType_gRPC.String(), + definition.ServiceType_HTTP.String(), + definition.ServiceType_Native.String(), + definition.ServiceType_Script.String(), + } + + if options.Services != nil { + for name := range options.Services.Services() { + supportedTypes = append(supportedTypes, name) + } + } + + sort.Strings(supportedTypes) + questions := []*survey.Question{ + // Service name + { + Name: "name", + Prompt: &survey.Input{ + Message: "Name. Can be a fully qualified service name (URL + name):", + }, + Validate: survey.ComposeValidators( + survey.Required, + survey.MinLength(0), + survey.MaxLength(512), + ), + }, + // Service type + { + Name: "type", + Prompt: &survey.Select{ + Message: "Select the type of service:", + Options: supportedTypes, + PageSize: len(supportedTypes), + }, + Validate: survey.Required, + }, + // Language + { + Name: "language", + Prompt: &survey.Select{ + Message: "Select the service main programming language:", + Options: definition.SupportedLanguages(), + PageSize: len(definition.SupportedLanguages()), + }, + Validate: survey.Required, + }, + // Version + { + Name: "version", + Prompt: &survey.Input{ + Message: "Version. A semver version string for the service, with 'v' as prefix (ex: v1.0.0):", + Default: "v0.1.0", + }, + Validate: func(val interface{}) error { + if str, ok := val.(string); ok { + if !definition.ValidateVersion(str) { + return errors.New("invalid version format") + } + + return nil + } + + return errors.New("version has an invalid value type") + }, + }, + // Product + { + Name: "product", + Prompt: &survey.Input{ + Message: "Product name. Enter the product name that the service belongs to:", + }, + Validate: survey.ComposeValidators( + survey.Required, + survey.MinLength(3), + survey.MaxLength(512), + ), + }, + // Lifecycle + { + Name: "lifecycle", + Prompt: &survey.MultiSelect{ + Message: "Select lifecycle events to handle in the service:", + Options: []string{"start", "finish"}, + }, + }, + } + + if options.Features != nil { + var ( + featureNames = options.FeatureNames + iter = options.Features.Iterator() + ) + + for f, next := iter.Next(); next; f, next = iter.Next() { + if api, ok := f.(msurvey.CLIFeature); ok { + if api.IsCLISupported() { + featureNames = append(featureNames, getFeatureUIName(f)) + } + } + } + + // Features + questions = append(questions, &survey.Question{ + Name: "features", + Prompt: &survey.MultiSelect{ + Message: "Select the features the service will have:", + Options: featureNames, + PageSize: len(featureNames), + }, + }) + } + + return questions +} + +func getFeatureUIName(feature plugin.Feature) string { + if api, ok := feature.(msurvey.FeatureSurveyUI); ok { + return api.UIName() + } + + return feature.Name() +} + +// runServiceSurvey executes the survey that a service may have implemented. +func runServiceSurvey(answers *initSurveyAnswers, options *InitOptions) error { + if options.Services == nil { + return nil + } + + s, ok := options.Services.Services()[answers.Type] + if !ok { + return nil + } + + cli, ok := s.(msurvey.CLIFeature) + if !ok || !cli.IsCLISupported() { + return nil + } + + api, ok := s.(msurvey.FeatureSurvey) + if !ok { + return nil + } + + svcSurvey := api.GetSurvey() + if s == nil { + return nil + } + + response, err := handleSurvey(answers.Type, svcSurvey) + if err != nil { + return err + } + + d, save, err := api.Answers(response) + if err != nil { + return err + } + + answers.SetServiceDefinitions(d, save) + return nil +} + +func handleSurvey(name string, featureSurvey *msurvey.Survey) (map[string]interface{}, error) { + if featureSurvey.ConfirmQuestion != nil { + var responses []map[string]interface{} + + loop: + for { + if !featureSurvey.ConfirmQuestion.ConfirmAfter { + res := msurvey.YesNo(featureSurvey.ConfirmQuestion.Message) + if !res { + break loop + } + } + + response, err := surveyFromQuestion(name, featureSurvey) + if err != nil { + return nil, err + } + responses = append(responses, response) + + if featureSurvey.ConfirmQuestion.ConfirmAfter { + res := msurvey.YesNo(featureSurvey.ConfirmQuestion.Message) + if !res { + break loop + } + } + } + + return map[string]interface{}{ + name: responses, + }, nil + } + + response, err := surveyFromQuestion(name, featureSurvey) + if err != nil { + return nil, err + } + + return response, nil +} + +func surveyFromQuestion(name string, entrySurvey *msurvey.Survey) (map[string]interface{}, error) { + var ( + s []*survey.Question + response = make(map[string]interface{}) + validate = validator.New() + ) + + for _, q := range entrySurvey.Questions { + if err := validate.Struct(q); err != nil { + return nil, err + } + + question := &survey.Question{ + Name: q.Name, + Validate: func() func(v interface{}) error { + if q.Validate != nil { + return q.Validate + } + + if q.Required { + return survey.Required + } + + return nil + }(), + } + + switch q.Prompt { + case msurvey.PromptSurvey: + if !validateInnerSurveyCondition(response, q.Condition) { + continue + } + + r, err := handleSurvey(q.Name, q.Survey) + if err != nil { + return nil, err + } + if r != nil { + r = sanitizeResponse(r) + response[q.Name] = r[q.Name] + } + + continue + + default: + question.Prompt = buildSurveyPrompt(name, q) + } + + if q.Prompt != msurvey.PromptSurvey { + s = append(s, question) + } + + if entrySurvey.AskOne { + if validateInnerSurveyCondition(response, q.Condition) { + r, err := askOne(question.Prompt, q) + if err != nil { + return nil, err + } + + response[question.Name] = r + response = sanitizeResponse(response) + } + } + } + + // If we don't have response we need to execute the survey entirely. + if len(response) == 0 { + if err := survey.Ask(s, &response); err != nil { + return nil, err + } + } + + return sanitizeResponse(response), nil +} + +func buildSurveyPrompt(name string, q *msurvey.Question) survey.Prompt { + switch q.Prompt { + case msurvey.PromptInput: + return &survey.Input{ + Message: fmt.Sprintf("[%s] %s", name, q.Message), + Default: q.Default, + } + + case msurvey.PromptSelect: + return &survey.Select{ + Message: fmt.Sprintf("[%s] %s", name, q.Message), + Options: q.Options, + PageSize: len(q.Options), + Default: q.Default, + } + + case msurvey.PromptMultiSelect: + return &survey.MultiSelect{ + Message: fmt.Sprintf("[%s] %s", name, q.Message), + Options: q.Options, + } + + case msurvey.PromptMultiline: + return &survey.Multiline{ + Message: fmt.Sprintf("[%s] %s", name, q.Message), + } + + case msurvey.PromptConfirm: + return &survey.Confirm{ + Message: fmt.Sprintf("[%s] %s", name, q.Message), + } + + default: + } + + return nil +} + +func validateInnerSurveyCondition(response map[string]interface{}, condition *msurvey.QuestionCondition) bool { + if condition != nil { + if r, ok := response[condition.Name]; ok { + switch value := condition.Value.(type) { + case []string: + if slices.Contains(value, r.(string)) { + return true + } + + case string: + if v, ok := r.(string); ok && v == value { + return true + } + } + } + + return false + } + + return true +} + +func askOne(prompt survey.Prompt, question *msurvey.Question) (interface{}, error) { + getOptions := func() survey.AskOpt { + if question.Validate != nil { + return survey.WithValidator(question.Validate) + } + if question.Required { + return survey.WithValidator(survey.Required) + } + + return nil + } + + if question.Prompt == msurvey.PromptSelect { + index := 0 + if err := survey.AskOne(prompt, &index, getOptions()); err != nil { + return nil, err + } + + return question.Options[index], nil + } + + if question.Prompt == msurvey.PromptMultiSelect { + var r []string + if err := survey.AskOne(prompt, &r, getOptions()); err != nil { + return nil, err + } + + return r, nil + } + + var r string + if err := survey.AskOne(prompt, &r, getOptions()); err != nil { + return nil, err + } + + return r, nil +} + +// sanitizeResponse sanitizes the response to avoid sending internal formats to +// the client. +func sanitizeResponse(response map[string]interface{}) map[string]interface{} { + for k, v := range response { + if s, ok := v.(survey.OptionAnswer); ok { + response[k] = s.Value + } + + if opts, ok := v.([]survey.OptionAnswer); ok { + var s []string + for _, o := range opts { + s = append(s, o.Value) + } + + response[k] = s + } + } + + return response +} + +func runFeatureSurvey(name string, options *InitOptions) (interface{}, bool, error) { + f, err := options.Features.Feature(name) + if err != nil { + // Search again using mikros feature prefix. Maybe it is an implementation + // of a mikros feature. + f, err = options.Features.Feature(moptions.FeatureNamePrefix + name) + if err != nil { + return nil, false, err + } + } + + api, ok := f.(msurvey.FeatureSurvey) + if !ok { + return nil, false, nil + } + + var response map[string]interface{} + + if s := api.GetSurvey(); s != nil { + res, err := handleSurvey(name, s) + if err != nil { + return nil, false, err + } + response = res + } + + defs, save, err := api.Answers(response) + if err != nil { + return nil, false, err + } + + return defs, save, nil +} diff --git a/internal/cmd/service/survey_answers.go b/internal/cmd/service/survey_answers.go index e183622..1bc1435 100644 --- a/internal/cmd/service/survey_answers.go +++ b/internal/cmd/service/survey_answers.go @@ -13,10 +13,17 @@ type initSurveyAnswers struct { Features []string Lifecycle []string + kind Kind featureDefinitions map[string]*surveyAnswersDefinitions serviceDefinitions *surveyAnswersDefinitions } +func newInitSurveyAnswers(kind Kind) *initSurveyAnswers { + return &initSurveyAnswers{ + kind: kind, + } +} + func (i *initSurveyAnswers) TemplateNames() []templates.TemplateFile { names := []templates.TemplateFile{ { From 982595eb7ac2cd993cf932908a8a1021b6a9af79 Mon Sep 17 00:00:00 2001 From: Rodrigo Freitas Date: Fri, 27 Dec 2024 17:14:48 -0300 Subject: [PATCH 3/5] feat: initial rust template support --- internal/answers/answers.go | 68 +++++ internal/assets/rust/embed.go | 8 + internal/assets/rust/main.tmpl | 13 + internal/cmd/service/init.go | 251 +++++------------- internal/cmd/service/init_golang.go | 1 - internal/cmd/service/survey.go | 127 ++++----- internal/cmd/service/survey_answers.go | 86 ------ .../template_context.go => golang/context.go} | 2 +- internal/golang/executioner.go | 237 +++++++++++++++++ internal/rust/cargo.go | 59 ++++ internal/rust/executioner.go | 98 +++++++ internal/templates/language.go | 21 ++ pkg/cmd/service_init.go | 10 +- 13 files changed, 641 insertions(+), 340 deletions(-) create mode 100644 internal/answers/answers.go create mode 100644 internal/assets/rust/embed.go create mode 100644 internal/assets/rust/main.tmpl delete mode 100644 internal/cmd/service/init_golang.go delete mode 100644 internal/cmd/service/survey_answers.go rename internal/{cmd/service/template_context.go => golang/context.go} (99%) create mode 100644 internal/golang/executioner.go create mode 100644 internal/rust/cargo.go create mode 100644 internal/rust/executioner.go create mode 100644 internal/templates/language.go diff --git a/internal/answers/answers.go b/internal/answers/answers.go new file mode 100644 index 0000000..3be1253 --- /dev/null +++ b/internal/answers/answers.go @@ -0,0 +1,68 @@ +package answers + +import ( + "github.com/somatech1/mikros-cli/internal/templates" +) + +type InitSurveyAnswers struct { + Name string + Type string + Version string + Product string + Features []string + Lifecycle []string + RustLifecycle bool `survey:"lifecycle_rust"` + + language templates.Language + featureDefinitions map[string]*SurveyAnswersDefinitions + serviceDefinitions *SurveyAnswersDefinitions +} + +func NewInitSurveyAnswers(language templates.Language) *InitSurveyAnswers { + return &InitSurveyAnswers{ + language: language, + } +} + +func (i *InitSurveyAnswers) Language() string { + return i.language.String() +} + +func (i *InitSurveyAnswers) AddFeatureDefinitions(name string, answers interface{}, save bool) { + if i.featureDefinitions == nil { + i.featureDefinitions = make(map[string]*SurveyAnswersDefinitions) + } + + i.featureDefinitions[name] = &SurveyAnswersDefinitions{ + save: save, + definitions: answers, + } +} + +func (i *InitSurveyAnswers) SetServiceDefinitions(answers interface{}, save bool) { + i.serviceDefinitions = &SurveyAnswersDefinitions{ + save: save, + definitions: answers, + } +} + +func (i *InitSurveyAnswers) ServiceDefinitions() *SurveyAnswersDefinitions { + return i.serviceDefinitions +} + +func (i *InitSurveyAnswers) FeatureDefinitions() map[string]*SurveyAnswersDefinitions { + return i.featureDefinitions +} + +type SurveyAnswersDefinitions struct { + save bool + definitions interface{} +} + +func (s *SurveyAnswersDefinitions) ShouldBeSaved() bool { + return s.save +} + +func (s *SurveyAnswersDefinitions) Definitions() interface{} { + return s.definitions +} diff --git a/internal/assets/rust/embed.go b/internal/assets/rust/embed.go new file mode 100644 index 0000000..9d3a355 --- /dev/null +++ b/internal/assets/rust/embed.go @@ -0,0 +1,8 @@ +package rust + +import ( + "embed" +) + +//go:embed *.tmpl +var Files embed.FS diff --git a/internal/assets/rust/main.tmpl b/internal/assets/rust/main.tmpl new file mode 100644 index 0000000..ae8ae92 --- /dev/null +++ b/internal/assets/rust/main.tmpl @@ -0,0 +1,13 @@ +use mikros::service::builder::ServiceBuilder; +use service::Service as AppService; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let s = AppService::new(); + let mut svc = ServiceBuilder::default() + {{.ServiceTypeBuilderCall}} + .script(Box::new(s)) + .build()?; + + Ok(svc.start().await?) +} diff --git a/internal/cmd/service/init.go b/internal/cmd/service/init.go index 107de72..af40fb5 100644 --- a/internal/cmd/service/init.go +++ b/internal/cmd/service/init.go @@ -2,66 +2,69 @@ package service import ( "embed" - "fmt" + "errors" + "github.com/somatech1/mikros-cli/internal/rust" "os" "path/filepath" - "slices" "strings" - "github.com/iancoleman/strcase" "github.com/somatech1/mikros/components/definition" "github.com/somatech1/mikros/components/plugin" - golang_templates "github.com/somatech1/mikros-cli/internal/assets/golang" + "github.com/somatech1/mikros-cli/internal/answers" "github.com/somatech1/mikros-cli/internal/golang" - "github.com/somatech1/mikros-cli/internal/protobuf" "github.com/somatech1/mikros-cli/internal/templates" "github.com/somatech1/mikros-cli/pkg/definitions" "github.com/somatech1/mikros-cli/pkg/path" mtemplates "github.com/somatech1/mikros-cli/pkg/templates" ) +type TemplateExecutioner interface { + // PreExecution must be responsible for initializing the service base for + // the new service that will be created. + PreExecution(serviceName, destinationPath string) error + + // GenerateContext is responsible for generating the context that will be + // used inside the templates when executed. + GenerateContext(answers *answers.InitSurveyAnswers, protoFilename string, externalTemplates interface{}) (interface{}, error) + + // Templates must return all templates that the executioner will use. + Templates() []mtemplates.TemplateFile + + // Files must return all .tmpl files that can be used by the executioner. + Files() embed.FS + + // PostExecution is another handler where custom modifications can be made + // into the service that is being created. At this point, both initial source + // code and the service.toml file are already created. + PostExecution(destinationPath string) error +} + type InitOptions struct { - Kind Kind + Language templates.Language Path string ProtoFilename string FeatureNames []string Features *plugin.FeatureSet Services *plugin.ServiceSet - ExternalTemplates *TemplateFileOptions -} - -type Kind int - -const ( - KindGolang Kind = iota - KindRust -) - -type TemplateFileOptions struct { - Files embed.FS - Templates []mtemplates.TemplateFile - Api map[string]interface{} - NewServiceArgs map[string]string - WithExternalFeaturesArg string - WithExternalServicesArg string + ExternalTemplates *golang.ExternalTemplates } // Init initializes a new service locally. func Init(options *InitOptions) error { - answers, err := runInitSurvey(options) + surveyAnswers, err := runInitSurvey(options) if err != nil { return err } - if err := generateTemplates(options, answers); err != nil { + if err := generateTemplates(options, surveyAnswers); err != nil { return err } return nil } -func generateTemplates(options *InitOptions, answers *initSurveyAnswers) error { +func generateTemplates(options *InitOptions, answers *answers.InitSurveyAnswers) error { var ( destinationPath = options.Path ) @@ -76,46 +79,49 @@ func generateTemplates(options *InitOptions, answers *initSurveyAnswers) error { destinationPath = filepath.Join(cwd, strings.ToLower(answers.Name)) } - if _, err := path.CreatePath(destinationPath); err != nil { + executioner, err := getTemplateExecutioner(options.Language) + if err != nil { return err } - // Creates the service.toml file - if err := writeServiceDefinitions(destinationPath, answers); err != nil { + if err := executioner.PreExecution(answers.Name, destinationPath); err != nil { return err } - // Switch to the destination path to create template sources - cwd, err := path.ChangeDir(destinationPath) - if err != nil { + // creates go source templates + if err := generateSources(destinationPath, options, answers, executioner); err != nil { return err } - defer func() { - if e := os.Chdir(cwd); e != nil { - err = e - } - }() - - // creates go.mod - if err := golang.ModInit(strcase.ToKebab(answers.Name)); err != nil { + // Creates the service.toml file + if err := writeServiceDefinitions(destinationPath, answers); err != nil { return err } - // creates go source templates - if err := generateSources(options, answers); err != nil { + if err := executioner.PostExecution(destinationPath); err != nil { return err } return nil } -func writeServiceDefinitions(path string, answers *initSurveyAnswers) error { +func getTemplateExecutioner(language templates.Language) (TemplateExecutioner, error) { + if language == templates.LanguageGolang { + return &golang.Executioner{}, nil + } + if language == templates.LanguageRust { + return &rust.Executioner{}, nil + } + + return nil, errors.New("unsupported language") +} + +func writeServiceDefinitions(path string, answers *answers.InitSurveyAnswers) error { defs := &definition.Definitions{ Name: answers.Name, Types: []string{answers.Type}, Version: answers.Version, - Language: answers.Language, + Language: answers.Language(), Product: strings.ToUpper(answers.Product), } @@ -140,162 +146,33 @@ func writeServiceDefinitions(path string, answers *initSurveyAnswers) error { return nil } -func generateSources(options *InitOptions, answers *initSurveyAnswers) error { - context, err := generateTemplateContext(options, answers) +func generateSources(destinationPath string, options *InitOptions, answers *answers.InitSurveyAnswers, executioner TemplateExecutioner) error { + // Switch to the service folder so the initial source code can be created. + cwd, err := path.ChangeDir(destinationPath) if err != nil { return err } - if err := createServiceTemplates(options, answers.TemplateNames(), context); err != nil { - return err - } - - return nil -} - -func generateTemplateContext(options *InitOptions, answers *initSurveyAnswers) (TemplateContext, error) { - var ( - svcDefs = answers.ServiceDefinitions() - defs interface{} - ) - - if svcDefs != nil { - defs = svcDefs.Definitions() - } - - externalService := func() bool { - switch answers.Type { - case definition.ServiceType_gRPC.String(), definition.ServiceType_HTTP.String(), definition.ServiceType_Script.String(), definition.ServiceType_Native.String(): - return false + defer func() { + if e := os.Chdir(cwd); e != nil { + err = e } + }() - return true - } - - newServiceArgs, err := generateNewServiceArgs(options, answers) + context, err := executioner.GenerateContext(answers, options.ProtoFilename, options.ExternalTemplates) if err != nil { - return TemplateContext{}, err - } - - context := TemplateContext{ - featuresExtensions: len(answers.Features) > 0, - servicesExtensions: externalService(), - onStartLifecycle: slices.Contains(answers.Lifecycle, "start"), - onFinishLifecycle: slices.Contains(answers.Lifecycle, "finish"), - serviceType: answers.Type, - NewServiceArgs: newServiceArgs, - ServiceName: answers.Name, - Imports: generateImports(answers), - ServiceTypeCustomAnswers: defs, - } - - if options.ExternalTemplates != nil { - context.ExternalServicesArg = options.ExternalTemplates.WithExternalServicesArg - context.ExternalFeaturesArg = options.ExternalTemplates.WithExternalFeaturesArg - } - - if filename := options.ProtoFilename; filename != "" { - pbFile, err := protobuf.Parse(filename) - if err != nil { - return TemplateContext{}, err - } - context.GrpcMethods = pbFile.Methods - } - - return context, nil -} - -func generateNewServiceArgs(options *InitOptions, answers *initSurveyAnswers) (string, error) { - svcSnake := strcase.ToSnake(answers.Name) - - switch answers.Type { - case definition.ServiceType_gRPC.String(): - return fmt.Sprintf(`Service: map[string]options.ServiceOptions{ - "grpc": &options.GrpcServiceOptions{ - ProtoServiceDescription: &%spb.%sService_ServiceDesc, - }, - },`, svcSnake, strcase.ToCamel(answers.Name)), nil - - case definition.ServiceType_HTTP.String(): - return fmt.Sprintf(`Service: map[string]options.ServiceOptions{ - "http": &options.HttpServiceOptions{ - ProtoHttpServer: %spb.NewHttpServer(), - }, - },`, svcSnake), nil - - case definition.ServiceType_Native.String(): - return `Service: map[string]options.ServiceOptions{ - "native": &options.NativeServiceOptions{}, - },`, nil - - case definition.ServiceType_Script.String(): - return `Service: map[string]options.ServiceOptions{ - "script": &options.ScriptServiceOptions{}, - },`, nil - - default: - if options.ExternalTemplates != nil { - var ( - svcDefs = answers.ServiceDefinitions() - defs interface{} - ) - - if svcDefs != nil { - defs = svcDefs.Definitions() - } - - if tpl, ok := options.ExternalTemplates.NewServiceArgs[answers.Type]; ok { - data := struct { - ServiceName string - ServiceType string - ServiceTypeCustomAnswers interface{} - }{ - ServiceName: answers.Type, - ServiceType: answers.Name, - ServiceTypeCustomAnswers: defs, - } - - block, err := templates.ParseBlock(tpl, options.ExternalTemplates.Api, data) - if err != nil { - return "", err - } - - return block, nil - } - } - } - - return "", nil -} - -func generateImports(answers *initSurveyAnswers) map[string][]ImportContext { - imports := map[string][]ImportContext{ - "main": { - { - Path: "github.com/somatech1/mikros", - }, - { - Path: "github.com/somatech1/mikros/components/options", - }, - }, - "service": { - { - Path: "github.com/somatech1/mikros", - }, - }, + return err } - if len(answers.Lifecycle) > 0 { - imports["lifecycle"] = append(imports["lifecycle"], ImportContext{ - Path: "context", - }) + if err := createServiceTemplates(options, executioner, context); err != nil { + return err } - return imports + return nil } -func createServiceTemplates(options *InitOptions, filenames []mtemplates.TemplateFile, context interface{}) error { - if err := runTemplates(golang_templates.Files, filenames, context, nil); err != nil { +func createServiceTemplates(options *InitOptions, executioner TemplateExecutioner, context interface{}) error { + if err := runTemplates(executioner.Files(), executioner.Templates(), context, nil); err != nil { return err } diff --git a/internal/cmd/service/init_golang.go b/internal/cmd/service/init_golang.go deleted file mode 100644 index 6d43c33..0000000 --- a/internal/cmd/service/init_golang.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/internal/cmd/service/survey.go b/internal/cmd/service/survey.go index 45720d6..0051b8a 100644 --- a/internal/cmd/service/survey.go +++ b/internal/cmd/service/survey.go @@ -9,38 +9,40 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/go-playground/validator/v10" "github.com/somatech1/mikros/components/definition" - moptions "github.com/somatech1/mikros/components/options" + "github.com/somatech1/mikros/components/options" "github.com/somatech1/mikros/components/plugin" + "github.com/somatech1/mikros-cli/internal/answers" + "github.com/somatech1/mikros-cli/internal/templates" msurvey "github.com/somatech1/mikros-cli/pkg/survey" ) -func runInitSurvey(options *InitOptions) (*initSurveyAnswers, error) { - answers := newInitSurveyAnswers(options.Kind) +func runInitSurvey(opt *InitOptions) (*answers.InitSurveyAnswers, error) { + surveyAnswers := answers.NewInitSurveyAnswers(opt.Language) - if err := survey.Ask(baseQuestions(options), answers); err != nil { + if err := survey.Ask(baseQuestions(opt), surveyAnswers); err != nil { return nil, err } - if err := runServiceSurvey(answers, options); err != nil { + if err := runServiceTypeSurvey(surveyAnswers, opt); err != nil { return nil, err } // Presents only questions from selected features - for _, name := range answers.Features { - defs, save, err := runFeatureSurvey(name, options) + for _, name := range surveyAnswers.Features { + defs, save, err := runFeatureSurvey(name, opt) if err != nil { return nil, err } if defs != nil { - answers.AddFeatureDefinitions(name, defs, save) + surveyAnswers.AddFeatureDefinitions(name, defs, save) } } - return answers, nil + return surveyAnswers, nil } -func baseQuestions(options *InitOptions) []*survey.Question { +func baseQuestions(opt *InitOptions) []*survey.Question { supportedTypes := []string{ definition.ServiceType_gRPC.String(), definition.ServiceType_HTTP.String(), @@ -48,8 +50,8 @@ func baseQuestions(options *InitOptions) []*survey.Question { definition.ServiceType_Script.String(), } - if options.Services != nil { - for name := range options.Services.Services() { + if opt.Services != nil { + for name := range opt.Services.Services() { supportedTypes = append(supportedTypes, name) } } @@ -78,16 +80,6 @@ func baseQuestions(options *InitOptions) []*survey.Question { }, Validate: survey.Required, }, - // Language - { - Name: "language", - Prompt: &survey.Select{ - Message: "Select the service main programming language:", - Options: definition.SupportedLanguages(), - PageSize: len(definition.SupportedLanguages()), - }, - Validate: survey.Required, - }, // Version { Name: "version", @@ -119,37 +111,49 @@ func baseQuestions(options *InitOptions) []*survey.Question { survey.MaxLength(512), ), }, - // Lifecycle - { + } + + if opt.Language == templates.LanguageGolang { + questions = append(questions, &survey.Question{ Name: "lifecycle", Prompt: &survey.MultiSelect{ Message: "Select lifecycle events to handle in the service:", Options: []string{"start", "finish"}, }, - }, - } - - if options.Features != nil { - var ( - featureNames = options.FeatureNames - iter = options.Features.Iterator() - ) + }) - for f, next := iter.Next(); next; f, next = iter.Next() { - if api, ok := f.(msurvey.CLIFeature); ok { - if api.IsCLISupported() { - featureNames = append(featureNames, getFeatureUIName(f)) + // Features is only supported for golang services by now. + if opt.Features != nil { + var ( + featureNames = opt.FeatureNames + iter = opt.Features.Iterator() + ) + + for f, next := iter.Next(); next; f, next = iter.Next() { + if api, ok := f.(msurvey.CLIFeature); ok { + if api.IsCLISupported() { + featureNames = append(featureNames, getFeatureUIName(f)) + } } } + + // Features + questions = append(questions, &survey.Question{ + Name: "features", + Prompt: &survey.MultiSelect{ + Message: "Select the features the service will have:", + Options: featureNames, + PageSize: len(featureNames), + }, + }) } + } - // Features + if opt.Language == templates.LanguageRust { questions = append(questions, &survey.Question{ - Name: "features", - Prompt: &survey.MultiSelect{ - Message: "Select the features the service will have:", - Options: featureNames, - PageSize: len(featureNames), + Name: "lifecycle_rust", + Prompt: &survey.Confirm{ + Message: "Enable lifecycle events to handle in the service?", }, }) } @@ -165,13 +169,14 @@ func getFeatureUIName(feature plugin.Feature) string { return feature.Name() } -// runServiceSurvey executes the survey that a service may have implemented. -func runServiceSurvey(answers *initSurveyAnswers, options *InitOptions) error { - if options.Services == nil { +// runServiceTypeSurvey executes the survey that a specific service type may +// have implemented. +func runServiceTypeSurvey(answers *answers.InitSurveyAnswers, opt *InitOptions) error { + if opt.Services == nil { return nil } - s, ok := options.Services.Services()[answers.Type] + s, ok := opt.Services.Services()[answers.Type] if !ok { return nil } @@ -320,36 +325,36 @@ func surveyFromQuestion(name string, entrySurvey *msurvey.Survey) (map[string]in return sanitizeResponse(response), nil } -func buildSurveyPrompt(name string, q *msurvey.Question) survey.Prompt { - switch q.Prompt { +func buildSurveyPrompt(name string, question *msurvey.Question) survey.Prompt { + switch question.Prompt { case msurvey.PromptInput: return &survey.Input{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Default: q.Default, + Message: fmt.Sprintf("[%s] %s", name, question.Message), + Default: question.Default, } case msurvey.PromptSelect: return &survey.Select{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Options: q.Options, - PageSize: len(q.Options), - Default: q.Default, + Message: fmt.Sprintf("[%s] %s", name, question.Message), + Options: question.Options, + PageSize: len(question.Options), + Default: question.Default, } case msurvey.PromptMultiSelect: return &survey.MultiSelect{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), - Options: q.Options, + Message: fmt.Sprintf("[%s] %s", name, question.Message), + Options: question.Options, } case msurvey.PromptMultiline: return &survey.Multiline{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), + Message: fmt.Sprintf("[%s] %s", name, question.Message), } case msurvey.PromptConfirm: return &survey.Confirm{ - Message: fmt.Sprintf("[%s] %s", name, q.Message), + Message: fmt.Sprintf("[%s] %s", name, question.Message), } default: @@ -439,12 +444,12 @@ func sanitizeResponse(response map[string]interface{}) map[string]interface{} { return response } -func runFeatureSurvey(name string, options *InitOptions) (interface{}, bool, error) { - f, err := options.Features.Feature(name) +func runFeatureSurvey(name string, opt *InitOptions) (interface{}, bool, error) { + f, err := opt.Features.Feature(name) if err != nil { // Search again using mikros feature prefix. Maybe it is an implementation // of a mikros feature. - f, err = options.Features.Feature(moptions.FeatureNamePrefix + name) + f, err = opt.Features.Feature(options.FeatureNamePrefix + name) if err != nil { return nil, false, err } diff --git a/internal/cmd/service/survey_answers.go b/internal/cmd/service/survey_answers.go deleted file mode 100644 index 1bc1435..0000000 --- a/internal/cmd/service/survey_answers.go +++ /dev/null @@ -1,86 +0,0 @@ -package service - -import ( - "github.com/somatech1/mikros-cli/pkg/templates" -) - -type initSurveyAnswers struct { - Name string - Type string - Language string - Version string - Product string - Features []string - Lifecycle []string - - kind Kind - featureDefinitions map[string]*surveyAnswersDefinitions - serviceDefinitions *surveyAnswersDefinitions -} - -func newInitSurveyAnswers(kind Kind) *initSurveyAnswers { - return &initSurveyAnswers{ - kind: kind, - } -} - -func (i *initSurveyAnswers) TemplateNames() []templates.TemplateFile { - names := []templates.TemplateFile{ - { - Name: "main", - Extension: "go", - }, - { - Name: "service", - Extension: "go", - }, - } - - if len(i.Lifecycle) > 0 { - names = append(names, templates.TemplateFile{ - Name: "lifecycle", - Extension: "go", - }) - } - - return names -} - -func (i *initSurveyAnswers) AddFeatureDefinitions(name string, answers interface{}, save bool) { - if i.featureDefinitions == nil { - i.featureDefinitions = make(map[string]*surveyAnswersDefinitions) - } - - i.featureDefinitions[name] = &surveyAnswersDefinitions{ - save: save, - definitions: answers, - } -} - -func (i *initSurveyAnswers) SetServiceDefinitions(answers interface{}, save bool) { - i.serviceDefinitions = &surveyAnswersDefinitions{ - save: save, - definitions: answers, - } -} - -func (i *initSurveyAnswers) ServiceDefinitions() *surveyAnswersDefinitions { - return i.serviceDefinitions -} - -func (i *initSurveyAnswers) FeatureDefinitions() map[string]*surveyAnswersDefinitions { - return i.featureDefinitions -} - -type surveyAnswersDefinitions struct { - save bool - definitions interface{} -} - -func (s *surveyAnswersDefinitions) ShouldBeSaved() bool { - return s.save -} - -func (s *surveyAnswersDefinitions) Definitions() interface{} { - return s.definitions -} diff --git a/internal/cmd/service/template_context.go b/internal/golang/context.go similarity index 99% rename from internal/cmd/service/template_context.go rename to internal/golang/context.go index 63ea977..a75f499 100644 --- a/internal/cmd/service/template_context.go +++ b/internal/golang/context.go @@ -1,4 +1,4 @@ -package service +package golang import ( "github.com/somatech1/mikros/components/definition" diff --git a/internal/golang/executioner.go b/internal/golang/executioner.go new file mode 100644 index 0000000..73af097 --- /dev/null +++ b/internal/golang/executioner.go @@ -0,0 +1,237 @@ +package golang + +import ( + "embed" + "fmt" + "os" + "slices" + + "github.com/iancoleman/strcase" + "github.com/somatech1/mikros/components/definition" + + "github.com/somatech1/mikros-cli/internal/answers" + "github.com/somatech1/mikros-cli/internal/assets/golang" + "github.com/somatech1/mikros-cli/internal/protobuf" + "github.com/somatech1/mikros-cli/internal/templates" + "github.com/somatech1/mikros-cli/pkg/path" + mtemplates "github.com/somatech1/mikros-cli/pkg/templates" +) + +type ExternalTemplates struct { + Files embed.FS + Templates []mtemplates.TemplateFile + Api map[string]interface{} + NewServiceArgs map[string]string + WithExternalFeaturesArg string + WithExternalServicesArg string +} + +type Executioner struct { + hasLifecycle bool +} + +func (e *Executioner) PreExecution(serviceName, destinationPath string) error { + if _, err := path.CreatePath(destinationPath); err != nil { + return err + } + + // Switch to the destination path to create required language files + cwd, err := path.ChangeDir(destinationPath) + if err != nil { + return err + } + + defer func() { + if e := os.Chdir(cwd); e != nil { + err = e + } + }() + + // creates go.mod + if err := ModInit(strcase.ToKebab(serviceName)); err != nil { + return err + } + + return nil +} + +func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, protoFilename string, data interface{}) (interface{}, error) { + var ( + externalTemplates *ExternalTemplates + svcDefs = answers.ServiceDefinitions() + defs interface{} + ) + + if e, ok := data.(*ExternalTemplates); ok { + externalTemplates = e + } + + if svcDefs != nil { + defs = svcDefs.Definitions() + } + + externalService := func() bool { + switch answers.Type { + case definition.ServiceType_gRPC.String(), definition.ServiceType_HTTP.String(), definition.ServiceType_Script.String(), definition.ServiceType_Native.String(): + return false + } + + return true + } + + newServiceArgs, err := generateNewServiceArgs(externalTemplates, answers) + if err != nil { + return TemplateContext{}, err + } + + context := TemplateContext{ + featuresExtensions: len(answers.Features) > 0, + servicesExtensions: externalService(), + onStartLifecycle: slices.Contains(answers.Lifecycle, "start"), + onFinishLifecycle: slices.Contains(answers.Lifecycle, "finish"), + serviceType: answers.Type, + NewServiceArgs: newServiceArgs, + ServiceName: answers.Name, + Imports: generateImports(answers), + ServiceTypeCustomAnswers: defs, + } + + if externalTemplates != nil { + context.ExternalServicesArg = externalTemplates.WithExternalServicesArg + context.ExternalFeaturesArg = externalTemplates.WithExternalFeaturesArg + } + + if filename := protoFilename; filename != "" { + pbFile, err := protobuf.Parse(filename) + if err != nil { + return TemplateContext{}, err + } + context.GrpcMethods = pbFile.Methods + } + + // Adjust some internal information to use inside other methods + if context.onStartLifecycle || context.onFinishLifecycle { + e.hasLifecycle = true + } + + return context, nil +} + +func generateNewServiceArgs(options *ExternalTemplates, answers *answers.InitSurveyAnswers) (string, error) { + svcSnake := strcase.ToSnake(answers.Name) + + switch answers.Type { + case definition.ServiceType_gRPC.String(): + return fmt.Sprintf(`Service: map[string]options.ServiceOptions{ + "grpc": &options.GrpcServiceOptions{ + ProtoServiceDescription: &%spb.%sService_ServiceDesc, + }, + },`, svcSnake, strcase.ToCamel(answers.Name)), nil + + case definition.ServiceType_HTTP.String(): + return fmt.Sprintf(`Service: map[string]options.ServiceOptions{ + "http": &options.HttpServiceOptions{ + ProtoHttpServer: %spb.NewHttpServer(), + }, + },`, svcSnake), nil + + case definition.ServiceType_Native.String(): + return `Service: map[string]options.ServiceOptions{ + "native": &options.NativeServiceOptions{}, + },`, nil + + case definition.ServiceType_Script.String(): + return `Service: map[string]options.ServiceOptions{ + "script": &options.ScriptServiceOptions{}, + },`, nil + + default: + if options != nil { + var ( + svcDefs = answers.ServiceDefinitions() + defs interface{} + ) + + if svcDefs != nil { + defs = svcDefs.Definitions() + } + + if tpl, ok := options.NewServiceArgs[answers.Type]; ok { + data := struct { + ServiceName string + ServiceType string + ServiceTypeCustomAnswers interface{} + }{ + ServiceName: answers.Type, + ServiceType: answers.Name, + ServiceTypeCustomAnswers: defs, + } + + block, err := templates.ParseBlock(tpl, options.Api, data) + if err != nil { + return "", err + } + + return block, nil + } + } + } + + return "", nil +} + +func generateImports(answers *answers.InitSurveyAnswers) map[string][]ImportContext { + imports := map[string][]ImportContext{ + "main": { + { + Path: "github.com/somatech1/mikros", + }, + { + Path: "github.com/somatech1/mikros/components/options", + }, + }, + "service": { + { + Path: "github.com/somatech1/mikros", + }, + }, + } + + if len(answers.Lifecycle) > 0 { + imports["lifecycle"] = append(imports["lifecycle"], ImportContext{ + Path: "context", + }) + } + + return imports +} + +func (e *Executioner) Templates() []mtemplates.TemplateFile { + names := []mtemplates.TemplateFile{ + { + Name: "main", + Extension: "go", + }, + { + Name: "service", + Extension: "go", + }, + } + + if e.hasLifecycle { + names = append(names, mtemplates.TemplateFile{ + Name: "lifecycle", + Extension: "go", + }) + } + + return names +} + +func (e *Executioner) Files() embed.FS { + return golang.Files +} + +func (e *Executioner) PostExecution(_ string) error { + return nil +} diff --git a/internal/rust/cargo.go b/internal/rust/cargo.go new file mode 100644 index 0000000..386786c --- /dev/null +++ b/internal/rust/cargo.go @@ -0,0 +1,59 @@ +package rust + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + mpath "github.com/somatech1/mikros-cli/pkg/path" + "github.com/somatech1/mikros-cli/pkg/process" +) + +func cargoInit(destinationPath, name string) error { + // Switch to the destination path to create the crate + cwd, err := mpath.ChangeDir(filepath.Dir(destinationPath)) + if err != nil { + return err + } + + defer func() { + _ = os.Chdir(cwd) + }() + + _, err = process.Exec("cargo", "init", name) + return err +} + +func cargoAdd(destinationPath, name, version, git, path string, features []string) error { + // Switch to the destination path to add the dependency + cwd, err := mpath.ChangeDir(destinationPath) + if err != nil { + return err + } + + defer func() { + _ = os.Chdir(cwd) + }() + + args := []string{ + "cargo", + "add", + } + if version != "" { + args = append(args, fmt.Sprintf("%v@%v", name, version)) + } + if git != "" { + args = append(args, "--git", git) + } + if path != "" { + args = append(args, "--path", path) + } + if len(features) > 0 { + args = append(args, "--features", strings.Join(features, ",")) + } + + out, err := process.Exec(args...) + println(string(out)) + return err +} diff --git a/internal/rust/executioner.go b/internal/rust/executioner.go new file mode 100644 index 0000000..a160025 --- /dev/null +++ b/internal/rust/executioner.go @@ -0,0 +1,98 @@ +package rust + +import ( + "embed" + + "github.com/somatech1/mikros/components/definition" + + "github.com/somatech1/mikros-cli/internal/answers" + "github.com/somatech1/mikros-cli/internal/assets/rust" + "github.com/somatech1/mikros-cli/pkg/templates" +) + +type Executioner struct { + hasLifecycle bool + serviceType string +} + +func (e *Executioner) PreExecution(serviceName, destinationPath string) error { + if err := cargoInit(destinationPath, serviceName); err != nil { + return err + } + + return nil +} + +func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, _ string, _ interface{}) (interface{}, error) { + // Store values to be used later + e.hasLifecycle = answers.RustLifecycle + e.serviceType = answers.Type + + return nil, nil +} + +func (e *Executioner) Templates() []templates.TemplateFile { + return []templates.TemplateFile{} +} + +func (e *Executioner) Files() embed.FS { + return rust.Files +} + +type dependency struct { + Name string + Version string + Git string + Path string + Feature []string +} + +func (e *Executioner) PostExecution(destinationPath string) error { + dependencies := []dependency{ + { + Name: "mikros", + // FIXME: change this when mikros-rs is published + Path: "/Users/rodrigo/desenv/github/rsfreitas/mikros-rs", + }, + { + Name: "tokio", + Version: "1.41.1", + Feature: []string{"full"}, + }, + } + + if e.serviceType == definition.ServiceType_Script.String() || e.serviceType == definition.ServiceType_Native.String() || e.hasLifecycle { + dependencies = append(dependencies, dependency{ + Name: "async-trait", + Version: "0.1.83", + }) + } + + if e.serviceType == definition.ServiceType_HTTP.String() { + dependencies = append(dependencies, dependency{ + Name: "axum", + Version: "0.7.7", + }) + } + + if e.serviceType == definition.ServiceType_gRPC.String() { + dependencies = append(dependencies, []dependency{ + { + Name: "prost", + Version: "0.13.3", + }, + { + Name: "tonic", + Version: "0.12.3", + }, + }...) + } + + for _, d := range dependencies { + if err := cargoAdd(destinationPath, d.Name, d.Version, d.Git, d.Path, d.Feature); err != nil { + return err + } + } + + return nil +} diff --git a/internal/templates/language.go b/internal/templates/language.go new file mode 100644 index 0000000..45563e0 --- /dev/null +++ b/internal/templates/language.go @@ -0,0 +1,21 @@ +package templates + +// Language represents a supported programming language for services generated +// by us. +type Language int + +const ( + LanguageGolang Language = iota + LanguageRust +) + +func (k Language) String() string { + switch k { + case LanguageGolang: + return "go" + case LanguageRust: + return "rust" + } + + return "unknown" +} diff --git a/pkg/cmd/service_init.go b/pkg/cmd/service_init.go index bbfa161..f4b1641 100644 --- a/pkg/cmd/service_init.go +++ b/pkg/cmd/service_init.go @@ -10,6 +10,8 @@ import ( "github.com/spf13/viper" "github.com/somatech1/mikros-cli/internal/cmd/service" + "github.com/somatech1/mikros-cli/internal/golang" + "github.com/somatech1/mikros-cli/internal/templates" ) type serviceInitCmdOptions struct { @@ -46,12 +48,12 @@ func serviceInitCmdInit(options *serviceInitCmdOptions) { serviceInitCmd.Run = func(cmd *cobra.Command, args []string) { initOptions := &service.InitOptions{ - Kind: service.KindGolang, + Language: templates.LanguageGolang, Path: viper.GetString("init-path"), ProtoFilename: viper.GetString("init-proto"), } if viper.GetBool("init-rust") { - initOptions.Kind = service.KindRust + initOptions.Language = templates.LanguageRust } if options != nil { @@ -59,7 +61,7 @@ func serviceInitCmdInit(options *serviceInitCmdOptions) { initOptions.Services = options.Services if options.AdditionalTemplates != nil { - initOptions.ExternalTemplates = &service.TemplateFileOptions{ + initOptions.ExternalTemplates = &golang.ExternalTemplates{ Files: options.AdditionalTemplates.Files, Templates: options.AdditionalTemplates.Templates, Api: options.AdditionalTemplates.Api, @@ -91,7 +93,7 @@ func serviceInitCmdInit(options *serviceInitCmdOptions) { } func disableServiceGlobalFlags() { - flagsToHide := []string{} + var flagsToHide []string serviceCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) { flagsToHide = append(flagsToHide, flag.Name) }) From f5ad31b88fa894490016c8551bd8377a7dc12a99 Mon Sep 17 00:00:00 2001 From: Rodrigo Freitas Date: Thu, 2 Jan 2025 17:46:44 -0300 Subject: [PATCH 4/5] feat: add rust service template for all types --- internal/assets/rust/main.tmpl | 3 +- internal/assets/rust/service.tmpl | 101 +++++++++++++++++++++++++++ internal/cmd/service/init.go | 2 +- internal/golang/executioner.go | 4 +- internal/protobuf/parser.go | 14 +++- internal/rust/context.go | 55 +++++++++++++++ internal/rust/executioner.go | 85 +++++++++++++++++++--- internal/rust/{cargo.go => utils.go} | 11 ++- 8 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 internal/assets/rust/service.tmpl create mode 100644 internal/rust/context.go rename internal/rust/{cargo.go => utils.go} (82%) diff --git a/internal/assets/rust/main.tmpl b/internal/assets/rust/main.tmpl index ae8ae92..1ebd7ba 100644 --- a/internal/assets/rust/main.tmpl +++ b/internal/assets/rust/main.tmpl @@ -1,3 +1,5 @@ +mod service; + use mikros::service::builder::ServiceBuilder; use service::Service as AppService; @@ -6,7 +8,6 @@ async fn main() -> Result<(), Box> { let s = AppService::new(); let mut svc = ServiceBuilder::default() {{.ServiceTypeBuilderCall}} - .script(Box::new(s)) .build()?; Ok(svc.start().await?) diff --git a/internal/assets/rust/service.tmpl b/internal/assets/rust/service.tmpl new file mode 100644 index 0000000..f009540 --- /dev/null +++ b/internal/assets/rust/service.tmpl @@ -0,0 +1,101 @@ + +use std::sync::Arc; +{{if or .IsGrpcService .IsHttpService}} +use tonic::{Request, Response, Status}; + +pub mod {{.ModuleName}} { + include!("generated/{{.ModuleName}}.rs"); +} +{{end}} +{{- if .IsGrpcService}} +use {{.ModuleName}}::{{toSnake .ServiceName}}_server::{{"{"}}{{.ServiceName}}, {{.ServiceName}}Server{{"}"}}; +{{- end}} +{{- if .IsHttpService}} +use api::router::Router; +{{- end}} +{{- if or .IsScriptService .IsNativeService}} +use mikros::errors as merrors; +use mikros::service::context::Context; +{{- end}} +{{- if .IsHttpService}} +use axum::extract::State; +use mikros::http::ServiceState; +use mikros::Mutex; +{{- end}} +{{if .IsHttpService}} +#[derive(Clone, Default)] +{{- else }} +#[derive(Clone)] +{{- end}} +pub struct Service; + +impl Service { + {{- if .IsGrpcService}} + pub fn new() -> {{.ServiceName}}Server { + let service = Arc::new(Self{}); + {{.ServiceName}}Server::from_arc(service) + } + {{- end}} + {{- if or .IsScriptService .IsNativeService}} + pub fn new() -> Self { + Self{} + } + {{- end}} + {{- if .IsHttpService}} + pub fn new() -> Router { + let service = Arc::new(Self::default()); + Router::new(service) + } + {{- end}} +} + +{{if or .IsGrpcService .IsHttpService -}}{{$moduleName := .ModuleName}} +#[tonic::async_trait] +impl {{.ServiceName}} for Service { + {{- range .Methods}} + async fn {{toSnake .Name}}( + &self, + request: Request<{{$moduleName}}::{{.RequestName}}>, + ) -> Result, Status> { + todo!() + } + {{end}} +} +{{- end}} +{{- if .IsScriptService}} +#[async_trait::async_trait] +impl mikros::service::script::ScriptService for Service { + async fn run(&self, ctx: &Context) -> merrors::Result<()> { + todo!() + } + + async fn cleanup(&self, ctx: &Context) { + todo!() + } +} +{{- end}} +{{- if .IsNativeService}} +#[async_trait::async_trait] +impl mikros::service::native::NativeService for Service { + async fn start(&self, ctx: &Context) -> merrors::Result<()> { + todo!() + } + + async fn stop(&self, ctx: &Context) { + todo!() + } +} +{{- end}} + +{{if .HasLifecycle}} +#[async_trait::async_trait] +impl mikros::service::lifecycle::Lifecycle for Service { + async fn on_start(&mut self, ctx: &Context) -> merrors::Result<()> { + todo!() + } + + async fn on_finish(&self) -> merrors::Result<()> { + todo!() + } +} +{{- end}} \ No newline at end of file diff --git a/internal/cmd/service/init.go b/internal/cmd/service/init.go index af40fb5..1330917 100644 --- a/internal/cmd/service/init.go +++ b/internal/cmd/service/init.go @@ -3,7 +3,6 @@ package service import ( "embed" "errors" - "github.com/somatech1/mikros-cli/internal/rust" "os" "path/filepath" "strings" @@ -13,6 +12,7 @@ import ( "github.com/somatech1/mikros-cli/internal/answers" "github.com/somatech1/mikros-cli/internal/golang" + "github.com/somatech1/mikros-cli/internal/rust" "github.com/somatech1/mikros-cli/internal/templates" "github.com/somatech1/mikros-cli/pkg/definitions" "github.com/somatech1/mikros-cli/pkg/path" diff --git a/internal/golang/executioner.go b/internal/golang/executioner.go index 73af097..9537106 100644 --- a/internal/golang/executioner.go +++ b/internal/golang/executioner.go @@ -10,7 +10,7 @@ import ( "github.com/somatech1/mikros/components/definition" "github.com/somatech1/mikros-cli/internal/answers" - "github.com/somatech1/mikros-cli/internal/assets/golang" + golang_tpl "github.com/somatech1/mikros-cli/internal/assets/golang" "github.com/somatech1/mikros-cli/internal/protobuf" "github.com/somatech1/mikros-cli/internal/templates" "github.com/somatech1/mikros-cli/pkg/path" @@ -229,7 +229,7 @@ func (e *Executioner) Templates() []mtemplates.TemplateFile { } func (e *Executioner) Files() embed.FS { - return golang.Files + return golang_tpl.Files } func (e *Executioner) PostExecution(_ string) error { diff --git a/internal/protobuf/parser.go b/internal/protobuf/parser.go index b3bea42..b5e13f1 100644 --- a/internal/protobuf/parser.go +++ b/internal/protobuf/parser.go @@ -7,12 +7,24 @@ import ( ) type Proto struct { - Methods []*Method + ModuleName string + ServiceName string + Methods []*Method } func (p *Proto) parse(definitions *protofile.Proto) { protofile.Walk(definitions, protofile.WithRPC(p.parseMethods)) + + protofile.Walk(definitions, + protofile.WithService(func(service *protofile.Service) { + p.ServiceName = service.Name + })) + + protofile.Walk(definitions, + protofile.WithPackage(func(pkg *protofile.Package) { + p.ModuleName = pkg.Name + })) } func (p *Proto) parseMethods(r *protofile.RPC) { diff --git a/internal/rust/context.go b/internal/rust/context.go new file mode 100644 index 0000000..9db1da6 --- /dev/null +++ b/internal/rust/context.go @@ -0,0 +1,55 @@ +package rust + +import ( + "github.com/somatech1/mikros/components/definition" +) + +type Context struct { + HasLifecycle bool + ServiceName string + ModuleName string + Methods []*Method + + serviceType string +} + +type Method struct { + Name string + RequestName string + ResponseName string +} + +func (t *Context) IsScriptService() bool { + return t.serviceType == definition.ServiceType_Script.String() +} + +func (t *Context) IsNativeService() bool { + return t.serviceType == definition.ServiceType_Native.String() +} + +func (t *Context) IsGrpcService() bool { + return t.serviceType == definition.ServiceType_gRPC.String() +} + +func (t *Context) IsHttpService() bool { + return t.serviceType == definition.ServiceType_HTTP.String() +} + +// ServiceTypeBuilderCall returns, in a string format, the function that +// initializes the service according its type. +func (t *Context) ServiceTypeBuilderCall() string { + if t.IsScriptService() { + return ".script(s)" + } + if t.IsNativeService() { + return ".native(s)" + } + if t.IsGrpcService() { + return ".grpc(s)" + } + if t.IsHttpService() { + return ".http(s.routes())" + } + + return "" +} diff --git a/internal/rust/executioner.go b/internal/rust/executioner.go index a160025..9b5bf46 100644 --- a/internal/rust/executioner.go +++ b/internal/rust/executioner.go @@ -2,11 +2,14 @@ package rust import ( "embed" + "os" + "path/filepath" "github.com/somatech1/mikros/components/definition" "github.com/somatech1/mikros-cli/internal/answers" "github.com/somatech1/mikros-cli/internal/assets/rust" + "github.com/somatech1/mikros-cli/internal/protobuf" "github.com/somatech1/mikros-cli/pkg/templates" ) @@ -23,31 +26,71 @@ func (e *Executioner) PreExecution(serviceName, destinationPath string) error { return nil } -func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, _ string, _ interface{}) (interface{}, error) { +func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, filename string, _ interface{}) (interface{}, error) { // Store values to be used later e.hasLifecycle = answers.RustLifecycle e.serviceType = answers.Type - return nil, nil + // Build the context + ctx := &Context{ + HasLifecycle: e.hasLifecycle, + ServiceName: answers.Name, + serviceType: answers.Type, + } + + if filename != "" { + pbFile, err := protobuf.Parse(filename) + if err != nil { + return Context{}, err + } + + ctx.ModuleName = pbFile.ModuleName + ctx.ServiceName = pbFile.ServiceName + + var methods []*Method + for _, m := range pbFile.Methods { + methods = append(methods, &Method{ + Name: m.Name, + RequestName: m.InputName, + ResponseName: m.OutputName, + }) + } + ctx.Methods = methods + } + + return ctx, nil } func (e *Executioner) Templates() []templates.TemplateFile { - return []templates.TemplateFile{} + tpls := []templates.TemplateFile{ + { + Name: "main", + Output: "src/main", + Extension: "rs", + }, + { + Name: "service", + Output: "src/service", + Extension: "rs", + }, + } + + return tpls } func (e *Executioner) Files() embed.FS { return rust.Files } -type dependency struct { - Name string - Version string - Git string - Path string - Feature []string -} - func (e *Executioner) PostExecution(destinationPath string) error { + type dependency struct { + Name string + Version string + Git string + Path string + Feature []string + } + dependencies := []dependency{ { Name: "mikros", @@ -94,5 +137,25 @@ func (e *Executioner) PostExecution(destinationPath string) error { } } + if err := formatCode(destinationPath); err != nil { + return err + } + return nil } + +func formatCode(destinationPath string) error { + return filepath.Walk(filepath.Join(destinationPath, "src"), func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if filepath.Ext(path) == ".rs" { + if err := rustFmt(path); err != nil { + return err + } + } + + return nil + }) +} diff --git a/internal/rust/cargo.go b/internal/rust/utils.go similarity index 82% rename from internal/rust/cargo.go rename to internal/rust/utils.go index 386786c..7693b6a 100644 --- a/internal/rust/cargo.go +++ b/internal/rust/utils.go @@ -21,7 +21,7 @@ func cargoInit(destinationPath, name string) error { _ = os.Chdir(cwd) }() - _, err = process.Exec("cargo", "init", name) + _, err = process.Exec("cargo", "init", "--quiet", name) return err } @@ -39,6 +39,7 @@ func cargoAdd(destinationPath, name, version, git, path string, features []strin args := []string{ "cargo", "add", + "--quiet", } if version != "" { args = append(args, fmt.Sprintf("%v@%v", name, version)) @@ -53,7 +54,11 @@ func cargoAdd(destinationPath, name, version, git, path string, features []strin args = append(args, "--features", strings.Join(features, ",")) } - out, err := process.Exec(args...) - println(string(out)) + _, err = process.Exec(args...) + return err +} + +func rustFmt(filename string) error { + _, err := process.Exec("rustfmt", "--edition", "2021", filename) return err } From bc16bff48ff3a29514647ab5e653b8b19b1f5530 Mon Sep 17 00:00:00 2001 From: Rodrigo Freitas Date: Fri, 7 Mar 2025 08:39:19 -0300 Subject: [PATCH 5/5] chore: rust support WIP --- internal/assets/rust/service.tmpl | 26 +++++++++----------------- internal/cmd/service/init.go | 2 -- internal/rust/executioner.go | 10 ++++++++-- internal/rust/utils.go | 6 ++++-- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/internal/assets/rust/service.tmpl b/internal/assets/rust/service.tmpl index f009540..71f9b10 100644 --- a/internal/assets/rust/service.tmpl +++ b/internal/assets/rust/service.tmpl @@ -17,28 +17,20 @@ use api::router::Router; use mikros::errors as merrors; use mikros::service::context::Context; {{- end}} -{{- if .IsHttpService}} -use axum::extract::State; -use mikros::http::ServiceState; -use mikros::Mutex; -{{- end}} -{{if .IsHttpService}} + #[derive(Clone, Default)] -{{- else }} -#[derive(Clone)] -{{- end}} pub struct Service; impl Service { {{- if .IsGrpcService}} pub fn new() -> {{.ServiceName}}Server { - let service = Arc::new(Self{}); + let service = Arc::new(Self::default()); {{.ServiceName}}Server::from_arc(service) } {{- end}} {{- if or .IsScriptService .IsNativeService}} pub fn new() -> Self { - Self{} + Self::default() } {{- end}} {{- if .IsHttpService}} @@ -51,7 +43,7 @@ impl Service { {{if or .IsGrpcService .IsHttpService -}}{{$moduleName := .ModuleName}} #[tonic::async_trait] -impl {{.ServiceName}} for Service { +impl {{$moduleName}}::{{.ServiceName}} for Service { {{- range .Methods}} async fn {{toSnake .Name}}( &self, @@ -65,11 +57,11 @@ impl {{.ServiceName}} for Service { {{- if .IsScriptService}} #[async_trait::async_trait] impl mikros::service::script::ScriptService for Service { - async fn run(&self, ctx: &Context) -> merrors::Result<()> { + async fn run(&self, ctx: Arc) -> merrors::Result<()> { todo!() } - async fn cleanup(&self, ctx: &Context) { + async fn cleanup(&self, ctx: Arc) { todo!() } } @@ -77,11 +69,11 @@ impl mikros::service::script::ScriptService for Service { {{- if .IsNativeService}} #[async_trait::async_trait] impl mikros::service::native::NativeService for Service { - async fn start(&self, ctx: &Context) -> merrors::Result<()> { + async fn start(&self, ctx: Arc) -> merrors::Result<()> { todo!() } - async fn stop(&self, ctx: &Context) { + async fn stop(&self, ctx: Arc) { todo!() } } @@ -90,7 +82,7 @@ impl mikros::service::native::NativeService for Service { {{if .HasLifecycle}} #[async_trait::async_trait] impl mikros::service::lifecycle::Lifecycle for Service { - async fn on_start(&mut self, ctx: &Context) -> merrors::Result<()> { + async fn on_start(&mut self, ctx: Arc) -> merrors::Result<()> { todo!() } diff --git a/internal/cmd/service/init.go b/internal/cmd/service/init.go index 1330917..07e9e2a 100644 --- a/internal/cmd/service/init.go +++ b/internal/cmd/service/init.go @@ -88,12 +88,10 @@ func generateTemplates(options *InitOptions, answers *answers.InitSurveyAnswers) return err } - // creates go source templates if err := generateSources(destinationPath, options, answers, executioner); err != nil { return err } - // Creates the service.toml file if err := writeServiceDefinitions(destinationPath, answers); err != nil { return err } diff --git a/internal/rust/executioner.go b/internal/rust/executioner.go index 9b5bf46..ed23eb5 100644 --- a/internal/rust/executioner.go +++ b/internal/rust/executioner.go @@ -4,6 +4,7 @@ import ( "embed" "os" "path/filepath" + "strings" "github.com/somatech1/mikros/components/definition" @@ -44,8 +45,8 @@ func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, filena return Context{}, err } - ctx.ModuleName = pbFile.ModuleName - ctx.ServiceName = pbFile.ServiceName + ctx.ModuleName = getServiceNameOnly(pbFile.ModuleName) + ctx.ServiceName = getServiceNameOnly(pbFile.ServiceName) var methods []*Method for _, m := range pbFile.Methods { @@ -61,6 +62,11 @@ func (e *Executioner) GenerateContext(answers *answers.InitSurveyAnswers, filena return ctx, nil } +func getServiceNameOnly(serviceName string) string { + parts := strings.Split(serviceName, ".") + return parts[len(parts)-1] +} + func (e *Executioner) Templates() []templates.TemplateFile { tpls := []templates.TemplateFile{ { diff --git a/internal/rust/utils.go b/internal/rust/utils.go index 7693b6a..9cedb04 100644 --- a/internal/rust/utils.go +++ b/internal/rust/utils.go @@ -54,11 +54,13 @@ func cargoAdd(destinationPath, name, version, git, path string, features []strin args = append(args, "--features", strings.Join(features, ",")) } - _, err = process.Exec(args...) + out, err := process.Exec(args...) + println(string(out)) return err } func rustFmt(filename string) error { - _, err := process.Exec("rustfmt", "--edition", "2021", filename) + out, err := process.Exec("rustfmt", "--edition", "2021", filename) + println(string(out)) return err }