From b20ad354855e3492afb1ef90e4fb0ad80fd316b1 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 10 Jan 2026 13:42:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(writer):=20=F0=9F=8E=89=20Add=20sanitizati?= =?UTF-8?q?on=20for=20struct=20tag=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement SanitizeStructTagValue function to clean identifiers for struct tags. * Update model data generation to use sanitized column names. * Ensure safe handling of backticks in column names and types across writers. --- pkg/writers/bun/template_data.go | 14 ++++++++++---- pkg/writers/bun/type_mapper.go | 14 ++++++++++---- pkg/writers/gorm/template_data.go | 14 ++++++++++---- pkg/writers/gorm/type_mapper.go | 14 ++++++++++---- pkg/writers/writer.go | 23 +++++++++++++++++++++++ 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/pkg/writers/bun/template_data.go b/pkg/writers/bun/template_data.go index 0b2ab3f..f607aa7 100644 --- a/pkg/writers/bun/template_data.go +++ b/pkg/writers/bun/template_data.go @@ -5,6 +5,7 @@ import ( "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 @@ -133,8 +134,10 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M // Find primary key for _, col := range table.Columns { if col.IsPrimaryKey { - model.PrimaryKeyField = SnakeCaseToPascalCase(col.Name) - model.IDColumnName = col.Name + // 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") @@ -154,10 +157,13 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M // columnToField converts a models.Column to FieldData func columnToField(col *models.Column, table *models.Table, typeMapper *TypeMapper) *FieldData { - fieldName := SnakeCaseToPascalCase(col.Name) + // 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) - jsonTag := col.Name // Use column name for JSON tag + // Use same sanitized name for JSON tag + jsonTag := safeName return &FieldData{ Name: fieldName, diff --git a/pkg/writers/bun/type_mapper.go b/pkg/writers/bun/type_mapper.go index d9e1b59..d8b9216 100644 --- a/pkg/writers/bun/type_mapper.go +++ b/pkg/writers/bun/type_mapper.go @@ -5,6 +5,7 @@ import ( "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" + "git.warky.dev/wdevs/relspecgo/pkg/writers" ) // TypeMapper handles type conversions between SQL and Go types for Bun @@ -164,11 +165,14 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st var parts []string // Column name comes first (no prefix) - parts = append(parts, column.Name) + // Sanitize to remove backticks which would break struct tag syntax + safeName := writers.SanitizeStructTagValue(column.Name) + parts = append(parts, safeName) // Add type if specified if column.Type != "" { - typeStr := column.Type + // Sanitize type to remove backticks + typeStr := writers.SanitizeStructTagValue(column.Type) if column.Length > 0 { typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length) } else if column.Precision > 0 { @@ -188,7 +192,9 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st // Default value if column.Default != nil { - parts = append(parts, fmt.Sprintf("default:%v", column.Default)) + // Sanitize default value to remove backticks + safeDefault := writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)) + parts = append(parts, fmt.Sprintf("default:%s", safeDefault)) } // Nullable (Bun uses nullzero for nullable fields) @@ -263,7 +269,7 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool { // GetSQLTypesImport returns the import path for sql_types (ResolveSpec common) func (tm *TypeMapper) GetSQLTypesImport() string { - return "github.com/bitechdev/ResolveSpec/pkg/common" + return "github.com/bitechdev/ResolveSpec/pkg/spectypes" } // GetBunImport returns the import path for Bun diff --git a/pkg/writers/gorm/template_data.go b/pkg/writers/gorm/template_data.go index 70fd8aa..9f6251b 100644 --- a/pkg/writers/gorm/template_data.go +++ b/pkg/writers/gorm/template_data.go @@ -4,6 +4,7 @@ import ( "sort" "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 @@ -131,8 +132,10 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M // Find primary key for _, col := range table.Columns { if col.IsPrimaryKey { - model.PrimaryKeyField = SnakeCaseToPascalCase(col.Name) - model.IDColumnName = col.Name + // Sanitize column name to remove backticks + safeName := writers.SanitizeStructTagValue(col.Name) + model.PrimaryKeyField = SnakeCaseToPascalCase(safeName) + model.IDColumnName = safeName break } } @@ -149,10 +152,13 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M // columnToField converts a models.Column to FieldData func columnToField(col *models.Column, table *models.Table, typeMapper *TypeMapper) *FieldData { - fieldName := SnakeCaseToPascalCase(col.Name) + // 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) gormTag := typeMapper.BuildGormTag(col, table) - jsonTag := col.Name // Use column name for JSON tag + // Use same sanitized name for JSON tag + jsonTag := safeName return &FieldData{ Name: fieldName, diff --git a/pkg/writers/gorm/type_mapper.go b/pkg/writers/gorm/type_mapper.go index 272c4e4..8a92836 100644 --- a/pkg/writers/gorm/type_mapper.go +++ b/pkg/writers/gorm/type_mapper.go @@ -5,6 +5,7 @@ import ( "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" + "git.warky.dev/wdevs/relspecgo/pkg/writers" ) // TypeMapper handles type conversions between SQL and Go types @@ -199,12 +200,15 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s var parts []string // Always include column name (lowercase as per user requirement) - parts = append(parts, fmt.Sprintf("column:%s", column.Name)) + // Sanitize to remove backticks which would break struct tag syntax + safeName := writers.SanitizeStructTagValue(column.Name) + parts = append(parts, fmt.Sprintf("column:%s", safeName)) // Add type if specified if column.Type != "" { // Include length, precision, scale if present - typeStr := column.Type + // Sanitize type to remove backticks + typeStr := writers.SanitizeStructTagValue(column.Type) if column.Length > 0 { typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length) } else if column.Precision > 0 { @@ -234,7 +238,9 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s // Default value if column.Default != nil { - parts = append(parts, fmt.Sprintf("default:%v", column.Default)) + // Sanitize default value to remove backticks + safeDefault := writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)) + parts = append(parts, fmt.Sprintf("default:%s", safeDefault)) } // Check for unique constraint @@ -331,5 +337,5 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool { // GetSQLTypesImport returns the import path for sql_types func (tm *TypeMapper) GetSQLTypesImport() string { - return "github.com/bitechdev/ResolveSpec/pkg/common/sql_types" + return "github.com/bitechdev/ResolveSpec/pkg/spectypes" } diff --git a/pkg/writers/writer.go b/pkg/writers/writer.go index 4a2810d..5f78d2f 100644 --- a/pkg/writers/writer.go +++ b/pkg/writers/writer.go @@ -61,3 +61,26 @@ func SanitizeFilename(name string) string { return name } + +// SanitizeStructTagValue sanitizes a value to be safely used inside Go struct tags. +// Go struct tags are delimited by backticks, so any backtick in the value would break the syntax. +// This function: +// - Removes DBML/DCTX comments in brackets +// - Removes all quotes (double, single, and backticks) +// - Returns a clean identifier safe for use in struct tags and field names +func SanitizeStructTagValue(value string) string { + // Remove DBML/DCTX style comments in brackets (e.g., [note: 'description']) + commentRegex := regexp.MustCompile(`\s*\[.*?\]\s*`) + value = commentRegex.ReplaceAllString(value, "") + + // Trim whitespace + value = strings.TrimSpace(value) + + // Remove all quotes: backticks, double quotes, and single quotes + // This ensures the value is clean for use as Go identifiers and struct tag values + value = strings.ReplaceAll(value, "`", "") + value = strings.ReplaceAll(value, `"`, "") + value = strings.ReplaceAll(value, `'`, "") + + return value +}