Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go coverage report
name: Coverage report

on:
workflow_run:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go quality checks
name: Quality checks

on:
push:
Expand Down
40 changes: 26 additions & 14 deletions cmd/openapi-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand All @@ -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"))
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/generator/typescript/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
5 changes: 3 additions & 2 deletions internal/parser/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions internal/template/embedded.go
Original file line number Diff line number Diff line change
@@ -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")
Comment on lines +46 to +47
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path construction in LoadPartialsFromFS uses simple string concatenation with "/" but doesn't handle potential edge cases where partial names might not normalize correctly. The original LoadPartials uses filepath.Rel to compute the relative path, then normalizes separators with strings.ReplaceAll. For consistency and to handle potential subdirectories within the template directory correctly, consider using a similar normalization approach:

Instead of:

name := strings.TrimPrefix(path, e.TemplateDir+"/")

Consider handling cases where the template directory path might not have a trailing separator, and ensure proper handling of nested partials. For example, if there are subdirectories in typescript-fetch, the partial name computation should be consistent with the filesystem-based version.

Copilot uses AI. Check for mistakes.

e.partials[name] = string(content)
if e.Verbose {
fmt.Printf("[TEMPLATE] Loaded embedded partial: %s\n", name)
}
return nil
})
}
32 changes: 27 additions & 5 deletions internal/template/mustache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down
9 changes: 9 additions & 0 deletions templates/embed.go
Original file line number Diff line number Diff line change
@@ -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