All checks were successful
CI / Test (1.24) (push) Successful in -27m27s
CI / Test (1.25) (push) Successful in -27m17s
CI / Lint (push) Successful in -27m27s
CI / Build (push) Successful in -27m38s
Release / Build and Release (push) Successful in -27m24s
Integration Tests / Integration Tests (push) Successful in -27m16s
* Update model name generation to include schema name. * Add gofmt execution after writing output files. * Refactor relationship field naming to include schema. * Update tests to reflect changes in model names and relationships.
287 lines
8.1 KiB
Go
287 lines
8.1 KiB
Go
package bun
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
// TemplateData represents the data passed to the template for code generation
|
|
type TemplateData struct {
|
|
PackageName string
|
|
Imports []string
|
|
Models []*ModelData
|
|
Config *MethodConfig
|
|
}
|
|
|
|
// ModelData represents a single model/struct in the template
|
|
type ModelData struct {
|
|
Name string
|
|
TableName string // schema.table format
|
|
SchemaName string
|
|
TableNameOnly string // just table name without schema
|
|
Comment string
|
|
Fields []*FieldData
|
|
Config *MethodConfig
|
|
PrimaryKeyField string // Name of the primary key field
|
|
PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call)
|
|
IDColumnName string // Name of the ID column in database
|
|
Prefix string // 3-letter prefix
|
|
}
|
|
|
|
// FieldData represents a single field in a struct
|
|
type FieldData struct {
|
|
Name string // Go field name (PascalCase)
|
|
Type string // Go type
|
|
BunTag string // Complete bun tag
|
|
JSONTag string // JSON tag
|
|
Comment string // Field comment
|
|
}
|
|
|
|
// MethodConfig controls which helper methods to generate
|
|
type MethodConfig struct {
|
|
GenerateTableName bool
|
|
GenerateSchemaName bool
|
|
GenerateTableNameOnly bool
|
|
GenerateGetID bool
|
|
GenerateGetIDStr bool
|
|
GenerateSetID bool
|
|
GenerateUpdateID bool
|
|
GenerateGetIDName bool
|
|
GenerateGetPrefix bool
|
|
}
|
|
|
|
// DefaultMethodConfig returns a MethodConfig with all methods enabled
|
|
func DefaultMethodConfig() *MethodConfig {
|
|
return &MethodConfig{
|
|
GenerateTableName: true,
|
|
GenerateSchemaName: true,
|
|
GenerateTableNameOnly: true,
|
|
GenerateGetID: true,
|
|
GenerateGetIDStr: true,
|
|
GenerateSetID: true,
|
|
GenerateUpdateID: true,
|
|
GenerateGetIDName: true,
|
|
GenerateGetPrefix: true,
|
|
}
|
|
}
|
|
|
|
// NewTemplateData creates a new TemplateData with the given package name and config
|
|
func NewTemplateData(packageName string, config *MethodConfig) *TemplateData {
|
|
if config == nil {
|
|
config = DefaultMethodConfig()
|
|
}
|
|
|
|
return &TemplateData{
|
|
PackageName: packageName,
|
|
Imports: make([]string, 0),
|
|
Models: make([]*ModelData, 0),
|
|
Config: config,
|
|
}
|
|
}
|
|
|
|
// AddModel adds a model to the template data
|
|
func (td *TemplateData) AddModel(model *ModelData) {
|
|
model.Config = td.Config
|
|
td.Models = append(td.Models, model)
|
|
}
|
|
|
|
// AddImport adds an import to the template data (deduplicates automatically)
|
|
func (td *TemplateData) AddImport(importPath string) {
|
|
// Check if already exists
|
|
for _, imp := range td.Imports {
|
|
if imp == importPath {
|
|
return
|
|
}
|
|
}
|
|
td.Imports = append(td.Imports, importPath)
|
|
}
|
|
|
|
// FinalizeImports sorts and organizes imports
|
|
func (td *TemplateData) FinalizeImports() {
|
|
// Sort imports alphabetically
|
|
sort.Strings(td.Imports)
|
|
}
|
|
|
|
// NewModelData creates a new ModelData from a models.Table
|
|
func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *ModelData {
|
|
tableName := table.Name
|
|
if schema != "" {
|
|
tableName = schema + "." + table.Name
|
|
}
|
|
|
|
// Generate model name: Model + Schema + Table (all PascalCase)
|
|
singularTable := Singularize(table.Name)
|
|
tablePart := SnakeCaseToPascalCase(singularTable)
|
|
|
|
// Include schema name in model name
|
|
var modelName string
|
|
if schema != "" {
|
|
schemaPart := SnakeCaseToPascalCase(schema)
|
|
modelName = "Model" + schemaPart + tablePart
|
|
} else {
|
|
modelName = "Model" + tablePart
|
|
}
|
|
|
|
model := &ModelData{
|
|
Name: modelName,
|
|
TableName: tableName,
|
|
SchemaName: schema,
|
|
TableNameOnly: table.Name,
|
|
Comment: formatComment(table.Description, table.Comment),
|
|
Fields: make([]*FieldData, 0),
|
|
Prefix: GeneratePrefix(table.Name),
|
|
}
|
|
|
|
// Find primary key
|
|
for _, col := range table.Columns {
|
|
if col.IsPrimaryKey {
|
|
// Sanitize column name to remove backticks
|
|
safeName := writers.SanitizeStructTagValue(col.Name)
|
|
model.PrimaryKeyField = SnakeCaseToPascalCase(safeName)
|
|
model.IDColumnName = safeName
|
|
// Check if PK type is a SQL type (contains resolvespec_common or sql_types)
|
|
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
|
model.PrimaryKeyIsSQL = strings.Contains(goType, "resolvespec_common") || strings.Contains(goType, "sql_types")
|
|
break
|
|
}
|
|
}
|
|
|
|
// Convert columns to fields (sorted by sequence or name)
|
|
columns := sortColumns(table.Columns)
|
|
for _, col := range columns {
|
|
field := columnToField(col, table, typeMapper)
|
|
// Check for name collision with generated methods and rename if needed
|
|
field.Name = resolveFieldNameCollision(field.Name)
|
|
model.Fields = append(model.Fields, field)
|
|
}
|
|
|
|
return model
|
|
}
|
|
|
|
// columnToField converts a models.Column to FieldData
|
|
func columnToField(col *models.Column, table *models.Table, typeMapper *TypeMapper) *FieldData {
|
|
// Sanitize column name first to remove backticks before generating field name
|
|
safeName := writers.SanitizeStructTagValue(col.Name)
|
|
fieldName := SnakeCaseToPascalCase(safeName)
|
|
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
|
bunTag := typeMapper.BuildBunTag(col, table)
|
|
// Use same sanitized name for JSON tag
|
|
jsonTag := safeName
|
|
|
|
return &FieldData{
|
|
Name: fieldName,
|
|
Type: goType,
|
|
BunTag: bunTag,
|
|
JSONTag: jsonTag,
|
|
Comment: formatComment(col.Description, col.Comment),
|
|
}
|
|
}
|
|
|
|
// AddRelationshipField adds a relationship field to the model
|
|
func (md *ModelData) AddRelationshipField(field *FieldData) {
|
|
md.Fields = append(md.Fields, field)
|
|
}
|
|
|
|
// formatComment combines description and comment into a single comment string
|
|
func formatComment(description, comment string) string {
|
|
if description != "" && comment != "" {
|
|
return description + " - " + comment
|
|
}
|
|
if description != "" {
|
|
return description
|
|
}
|
|
return comment
|
|
}
|
|
|
|
// resolveFieldNameCollision checks if a field name conflicts with generated method names
|
|
// and adds an underscore suffix if there's a collision
|
|
func resolveFieldNameCollision(fieldName string) string {
|
|
// List of method names that are generated by the template
|
|
reservedNames := map[string]bool{
|
|
"TableName": true,
|
|
"TableNameOnly": true,
|
|
"SchemaName": true,
|
|
"GetID": true,
|
|
"GetIDStr": true,
|
|
"SetID": true,
|
|
"UpdateID": true,
|
|
"GetIDName": true,
|
|
"GetPrefix": true,
|
|
}
|
|
|
|
// Check if field name conflicts with a reserved method name
|
|
if reservedNames[fieldName] {
|
|
return fieldName + "_"
|
|
}
|
|
|
|
return fieldName
|
|
}
|
|
|
|
// sortColumns sorts columns by sequence, then by name
|
|
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
|
result := make([]*models.Column, 0, len(columns))
|
|
for _, col := range columns {
|
|
result = append(result, col)
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
|
// Sort by sequence if both have it
|
|
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
|
return result[i].Sequence < result[j].Sequence
|
|
}
|
|
|
|
// Put primary keys first
|
|
if result[i].IsPrimaryKey != result[j].IsPrimaryKey {
|
|
return result[i].IsPrimaryKey
|
|
}
|
|
|
|
// Otherwise sort alphabetically
|
|
return result[i].Name < result[j].Name
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
// LoadMethodConfigFromMetadata loads method configuration from metadata map
|
|
func LoadMethodConfigFromMetadata(metadata map[string]interface{}) *MethodConfig {
|
|
config := DefaultMethodConfig()
|
|
|
|
if metadata == nil {
|
|
return config
|
|
}
|
|
|
|
// Load each setting from metadata if present
|
|
if val, ok := metadata["generate_table_name"].(bool); ok {
|
|
config.GenerateTableName = val
|
|
}
|
|
if val, ok := metadata["generate_schema_name"].(bool); ok {
|
|
config.GenerateSchemaName = val
|
|
}
|
|
if val, ok := metadata["generate_table_name_only"].(bool); ok {
|
|
config.GenerateTableNameOnly = val
|
|
}
|
|
if val, ok := metadata["generate_get_id"].(bool); ok {
|
|
config.GenerateGetID = val
|
|
}
|
|
if val, ok := metadata["generate_get_id_str"].(bool); ok {
|
|
config.GenerateGetIDStr = val
|
|
}
|
|
if val, ok := metadata["generate_set_id"].(bool); ok {
|
|
config.GenerateSetID = val
|
|
}
|
|
if val, ok := metadata["generate_update_id"].(bool); ok {
|
|
config.GenerateUpdateID = val
|
|
}
|
|
if val, ok := metadata["generate_get_id_name"].(bool); ok {
|
|
config.GenerateGetIDName = val
|
|
}
|
|
if val, ok := metadata["generate_get_prefix"].(bool); ok {
|
|
config.GenerateGetPrefix = val
|
|
}
|
|
|
|
return config
|
|
}
|