Skip to content

Conversation

@coolwednesday
Copy link
Member

@coolwednesday coolwednesday commented Sep 11, 2025

Add Store Layer Generator to gofr-cli

Pull Request: Enhanced Store Generator & Documentation Overhaul

Overview

This PR delivers a fully-refactored GoFr Store Generator that is linter-clean, feature-rich, and thoroughly documented.

Key deliverables:

  1. generator.go
  2. templates.go – all code-gen templates extracted into constants with trimmed whitespace, improved spacing, and descriptive comments.
  3. example.yaml – end-to-end sample that mixes external and generated models across four stores (user, product, order, category) showcasing every query & return type.
  4. README.md – expanded guide covering:
    • robust appending logic and duplicate prevention in stores/all.go
    • CLI commands (init / generate) and multi-store workflow
    • step-by-step main.go integration, registry usage, DI pattern, env overrides
    • advanced YAML examples, troubleshooting tips, and linter guarantees.
  5. Tested Improvements
    • correct handling of import sections whether absent or present
    • explicit file-close calls inside loops
    • fallback regeneration when registry parsing fails

How to Test

  1. gofr store init -name=user then gofr store init -name=product – check stores/all.go appends without dupes.
  2. gofr store generate -config=example.yaml – generates four stores and a clean registry; go vet ./... and go test succeed.

Backward Compatibility

  • Single-store YAML format is supported.
  • Existing stores/all.go files are preserved—new stores append safely.
  • No breaking API changes to generated interfaces or method signatures.

Next Steps

  • Optional: add integration tests that compile generated code in CI.
  • Consider packaging CLI as a standalone binary for non-GoFr projects.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a comprehensive store layer generator to gofr-cli that generates GoFr-compatible store interfaces and implementations from YAML configuration files. The generator supports multi-store architecture with external model referencing and smart generation features.

  • Adds YAML-driven store layer code generation with CRUD operations support
  • Implements multi-store architecture allowing multiple isolated stores in a single configuration
  • Introduces external model referencing to use existing model files instead of generating new ones

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

File Description
store/generator.go Core generator implementation with YAML parsing, templating, and file generation logic
store/example.yaml Comprehensive example configuration demonstrating multi-store setup with external and generated models
store/README.md Detailed documentation covering features, usage, configuration options, and best practices
main.go Integration of store commands into the CLI application

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@Umang01-hash
Copy link
Member

@coolwednesday Looks like your PR has some code quality issues:
https://github.com/gofr-dev/gofr-cli/actions/runs/17641393880/job/50129112308?pr=64

Can we please fix them.

@@ -0,0 +1,700 @@
# GoFr Store Generator
Copy link
Member

Choose a reason for hiding this comment

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

The README is currently too long and repetitive - concepts like "registry management" and "appending logic" appear in multiple sections. Consider reorganizing into: 1) Quick Start, 2) Basic Usage, 3) Configuration Reference, and 4) Advanced Topics. Cut duplicate explanations and focus on practical examples rather than implementation details.

// generateSingleStore generates a single store.
func generateSingleStore(ctx *gofr.Context, config *Config, store *Info) error {
outputDir := store.OutputDir
if outputDir == "" {
Copy link
Member

Choose a reason for hiding this comment

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

Shoudn't we have a validation here over store name before it is used to create directories? What if user gives 123store or user-profile?? These are not correct go package naming conventions.

}

// collectImports collects all required imports for the generated code.
func collectImports(config *Config) []string {
Copy link
Member

Choose a reason for hiding this comment

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

This method should canonicalize import paths (trimmed, unquoted, no indentation) and add alias support, so comparisons are reliable and duplicates or name collisions are avoided.


// appendStoreEntries appends new stores to stores/all.go without overwriting existing entries.
func appendStoreEntries(ctx *gofr.Context, newStores []Entry) error {
projectModule := detectProjectModule()
Copy link
Member

Choose a reason for hiding this comment

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

Normalize imports consistently; current whitespace mismatch can add duplicates. Prefer AST for imports/map insertion and write files atomically via go/format.

}

// processExistingAllFile handles the logic for updating an existing all.go file.
func processExistingAllFile(ctx *gofr.Context, content []byte,
Copy link
Member

Choose a reason for hiding this comment

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

Normalize imports consistently; current whitespace mismatch can add duplicates. Prefer AST for imports/map insertion and write files atomically via go/format.

}

// handleImportSection adds import section if missing or appends to existing one.
func handleImportSection(lines, importsToAdd []string) []string {
Copy link
Member

Choose a reason for hiding this comment

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

Consolidate scattered import logic into one helper or use astutil.AddImport; reduces duplication and formatting issues.

}

// findMapInsertionPoint finds where to insert new store entries in the map.
func findMapInsertionPoint(lines []string) int {
Copy link
Member

Choose a reason for hiding this comment

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

String-based brace counting is fragile; replace with AST parsing to locate func All() return map reliably.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

`

// ImplementationTemplate is the template for generating store implementations.
const ImplementationTemplate = `// Code generated by gofr.dev/cli/gofr.
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

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

Missing 'DO NOT EDIT.' comment in the ImplementationTemplate header. This should match the format used in other templates.

Suggested change
const ImplementationTemplate = `// Code generated by gofr.dev/cli/gofr.
const ImplementationTemplate = `// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.

Copilot uses AI. Check for mistakes.
)

// storeRegex matches store entries in all.go file.
var storeRegex = regexp.MustCompile(`^\s*"([^"]+)"\s*:\s*func\(\)\s*any\s*\{`)
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

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

The regex pattern should be more robust to handle variations in whitespace and formatting. Consider using a more flexible pattern that accounts for different spacing and line breaks that might exist in real files.

Suggested change
var storeRegex = regexp.MustCompile(`^\s*"([^"]+)"\s*:\s*func\(\)\s*any\s*\{`)
var storeRegex = regexp.MustCompile(`(?m)\s*"([^"]+)"\s*:\s*func\s*\(\s*\)\s*any\s*\{`)

Copilot uses AI. Check for mistakes.
Comment on lines +401 to +426
t, err := template.New("model").Funcs(template.FuncMap{
"lower": strings.ToLower,
}).Parse(ModelTemplate)
if err != nil {
return fmt.Errorf("failed to parse model template: %w", err)
}

file, err := os.Create(modelFile)
if err != nil {
return fmt.Errorf("failed to create model file: %w", err)
}

// Pass the store and model context correctly
store := config.Stores[0]
if err := t.Execute(file, struct {
Store Info
Model Model
}{store, model}); err != nil {
file.Close() // Close file before returning error
return fmt.Errorf("failed to execute model template: %w", err)
}

file.Close() // Close file explicitly instead of defer in loop
ctx.Logger.Infof("Generated model file: %s", modelFile)
}

Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

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

The explicit file.Close() calls are good practice in loops, but consider using a function to encapsulate the model generation logic to maintain proper defer usage while avoiding the loop defer issue.

Suggested change
t, err := template.New("model").Funcs(template.FuncMap{
"lower": strings.ToLower,
}).Parse(ModelTemplate)
if err != nil {
return fmt.Errorf("failed to parse model template: %w", err)
}
file, err := os.Create(modelFile)
if err != nil {
return fmt.Errorf("failed to create model file: %w", err)
}
// Pass the store and model context correctly
store := config.Stores[0]
if err := t.Execute(file, struct {
Store Info
Model Model
}{store, model}); err != nil {
file.Close() // Close file before returning error
return fmt.Errorf("failed to execute model template: %w", err)
}
file.Close() // Close file explicitly instead of defer in loop
ctx.Logger.Infof("Generated model file: %s", modelFile)
}
store := config.Stores[0]
if err := generateModelFile(modelFile, store, model); err != nil {
return err
}
ctx.Logger.Infof("Generated model file: %s", modelFile)
}
return nil
}
// generateModelFile creates a model file using the provided store and model information.
func generateModelFile(modelFile string, store Info, model Model) error {
t, err := template.New("model").Funcs(template.FuncMap{
"lower": strings.ToLower,
}).Parse(ModelTemplate)
if err != nil {
return fmt.Errorf("failed to parse model template: %w", err)
}
file, err := os.Create(modelFile)
if err != nil {
return fmt.Errorf("failed to create model file: %w", err)
}
defer file.Close()
if err := t.Execute(file, struct {
Store Info
Model Model
}{store, model}); err != nil {
return fmt.Errorf("failed to execute model template: %w", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +551 to +572
// processExistingAllFile handles the logic for updating an existing all.go file.
func processExistingAllFile(ctx *gofr.Context, content []byte,
newStores []Entry, projectModule string) error {
lines := strings.Split(string(content), "\n")

// Parse existing stores and imports more carefully
existingStores, existingImports := parseExistingAllFile(lines)

// Filter out stores that already exist and collect imports to add
storesToAdd, importsToAdd := filterNewStores(newStores,
existingStores, existingImports, projectModule)

if len(storesToAdd) == 0 {
ctx.Logger.Info("All stores already exist in all.go")

return nil
}

return updateAllFileWithNewStores(ctx, lines, storesToAdd,
importsToAdd, existingStores, projectModule)
}

Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

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

[nitpick] This function has multiple responsibilities (parsing, filtering, updating). Consider breaking it down into smaller, single-responsibility functions for better maintainability and testing.

Suggested change
// processExistingAllFile handles the logic for updating an existing all.go file.
func processExistingAllFile(ctx *gofr.Context, content []byte,
newStores []Entry, projectModule string) error {
lines := strings.Split(string(content), "\n")
// Parse existing stores and imports more carefully
existingStores, existingImports := parseExistingAllFile(lines)
// Filter out stores that already exist and collect imports to add
storesToAdd, importsToAdd := filterNewStores(newStores,
existingStores, existingImports, projectModule)
if len(storesToAdd) == 0 {
ctx.Logger.Info("All stores already exist in all.go")
return nil
}
return updateAllFileWithNewStores(ctx, lines, storesToAdd,
importsToAdd, existingStores, projectModule)
}
// splitContentToLines splits the file content into lines.
func splitContentToLines(content []byte) []string {
return strings.Split(string(content), "\n")
}
// handleExistingAllFile orchestrates the update process for an existing all.go file.
func handleExistingAllFile(ctx *gofr.Context, content []byte, newStores []Entry, projectModule string) error {
lines := splitContentToLines(content)
existingStores, existingImports := parseExistingAllFile(lines)
storesToAdd, importsToAdd := filterNewStores(newStores, existingStores, existingImports, projectModule)
if len(storesToAdd) == 0 {
ctx.Logger.Info("All stores already exist in all.go")
return nil
}
return updateAllFileWithNewStores(ctx, lines, storesToAdd, importsToAdd, existingStores, projectModule)
}
// processExistingAllFile delegates to handleExistingAllFile for backward compatibility.
func processExistingAllFile(ctx *gofr.Context, content []byte, newStores []Entry, projectModule string) error {
return handleExistingAllFile(ctx, content, newStores, projectModule)
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Store layer for GoFr Framework Users

2 participants