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) }