add support for dynamic tables
This commit is contained in:
@ -225,14 +225,14 @@ func generateCmd() *cli.Command {
|
|||||||
return fmt.Errorf("error generating typescript code | %w", err)
|
return fmt.Errorf("error generating typescript code | %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure src/generated-components exists
|
// make sure src/generated-sample-components exists
|
||||||
err = os.Mkdir("src/generated-components", 0755)
|
err = os.Mkdir("src/generated-sample-components", 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating src/generated-components directory | %w", err)
|
return fmt.Errorf("error creating src/generated-components directory | %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate vue components
|
// generate vue components
|
||||||
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-components")
|
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error generating vue components | %w", err)
|
return fmt.Errorf("error generating vue components | %w", err)
|
||||||
}
|
}
|
||||||
@ -249,8 +249,6 @@ func generateCmd() *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: update typescript code gen to use this command `npx openapi-typescript-codegen --input ../gen/openapi/proto/service.swagger.json --output src/generated/ts-client --client fetch`
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,15 @@ import "google/api/annotations.proto";
|
|||||||
|
|
||||||
option go_package = "./;pb";
|
option go_package = "./;pb";
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||||
|
info: {
|
||||||
|
title: "Your API Title"
|
||||||
|
version: "v1.0"
|
||||||
|
description: "Your API description"
|
||||||
|
}
|
||||||
|
host: "localhost:8080" // Set the server host
|
||||||
|
};
|
||||||
|
|
||||||
service {{ .AppNameCaps }} {
|
service {{ .AppNameCaps }} {
|
||||||
option (gorm.server).autogen = true;
|
option (gorm.server).autogen = true;
|
||||||
// Add your service methods here
|
// Add your service methods here
|
||||||
|
@ -11,21 +11,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ----- Structures for swagger file parsing -----
|
// ----- Structures for swagger file parsing -----
|
||||||
|
//
|
||||||
|
// Note: In order to support list response scanning we add a Responses field
|
||||||
|
// to OperationObject and a new ResponseObject type.
|
||||||
|
|
||||||
// Swagger holds both Definitions and Paths.
|
|
||||||
type Swagger struct {
|
type Swagger struct {
|
||||||
Definitions map[string]Definition `json:"definitions"`
|
Definitions map[string]Definition `json:"definitions"`
|
||||||
Paths map[string]map[string]OperationObject `json:"paths"`
|
Paths map[string]map[string]OperationObject `json:"paths"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Definition represents a swagger definition.
|
|
||||||
type Definition struct {
|
type Definition struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Properties map[string]Property `json:"properties"`
|
Properties map[string]Property `json:"properties"`
|
||||||
Required []string `json:"required"`
|
Required []string `json:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Property represents one field of a swagger definition.
|
|
||||||
type Property struct {
|
type Property struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Format string `json:"format,omitempty"`
|
Format string `json:"format,omitempty"`
|
||||||
@ -35,46 +35,48 @@ type Property struct {
|
|||||||
Enum []string `json:"enum,omitempty"`
|
Enum []string `json:"enum,omitempty"`
|
||||||
Ref string `json:"$ref,omitempty"`
|
Ref string `json:"$ref,omitempty"`
|
||||||
Items *Property `json:"items,omitempty"`
|
Items *Property `json:"items,omitempty"`
|
||||||
Properties map[string]Property `json:"properties,omitempty"` // For inline object definitions.
|
Properties map[string]Property `json:"properties,omitempty"` // For inline definitions.
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperationObject represents an operation (like get, post, put, etc.) from swagger Paths.
|
|
||||||
type OperationObject struct {
|
type OperationObject struct {
|
||||||
OperationID string `json:"operationId"`
|
OperationID string `json:"operationId"`
|
||||||
Parameters []Parameter `json:"parameters,omitempty"`
|
Parameters []Parameter `json:"parameters,omitempty"`
|
||||||
|
Responses map[string]ResponseObject `json:"responses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameter represents a swagger parameter. We are interested in "body" parameters.
|
|
||||||
type Parameter struct {
|
type Parameter struct {
|
||||||
In string `json:"in"`
|
In string `json:"in"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Schema *SchemaObject `json:"schema,omitempty"`
|
Schema *SchemaObject `json:"schema,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchemaObject represents a schema reference.
|
|
||||||
type SchemaObject struct {
|
type SchemaObject struct {
|
||||||
Ref string `json:"$ref,omitempty"`
|
Ref string `json:"$ref,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperationInfo is used to store the service call information for a given request payload.
|
type ResponseObject struct {
|
||||||
|
Schema *SchemaObject `json:"schema,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperationInfo is used when generating a form component.
|
||||||
type OperationInfo struct {
|
type OperationInfo struct {
|
||||||
ServiceName string // e.g., "PeachService"
|
ServiceName string // e.g., "PeachService"
|
||||||
MethodName string // e.g., "peachListProducts"
|
MethodName string // e.g., "peachListProducts"
|
||||||
HTTPMethod string // e.g., "post", "put", etc.
|
HTTPMethod string // e.g., "post", "put", etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Structures for Vue component generation -----
|
// ----- Structures for Vue form generation -----
|
||||||
|
|
||||||
type FormInput struct {
|
type FormInput struct {
|
||||||
ID string // id in the Vue interface
|
ID string // id in the Vue interface
|
||||||
Label string // label shown to the user
|
Label string // label shown to the user
|
||||||
Type string // the input type, e.g., text, number, select, checkbox, etc.
|
Type string // e.g., text, number, select, checkbox, etc.
|
||||||
DefaultValue string // optional default value (as string)
|
DefaultValue string // optional default value (as string)
|
||||||
Placeholder string // optional placeholder
|
Placeholder string // optional placeholder
|
||||||
Required bool // required field?
|
Required bool // required flag
|
||||||
Min string // string representation of minimum
|
Min string // string representation of minimum
|
||||||
Max string // string representation of maximum
|
Max string // string representation of maximum
|
||||||
Options []Option // for select or similar
|
Options []Option // for select fields
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
@ -82,22 +84,44 @@ type Option struct {
|
|||||||
Value string // option value
|
Value string // option value
|
||||||
}
|
}
|
||||||
|
|
||||||
// VueComponentData holds the data passed to our Vue template.
|
|
||||||
type VueComponentData struct {
|
type VueComponentData struct {
|
||||||
// ComponentName is the request definition name with "Form" appended (capital F).
|
ComponentName string
|
||||||
ComponentName string
|
DefaultFormTitle string
|
||||||
// DefaultFormTitle is used if no formTitle prop is provided.
|
SubmitLabel string
|
||||||
DefaultFormTitle string
|
FormInputs []FormInput
|
||||||
SubmitLabel string // label for the submit button
|
|
||||||
FormInputs []FormInput // form inputs computed from the schema
|
|
||||||
|
|
||||||
// Operation properties (if found)
|
|
||||||
OperationFound bool
|
OperationFound bool
|
||||||
ServiceName string // e.g., "PeachService"
|
ServiceName string // e.g., "PeachService"
|
||||||
ServiceMethod string // e.g., "peachListProducts"
|
ServiceMethod string // e.g., "peachListProducts"
|
||||||
OperationHTTPMethod string // e.g., "post", "put", etc.
|
OperationHTTPMethod string // e.g., "post", "put", etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Structures for Vue Table generation -----
|
||||||
|
|
||||||
|
// TableColumn type maps to your supported TS interface.
|
||||||
|
type VueTableColumn struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
Type string // e.g., "text", "email", "number", etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
type VueTableData struct {
|
||||||
|
ComponentName string
|
||||||
|
DefaultTableTitle string
|
||||||
|
OperationFound bool
|
||||||
|
ServiceName string // e.g., "PeachService"
|
||||||
|
ServiceMethod string // e.g., "peachListProducts"
|
||||||
|
TableColumns []VueTableColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Main Generation Function -----
|
||||||
|
|
||||||
|
// GenVueFromSwagger reads the swagger spec and generates:
|
||||||
|
// - Form components for definitions used as request bodies,
|
||||||
|
// - Table components for list operations (matching the pattern Service_ListXYZ).
|
||||||
|
//
|
||||||
|
// For table components the generator dynamically creates the column definitions based on
|
||||||
|
// the response definition that is expected to have a "results" property referencing
|
||||||
|
// another definition.
|
||||||
func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
||||||
// Read the swagger JSON file.
|
// Read the swagger JSON file.
|
||||||
data, err := os.ReadFile(swaggerPath)
|
data, err := os.ReadFile(swaggerPath)
|
||||||
@ -119,15 +143,14 @@ func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
|||||||
return fmt.Errorf("Error creating output directory: %v\n", err)
|
return fmt.Errorf("Error creating output directory: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate forms only for definitions used as payload (i.e., in the operationMap).
|
// ----- Generate Form Components for Request Payloads -----
|
||||||
for defName, def := range swagger.Definitions {
|
for defName, def := range swagger.Definitions {
|
||||||
// Skip definitions that are not used as request bodies.
|
|
||||||
opInfo, ok := operationMap[defName]
|
opInfo, ok := operationMap[defName]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
componentName := defName + "Form" // Append "Form" with a capital F.
|
componentName := defName + "Form"
|
||||||
componentData := VueComponentData{
|
componentData := VueComponentData{
|
||||||
ComponentName: componentName,
|
ComponentName: componentName,
|
||||||
DefaultFormTitle: defName,
|
DefaultFormTitle: defName,
|
||||||
@ -139,16 +162,14 @@ func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
|||||||
OperationHTTPMethod: opInfo.HTTPMethod,
|
OperationHTTPMethod: opInfo.HTTPMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the schema to use for the form.
|
|
||||||
// If a 'payload' property exists, unwrap it.
|
|
||||||
schemaForForm := getFormSchema(def, swagger.Definitions)
|
schemaForForm := getFormSchema(def, swagger.Definitions)
|
||||||
|
|
||||||
// Process each property from the chosen schema.
|
|
||||||
for propName, prop := range schemaForForm {
|
for propName, prop := range schemaForForm {
|
||||||
// Only skip the "id" field for create requests (which we assume use the POST method).
|
// Skip "id" for create requests (assuming POST).
|
||||||
if strings.ToLower(propName) == "id" && strings.ToLower(componentData.OperationHTTPMethod) == "post" {
|
if strings.ToLower(propName) == "id" && strings.ToLower(componentData.OperationHTTPMethod) == "post" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
input := FormInput{
|
input := FormInput{
|
||||||
ID: propName,
|
ID: propName,
|
||||||
Label: labelFromProperty(prop, propName),
|
Label: labelFromProperty(prop, propName),
|
||||||
@ -156,7 +177,6 @@ func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
|||||||
Required: isRequired(propName, def.Required),
|
Required: isRequired(propName, def.Required),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the proper type and constraints.
|
|
||||||
if prop.Ref != "" {
|
if prop.Ref != "" {
|
||||||
refParts := strings.Split(prop.Ref, "/")
|
refParts := strings.Split(prop.Ref, "/")
|
||||||
refName := refParts[len(refParts)-1]
|
refName := refParts[len(refParts)-1]
|
||||||
@ -207,19 +227,66 @@ func GenVueFromSwagger(swaggerPath string, outDir string) error {
|
|||||||
componentData.FormInputs = append(componentData.FormInputs, input)
|
componentData.FormInputs = append(componentData.FormInputs, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the Vue component.
|
|
||||||
if err := renderVueComponent(componentData, outDir); err != nil {
|
if err := renderVueComponent(componentData, outDir); err != nil {
|
||||||
return fmt.Errorf("Error generating component for %s: %v\n", defName, err)
|
return fmt.Errorf("Error generating component for %s: %v\n", defName, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Successfully generated request form component for %s\n", componentName)
|
fmt.Printf("Successfully generated request form component for %s\n", componentName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Generate Table Components for List Operations -----
|
||||||
|
listOpsGenerated := make(map[string]bool)
|
||||||
|
for _, methods := range swagger.Paths {
|
||||||
|
for _, op := range methods {
|
||||||
|
if op.OperationID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check for operations using the _List pattern.
|
||||||
|
if strings.Contains(op.OperationID, "_List") {
|
||||||
|
if listOpsGenerated[op.OperationID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
listOpsGenerated[op.OperationID] = true
|
||||||
|
|
||||||
|
parts := strings.Split(op.OperationID, "_")
|
||||||
|
if len(parts) != 2 || !strings.HasPrefix(parts[1], "List") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := parts[0] + "Service"
|
||||||
|
serviceMethod := lowerFirst(strings.ReplaceAll(op.OperationID, "_", ""))
|
||||||
|
resourceName := parts[1][len("List"):] // e.g., "Products"
|
||||||
|
tableTitle := resourceName + " List"
|
||||||
|
componentName := op.OperationID + "Table"
|
||||||
|
|
||||||
|
// Instead of using the resourceName or definition name, we build table columns by
|
||||||
|
// looking at the response. We expect the response to have a "results" property that
|
||||||
|
// either directly references or is an array of a definition.
|
||||||
|
tableColumns := getResponseTableColumns(op, swagger.Definitions)
|
||||||
|
|
||||||
|
tableData := VueTableData{
|
||||||
|
ComponentName: componentName,
|
||||||
|
DefaultTableTitle: tableTitle,
|
||||||
|
OperationFound: true,
|
||||||
|
ServiceName: serviceName,
|
||||||
|
ServiceMethod: serviceMethod,
|
||||||
|
TableColumns: tableColumns,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := renderVueTableComponent(tableData, outDir); err != nil {
|
||||||
|
return fmt.Errorf("Error generating table component for %s: %v\n", op.OperationID, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Successfully generated table component for %s\n", componentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFormSchema checks if the given definition has a 'payload' property.
|
// getFormSchema checks whether a "payload" property exists.
|
||||||
// If present and the payload property either has inline properties or a $ref,
|
// If so, it will return its nested properties; otherwise, the top-level properties.
|
||||||
// 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 {
|
func getFormSchema(def Definition, allDefs map[string]Definition) map[string]Property {
|
||||||
if payloadProp, exists := def.Properties["payload"]; exists {
|
if payloadProp, exists := def.Properties["payload"]; exists {
|
||||||
if len(payloadProp.Properties) > 0 {
|
if len(payloadProp.Properties) > 0 {
|
||||||
@ -235,27 +302,110 @@ func getFormSchema(def Definition, allDefs map[string]Definition) map[string]Pro
|
|||||||
return def.Properties
|
return def.Properties
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildOperationMap scans swagger paths and returns a mapping from the referenced definition
|
// getResponseTableColumns builds the table columns based on the response schema for a list operation.
|
||||||
// (in a body parameter) to an OperationInfo that holds service call details.
|
// It looks for the 200 response, then expects its schema to reference a definition that has a "results" property.
|
||||||
// It considers the HTTP verb (key) to help decide whether a request is a create request.
|
// That property should either directly reference another definition (or, if an array, have items referencing one).
|
||||||
|
func getResponseTableColumns(op OperationObject, allDefs map[string]Definition) []VueTableColumn {
|
||||||
|
resp, ok := op.Responses["200"]
|
||||||
|
if !ok || resp.Schema == nil {
|
||||||
|
return defaultColumns()
|
||||||
|
}
|
||||||
|
// Get the definition of the response.
|
||||||
|
respDef := getDefinitionFromSchema(resp.Schema, allDefs)
|
||||||
|
if respDef == nil {
|
||||||
|
return defaultColumns()
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsProp, exists := respDef.Properties["results"]
|
||||||
|
if !exists {
|
||||||
|
return defaultColumns()
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetDef *Definition
|
||||||
|
// Either the results property has a direct reference…
|
||||||
|
if resultsProp.Ref != "" {
|
||||||
|
if def, ok := allDefs[getRefName(resultsProp.Ref)]; ok {
|
||||||
|
targetDef = &def
|
||||||
|
}
|
||||||
|
// … or it is an array whose items reference a definition.
|
||||||
|
} else if resultsProp.Type == "array" && resultsProp.Items != nil && resultsProp.Items.Ref != "" {
|
||||||
|
if def, ok := allDefs[getRefName(resultsProp.Items.Ref)]; ok {
|
||||||
|
targetDef = &def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetDef == nil {
|
||||||
|
return defaultColumns()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build table columns from the target definition's properties.
|
||||||
|
var cols []VueTableColumn
|
||||||
|
for propName, prop := range targetDef.Properties {
|
||||||
|
colType := "text"
|
||||||
|
if prop.Type == "number" || prop.Type == "integer" {
|
||||||
|
colType = "number"
|
||||||
|
} else if prop.Type == "boolean" {
|
||||||
|
colType = "checkbox"
|
||||||
|
} else if prop.Type == "string" {
|
||||||
|
colType = "text"
|
||||||
|
if prop.Format == "date" {
|
||||||
|
colType = "date"
|
||||||
|
} else if prop.Format == "email" {
|
||||||
|
colType = "email"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cols = append(cols, VueTableColumn{
|
||||||
|
ID: propName,
|
||||||
|
Label: strings.Title(propName),
|
||||||
|
Type: colType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(cols) == 0 {
|
||||||
|
return defaultColumns()
|
||||||
|
}
|
||||||
|
return cols
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultColumns() []VueTableColumn {
|
||||||
|
return []VueTableColumn{
|
||||||
|
{ID: "id", Label: "ID", Type: "text"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefinitionFromSchema retrieves a definition given a SchemaObject with a $ref.
|
||||||
|
func getDefinitionFromSchema(schema *SchemaObject, allDefs map[string]Definition) *Definition {
|
||||||
|
if schema == nil || schema.Ref == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
refName := getRefName(schema.Ref)
|
||||||
|
if def, ok := allDefs[refName]; ok {
|
||||||
|
return &def
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRefName extracts the definition name from a reference, e.g., "#/definitions/PeachProduct" -> "PeachProduct".
|
||||||
|
func getRefName(ref string) string {
|
||||||
|
parts := strings.Split(ref, "/")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOperationMap scans swagger paths and returns a mapping from the definition referenced in a body parameter
|
||||||
|
// to an OperationInfo.
|
||||||
func buildOperationMap(paths map[string]map[string]OperationObject) map[string]OperationInfo {
|
func buildOperationMap(paths map[string]map[string]OperationObject) map[string]OperationInfo {
|
||||||
opMap := make(map[string]OperationInfo)
|
opMap := make(map[string]OperationInfo)
|
||||||
for _, methods := range paths {
|
for _, methods := range paths {
|
||||||
for httpMethod, op := range methods {
|
for httpMethod, op := range methods {
|
||||||
// Look for a "body" parameter.
|
|
||||||
for _, param := range op.Parameters {
|
for _, param := range op.Parameters {
|
||||||
if strings.ToLower(param.In) == "body" && param.Schema != nil && param.Schema.Ref != "" {
|
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, "/")
|
refParts := strings.Split(param.Schema.Ref, "/")
|
||||||
defName := refParts[len(refParts)-1]
|
defName := refParts[len(refParts)-1]
|
||||||
if op.OperationID != "" {
|
if op.OperationID != "" {
|
||||||
// Assume operationID is something like "Peach_ListProducts" for requests.
|
|
||||||
parts := strings.Split(op.OperationID, "_")
|
parts := strings.Split(op.OperationID, "_")
|
||||||
serviceName := ""
|
serviceName := ""
|
||||||
methodName := ""
|
methodName := ""
|
||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
serviceName = parts[0] + "Service"
|
serviceName = parts[0] + "Service"
|
||||||
// Remove underscores and lower-case the first letter for method name.
|
|
||||||
noUnderscore := strings.ReplaceAll(op.OperationID, "_", "")
|
noUnderscore := strings.ReplaceAll(op.OperationID, "_", "")
|
||||||
methodName = lowerFirst(noUnderscore)
|
methodName = lowerFirst(noUnderscore)
|
||||||
}
|
}
|
||||||
@ -274,7 +424,6 @@ func buildOperationMap(paths map[string]map[string]OperationObject) map[string]O
|
|||||||
return opMap
|
return opMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// lowerFirst lower-cases the first letter of the given string.
|
|
||||||
func lowerFirst(s string) string {
|
func lowerFirst(s string) string {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return s
|
return s
|
||||||
@ -282,7 +431,6 @@ func lowerFirst(s string) string {
|
|||||||
return strings.ToLower(s[:1]) + s[1:]
|
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 {
|
func labelFromProperty(prop Property, propName string) string {
|
||||||
if prop.Description != "" {
|
if prop.Description != "" {
|
||||||
return prop.Description
|
return prop.Description
|
||||||
@ -290,7 +438,6 @@ func labelFromProperty(prop Property, propName string) string {
|
|||||||
return strings.ToUpper(propName[:1]) + propName[1:]
|
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 {
|
func isRequired(propName string, required []string) bool {
|
||||||
for _, r := range required {
|
for _, r := range required {
|
||||||
if r == propName {
|
if r == propName {
|
||||||
@ -300,13 +447,9 @@ func isRequired(propName string, required []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderVueComponent uses a Go text/template to render a Vue component file.
|
// ----- Rendering Templates -----
|
||||||
|
|
||||||
func renderVueComponent(data VueComponentData, outDir string) error {
|
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>
|
const vueTemplate = `<template>
|
||||||
<DynamicForm
|
<DynamicForm
|
||||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||||
@ -324,11 +467,8 @@ import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
|||||||
import { {{ .ServiceName }} } from '@/generated';
|
import { {{ .ServiceName }} } from '@/generated';
|
||||||
{{- end }}
|
{{- 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<{
|
const props = defineProps<{
|
||||||
formTitle?: string,
|
formTitle?: string,
|
||||||
// Pass in an object with initial field values for update requests.
|
|
||||||
initialValues?: Record<string, any>
|
initialValues?: Record<string, any>
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -336,7 +476,6 @@ const emit = defineEmits(['send']);
|
|||||||
|
|
||||||
const formTitleComputed = computed(() => props.formTitle || '{{ .DefaultFormTitle }}');
|
const formTitleComputed = computed(() => props.formTitle || '{{ .DefaultFormTitle }}');
|
||||||
|
|
||||||
// The formInputs array was generated from our Go code.
|
|
||||||
const formInputs = [
|
const formInputs = [
|
||||||
{{- range $index, $input := .FormInputs }}
|
{{- range $index, $input := .FormInputs }}
|
||||||
{
|
{
|
||||||
@ -350,8 +489,8 @@ const formInputs = [
|
|||||||
{{- if $input.Max }} max: '{{ $input.Max }}',{{ end }}
|
{{- if $input.Max }} max: '{{ $input.Max }}',{{ end }}
|
||||||
{{- if $input.Options }}
|
{{- if $input.Options }}
|
||||||
options: [
|
options: [
|
||||||
{{- range $optIndex, $opt := $input.Options }}
|
{{- range $i, $opt := $input.Options }}
|
||||||
{ label: '{{ $opt.Label }}', value: '{{ $opt.Value }}' }{{ if not (eq (add1 $optIndex) (len $input.Options)) }},{{ end }}
|
{ label: '{{ $opt.Label }}', value: '{{ $opt.Value }}' }{{ if not (eq (add1 $i) (len $input.Options)) }},{{ end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
],
|
],
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@ -359,11 +498,8 @@ const formInputs = [
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
|
||||||
const updatedFormInputs = computed(() => {
|
const updatedFormInputs = computed(() => {
|
||||||
return formInputs.map(input => {
|
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 {
|
return {
|
||||||
...input,
|
...input,
|
||||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||||
@ -372,12 +508,10 @@ const updatedFormInputs = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(payload: any) {
|
function handleSubmit(payload: any) {
|
||||||
// Wrap the form values into a "payload" key.
|
|
||||||
const requestData = { payload: payload };
|
const requestData = { payload: payload };
|
||||||
{{- if .OperationFound }}
|
{{- if .OperationFound }}
|
||||||
{{ .ServiceName }}.{{ .ServiceMethod }}(requestData)
|
{{ .ServiceName }}.{{ .ServiceMethod }}(requestData)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Emit an event with the payload after a successful call.
|
|
||||||
emit('send', requestData.payload);
|
emit('send', requestData.payload);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -389,7 +523,6 @@ function handleSubmit(payload: any) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
`
|
`
|
||||||
// Create a template with a helper to add one (for indexing).
|
|
||||||
tmpl := template.New("vueComponent").Funcs(template.FuncMap{
|
tmpl := template.New("vueComponent").Funcs(template.FuncMap{
|
||||||
"add1": func(i int) int { return i + 1 },
|
"add1": func(i int) int { return i + 1 },
|
||||||
})
|
})
|
||||||
@ -407,3 +540,79 @@ function handleSubmit(payload: any) {
|
|||||||
|
|
||||||
return tmpl.Execute(outFile, data)
|
return tmpl.Execute(outFile, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderVueTableComponent(data VueTableData, outDir string) error {
|
||||||
|
const vueTableTemplate = `<template>
|
||||||
|
<DynamicTable
|
||||||
|
class="bg-white shadow-lg rounded-lg p-6"
|
||||||
|
:tableTitle="tableTitleComputed"
|
||||||
|
:tableColumns="tableColumns"
|
||||||
|
:tableData="tableData"
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { DynamicTable } from '@masonitestudios/dynamic-vue';
|
||||||
|
import type { TableColumn } from '@masonitestudios/dynamic-vue';
|
||||||
|
{{- if .OperationFound }}
|
||||||
|
import { {{ .ServiceName }} } from '@/generated';
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tableTitle?: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['row-click']);
|
||||||
|
|
||||||
|
const tableColumns = [
|
||||||
|
{{- range $index, $col := .TableColumns }}
|
||||||
|
{ id: '{{ $col.ID }}', label: '{{ $col.Label }}', type: '{{ $col.Type }}' }{{ if not (eq (add1 $index) (len $.TableColumns)) }},{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData = ref<any[]>([]);
|
||||||
|
const tableTitleComputed = computed(() => '{{ .DefaultTableTitle }}');
|
||||||
|
{{- if .OperationFound }}
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
{{ .ServiceName }}.{{ .ServiceMethod }}().then((res: any) => {
|
||||||
|
if (res.results) {
|
||||||
|
tableData.value = res.results;
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching data:', res);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Service error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
defineExpose({fetchData});
|
||||||
|
|
||||||
|
function handleRowClick(row: any) {
|
||||||
|
emit('row-click', row);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
tmpl := template.New("vueTableComponent").Funcs(template.FuncMap{
|
||||||
|
"add1": func(i int) int { return i + 1 },
|
||||||
|
})
|
||||||
|
tmpl, err := tmpl.Parse(vueTableTemplate)
|
||||||
|
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