add ability to generate vue componenets based on swagger
This commit is contained in:
@ -13,6 +13,7 @@ func main() {
|
|||||||
webappCmd(),
|
webappCmd(),
|
||||||
tailwindCmd(),
|
tailwindCmd(),
|
||||||
setupCmd(),
|
setupCmd(),
|
||||||
|
vueGenCmd(),
|
||||||
}
|
}
|
||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
vue_gen "masonry/vue-gen"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -16,10 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/proto/application.proto.tmpl
|
//go:embed templates/proto/application.proto.tmpl
|
||||||
var protoTemplate string
|
var protoTemplateSrc string
|
||||||
|
|
||||||
//go:embed templates/backend/main.go.tmpl
|
//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/*
|
//go:embed proto_include/*
|
||||||
var protoInclude embed.FS
|
var protoInclude embed.FS
|
||||||
@ -98,12 +102,26 @@ func createCmd() *cli.Command {
|
|||||||
titleMaker := cases.Title(language.English)
|
titleMaker := cases.Title(language.English)
|
||||||
|
|
||||||
// render the main.go file from the template
|
// 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)})
|
err = goTemplate.Execute(mainFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error rendering main.go file | %w", err)
|
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
|
// create a proto file
|
||||||
protoFile, err := os.Create("proto/service.proto")
|
protoFile, err := os.Create("proto/service.proto")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -112,7 +130,7 @@ func createCmd() *cli.Command {
|
|||||||
defer protoFile.Close()
|
defer protoFile.Close()
|
||||||
|
|
||||||
// render the proto file from the template
|
// 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"})
|
err = t.Execute(protoFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName), "ObjName": "Product"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error rendering proto file | %w", err)
|
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)
|
return fmt.Errorf("error generating typescript code | %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if on windows
|
// make sure src/generated-components exists
|
||||||
if runtime.GOOS == "windows" {
|
err = os.Mkdir("src/generated-components", 0755)
|
||||||
//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")
|
if err != nil {
|
||||||
//cmd.Stdout = os.Stdout
|
return fmt.Errorf("error creating src/generated-components directory | %w", err)
|
||||||
//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)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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("..")
|
err = os.Chdir("..")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error changing directory back to root | %w", err)
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3
cmd/cli/templates/backend/gitignore.tmpl
Normal file
3
cmd/cli/templates/backend/gitignore.tmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
local.db
|
||||||
|
gen/
|
||||||
|
webapp/src/generated/
|
@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -9,40 +9,63 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
"github.com/payne8/go-libsql-dual-driver"
|
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
sqlite "github.com/ytsruh/gorm-libsql"
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
"gorm.io/gorm"
|
"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() {
|
func main() {
|
||||||
logger := log.New(os.Stdout, "{{ .AppName }} ", log.LstdFlags)
|
logger := log.New(os.Stdout, "{{ .AppName }} ", log.LstdFlags)
|
||||||
primaryUrl := os.Getenv("LIBSQL_DATABASE_URL")
|
|
||||||
authToken := os.Getenv("LIBSQL_AUTH_TOKEN")
|
|
||||||
|
|
||||||
tdb, err := libsqldb.NewLibSqlDB(
|
// instantiate the grom ORM
|
||||||
primaryUrl,
|
|
||||||
// libsqldb.WithMigrationFiles(migrationFiles),
|
/*
|
||||||
libsqldb.WithAuthToken(authToken),
|
uncomment the following code to switch to a production ready remote database
|
||||||
libsqldb.WithLocalDBName("local.db"), // will not be used for remote-only
|
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 {
|
if err != nil {
|
||||||
logger.Printf("failed to open db %s: %s", primaryUrl, err)
|
logger.Printf("failed to open gorm db: %s", 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)
|
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// -- end of local database code --
|
||||||
|
|
||||||
// Uncomment these lines if you need automatic migration
|
// Uncomment these lines if you need automatic migration
|
||||||
// err = gormDB.AutoMigrate(&pb.UserORM{})
|
// err = gormDB.AutoMigrate(&pb.UserORM{})
|
||||||
|
409
vue-gen/vue-gen.go
Normal file
409
vue-gen/vue-gen.go
Normal file
@ -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 = `<template>
|
||||||
|
<DynamicForm
|
||||||
|
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||||
|
:formInputs="updatedFormInputs"
|
||||||
|
:formTitle="formTitleComputed"
|
||||||
|
submit-label="{{ .SubmitLabel }}"
|
||||||
|
@send="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||||
|
{{- if .OperationFound }}
|
||||||
|
import { {{ .ServiceName }} } from '@/generated';
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
// For update forms, we allow passing in initial values.
|
||||||
|
// These values will be mapped onto the defaultValue field of each form input.
|
||||||
|
const props = defineProps<{
|
||||||
|
formTitle?: string,
|
||||||
|
// Pass in an object with initial field values for update requests.
|
||||||
|
initialValues?: Record<string, any>
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['send']);
|
||||||
|
|
||||||
|
const formTitleComputed = computed(() => props.formTitle || '{{ .DefaultFormTitle }}');
|
||||||
|
|
||||||
|
// The formInputs array was generated from our Go code.
|
||||||
|
const formInputs = [
|
||||||
|
{{- range $index, $input := .FormInputs }}
|
||||||
|
{
|
||||||
|
id: '{{ $input.ID }}',
|
||||||
|
label: '{{ $input.Label }}',
|
||||||
|
type: '{{ $input.Type }}',
|
||||||
|
placeholder: '{{ $input.Placeholder }}',
|
||||||
|
required: {{ $input.Required }},
|
||||||
|
defaultValue: '{{ $input.DefaultValue }}',
|
||||||
|
{{- if $input.Min }} min: '{{ $input.Min }}',{{ end }}
|
||||||
|
{{- if $input.Max }} max: '{{ $input.Max }}',{{ end }}
|
||||||
|
{{- if $input.Options }}
|
||||||
|
options: [
|
||||||
|
{{- range $optIndex, $opt := $input.Options }}
|
||||||
|
{ label: '{{ $opt.Label }}', value: '{{ $opt.Value }}' }{{ if not (eq (add1 $optIndex) (len $input.Options)) }},{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
],
|
||||||
|
{{- end }}
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
||||||
|
const updatedFormInputs = computed(() => {
|
||||||
|
return formInputs.map(input => {
|
||||||
|
// If props.initialValues is passed in and contains a value for input.id, use it.
|
||||||
|
// Otherwise, fall back to the defaultValue generated (or an empty string).
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(payload: any) {
|
||||||
|
// Wrap the form values into a "payload" key.
|
||||||
|
const requestData = { payload: payload };
|
||||||
|
{{- if .OperationFound }}
|
||||||
|
{{ .ServiceName }}.{{ .ServiceMethod }}(requestData)
|
||||||
|
.then(response => {
|
||||||
|
// Emit an event with the payload after a successful call.
|
||||||
|
emit('send', requestData.payload);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Service error:', error);
|
||||||
|
});
|
||||||
|
{{- else }}
|
||||||
|
emit('send', requestData.payload);
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
// 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)
|
||||||
|
}
|
Reference in New Issue
Block a user