package vue_gen import ( "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "text/template" ) // ----- 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. type Swagger struct { Definitions map[string]Definition `json:"definitions"` Paths map[string]map[string]OperationObject `json:"paths"` } type Definition struct { Type string `json:"type"` Properties map[string]Property `json:"properties"` Required []string `json:"required"` } 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 definitions. } type OperationObject struct { OperationID string `json:"operationId"` Parameters []Parameter `json:"parameters,omitempty"` Responses map[string]ResponseObject `json:"responses,omitempty"` } type Parameter struct { In string `json:"in"` Name string `json:"name"` Schema *SchemaObject `json:"schema,omitempty"` } type SchemaObject struct { Ref string `json:"$ref,omitempty"` } type ResponseObject struct { Schema *SchemaObject `json:"schema,omitempty"` } // OperationInfo is used when generating a form component. type OperationInfo struct { ServiceName string // e.g., "PeachService" MethodName string // e.g., "peachListProducts" HTTPMethod string // e.g., "post", "put", etc. } // ----- Structures for Vue form generation ----- type FormInput struct { ID string // id in the Vue interface Label string // label shown to the user Type string // e.g., text, number, select, checkbox, etc. DefaultValue string // optional default value (as string) Placeholder string // optional placeholder Required bool // required flag Min string // string representation of minimum Max string // string representation of maximum Options []Option // for select fields } type Option struct { Label string // option label Value string // option value } type VueComponentData struct { ComponentName string DefaultFormTitle string SubmitLabel string FormInputs []FormInput OperationFound bool ServiceName string // e.g., "PeachService" ServiceMethod string // e.g., "peachListProducts" 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 { // 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 Form Components for Request Payloads ----- for defName, def := range swagger.Definitions { opInfo, ok := operationMap[defName] if !ok { continue } componentName := defName + "Form" componentData := VueComponentData{ ComponentName: componentName, DefaultFormTitle: defName, SubmitLabel: "Submit", FormInputs: []FormInput{}, OperationFound: true, ServiceName: opInfo.ServiceName, ServiceMethod: opInfo.MethodName, OperationHTTPMethod: opInfo.HTTPMethod, } schemaForForm := getFormSchema(def, swagger.Definitions) for propName, prop := range schemaForForm { // Skip "id" for create requests (assuming POST). 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), } 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) } 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) } } // ----- 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 } // getFormSchema checks whether a "payload" property exists. // If so, it will return its nested properties; otherwise, 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 } // getResponseTableColumns builds the table columns based on the response schema for a list operation. // It looks for the 200 response, then expects its schema to reference a definition that has a "results" property. // 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 { opMap := make(map[string]OperationInfo) for _, methods := range paths { for httpMethod, op := range methods { for _, param := range op.Parameters { if strings.ToLower(param.In) == "body" && param.Schema != nil && param.Schema.Ref != "" { refParts := strings.Split(param.Schema.Ref, "/") defName := refParts[len(refParts)-1] if op.OperationID != "" { parts := strings.Split(op.OperationID, "_") serviceName := "" methodName := "" if len(parts) >= 2 { serviceName = parts[0] + "Service" noUnderscore := strings.ReplaceAll(op.OperationID, "_", "") methodName = lowerFirst(noUnderscore) } if serviceName != "" && methodName != "" { opMap[defName] = OperationInfo{ ServiceName: serviceName, MethodName: methodName, HTTPMethod: strings.ToLower(httpMethod), } } } } } } } return opMap } func lowerFirst(s string) string { if len(s) == 0 { return s } return strings.ToLower(s[:1]) + s[1:] } func labelFromProperty(prop Property, propName string) string { if prop.Description != "" { return prop.Description } return strings.ToUpper(propName[:1]) + propName[1:] } func isRequired(propName string, required []string) bool { for _, r := range required { if r == propName { return true } } return false } // ----- Rendering Templates ----- func renderVueComponent(data VueComponentData, outDir string) error { const vueTemplate = ` ` 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) } func renderVueTableComponent(data VueTableData, outDir string) error { const vueTableTemplate = ` ` 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) }