diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go
index 23310c0..6c7f7a4 100644
--- a/cmd/cli/cli.go
+++ b/cmd/cli/cli.go
@@ -13,6 +13,7 @@ func main() {
webappCmd(),
tailwindCmd(),
setupCmd(),
+ vueGenCmd(),
}
app := &cli.App{
diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go
index 796057f..50c5b36 100644
--- a/cmd/cli/commands.go
+++ b/cmd/cli/commands.go
@@ -8,6 +8,7 @@ import (
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
+ vue_gen "masonry/vue-gen"
"os"
"os/exec"
"runtime"
@@ -16,10 +17,13 @@ import (
)
//go:embed templates/proto/application.proto.tmpl
-var protoTemplate string
+var protoTemplateSrc string
//go:embed templates/backend/main.go.tmpl
-var mainGoTemplate string
+var mainGoTemplateSrc string
+
+//go:embed templates/backend/gitignore.tmpl
+var gitignoreTemplateSrc string
//go:embed proto_include/*
var protoInclude embed.FS
@@ -98,12 +102,26 @@ func createCmd() *cli.Command {
titleMaker := cases.Title(language.English)
// render the main.go file from the template
- goTemplate := template.Must(template.New("main").Parse(mainGoTemplate))
+ goTemplate := template.Must(template.New("main").Parse(mainGoTemplateSrc))
err = goTemplate.Execute(mainFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName)})
if err != nil {
return fmt.Errorf("error rendering main.go file | %w", err)
}
+ // create a gitignore file
+ gitignoreFile, err := os.Create(".gitignore")
+ if err != nil {
+ return fmt.Errorf("error creating gitignore file | %w", err)
+ }
+ defer gitignoreFile.Close()
+
+ // render the gitignore file from the template
+ gitignoreTemplate := template.Must(template.New("gitignore").Parse(gitignoreTemplateSrc))
+ err = gitignoreTemplate.Execute(gitignoreFile, nil)
+ if err != nil {
+ return fmt.Errorf("error rendering gitignore file | %w", err)
+ }
+
// create a proto file
protoFile, err := os.Create("proto/service.proto")
if err != nil {
@@ -112,7 +130,7 @@ func createCmd() *cli.Command {
defer protoFile.Close()
// render the proto file from the template
- t := template.Must(template.New("proto").Parse(protoTemplate))
+ t := template.Must(template.New("proto").Parse(protoTemplateSrc))
err = t.Execute(protoFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName), "ObjName": "Product"})
if err != nil {
return fmt.Errorf("error rendering proto file | %w", err)
@@ -207,24 +225,24 @@ func generateCmd() *cli.Command {
return fmt.Errorf("error generating typescript code | %w", err)
}
- // if on windows
- if runtime.GOOS == "windows" {
- //cmd = exec.Command("npx", "protoc", "--plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd", "--ts_proto_out=./src/generated", "--ts_proto_opt=outputServices=generic,esModuleInterop=true", "--proto_path=../proto", "--proto_path=../include", "../proto/service.proto")
- //cmd.Stdout = os.Stdout
- //cmd.Stderr = os.Stderr
- //err = cmd.Run()
- //if err != nil {
- // return fmt.Errorf("error generating typescript code | %w", err)
- //}
- } else {
- //cmd = exec.Command("npx", "protoc", "--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto", "--ts_proto_out=./src/generated", "--ts_proto_opt=outputServices=generic,esModuleInterop=true", "--proto_path=../proto", "--proto_path=../include", "../proto/service.proto")
- //cmd.Stdout = os.Stdout
- //cmd.Stderr = os.Stderr
- //err = cmd.Run()
- //if err != nil {
- // return fmt.Errorf("error generating typescript code | %w", err)
- //}
+ // make sure src/generated-components exists
+ err = os.Mkdir("src/generated-components", 0755)
+ if err != nil {
+ return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
+
+ // generate vue components
+ err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-components")
+ if err != nil {
+ return fmt.Errorf("error generating vue components | %w", err)
+ }
+
+ cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
+ err = cmd.Run()
+ if err != nil {
+ return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
+ }
+
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
@@ -299,3 +317,37 @@ func setupCmd() *cli.Command {
},
}
}
+
+func vueGenCmd() *cli.Command {
+ return &cli.Command{
+ Name: "vuegen",
+ Aliases: []string{"vg"},
+ Usage: "Generate vue components based on a swagger file",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "input",
+ Aliases: []string{"i"},
+ Usage: "The input swagger file",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "output",
+ Aliases: []string{"o"},
+ Usage: "The output directory",
+ Required: true,
+ },
+ },
+ Action: func(c *cli.Context) error {
+ fmt.Println("Generating vue components")
+ input := c.String("input")
+ output := c.String("output")
+
+ err := vue_gen.GenVueFromSwagger(input, output)
+ if err != nil {
+ return fmt.Errorf("error generating vue components | %w", err)
+ }
+
+ return nil
+ },
+ }
+}
diff --git a/cmd/cli/templates/backend/gitignore.tmpl b/cmd/cli/templates/backend/gitignore.tmpl
new file mode 100644
index 0000000..b11c694
--- /dev/null
+++ b/cmd/cli/templates/backend/gitignore.tmpl
@@ -0,0 +1,3 @@
+local.db
+gen/
+webapp/src/generated/
\ No newline at end of file
diff --git a/cmd/cli/templates/backend/main.go.tmpl b/cmd/cli/templates/backend/main.go.tmpl
index bc1cfda..7517726 100644
--- a/cmd/cli/templates/backend/main.go.tmpl
+++ b/cmd/cli/templates/backend/main.go.tmpl
@@ -1,7 +1,7 @@
package main
import (
- "context"
+ "context"
"log"
"net"
"net/http"
@@ -9,40 +9,63 @@ import (
"time"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
- "github.com/payne8/go-libsql-dual-driver"
"github.com/rs/cors"
- sqlite "github.com/ytsruh/gorm-libsql"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
"gorm.io/gorm"
- pb "{{ .AppName }}/gen/go"
+ pb "{{ .AppName }}/gen/go"
+
+ // the following line is used for the local database
+ "gorm.io/driver/sqlite"
+ // uncomment the following lines to switch to a production ready remote database
+ //libsqldb "github.com/payne8/go-libsql-dual-driver"
+ //sqlite "github.com/ytsruh/gorm-libsql"
)
func main() {
logger := log.New(os.Stdout, "{{ .AppName }} ", log.LstdFlags)
- primaryUrl := os.Getenv("LIBSQL_DATABASE_URL")
- authToken := os.Getenv("LIBSQL_AUTH_TOKEN")
- tdb, err := libsqldb.NewLibSqlDB(
- primaryUrl,
- // libsqldb.WithMigrationFiles(migrationFiles),
- libsqldb.WithAuthToken(authToken),
- libsqldb.WithLocalDBName("local.db"), // will not be used for remote-only
- )
+ // instantiate the grom ORM
+
+ /*
+ uncomment the following code to switch to a production ready remote database
+ you will need to set the environment variables
+ */
+
+ // --------------------- start of remote database code ---------------------
+ //primaryUrl := os.Getenv("LIBSQL_DATABASE_URL")
+ //authToken := os.Getenv("LIBSQL_AUTH_TOKEN")
+
+ //tdb, err := libsqldb.NewLibSqlDB(
+ // primaryUrl,
+ // //libsqldb.WithMigrationFiles(migrationFiles),
+ // libsqldb.WithAuthToken(authToken),
+ // libsqldb.WithLocalDBName("local.db"), // will not be used for remote-only
+ //)
+ //if err != nil {
+ // logger.Printf("failed to open db %s: %s", primaryUrl, err)
+ // log.Fatalln(err)
+ // return
+ //}
+
+ //gormDB, err := gorm.Open(sqlite.New(sqlite.Config{Conn: tdb.DB}), &gorm.Config{})
+ //if err != nil {
+ // logger.Printf("failed to open gorm db %s: %s", primaryUrl, err)
+ // log.Fatalln(err)
+ // return
+ //}
+
+ // --------------------- end of remote database code ---------------------
+
+ // -- start of local database code --
+ gormDB, err := gorm.Open(sqlite.Open("local.db"), &gorm.Config{})
if err != nil {
- logger.Printf("failed to open db %s: %s", primaryUrl, err)
- log.Fatalln(err)
- return
- }
-
- // Instantiate the gorm ORM
- gormDB, err := gorm.Open(sqlite.New(sqlite.Config{Conn: tdb.DB}), &gorm.Config{})
- if err != nil {
- logger.Printf("failed to open gorm db %s: %s", primaryUrl, err)
+ logger.Printf("failed to open gorm db: %s", err)
log.Fatalln(err)
return
}
+ // -- end of local database code --
// Uncomment these lines if you need automatic migration
// err = gormDB.AutoMigrate(&pb.UserORM{})
diff --git a/vue-gen/vue-gen.go b/vue-gen/vue-gen.go
new file mode 100644
index 0000000..aa53dab
--- /dev/null
+++ b/vue-gen/vue-gen.go
@@ -0,0 +1,409 @@
+package vue_gen
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "text/template"
+)
+
+// ----- Structures for swagger file parsing -----
+
+// Swagger holds both Definitions and Paths.
+type Swagger struct {
+ Definitions map[string]Definition `json:"definitions"`
+ Paths map[string]map[string]OperationObject `json:"paths"`
+}
+
+// Definition represents a swagger definition.
+type Definition struct {
+ Type string `json:"type"`
+ Properties map[string]Property `json:"properties"`
+ Required []string `json:"required"`
+}
+
+// Property represents one field of a swagger definition.
+type Property struct {
+ Type string `json:"type,omitempty"`
+ Format string `json:"format,omitempty"`
+ Description string `json:"description,omitempty"`
+ Minimum *float64 `json:"minimum,omitempty"`
+ Maximum *float64 `json:"maximum,omitempty"`
+ Enum []string `json:"enum,omitempty"`
+ Ref string `json:"$ref,omitempty"`
+ Items *Property `json:"items,omitempty"`
+ Properties map[string]Property `json:"properties,omitempty"` // For inline object definitions.
+}
+
+// OperationObject represents an operation (like get, post, put, etc.) from swagger Paths.
+type OperationObject struct {
+ OperationID string `json:"operationId"`
+ Parameters []Parameter `json:"parameters,omitempty"`
+}
+
+// Parameter represents a swagger parameter. We are interested in "body" parameters.
+type Parameter struct {
+ In string `json:"in"`
+ Name string `json:"name"`
+ Schema *SchemaObject `json:"schema,omitempty"`
+}
+
+// SchemaObject represents a schema reference.
+type SchemaObject struct {
+ Ref string `json:"$ref,omitempty"`
+}
+
+// OperationInfo is used to store the service call information for a given request payload.
+type OperationInfo struct {
+ ServiceName string // e.g., "PeachService"
+ MethodName string // e.g., "peachListProducts"
+ HTTPMethod string // e.g., "post", "put", etc.
+}
+
+// ----- Structures for Vue component generation -----
+
+type FormInput struct {
+ ID string // id in the Vue interface
+ Label string // label shown to the user
+ Type string // the input type, e.g., text, number, select, checkbox, etc.
+ DefaultValue string // optional default value (as string)
+ Placeholder string // optional placeholder
+ Required bool // required field?
+ Min string // string representation of minimum
+ Max string // string representation of maximum
+ Options []Option // for select or similar
+}
+
+type Option struct {
+ Label string // option label
+ Value string // option value
+}
+
+// VueComponentData holds the data passed to our Vue template.
+type VueComponentData struct {
+ // ComponentName is the request definition name with "Form" appended (capital F).
+ ComponentName string
+ // DefaultFormTitle is used if no formTitle prop is provided.
+ DefaultFormTitle string
+ SubmitLabel string // label for the submit button
+ FormInputs []FormInput // form inputs computed from the schema
+
+ // Operation properties (if found)
+ OperationFound bool
+ ServiceName string // e.g., "PeachService"
+ ServiceMethod string // e.g., "peachListProducts"
+ OperationHTTPMethod string // e.g., "post", "put", etc.
+}
+
+func GenVueFromSwagger(swaggerPath string, outDir string) error {
+ // Read the swagger JSON file.
+ data, err := os.ReadFile(swaggerPath)
+ if err != nil {
+ return fmt.Errorf("error reading swagger file: %v", err)
+ }
+
+ // Unmarshal the swagger file.
+ var swagger Swagger
+ if err := json.Unmarshal(data, &swagger); err != nil {
+ return fmt.Errorf("Error parsing swagger JSON: %v\n", err)
+ }
+
+ // Build a mapping from definition name (used in a body parameter) to OperationInfo.
+ operationMap := buildOperationMap(swagger.Paths)
+
+ // Ensure output directory exists.
+ if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
+ return fmt.Errorf("Error creating output directory: %v\n", err)
+ }
+
+ // Generate forms only for definitions used as payload (i.e., in the operationMap).
+ for defName, def := range swagger.Definitions {
+ // Skip definitions that are not used as request bodies.
+ opInfo, ok := operationMap[defName]
+ if !ok {
+ continue
+ }
+
+ componentName := defName + "Form" // Append "Form" with a capital F.
+ componentData := VueComponentData{
+ ComponentName: componentName,
+ DefaultFormTitle: defName,
+ SubmitLabel: "Submit",
+ FormInputs: []FormInput{},
+ OperationFound: true,
+ ServiceName: opInfo.ServiceName,
+ ServiceMethod: opInfo.MethodName,
+ OperationHTTPMethod: opInfo.HTTPMethod,
+ }
+
+ // Get the schema to use for the form.
+ // If a 'payload' property exists, unwrap it.
+ schemaForForm := getFormSchema(def, swagger.Definitions)
+
+ // Process each property from the chosen schema.
+ for propName, prop := range schemaForForm {
+ // Only skip the "id" field for create requests (which we assume use the POST method).
+ if strings.ToLower(propName) == "id" && strings.ToLower(componentData.OperationHTTPMethod) == "post" {
+ continue
+ }
+ input := FormInput{
+ ID: propName,
+ Label: labelFromProperty(prop, propName),
+ Placeholder: "",
+ Required: isRequired(propName, def.Required),
+ }
+
+ // Set the proper type and constraints.
+ if prop.Ref != "" {
+ refParts := strings.Split(prop.Ref, "/")
+ refName := refParts[len(refParts)-1]
+ input.Type = "text"
+ input.Placeholder = "Reference: " + refName
+ } else if len(prop.Enum) > 0 {
+ input.Type = "select"
+ for _, enumVal := range prop.Enum {
+ input.Options = append(input.Options, Option{
+ Label: enumVal,
+ Value: enumVal,
+ })
+ }
+ } else if prop.Type == "string" {
+ input.Type = "text"
+ if prop.Format == "date" {
+ input.Type = "date"
+ } else if prop.Format == "email" {
+ input.Type = "email"
+ }
+ } else if prop.Type == "number" || prop.Type == "integer" {
+ input.Type = "number"
+ if prop.Minimum != nil {
+ input.Min = strconv.FormatFloat(*prop.Minimum, 'f', -1, 64)
+ }
+ if prop.Maximum != nil {
+ input.Max = strconv.FormatFloat(*prop.Maximum, 'f', -1, 64)
+ }
+ } else if prop.Type == "boolean" {
+ input.Type = "checkbox"
+ } else if prop.Type == "array" && prop.Items != nil {
+ if len(prop.Items.Enum) > 0 {
+ input.Type = "select"
+ for _, enumVal := range prop.Items.Enum {
+ input.Options = append(input.Options, Option{
+ Label: enumVal,
+ Value: enumVal,
+ })
+ }
+ } else {
+ input.Type = "text"
+ input.Placeholder = "Comma separated values"
+ }
+ } else {
+ input.Type = "text"
+ }
+
+ componentData.FormInputs = append(componentData.FormInputs, input)
+ }
+
+ // Render the Vue component.
+ if err := renderVueComponent(componentData, outDir); err != nil {
+ return fmt.Errorf("Error generating component for %s: %v\n", defName, err)
+ } else {
+ fmt.Printf("Successfully generated request form component for %s\n", componentName)
+ }
+ }
+ return nil
+}
+
+// getFormSchema checks if the given definition has a 'payload' property.
+// If present and the payload property either has inline properties or a $ref,
+// it returns the property map from that object. Otherwise, it returns the top-level properties.
+func getFormSchema(def Definition, allDefs map[string]Definition) map[string]Property {
+ if payloadProp, exists := def.Properties["payload"]; exists {
+ if len(payloadProp.Properties) > 0 {
+ return payloadProp.Properties
+ } else if payloadProp.Ref != "" {
+ refParts := strings.Split(payloadProp.Ref, "/")
+ refName := refParts[len(refParts)-1]
+ if refDef, ok := allDefs[refName]; ok {
+ return refDef.Properties
+ }
+ }
+ }
+ return def.Properties
+}
+
+// buildOperationMap scans swagger paths and returns a mapping from the referenced definition
+// (in a body parameter) to an OperationInfo that holds service call details.
+// It considers the HTTP verb (key) to help decide whether a request is a create request.
+func buildOperationMap(paths map[string]map[string]OperationObject) map[string]OperationInfo {
+ opMap := make(map[string]OperationInfo)
+ for _, methods := range paths {
+ for httpMethod, op := range methods {
+ // Look for a "body" parameter.
+ for _, param := range op.Parameters {
+ if strings.ToLower(param.In) == "body" && param.Schema != nil && param.Schema.Ref != "" {
+ // Get the definition name referenced in the schema.
+ refParts := strings.Split(param.Schema.Ref, "/")
+ defName := refParts[len(refParts)-1]
+ if op.OperationID != "" {
+ // Assume operationID is something like "Peach_ListProducts" for requests.
+ parts := strings.Split(op.OperationID, "_")
+ serviceName := ""
+ methodName := ""
+ if len(parts) >= 2 {
+ serviceName = parts[0] + "Service"
+ // Remove underscores and lower-case the first letter for method name.
+ noUnderscore := strings.ReplaceAll(op.OperationID, "_", "")
+ methodName = lowerFirst(noUnderscore)
+ }
+ if serviceName != "" && methodName != "" {
+ opMap[defName] = OperationInfo{
+ ServiceName: serviceName,
+ MethodName: methodName,
+ HTTPMethod: strings.ToLower(httpMethod),
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return opMap
+}
+
+// lowerFirst lower-cases the first letter of the given string.
+func lowerFirst(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+ return strings.ToLower(s[:1]) + s[1:]
+}
+
+// labelFromProperty returns a label derived from the property's description or capitalizes the property name.
+func labelFromProperty(prop Property, propName string) string {
+ if prop.Description != "" {
+ return prop.Description
+ }
+ return strings.ToUpper(propName[:1]) + propName[1:]
+}
+
+// isRequired checks whether the given property is among the required fields.
+func isRequired(propName string, required []string) bool {
+ for _, r := range required {
+ if r == propName {
+ return true
+ }
+ }
+ return false
+}
+
+// renderVueComponent uses a Go text/template to render a Vue component file.
+func renderVueComponent(data VueComponentData, outDir string) error {
+ // The template now:
+ // • Imports DynamicForm from "@masonitestudios/dynamic-vue"
+ // • Declares emits and calls emit('send', payload) on a successful network call.
+ // • Wraps the form values under a "payload" key.
+ // • Accepts a new prop, initialValues, and maps those into each FormInput's defaultValue.
+ const vueTemplate = `
+
+
+
+
+`
+ // Create a template with a helper to add one (for indexing).
+ tmpl := template.New("vueComponent").Funcs(template.FuncMap{
+ "add1": func(i int) int { return i + 1 },
+ })
+ tmpl, err := tmpl.Parse(vueTemplate)
+ if err != nil {
+ return err
+ }
+
+ filename := filepath.Join(outDir, data.ComponentName+".vue")
+ outFile, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer outFile.Close()
+
+ return tmpl.Execute(outFile, data)
+}