diff --git a/.github/workflows/go-coverage.yml b/.github/workflows/coverage.yml similarity index 99% rename from .github/workflows/go-coverage.yml rename to .github/workflows/coverage.yml index b1e4ed9..c34842c 100644 --- a/.github/workflows/go-coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: Go coverage report +name: Coverage report on: workflow_run: diff --git a/.github/workflows/go-quality.yml b/.github/workflows/quality.yml similarity index 98% rename from .github/workflows/go-quality.yml rename to .github/workflows/quality.yml index 48e46b0..2d9adb2 100644 --- a/.github/workflows/go-quality.yml +++ b/.github/workflows/quality.yml @@ -1,4 +1,4 @@ -name: Go quality checks +name: Quality checks on: push: diff --git a/cmd/openapi-generator/main.go b/cmd/openapi-generator/main.go index b1878a8..edbf194 100644 --- a/cmd/openapi-generator/main.go +++ b/cmd/openapi-generator/main.go @@ -16,6 +16,7 @@ import ( "github.com/xseman/openapi-generator/internal/generator/typescript" "github.com/xseman/openapi-generator/internal/parser" "github.com/xseman/openapi-generator/internal/template" + "github.com/xseman/openapi-generator/templates" "gopkg.in/yaml.v3" ) @@ -341,19 +342,30 @@ func runGenerate(cmd *cobra.Command, args []string) error { tmplDir = findTemplateDir(generatorName) } - if tmplDir == "" { - return fmt.Errorf("template directory not found, use --template-dir") - } + var engine *template.Engine - if verbose { - fmt.Printf("Using templates from: %s\n", tmplDir) + if tmplDir != "" { + // Use filesystem templates + if verbose { + fmt.Printf("Using templates from: %s\n", tmplDir) + } + engine = template.NewEngine(tmplDir) + engine.Verbose = verbose + if err := engine.LoadPartials(); err != nil { + return fmt.Errorf("failed to load template partials: %w", err) + } + } else { + // Fall back to embedded templates + if verbose { + fmt.Println("Using embedded templates") + } + engine = template.NewEngineFromFS(templates.FS, generatorName) + engine.Verbose = verbose + if err := engine.LoadPartialsFromFS(); err != nil { + return fmt.Errorf("failed to load embedded template partials: %w", err) + } } - engine := template.NewEngine(tmplDir) - engine.Verbose = verbose // Enable template execution logging - if err := engine.LoadPartials(); err != nil { - return fmt.Errorf("failed to load template partials: %w", err) - } engine.RegisterDefaultLambdas() // Prepare template data @@ -553,7 +565,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { if err := os.MkdirAll(filepath.Dir(modelIndexPath), 0755); err != nil { return fmt.Errorf("failed to create model index directory: %w", err) } - if err := os.WriteFile(modelIndexPath, []byte(modelIndex), 0644); err != nil { + if err := os.WriteFile(modelIndexPath, []byte(modelIndex), 0600); err != nil { return fmt.Errorf("failed to write model index: %w", err) } generatedFiles = append(generatedFiles, filepath.Join(gen.ModelPackage, "index.ts")) @@ -566,7 +578,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { if err := os.MkdirAll(filepath.Dir(apiIndexPath), 0755); err != nil { return fmt.Errorf("failed to create API index directory: %w", err) } - if err := os.WriteFile(apiIndexPath, []byte(apiIndex), 0644); err != nil { + if err := os.WriteFile(apiIndexPath, []byte(apiIndex), 0600); err != nil { return fmt.Errorf("failed to write API index: %w", err) } generatedFiles = append(generatedFiles, filepath.Join(gen.ApiPackage, "index.ts")) @@ -847,14 +859,14 @@ func generateMetadata(outputDir string, generatedFiles []string, version string) // Write FILES filesPath := filepath.Join(metaDir, "FILES") - if err := os.WriteFile(filesPath, []byte(filesContent.String()), 0644); err != nil { + if err := os.WriteFile(filesPath, []byte(filesContent.String()), 0600); err != nil { return fmt.Errorf("failed to write FILES: %w", err) } // Write VERSION versionPath := filepath.Join(metaDir, "VERSION") versionContent := fmt.Sprintf("%s\n", version) - if err := os.WriteFile(versionPath, []byte(versionContent), 0644); err != nil { + if err := os.WriteFile(versionPath, []byte(versionContent), 0600); err != nil { return fmt.Errorf("failed to write VERSION: %w", err) } diff --git a/internal/generator/typescript/base.go b/internal/generator/typescript/base.go index a772e8d..b3358cf 100644 --- a/internal/generator/typescript/base.go +++ b/internal/generator/typescript/base.go @@ -444,7 +444,7 @@ func Camelize(s string, lowercaseFirst bool) string { var current strings.Builder for i, r := range s { - if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) { + if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') { // Non-alphanumeric character - end current word if current.Len() > 0 { words = append(words, current.String()) diff --git a/internal/parser/openapi.go b/internal/parser/openapi.go index cb1724f..fad13c2 100644 --- a/internal/parser/openapi.go +++ b/internal/parser/openapi.go @@ -964,9 +964,10 @@ func (p *Parser) schemaToProperty(name string, schema *openapi3.Schema, required prop.IsNumber = true prop.IsNumeric = true prop.IsPrimitiveType = true - if schema.Format == "float" { + switch schema.Format { + case "float": prop.IsFloat = true - } else if schema.Format == "double" { + case "double": prop.IsDouble = true } prop.DataType = "number" diff --git a/internal/template/embedded.go b/internal/template/embedded.go new file mode 100644 index 0000000..81fd5d7 --- /dev/null +++ b/internal/template/embedded.go @@ -0,0 +1,55 @@ +package template + +import ( + "fmt" + "io/fs" + "strings" + + "github.com/cbroglie/mustache" +) + +// NewEngineFromFS creates a new template engine from an embedded filesystem. +// The subdir parameter specifies the subdirectory within the fs.FS to use as +// the template root (e.g., "typescript-fetch"). +func NewEngineFromFS(fsys fs.FS, subdir string) *Engine { + return &Engine{ + TemplateDir: subdir, + partials: make(map[string]string), + Lambdas: make(map[string]func(text string, render mustache.RenderFunc) (string, error)), + fsys: fsys, + } +} + +// LoadPartialsFromFS loads all partial templates from the embedded filesystem. +func (e *Engine) LoadPartialsFromFS() error { + if e.fsys == nil { + return fmt.Errorf("no embedded filesystem configured") + } + + return fs.WalkDir(e.fsys, e.TemplateDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() || !strings.HasSuffix(path, ".mustache") { + return nil + } + + content, err := fs.ReadFile(e.fsys, path) + if err != nil { + return fmt.Errorf("failed to read partial %s: %w", path, err) + } + + // Get relative name without extension + // path is like "typescript-fetch/models.mustache" + // We want just "models" as the partial name + name := strings.TrimPrefix(path, e.TemplateDir+"/") + name = strings.TrimSuffix(name, ".mustache") + + e.partials[name] = string(content) + if e.Verbose { + fmt.Printf("[TEMPLATE] Loaded embedded partial: %s\n", name) + } + return nil + }) +} diff --git a/internal/template/mustache.go b/internal/template/mustache.go index f723a0b..228b3b3 100644 --- a/internal/template/mustache.go +++ b/internal/template/mustache.go @@ -12,6 +12,9 @@ import ( "github.com/cbroglie/mustache" ) +// RenderFunc is the function signature for mustache rendering. +type RenderFunc = mustache.RenderFunc + // Engine handles Mustache template rendering. type Engine struct { // TemplateDir is the directory containing templates @@ -25,6 +28,9 @@ type Engine struct { // Verbose enables debug logging of template execution Verbose bool + + // fsys is the embedded filesystem (optional, for embedded templates) + fsys fs.FS } // NewEngine creates a new template engine. @@ -155,15 +161,31 @@ func (e *Engine) RegisterDefaultLambdas() { } // Render renders a template with the given data. +// If the engine was created with NewEngineFromFS, it reads from the embedded filesystem. func (e *Engine) Render(templateName string, data any) (string, error) { if e.Verbose { fmt.Printf("[TEMPLATE] Rendering template: %s\n", templateName) } - templatePath := filepath.Join(e.TemplateDir, templateName) - content, err := os.ReadFile(templatePath) - if err != nil { - return "", fmt.Errorf("failed to read template %s: %w", templateName, err) + var content []byte + var err error + + if e.fsys != nil { + // Read from embedded filesystem. Paths in embed.FS must always use forward slashes + // per the Go specification, so we intentionally construct the path with "/" rather + // than using filepath.Join (which is OS-dependent). + templatePath := e.TemplateDir + "/" + templateName + content, err = fs.ReadFile(e.fsys, templatePath) + if err != nil { + return "", fmt.Errorf("failed to read embedded template %s: %w", templateName, err) + } + } else { + // Read from filesystem + templatePath := filepath.Join(e.TemplateDir, templateName) + content, err = os.ReadFile(templatePath) + if err != nil { + return "", fmt.Errorf("failed to read template %s: %w", templateName, err) + } } return e.RenderString(string(content), data) @@ -208,7 +230,7 @@ func (e *Engine) RenderToFile(templateName string, data any, outputPath string) } // Write file - if err := os.WriteFile(outputPath, []byte(result), 0644); err != nil { + if err := os.WriteFile(outputPath, []byte(result), 0600); err != nil { return fmt.Errorf("failed to write file %s: %w", outputPath, err) } diff --git a/templates/embed.go b/templates/embed.go new file mode 100644 index 0000000..4f793bb --- /dev/null +++ b/templates/embed.go @@ -0,0 +1,9 @@ +// Package templates provides embedded template files for code generation. +package templates + +import "embed" + +// FS contains all embedded template files. +// +//go:embed typescript-fetch/*.mustache typescript-fetch/*.md +var FS embed.FS