feat(templ): ✨ added templ to command line that reads go template and outputs code
This commit is contained in:
@@ -20,4 +20,5 @@ func init() {
|
||||
rootCmd.AddCommand(diffCmd)
|
||||
rootCmd.AddCommand(inspectCmd)
|
||||
rootCmd.AddCommand(scriptsCmd)
|
||||
rootCmd.AddCommand(templCmd)
|
||||
}
|
||||
|
||||
167
cmd/relspec/templ.go
Normal file
167
cmd/relspec/templ.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wtemplate "git.warky.dev/wdevs/relspecgo/pkg/writers/template"
|
||||
)
|
||||
|
||||
var (
|
||||
templSourceType string
|
||||
templSourcePath string
|
||||
templSourceConn string
|
||||
templTemplatePath string
|
||||
templOutputPath string
|
||||
templSchemaFilter string
|
||||
templMode string
|
||||
templFilenamePattern string
|
||||
)
|
||||
|
||||
var templCmd = &cobra.Command{
|
||||
Use: "templ",
|
||||
Short: "Apply custom templates to database schemas",
|
||||
Long: `Apply custom Go text templates to database schemas with flexible execution modes.
|
||||
|
||||
The templ command allows you to transform database schemas using custom Go text
|
||||
templates. It supports multiple execution modes for different use cases:
|
||||
|
||||
Execution Modes:
|
||||
database Execute template once for entire database (single output file)
|
||||
schema Execute template once per schema (one file per schema)
|
||||
script Execute template once per script (one file per script)
|
||||
table Execute template once per table (one file per table)
|
||||
|
||||
Supported Input Formats:
|
||||
dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql
|
||||
|
||||
Template Functions:
|
||||
String utilities: toUpper, toLower, toCamelCase, toPascalCase, toSnakeCase, toKebabCase,
|
||||
pluralize, singularize, title, trim, split, join, replace
|
||||
|
||||
Type conversion: sqlToGo, sqlToTypeScript, sqlToJava, sqlToPython, sqlToRust,
|
||||
sqlToCSharp, sqlToPhp
|
||||
|
||||
Filtering: filterTables, filterColumns, filterPrimaryKeys, filterForeignKeys,
|
||||
filterNullable, filterNotNull, filterColumnsByType
|
||||
|
||||
Formatting: toJSON, toJSONPretty, toYAML, indent, escape, comment
|
||||
|
||||
Loop helpers: enumerate, batch, reverse, first, last, skip, take, concat,
|
||||
unique, sortBy, groupBy
|
||||
|
||||
Safe access: get, getOr, getPath, has, keys, values, merge, pick, omit,
|
||||
sliceContains, indexOf, pluck
|
||||
|
||||
Examples:
|
||||
# Generate documentation from PostgreSQL database
|
||||
relspec templ --from pgsql --from-conn "postgres://user:pass@localhost/db" \
|
||||
--template docs.tmpl --output schema-docs.md
|
||||
|
||||
# Generate one TypeScript model file per table
|
||||
relspec templ --from dbml --from-path schema.dbml \
|
||||
--template ts-model.tmpl --mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Generate schema documentation files
|
||||
relspec templ --from json --from-path db.json \
|
||||
--template schema.tmpl --mode schema \
|
||||
--output ./docs/ \
|
||||
--filename-pattern "{{.Name}}_schema.md"`,
|
||||
RunE: runTempl,
|
||||
}
|
||||
|
||||
func init() {
|
||||
templCmd.Flags().StringVar(&templSourceType, "from", "", "Source format (dbml, pgsql, json, etc.)")
|
||||
templCmd.Flags().StringVar(&templSourcePath, "from-path", "", "Source file path (for file-based sources)")
|
||||
templCmd.Flags().StringVar(&templSourceConn, "from-conn", "", "Source connection string (for database sources)")
|
||||
templCmd.Flags().StringVar(&templTemplatePath, "template", "", "Template file path (required)")
|
||||
templCmd.Flags().StringVar(&templOutputPath, "output", "", "Output path (file or directory, empty for stdout)")
|
||||
templCmd.Flags().StringVar(&templSchemaFilter, "schema", "", "Filter to specific schema")
|
||||
templCmd.Flags().StringVar(&templMode, "mode", "database", "Execution mode: database, schema, script, or table")
|
||||
templCmd.Flags().StringVar(&templFilenamePattern, "filename-pattern", "{{.Name}}.txt", "Filename pattern for multi-output modes")
|
||||
|
||||
_ = templCmd.MarkFlagRequired("from")
|
||||
_ = templCmd.MarkFlagRequired("template")
|
||||
}
|
||||
|
||||
func runTempl(cmd *cobra.Command, args []string) error {
|
||||
// Print header
|
||||
fmt.Fprintf(os.Stderr, "=== RelSpec Template Execution ===\n")
|
||||
fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp())
|
||||
|
||||
// Read database using the same function as convert
|
||||
fmt.Fprintf(os.Stderr, "Reading from %s...\n", templSourceType)
|
||||
db, err := readDatabaseForConvert(templSourceType, templSourcePath, templSourceConn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source: %w", err)
|
||||
}
|
||||
|
||||
// Print database stats
|
||||
schemaCount := len(db.Schemas)
|
||||
tableCount := 0
|
||||
for _, schema := range db.Schemas {
|
||||
tableCount += len(schema.Tables)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ Successfully read database: %s\n", db.Name)
|
||||
fmt.Fprintf(os.Stderr, " Schemas: %d\n", schemaCount)
|
||||
fmt.Fprintf(os.Stderr, " Tables: %d\n\n", tableCount)
|
||||
|
||||
// Apply schema filter if specified
|
||||
if templSchemaFilter != "" {
|
||||
fmt.Fprintf(os.Stderr, "Filtering to schema: %s\n", templSchemaFilter)
|
||||
found := false
|
||||
for _, schema := range db.Schemas {
|
||||
if schema.Name == templSchemaFilter {
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("schema not found: %s", templSchemaFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// Create template writer
|
||||
fmt.Fprintf(os.Stderr, "Loading template: %s\n", templTemplatePath)
|
||||
fmt.Fprintf(os.Stderr, "Execution mode: %s\n", templMode)
|
||||
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": templTemplatePath,
|
||||
"mode": templMode,
|
||||
"filename_pattern": templFilenamePattern,
|
||||
}
|
||||
|
||||
writerOpts := &writers.WriterOptions{
|
||||
OutputPath: templOutputPath,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
writer, err := wtemplate.NewWriter(writerOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create template writer: %w", err)
|
||||
}
|
||||
|
||||
// Execute template
|
||||
fmt.Fprintf(os.Stderr, "\nExecuting template...\n")
|
||||
if err := writer.WriteDatabase(db); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
// Print success message
|
||||
fmt.Fprintf(os.Stderr, "\n✓ Template executed successfully\n")
|
||||
if templOutputPath != "" {
|
||||
fmt.Fprintf(os.Stderr, "Output written to: %s\n", templOutputPath)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Output written to stdout\n")
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Completed at: %s\n", getCurrentTimestamp())
|
||||
|
||||
return nil
|
||||
}
|
||||
552
docs/TEMPLATE_MODE.md
Normal file
552
docs/TEMPLATE_MODE.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# RelSpec Template Mode
|
||||
|
||||
The `templ` command allows you to transform database schemas using custom Go text templates. It provides powerful template functions and flexible execution modes for generating any type of output from your database schema.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Execution Modes](#execution-modes)
|
||||
- [Template Functions](#template-functions)
|
||||
- [String Utilities](#string-utilities)
|
||||
- [Type Conversion](#type-conversion)
|
||||
- [Filtering](#filtering)
|
||||
- [Formatting](#formatting)
|
||||
- [Loop Helpers](#loop-helpers)
|
||||
- [Sorting Helpers](#sorting-helpers)
|
||||
- [Safe Access](#safe-access)
|
||||
- [Utility Functions](#utility-functions)
|
||||
- [Data Model](#data-model)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Generate documentation from a database
|
||||
relspec templ --from pgsql --from-conn "postgres://user:pass@localhost/db" \
|
||||
--template docs.tmpl --output schema-docs.md
|
||||
|
||||
# Generate TypeScript models (one file per table)
|
||||
relspec templ --from dbml --from-path schema.dbml \
|
||||
--template model.tmpl --mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Output to stdout
|
||||
relspec templ --from json --from-path schema.json \
|
||||
--template report.tmpl
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The `--mode` flag controls how the template is executed:
|
||||
|
||||
| Mode | Description | Output | When to Use |
|
||||
|------|-------------|--------|-------------|
|
||||
| `database` | Execute once for entire database | Single file | Documentation, reports, overview files |
|
||||
| `schema` | Execute once per schema | One file per schema | Schema-specific documentation |
|
||||
| `script` | Execute once per script | One file per script | Script processing |
|
||||
| `table` | Execute once per table | One file per table | Model generation, table docs |
|
||||
|
||||
### Filename Patterns
|
||||
|
||||
For multi-file modes (`schema`, `script`, `table`), use `--filename-pattern` to control output filenames:
|
||||
|
||||
```bash
|
||||
# Default pattern
|
||||
--filename-pattern "{{.Name}}.txt"
|
||||
|
||||
# With transformations
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
|
||||
# Nested directories
|
||||
--filename-pattern "{{.Schema}}/{{.Name}}.md"
|
||||
|
||||
# Complex patterns
|
||||
--filename-pattern "{{.ParentSchema.Name}}/models/{{.Name | toPascalCase}}Model.java"
|
||||
```
|
||||
|
||||
## Template Functions
|
||||
|
||||
### String Utilities
|
||||
|
||||
Transform and manipulate strings in your templates.
|
||||
|
||||
| Function | Description | Example | Output |
|
||||
|----------|-------------|---------|--------|
|
||||
| `toUpper` | Convert to uppercase | `{{ "hello" \| toUpper }}` | `HELLO` |
|
||||
| `toLower` | Convert to lowercase | `{{ "HELLO" \| toLower }}` | `hello` |
|
||||
| `toCamelCase` | Convert to camelCase | `{{ "user_name" \| toCamelCase }}` | `userName` |
|
||||
| `toPascalCase` | Convert to PascalCase | `{{ "user_name" \| toPascalCase }}` | `UserName` |
|
||||
| `toSnakeCase` | Convert to snake_case | `{{ "UserName" \| toSnakeCase }}` | `user_name` |
|
||||
| `toKebabCase` | Convert to kebab-case | `{{ "UserName" \| toKebabCase }}` | `user-name` |
|
||||
| `pluralize` | Convert to plural | `{{ "user" \| pluralize }}` | `users` |
|
||||
| `singularize` | Convert to singular | `{{ "users" \| singularize }}` | `user` |
|
||||
| `title` | Capitalize first letter | `{{ "hello world" \| title }}` | `Hello World` |
|
||||
| `trim` | Trim whitespace | `{{ " hello " \| trim }}` | `hello` |
|
||||
| `trimPrefix` | Remove prefix | `{{ trimPrefix "tbl_users" "tbl_" }}` | `users` |
|
||||
| `trimSuffix` | Remove suffix | `{{ trimSuffix "users_old" "_old" }}` | `users` |
|
||||
| `replace` | Replace occurrences | `{{ replace "hello" "l" "L" -1 }}` | `heLLo` |
|
||||
| `stringContains` | Check if contains substring | `{{ stringContains "hello" "ell" }}` | `true` |
|
||||
| `hasPrefix` | Check if starts with | `{{ hasPrefix "hello" "hel" }}` | `true` |
|
||||
| `hasSuffix` | Check if ends with | `{{ hasSuffix "hello" "llo" }}` | `true` |
|
||||
| `split` | Split by separator | `{{ split "a,b,c" "," }}` | `[a b c]` |
|
||||
| `join` | Join with separator | `{{ join (list "a" "b") "," }}` | `a,b` |
|
||||
|
||||
### Type Conversion
|
||||
|
||||
Convert SQL types to various programming language types.
|
||||
|
||||
| Function | Parameters | Description | Example |
|
||||
|----------|------------|-------------|---------|
|
||||
| `sqlToGo` | `sqlType`, `nullable` | SQL to Go | `{{ sqlToGo "varchar" true }}` → `string` |
|
||||
| `sqlToTypeScript` | `sqlType`, `nullable` | SQL to TypeScript | `{{ sqlToTypeScript "integer" false }}` → `number \| null` |
|
||||
| `sqlToJava` | `sqlType`, `nullable` | SQL to Java | `{{ sqlToJava "varchar" true }}` → `String` |
|
||||
| `sqlToPython` | `sqlType` | SQL to Python | `{{ sqlToPython "integer" }}` → `int` |
|
||||
| `sqlToRust` | `sqlType`, `nullable` | SQL to Rust | `{{ sqlToRust "varchar" false }}` → `Option<String>` |
|
||||
| `sqlToCSharp` | `sqlType`, `nullable` | SQL to C# | `{{ sqlToCSharp "integer" false }}` → `int?` |
|
||||
| `sqlToPhp` | `sqlType`, `nullable` | SQL to PHP | `{{ sqlToPhp "varchar" false }}` → `?string` |
|
||||
|
||||
**Supported SQL Types:**
|
||||
- Integer: `integer`, `int`, `smallint`, `bigint`, `serial`, `bigserial`
|
||||
- String: `text`, `varchar`, `char`, `character`, `citext`
|
||||
- Boolean: `boolean`, `bool`
|
||||
- Float: `real`, `float`, `double precision`, `numeric`, `decimal`
|
||||
- Date/Time: `timestamp`, `date`, `time`, `timestamptz`
|
||||
- Binary: `bytea`
|
||||
- Special: `uuid`, `json`, `jsonb`, `array`
|
||||
|
||||
### Filtering
|
||||
|
||||
Filter and select specific database objects.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `filterTables` | Filter tables by pattern | `{{ filterTables .Schema.Tables "user_*" }}` |
|
||||
| `filterTablesByPattern` | Alias for filterTables | `{{ filterTablesByPattern .Schema.Tables "temp_*" }}` |
|
||||
| `filterColumns` | Filter columns by pattern | `{{ filterColumns .Table.Columns "*_id" }}` |
|
||||
| `filterColumnsByType` | Filter by SQL type | `{{ filterColumnsByType .Table.Columns "varchar" }}` |
|
||||
| `filterPrimaryKeys` | Get primary key columns | `{{ filterPrimaryKeys .Table.Columns }}` |
|
||||
| `filterForeignKeys` | Get foreign key constraints | `{{ filterForeignKeys .Table.Constraints }}` |
|
||||
| `filterUniqueConstraints` | Get unique constraints | `{{ filterUniqueConstraints .Table.Constraints }}` |
|
||||
| `filterCheckConstraints` | Get check constraints | `{{ filterCheckConstraints .Table.Constraints }}` |
|
||||
| `filterNullable` | Get nullable columns | `{{ filterNullable .Table.Columns }}` |
|
||||
| `filterNotNull` | Get non-nullable columns | `{{ filterNotNull .Table.Columns }}` |
|
||||
|
||||
**Pattern Matching:**
|
||||
- `*` - Match any characters
|
||||
- `?` - Match single character
|
||||
- Example: `user_*` matches `user_profile`, `user_settings`
|
||||
|
||||
### Formatting
|
||||
|
||||
Format output and add structure to generated code.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `toJSON` | Convert to JSON | `{{ .Database \| toJSON }}` |
|
||||
| `toJSONPretty` | Pretty-print JSON | `{{ toJSONPretty .Table " " }}` |
|
||||
| `toYAML` | Convert to YAML | `{{ .Schema \| toYAML }}` |
|
||||
| `indent` | Indent by spaces | `{{ indent .Column.Description 4 }}` |
|
||||
| `indentWith` | Indent with prefix | `{{ indentWith .Comment " " }}` |
|
||||
| `escape` | Escape special chars | `{{ escape .Column.Default }}` |
|
||||
| `escapeQuotes` | Escape quotes only | `{{ escapeQuotes .String }}` |
|
||||
| `comment` | Add comment prefix | `{{ comment .Description "//" }}` |
|
||||
| `quoteString` | Add quotes | `{{ quoteString "value" }}` → `"value"` |
|
||||
| `unquoteString` | Remove quotes | `{{ unquoteString "\"value\"" }}` → `value` |
|
||||
|
||||
**Comment Styles:**
|
||||
- `//` - C/Go/JavaScript style
|
||||
- `#` - Python/Shell style
|
||||
- `--` - SQL style
|
||||
- `/* */` - Block comment style
|
||||
|
||||
### Loop Helpers
|
||||
|
||||
Iterate and manipulate collections.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `enumerate` | Add index to items | `{{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}` |
|
||||
| `batch` | Split into chunks | `{{ range batch .Columns 3 }}...{{ end }}` |
|
||||
| `chunk` | Alias for batch | `{{ range chunk .Columns 5 }}...{{ end }}` |
|
||||
| `reverse` | Reverse order | `{{ range reverse .Tables }}...{{ end }}` |
|
||||
| `first` | Get first N items | `{{ range first .Tables 5 }}...{{ end }}` |
|
||||
| `last` | Get last N items | `{{ range last .Tables 3 }}...{{ end }}` |
|
||||
| `skip` | Skip first N items | `{{ range skip .Tables 2 }}...{{ end }}` |
|
||||
| `take` | Take first N (alias) | `{{ range take .Tables 10 }}...{{ end }}` |
|
||||
| `concat` | Concatenate slices | `{{ $all := concat .Schema1.Tables .Schema2.Tables }}` |
|
||||
| `unique` | Remove duplicates | `{{ $unique := unique .Items }}` |
|
||||
| `sortBy` | Sort by field | `{{ $sorted := sortBy .Tables "Name" }}` |
|
||||
| `groupBy` | Group by field | `{{ $grouped := groupBy .Tables "Schema" }}` |
|
||||
|
||||
### Sorting Helpers
|
||||
|
||||
Sort database objects by name or sequence number. All sort functions modify the slice in-place.
|
||||
|
||||
**Schema Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortSchemasByName` | Sort schemas by name | `{{ sortSchemasByName .Database.Schemas false }}` |
|
||||
| `sortSchemasBySequence` | Sort schemas by sequence | `{{ sortSchemasBySequence .Database.Schemas false }}` |
|
||||
|
||||
**Table Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortTablesByName` | Sort tables by name | `{{ sortTablesByName .Schema.Tables false }}` |
|
||||
| `sortTablesBySequence` | Sort tables by sequence | `{{ sortTablesBySequence .Schema.Tables true }}` |
|
||||
|
||||
**Column Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortColumnsMapByName` | Convert column map to sorted slice by name | `{{ $cols := sortColumnsMapByName .Table.Columns false }}` |
|
||||
| `sortColumnsMapBySequence` | Convert column map to sorted slice by sequence | `{{ $cols := sortColumnsMapBySequence .Table.Columns false }}` |
|
||||
| `sortColumnsByName` | Sort column slice by name | `{{ sortColumnsByName $columns false }}` |
|
||||
| `sortColumnsBySequence` | Sort column slice by sequence | `{{ sortColumnsBySequence $columns true }}` |
|
||||
|
||||
**Other Object Sorting:**
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `sortViewsByName` | Sort views by name | `{{ sortViewsByName .Schema.Views false }}` |
|
||||
| `sortViewsBySequence` | Sort views by sequence | `{{ sortViewsBySequence .Schema.Views false }}` |
|
||||
| `sortSequencesByName` | Sort sequences by name | `{{ sortSequencesByName .Schema.Sequences false }}` |
|
||||
| `sortSequencesBySequence` | Sort sequences by sequence | `{{ sortSequencesBySequence .Schema.Sequences false }}` |
|
||||
| `sortIndexesMapByName` | Convert index map to sorted slice by name | `{{ $idx := sortIndexesMapByName .Table.Indexes false }}` |
|
||||
| `sortIndexesMapBySequence` | Convert index map to sorted slice by sequence | `{{ $idx := sortIndexesMapBySequence .Table.Indexes false }}` |
|
||||
| `sortIndexesByName` | Sort index slice by name | `{{ sortIndexesByName $indexes false }}` |
|
||||
| `sortIndexesBySequence` | Sort index slice by sequence | `{{ sortIndexesBySequence $indexes false }}` |
|
||||
| `sortConstraintsMapByName` | Convert constraint map to sorted slice by name | `{{ $cons := sortConstraintsMapByName .Table.Constraints false }}` |
|
||||
| `sortConstraintsByName` | Sort constraint slice by name | `{{ sortConstraintsByName $constraints false }}` |
|
||||
| `sortRelationshipsMapByName` | Convert relationship map to sorted slice by name | `{{ $rels := sortRelationshipsMapByName .Table.Relationships false }}` |
|
||||
| `sortRelationshipsByName` | Sort relationship slice by name | `{{ sortRelationshipsByName $relationships false }}` |
|
||||
| `sortScriptsByName` | Sort scripts by name | `{{ sortScriptsByName .Schema.Scripts false }}` |
|
||||
| `sortEnumsByName` | Sort enums by name | `{{ sortEnumsByName .Schema.Enums false }}` |
|
||||
|
||||
**Sort Parameters:**
|
||||
- Second parameter: `false` = ascending, `true` = descending
|
||||
- Example: `{{ sortTablesByName .Schema.Tables true }}` sorts descending (Z-A)
|
||||
|
||||
### Safe Access
|
||||
|
||||
Safely access nested data without panicking.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `get` | Get map value | `{{ get .Metadata "key" }}` |
|
||||
| `getOr` | Get with default | `{{ getOr .Metadata "key" "default" }}` |
|
||||
| `getPath` | Nested access | `{{ getPath .Config "database.host" }}` |
|
||||
| `getPathOr` | Nested with default | `{{ getPathOr .Config "db.port" 5432 }}` |
|
||||
| `safeIndex` | Safe array access | `{{ safeIndex .Tables 0 }}` |
|
||||
| `safeIndexOr` | Safe with default | `{{ safeIndexOr .Tables 0 nil }}` |
|
||||
| `has` | Check key exists | `{{ if has .Metadata "key" }}...{{ end }}` |
|
||||
| `hasPath` | Check nested path | `{{ if hasPath .Config "db.host" }}...{{ end }}` |
|
||||
| `keys` | Get map keys | `{{ range keys .Metadata }}...{{ end }}` |
|
||||
| `values` | Get map values | `{{ range values .Table.Columns }}...{{ end }}` |
|
||||
| `merge` | Merge maps | `{{ $merged := merge .Map1 .Map2 }}` |
|
||||
| `pick` | Select keys | `{{ $subset := pick .Metadata "name" "desc" }}` |
|
||||
| `omit` | Exclude keys | `{{ $filtered := omit .Metadata "internal" }}` |
|
||||
| `sliceContains` | Check contains | `{{ if sliceContains .Names "admin" }}...{{ end }}` |
|
||||
| `indexOf` | Find index | `{{ $idx := indexOf .Names "admin" }}` |
|
||||
| `pluck` | Extract field | `{{ $names := pluck .Tables "Name" }}` |
|
||||
|
||||
### Utility Functions
|
||||
|
||||
General-purpose template helpers.
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `add` | Add numbers | `{{ add 5 3 }}` → `8` |
|
||||
| `sub` | Subtract | `{{ sub 10 3 }}` → `7` |
|
||||
| `mul` | Multiply | `{{ mul 4 5 }}` → `20` |
|
||||
| `div` | Divide | `{{ div 10 2 }}` → `5` |
|
||||
| `mod` | Modulo | `{{ mod 10 3 }}` → `1` |
|
||||
| `default` | Default value | `{{ default "unknown" .Name }}` |
|
||||
| `dict` | Create map | `{{ $m := dict "key1" "val1" "key2" "val2" }}` |
|
||||
| `list` | Create list | `{{ $l := list "a" "b" "c" }}` |
|
||||
| `seq` | Number sequence | `{{ range seq 1 5 }}{{ . }}{{ end }}` → `12345` |
|
||||
|
||||
## Data Model
|
||||
|
||||
The data available in templates depends on the execution mode:
|
||||
|
||||
### Database Mode
|
||||
```go
|
||||
.Database // *models.Database - Full database
|
||||
.ParentDatabase // *models.Database - Same as .Database
|
||||
.FlatColumns // []*models.FlatColumn - All columns flattened
|
||||
.FlatTables // []*models.FlatTable - All tables flattened
|
||||
.FlatConstraints // []*models.FlatConstraint - All constraints
|
||||
.FlatRelationships // []*models.FlatRelationship - All relationships
|
||||
.Summary // *models.DatabaseSummary - Statistics
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Schema Mode
|
||||
```go
|
||||
.Schema // *models.Schema - Current schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.FlatColumns // []*models.FlatColumn - Schema's columns flattened
|
||||
.FlatTables // []*models.FlatTable - Schema's tables flattened
|
||||
.FlatConstraints // []*models.FlatConstraint - Schema's constraints
|
||||
.FlatRelationships // []*models.FlatRelationship - Schema's relationships
|
||||
.Summary // *models.DatabaseSummary - Statistics
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Table Mode
|
||||
```go
|
||||
.Table // *models.Table - Current table
|
||||
.ParentSchema // *models.Schema - Parent schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Script Mode
|
||||
```go
|
||||
.Script // *models.Script - Current script
|
||||
.ParentSchema // *models.Schema - Parent schema
|
||||
.ParentDatabase // *models.Database - Parent database context
|
||||
.Metadata // map[string]interface{} - User metadata
|
||||
```
|
||||
|
||||
### Model Structures
|
||||
|
||||
**Database:**
|
||||
- `.Name` - Database name
|
||||
- `.Schemas` - List of schemas
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
**Schema:**
|
||||
- `.Name` - Schema name
|
||||
- `.Tables` - List of tables
|
||||
- `.Views`, `.Sequences`, `.Scripts` - Other objects
|
||||
- `.Enums` - Enum types
|
||||
|
||||
**Table:**
|
||||
- `.Name` - Table name
|
||||
- `.Schema` - Schema name
|
||||
- `.Columns` - Map of columns (use `values` function to iterate)
|
||||
- `.Constraints` - Map of constraints
|
||||
- `.Indexes` - Map of indexes
|
||||
- `.Relationships` - Map of relationships
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
**Column:**
|
||||
- `.Name` - Column name
|
||||
- `.Type` - SQL type
|
||||
- `.NotNull` - Is NOT NULL
|
||||
- `.IsPrimaryKey` - Is primary key
|
||||
- `.Default` - Default value
|
||||
- `.Description`, `.Comment` - Documentation
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: TypeScript Interfaces (Table Mode)
|
||||
|
||||
**Template:** `typescript-interface.tmpl`
|
||||
```typescript
|
||||
// Generated from {{ .ParentDatabase.Name }}.{{ .ParentSchema.Name }}.{{ .Table.Name }}
|
||||
|
||||
export interface {{ .Table.Name | toPascalCase }} {
|
||||
{{- range .Table.Columns | values }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type .NotNull }};
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{- $fks := filterForeignKeys .Table.Constraints }}
|
||||
{{- if $fks }}
|
||||
|
||||
// Foreign Keys:
|
||||
{{- range $fks }}
|
||||
// - {{ .Name }}: references {{ .ReferencedTable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
relspec templ --from pgsql --from-conn "..." \
|
||||
--template typescript-interface.tmpl \
|
||||
--mode table \
|
||||
--output ./src/types/ \
|
||||
--filename-pattern "{{.Name | toCamelCase}}.ts"
|
||||
```
|
||||
|
||||
### Example 2: Markdown Documentation (Database Mode)
|
||||
|
||||
**Template:** `database-docs.tmpl`
|
||||
```markdown
|
||||
# Database: {{ .Database.Name }}
|
||||
|
||||
{{ if .Database.Description }}{{ .Database.Description }}{{ end }}
|
||||
|
||||
**Statistics:**
|
||||
- Schemas: {{ len .Database.Schemas }}
|
||||
- Tables: {{ .Summary.TotalTables }}
|
||||
- Columns: {{ .Summary.TotalColumns }}
|
||||
|
||||
{{ range .Database.Schemas }}
|
||||
## Schema: {{ .Name }}
|
||||
|
||||
{{ range .Tables }}
|
||||
### {{ .Name }}
|
||||
|
||||
{{ if .Description }}{{ .Description }}{{ end }}
|
||||
|
||||
**Columns:**
|
||||
|
||||
| Column | Type | Nullable | PK | Description |
|
||||
|--------|------|----------|----|----|
|
||||
{{- range .Columns | values }}
|
||||
| {{ .Name }} | `{{ .Type }}` | {{ if .NotNull }}No{{ else }}Yes{{ end }} | {{ if .IsPrimaryKey }}✓{{ end }} | {{ .Description }} |
|
||||
{{- end }}
|
||||
|
||||
{{- $fks := filterForeignKeys .Constraints }}
|
||||
{{- if $fks }}
|
||||
|
||||
**Foreign Keys:**
|
||||
|
||||
{{ range $fks }}
|
||||
- `{{ .Name }}`: {{ join .Columns ", " }} → {{ .ReferencedTable }}({{ join .ReferencedColumns ", " }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Example 3: Python SQLAlchemy Models (Table Mode)
|
||||
|
||||
**Template:** `python-model.tmpl`
|
||||
```python
|
||||
"""{{ .Table.Name | toPascalCase }} model for {{ .ParentDatabase.Name }}.{{ .ParentSchema.Name }}"""
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class {{ .Table.Name | toPascalCase }}(Base):
|
||||
"""{{ if .Table.Description }}{{ .Table.Description }}{{ else }}{{ .Table.Name }} table{{ end }}"""
|
||||
|
||||
__tablename__ = "{{ .Table.Name }}"
|
||||
__table_args__ = {"schema": "{{ .ParentSchema.Name }}"}
|
||||
|
||||
{{- range .Table.Columns | values }}
|
||||
{{ .Name }} = Column({{ sqlToPython .Type }}{{ if .IsPrimaryKey }}, primary_key=True{{ end }}{{ if .NotNull }}, nullable=False{{ end }})
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
### Example 4: GraphQL Schema (Schema Mode)
|
||||
|
||||
**Template:** `graphql-schema.tmpl`
|
||||
```graphql
|
||||
"""{{ .Schema.Name }} schema"""
|
||||
|
||||
{{ range .Schema.Tables }}
|
||||
type {{ .Name | toPascalCase }} {
|
||||
{{- range .Columns | values }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type .NotNull | replace " | null" "" }}{{ if not .NotNull }}{{ end }}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
input {{ .Name | toPascalCase }}Input {
|
||||
{{- $cols := filterNotNull .Columns | filterPrimaryKeys }}
|
||||
{{- range $cols }}
|
||||
{{ .Name | toCamelCase }}: {{ sqlToTypeScript .Type true | replace " | null" "" }}!
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Example 5: SQL Migration (Database Mode)
|
||||
|
||||
**Template:** `migration.tmpl`
|
||||
```sql
|
||||
-- Migration for {{ .Database.Name }}
|
||||
-- Generated: {{ .Metadata.timestamp }}
|
||||
|
||||
BEGIN;
|
||||
|
||||
{{ range .Database.Schemas }}
|
||||
-- Schema: {{ .Name }}
|
||||
CREATE SCHEMA IF NOT EXISTS {{ .Name }};
|
||||
|
||||
{{ range .Tables }}
|
||||
CREATE TABLE {{ $.Database.Name }}.{{ .Schema }}.{{ .Name }} (
|
||||
{{- range $i, $col := .Columns | values }}
|
||||
{{- if $i }},{{ end }}
|
||||
{{ $col.Name }} {{ $col.Type }}{{ if $col.NotNull }} NOT NULL{{ end }}{{ if $col.Default }} DEFAULT {{ $col.Default }}{{ end }}
|
||||
{{- end }}
|
||||
);
|
||||
|
||||
{{- $pks := filterPrimaryKeys .Columns }}
|
||||
{{- if $pks }}
|
||||
|
||||
ALTER TABLE {{ $.Database.Name }}.{{ .Schema }}.{{ .Name }}
|
||||
ADD PRIMARY KEY ({{ range $i, $pk := $pks }}{{ if $i }}, {{ end }}{{ $pk.Name }}{{ end }});
|
||||
{{- end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Hyphen for Whitespace Control:**
|
||||
```
|
||||
{{- removes whitespace before
|
||||
-}} removes whitespace after
|
||||
```
|
||||
|
||||
2. **Store Intermediate Results:**
|
||||
```
|
||||
{{ $pks := filterPrimaryKeys .Table.Columns }}
|
||||
{{ if $pks }}...{{ end }}
|
||||
```
|
||||
|
||||
3. **Check Before Accessing:**
|
||||
```
|
||||
{{ if .Table.Description }}{{ .Table.Description }}{{ end }}
|
||||
```
|
||||
|
||||
4. **Use Safe Access for Maps:**
|
||||
```
|
||||
{{ getOr .Metadata "key" "default-value" }}
|
||||
```
|
||||
|
||||
5. **Iterate Map Values:**
|
||||
```
|
||||
{{ range .Table.Columns | values }}...{{ end }}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Error: "wrong type for value"**
|
||||
- Check function parameter order (e.g., `sqlToGo .Type .NotNull` not `.NotNull .Type`)
|
||||
|
||||
**Error: "can't evaluate field"**
|
||||
- Field doesn't exist on the object
|
||||
- Use `{{ if .Field }}` to check before accessing
|
||||
|
||||
**Empty Output:**
|
||||
- Check your mode matches your template expectations
|
||||
- Verify data exists (use `{{ .Database | toJSON }}` to inspect)
|
||||
|
||||
**Whitespace Issues:**
|
||||
- Use `{{-` and `-}}` to control whitespace
|
||||
- Run output through a formatter if needed
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Go Template Documentation](https://pkg.go.dev/text/template)
|
||||
- [RelSpec Documentation](../README.md)
|
||||
- [Model Structure Reference](../pkg/models/)
|
||||
- [Example Templates](../examples/templates/)
|
||||
74
pkg/commontypes/csharp.go
Normal file
74
pkg/commontypes/csharp.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// CSharpTypeMap maps PostgreSQL types to C# types
|
||||
var CSharpTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "short",
|
||||
"int2": "short",
|
||||
"bigint": "long",
|
||||
"int8": "long",
|
||||
"serial": "int",
|
||||
"bigserial": "long",
|
||||
"smallserial": "short",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "Guid",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "double",
|
||||
"float8": "double",
|
||||
"numeric": "decimal",
|
||||
"decimal": "decimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "DateTime",
|
||||
"timestamp without time zone": "DateTime",
|
||||
"timestamp with time zone": "DateTimeOffset",
|
||||
"timestamptz": "DateTimeOffset",
|
||||
"date": "DateTime",
|
||||
"time": "TimeSpan",
|
||||
"time without time zone": "TimeSpan",
|
||||
"time with time zone": "DateTimeOffset",
|
||||
"timetz": "DateTimeOffset",
|
||||
|
||||
// Binary
|
||||
"bytea": "byte[]",
|
||||
|
||||
// JSON
|
||||
"json": "string",
|
||||
"jsonb": "string",
|
||||
}
|
||||
|
||||
// SQLToCSharp converts SQL types to C# types
|
||||
func SQLToCSharp(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
csType, ok := CSharpTypeMap[baseType]
|
||||
if !ok {
|
||||
csType = "object"
|
||||
}
|
||||
|
||||
// Handle nullable value types (reference types are already nullable)
|
||||
if !nullable && csType != "string" && !strings.HasSuffix(csType, "[]") && csType != "object" {
|
||||
return csType + "?"
|
||||
}
|
||||
|
||||
return csType
|
||||
}
|
||||
89
pkg/commontypes/golang.go
Normal file
89
pkg/commontypes/golang.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// GoTypeMap maps PostgreSQL types to Go types
|
||||
var GoTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int32",
|
||||
"int": "int32",
|
||||
"int4": "int32",
|
||||
"smallint": "int16",
|
||||
"int2": "int16",
|
||||
"bigint": "int64",
|
||||
"int8": "int64",
|
||||
"serial": "int32",
|
||||
"bigserial": "int64",
|
||||
"smallserial": "int16",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float32",
|
||||
"float4": "float32",
|
||||
"double precision": "float64",
|
||||
"float8": "float64",
|
||||
"numeric": "float64",
|
||||
"decimal": "float64",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "time.Time",
|
||||
"timestamp without time zone": "time.Time",
|
||||
"timestamp with time zone": "time.Time",
|
||||
"timestamptz": "time.Time",
|
||||
"date": "time.Time",
|
||||
"time": "time.Time",
|
||||
"time without time zone": "time.Time",
|
||||
"time with time zone": "time.Time",
|
||||
"timetz": "time.Time",
|
||||
|
||||
// Binary
|
||||
"bytea": "[]byte",
|
||||
|
||||
// UUID
|
||||
"uuid": "string",
|
||||
|
||||
// JSON
|
||||
"json": "string",
|
||||
"jsonb": "string",
|
||||
|
||||
// Array
|
||||
"array": "[]string",
|
||||
}
|
||||
|
||||
// SQLToGo converts SQL types to Go types
|
||||
func SQLToGo(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
goType, ok := GoTypeMap[baseType]
|
||||
if !ok {
|
||||
goType = "interface{}"
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
if nullable {
|
||||
return goType
|
||||
}
|
||||
|
||||
// For nullable, use pointer types (except for slices and interfaces)
|
||||
if !strings.HasPrefix(goType, "[]") && goType != "interface{}" {
|
||||
return "*" + goType
|
||||
}
|
||||
|
||||
return goType
|
||||
}
|
||||
|
||||
// NeedsTimeImport checks if a Go type requires the time package
|
||||
func NeedsTimeImport(goType string) bool {
|
||||
return strings.Contains(goType, "time.Time")
|
||||
}
|
||||
68
pkg/commontypes/java.go
Normal file
68
pkg/commontypes/java.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package commontypes
|
||||
|
||||
// JavaTypeMap maps PostgreSQL types to Java types
|
||||
var JavaTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "Integer",
|
||||
"int": "Integer",
|
||||
"int4": "Integer",
|
||||
"smallint": "Short",
|
||||
"int2": "Short",
|
||||
"bigint": "Long",
|
||||
"int8": "Long",
|
||||
"serial": "Integer",
|
||||
"bigserial": "Long",
|
||||
"smallserial": "Short",
|
||||
|
||||
// String types
|
||||
"text": "String",
|
||||
"varchar": "String",
|
||||
"char": "String",
|
||||
"character": "String",
|
||||
"citext": "String",
|
||||
"bpchar": "String",
|
||||
"uuid": "UUID",
|
||||
|
||||
// Boolean
|
||||
"boolean": "Boolean",
|
||||
"bool": "Boolean",
|
||||
|
||||
// Float types
|
||||
"real": "Float",
|
||||
"float4": "Float",
|
||||
"double precision": "Double",
|
||||
"float8": "Double",
|
||||
"numeric": "BigDecimal",
|
||||
"decimal": "BigDecimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "Timestamp",
|
||||
"timestamp without time zone": "Timestamp",
|
||||
"timestamp with time zone": "Timestamp",
|
||||
"timestamptz": "Timestamp",
|
||||
"date": "Date",
|
||||
"time": "Time",
|
||||
"time without time zone": "Time",
|
||||
"time with time zone": "Time",
|
||||
"timetz": "Time",
|
||||
|
||||
// Binary
|
||||
"bytea": "byte[]",
|
||||
|
||||
// JSON
|
||||
"json": "String",
|
||||
"jsonb": "String",
|
||||
}
|
||||
|
||||
// SQLToJava converts SQL types to Java types
|
||||
func SQLToJava(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
javaType, ok := JavaTypeMap[baseType]
|
||||
if !ok {
|
||||
javaType = "Object"
|
||||
}
|
||||
|
||||
// Java uses wrapper classes for nullable types by default
|
||||
return javaType
|
||||
}
|
||||
72
pkg/commontypes/php.go
Normal file
72
pkg/commontypes/php.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package commontypes
|
||||
|
||||
// PHPTypeMap maps PostgreSQL types to PHP types
|
||||
var PHPTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "int",
|
||||
"int2": "int",
|
||||
"bigint": "int",
|
||||
"int8": "int",
|
||||
"serial": "int",
|
||||
"bigserial": "int",
|
||||
"smallserial": "int",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "float",
|
||||
"float8": "float",
|
||||
"numeric": "float",
|
||||
"decimal": "float",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "\\DateTime",
|
||||
"timestamp without time zone": "\\DateTime",
|
||||
"timestamp with time zone": "\\DateTime",
|
||||
"timestamptz": "\\DateTime",
|
||||
"date": "\\DateTime",
|
||||
"time": "\\DateTime",
|
||||
"time without time zone": "\\DateTime",
|
||||
"time with time zone": "\\DateTime",
|
||||
"timetz": "\\DateTime",
|
||||
|
||||
// Binary
|
||||
"bytea": "string",
|
||||
|
||||
// JSON
|
||||
"json": "array",
|
||||
"jsonb": "array",
|
||||
}
|
||||
|
||||
// SQLToPhp converts SQL types to PHP types
|
||||
func SQLToPhp(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
phpType, ok := PHPTypeMap[baseType]
|
||||
if !ok {
|
||||
phpType = "mixed"
|
||||
}
|
||||
|
||||
// PHP 7.1+ supports nullable types with ?Type syntax
|
||||
if !nullable && phpType != "mixed" {
|
||||
return "?" + phpType
|
||||
}
|
||||
|
||||
return phpType
|
||||
}
|
||||
71
pkg/commontypes/python.go
Normal file
71
pkg/commontypes/python.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package commontypes
|
||||
|
||||
// PythonTypeMap maps PostgreSQL types to Python types
|
||||
var PythonTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "int",
|
||||
"int": "int",
|
||||
"int4": "int",
|
||||
"smallint": "int",
|
||||
"int2": "int",
|
||||
"bigint": "int",
|
||||
"int8": "int",
|
||||
"serial": "int",
|
||||
"bigserial": "int",
|
||||
"smallserial": "int",
|
||||
|
||||
// String types
|
||||
"text": "str",
|
||||
"varchar": "str",
|
||||
"char": "str",
|
||||
"character": "str",
|
||||
"citext": "str",
|
||||
"bpchar": "str",
|
||||
"uuid": "UUID",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "float",
|
||||
"float4": "float",
|
||||
"double precision": "float",
|
||||
"float8": "float",
|
||||
"numeric": "Decimal",
|
||||
"decimal": "Decimal",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "datetime",
|
||||
"timestamp without time zone": "datetime",
|
||||
"timestamp with time zone": "datetime",
|
||||
"timestamptz": "datetime",
|
||||
"date": "date",
|
||||
"time": "time",
|
||||
"time without time zone": "time",
|
||||
"time with time zone": "time",
|
||||
"timetz": "time",
|
||||
|
||||
// Binary
|
||||
"bytea": "bytes",
|
||||
|
||||
// JSON
|
||||
"json": "dict",
|
||||
"jsonb": "dict",
|
||||
|
||||
// Array
|
||||
"array": "list",
|
||||
}
|
||||
|
||||
// SQLToPython converts SQL types to Python types
|
||||
func SQLToPython(sqlType string) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
pyType, ok := PythonTypeMap[baseType]
|
||||
if !ok {
|
||||
pyType = "Any"
|
||||
}
|
||||
|
||||
// Python uses Optional[Type] for nullable, but we return the base type
|
||||
return pyType
|
||||
}
|
||||
72
pkg/commontypes/rust.go
Normal file
72
pkg/commontypes/rust.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package commontypes
|
||||
|
||||
// RustTypeMap maps PostgreSQL types to Rust types
|
||||
var RustTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "i32",
|
||||
"int": "i32",
|
||||
"int4": "i32",
|
||||
"smallint": "i16",
|
||||
"int2": "i16",
|
||||
"bigint": "i64",
|
||||
"int8": "i64",
|
||||
"serial": "i32",
|
||||
"bigserial": "i64",
|
||||
"smallserial": "i16",
|
||||
|
||||
// String types
|
||||
"text": "String",
|
||||
"varchar": "String",
|
||||
"char": "String",
|
||||
"character": "String",
|
||||
"citext": "String",
|
||||
"bpchar": "String",
|
||||
"uuid": "String",
|
||||
|
||||
// Boolean
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
|
||||
// Float types
|
||||
"real": "f32",
|
||||
"float4": "f32",
|
||||
"double precision": "f64",
|
||||
"float8": "f64",
|
||||
"numeric": "f64",
|
||||
"decimal": "f64",
|
||||
|
||||
// Date/Time types (using chrono crate)
|
||||
"timestamp": "NaiveDateTime",
|
||||
"timestamp without time zone": "NaiveDateTime",
|
||||
"timestamp with time zone": "DateTime<Utc>",
|
||||
"timestamptz": "DateTime<Utc>",
|
||||
"date": "NaiveDate",
|
||||
"time": "NaiveTime",
|
||||
"time without time zone": "NaiveTime",
|
||||
"time with time zone": "DateTime<Utc>",
|
||||
"timetz": "DateTime<Utc>",
|
||||
|
||||
// Binary
|
||||
"bytea": "Vec<u8>",
|
||||
|
||||
// JSON
|
||||
"json": "serde_json::Value",
|
||||
"jsonb": "serde_json::Value",
|
||||
}
|
||||
|
||||
// SQLToRust converts SQL types to Rust types
|
||||
func SQLToRust(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
rustType, ok := RustTypeMap[baseType]
|
||||
if !ok {
|
||||
rustType = "String"
|
||||
}
|
||||
|
||||
// Handle nullable types with Option<T>
|
||||
if nullable {
|
||||
return rustType
|
||||
}
|
||||
|
||||
return "Option<" + rustType + ">"
|
||||
}
|
||||
22
pkg/commontypes/sql.go
Normal file
22
pkg/commontypes/sql.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package commontypes
|
||||
|
||||
import "strings"
|
||||
|
||||
// ExtractBaseType extracts the base type from a SQL type string
|
||||
// Examples: varchar(100) → varchar, numeric(10,2) → numeric
|
||||
func ExtractBaseType(sqlType string) string {
|
||||
sqlType = strings.ToLower(strings.TrimSpace(sqlType))
|
||||
|
||||
// Remove everything after '('
|
||||
if idx := strings.Index(sqlType, "("); idx > 0 {
|
||||
sqlType = sqlType[:idx]
|
||||
}
|
||||
|
||||
return sqlType
|
||||
}
|
||||
|
||||
// NormalizeType normalizes a SQL type to its base form
|
||||
// Alias for ExtractBaseType for backwards compatibility
|
||||
func NormalizeType(sqlType string) string {
|
||||
return ExtractBaseType(sqlType)
|
||||
}
|
||||
75
pkg/commontypes/typescript.go
Normal file
75
pkg/commontypes/typescript.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package commontypes
|
||||
|
||||
// TypeScriptTypeMap maps PostgreSQL types to TypeScript types
|
||||
var TypeScriptTypeMap = map[string]string{
|
||||
// Integer types
|
||||
"integer": "number",
|
||||
"int": "number",
|
||||
"int4": "number",
|
||||
"smallint": "number",
|
||||
"int2": "number",
|
||||
"bigint": "number",
|
||||
"int8": "number",
|
||||
"serial": "number",
|
||||
"bigserial": "number",
|
||||
"smallserial": "number",
|
||||
|
||||
// String types
|
||||
"text": "string",
|
||||
"varchar": "string",
|
||||
"char": "string",
|
||||
"character": "string",
|
||||
"citext": "string",
|
||||
"bpchar": "string",
|
||||
"uuid": "string",
|
||||
|
||||
// Boolean
|
||||
"boolean": "boolean",
|
||||
"bool": "boolean",
|
||||
|
||||
// Float types
|
||||
"real": "number",
|
||||
"float4": "number",
|
||||
"double precision": "number",
|
||||
"float8": "number",
|
||||
"numeric": "number",
|
||||
"decimal": "number",
|
||||
|
||||
// Date/Time types
|
||||
"timestamp": "Date",
|
||||
"timestamp without time zone": "Date",
|
||||
"timestamp with time zone": "Date",
|
||||
"timestamptz": "Date",
|
||||
"date": "Date",
|
||||
"time": "Date",
|
||||
"time without time zone": "Date",
|
||||
"time with time zone": "Date",
|
||||
"timetz": "Date",
|
||||
|
||||
// Binary
|
||||
"bytea": "Buffer",
|
||||
|
||||
// JSON
|
||||
"json": "any",
|
||||
"jsonb": "any",
|
||||
|
||||
// Array
|
||||
"array": "any[]",
|
||||
}
|
||||
|
||||
// SQLToTypeScript converts SQL types to TypeScript types
|
||||
func SQLToTypeScript(sqlType string, nullable bool) string {
|
||||
baseType := ExtractBaseType(sqlType)
|
||||
|
||||
tsType, ok := TypeScriptTypeMap[baseType]
|
||||
if !ok {
|
||||
tsType = "any"
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
if nullable {
|
||||
return tsType
|
||||
}
|
||||
|
||||
return tsType + " | null"
|
||||
}
|
||||
266
pkg/models/sorting.go
Normal file
266
pkg/models/sorting.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SortOrder represents the sort direction
|
||||
type SortOrder bool
|
||||
|
||||
const (
|
||||
// Ascending sort order
|
||||
Ascending SortOrder = false
|
||||
// Descending sort order
|
||||
Descending SortOrder = true
|
||||
)
|
||||
|
||||
// Schema Sorting
|
||||
|
||||
// SortSchemasByName sorts schemas by name
|
||||
func SortSchemasByName(schemas []*Schema, desc bool) {
|
||||
sort.SliceStable(schemas, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(schemas[i].Name), strings.ToLower(schemas[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortSchemasBySequence sorts schemas by sequence number
|
||||
func SortSchemasBySequence(schemas []*Schema, desc bool) {
|
||||
sort.SliceStable(schemas, func(i, j int) bool {
|
||||
if desc {
|
||||
return schemas[i].Sequence > schemas[j].Sequence
|
||||
}
|
||||
return schemas[i].Sequence < schemas[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// Table Sorting
|
||||
|
||||
// SortTablesByName sorts tables by name
|
||||
func SortTablesByName(tables []*Table, desc bool) {
|
||||
sort.SliceStable(tables, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(tables[i].Name), strings.ToLower(tables[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortTablesBySequence sorts tables by sequence number
|
||||
func SortTablesBySequence(tables []*Table, desc bool) {
|
||||
sort.SliceStable(tables, func(i, j int) bool {
|
||||
if desc {
|
||||
return tables[i].Sequence > tables[j].Sequence
|
||||
}
|
||||
return tables[i].Sequence < tables[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// Column Sorting
|
||||
|
||||
// SortColumnsMapByName converts column map to sorted slice by name
|
||||
func SortColumnsMapByName(columns map[string]*Column, desc bool) []*Column {
|
||||
result := make([]*Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
SortColumnsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortColumnsMapBySequence converts column map to sorted slice by sequence
|
||||
func SortColumnsMapBySequence(columns map[string]*Column, desc bool) []*Column {
|
||||
result := make([]*Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
SortColumnsBySequence(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortColumnsByName sorts columns by name
|
||||
func SortColumnsByName(columns []*Column, desc bool) {
|
||||
sort.SliceStable(columns, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(columns[i].Name), strings.ToLower(columns[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortColumnsBySequence sorts columns by sequence number
|
||||
func SortColumnsBySequence(columns []*Column, desc bool) {
|
||||
sort.SliceStable(columns, func(i, j int) bool {
|
||||
if desc {
|
||||
return columns[i].Sequence > columns[j].Sequence
|
||||
}
|
||||
return columns[i].Sequence < columns[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// View Sorting
|
||||
|
||||
// SortViewsByName sorts views by name
|
||||
func SortViewsByName(views []*View, desc bool) {
|
||||
sort.SliceStable(views, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(views[i].Name), strings.ToLower(views[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortViewsBySequence sorts views by sequence number
|
||||
func SortViewsBySequence(views []*View, desc bool) {
|
||||
sort.SliceStable(views, func(i, j int) bool {
|
||||
if desc {
|
||||
return views[i].Sequence > views[j].Sequence
|
||||
}
|
||||
return views[i].Sequence < views[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// Sequence Sorting
|
||||
|
||||
// SortSequencesByName sorts sequences by name
|
||||
func SortSequencesByName(sequences []*Sequence, desc bool) {
|
||||
sort.SliceStable(sequences, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(sequences[i].Name), strings.ToLower(sequences[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortSequencesBySequence sorts sequences by sequence number
|
||||
func SortSequencesBySequence(sequences []*Sequence, desc bool) {
|
||||
sort.SliceStable(sequences, func(i, j int) bool {
|
||||
if desc {
|
||||
return sequences[i].Sequence > sequences[j].Sequence
|
||||
}
|
||||
return sequences[i].Sequence < sequences[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// Index Sorting
|
||||
|
||||
// SortIndexesMapByName converts index map to sorted slice by name
|
||||
func SortIndexesMapByName(indexes map[string]*Index, desc bool) []*Index {
|
||||
result := make([]*Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
SortIndexesByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortIndexesMapBySequence converts index map to sorted slice by sequence
|
||||
func SortIndexesMapBySequence(indexes map[string]*Index, desc bool) []*Index {
|
||||
result := make([]*Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
SortIndexesBySequence(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortIndexesByName sorts indexes by name
|
||||
func SortIndexesByName(indexes []*Index, desc bool) {
|
||||
sort.SliceStable(indexes, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(indexes[i].Name), strings.ToLower(indexes[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// SortIndexesBySequence sorts indexes by sequence number
|
||||
func SortIndexesBySequence(indexes []*Index, desc bool) {
|
||||
sort.SliceStable(indexes, func(i, j int) bool {
|
||||
if desc {
|
||||
return indexes[i].Sequence > indexes[j].Sequence
|
||||
}
|
||||
return indexes[i].Sequence < indexes[j].Sequence
|
||||
})
|
||||
}
|
||||
|
||||
// Constraint Sorting
|
||||
|
||||
// SortConstraintsMapByName converts constraint map to sorted slice by name
|
||||
func SortConstraintsMapByName(constraints map[string]*Constraint, desc bool) []*Constraint {
|
||||
result := make([]*Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
SortConstraintsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortConstraintsByName sorts constraints by name
|
||||
func SortConstraintsByName(constraints []*Constraint, desc bool) {
|
||||
sort.SliceStable(constraints, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(constraints[i].Name), strings.ToLower(constraints[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// Relationship Sorting
|
||||
|
||||
// SortRelationshipsMapByName converts relationship map to sorted slice by name
|
||||
func SortRelationshipsMapByName(relationships map[string]*Relationship, desc bool) []*Relationship {
|
||||
result := make([]*Relationship, 0, len(relationships))
|
||||
for _, r := range relationships {
|
||||
result = append(result, r)
|
||||
}
|
||||
SortRelationshipsByName(result, desc)
|
||||
return result
|
||||
}
|
||||
|
||||
// SortRelationshipsByName sorts relationships by name
|
||||
func SortRelationshipsByName(relationships []*Relationship, desc bool) {
|
||||
sort.SliceStable(relationships, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(relationships[i].Name), strings.ToLower(relationships[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// Script Sorting
|
||||
|
||||
// SortScriptsByName sorts scripts by name
|
||||
func SortScriptsByName(scripts []*Script, desc bool) {
|
||||
sort.SliceStable(scripts, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(scripts[i].Name), strings.ToLower(scripts[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// Enum Sorting
|
||||
|
||||
// SortEnumsByName sorts enums by name
|
||||
func SortEnumsByName(enums []*Enum, desc bool) {
|
||||
sort.SliceStable(enums, func(i, j int) bool {
|
||||
cmp := strings.Compare(strings.ToLower(enums[i].Name), strings.ToLower(enums[j].Name))
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
326
pkg/reflectutil/helpers.go
Normal file
326
pkg/reflectutil/helpers.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package reflectutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Deref dereferences pointers until it reaches a non-pointer value
|
||||
// Returns the dereferenced value and true if successful, or the original value and false if nil
|
||||
func Deref(v reflect.Value) (reflect.Value, bool) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return v, false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
// DerefInterface dereferences an interface{} until it reaches a non-pointer value
|
||||
func DerefInterface(i interface{}) reflect.Value {
|
||||
v := reflect.ValueOf(i)
|
||||
v, _ = Deref(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// GetFieldValue extracts a field value from a struct, map, or pointer
|
||||
// Returns nil if the field doesn't exist or can't be accessed
|
||||
func GetFieldValue(item interface{}, field string) interface{} {
|
||||
v := reflect.ValueOf(item)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
fieldVal := v.FieldByName(field)
|
||||
if fieldVal.IsValid() {
|
||||
return fieldVal.Interface()
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(field)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
return mapVal.Interface()
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsSliceOrArray checks if an interface{} is a slice or array
|
||||
func IsSliceOrArray(i interface{}) bool {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
k := v.Kind()
|
||||
return k == reflect.Slice || k == reflect.Array
|
||||
}
|
||||
|
||||
// IsMap checks if an interface{} is a map
|
||||
func IsMap(i interface{}) bool {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return v.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// SliceLen returns the length of a slice/array, or 0 if not a slice/array
|
||||
func SliceLen(i interface{}) int {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return 0
|
||||
}
|
||||
return v.Len()
|
||||
}
|
||||
|
||||
// MapLen returns the length of a map, or 0 if not a map
|
||||
func MapLen(i interface{}) int {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if v.Kind() != reflect.Map {
|
||||
return 0
|
||||
}
|
||||
return v.Len()
|
||||
}
|
||||
|
||||
// SliceToInterfaces converts a slice/array to []interface{}
|
||||
// Returns empty slice if not a slice/array
|
||||
func SliceToInterfaces(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeys returns all keys from a map as []interface{}
|
||||
// Returns empty slice if not a map
|
||||
func MapKeys(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
keys := v.MapKeys()
|
||||
result := make([]interface{}, len(keys))
|
||||
for i, key := range keys {
|
||||
result[i] = key.Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapValues returns all values from a map as []interface{}
|
||||
// Returns empty slice if not a map
|
||||
func MapValues(i interface{}) []interface{} {
|
||||
v := reflect.ValueOf(i)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, 0, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
result = append(result, iter.Value().Interface())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapGet safely gets a value from a map by key
|
||||
// Returns nil if key doesn't exist or not a map
|
||||
func MapGet(m interface{}, key interface{}) interface{} {
|
||||
v := reflect.ValueOf(m)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyVal := reflect.ValueOf(key)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
return mapVal.Interface()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SliceIndex safely gets an element from a slice/array by index
|
||||
// Returns nil if index out of bounds or not a slice/array
|
||||
func SliceIndex(slice interface{}, index int) interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return nil
|
||||
}
|
||||
|
||||
if index < 0 || index >= v.Len() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.Index(index).Interface()
|
||||
}
|
||||
|
||||
// CompareValues compares two values for sorting
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b
|
||||
func CompareValues(a, b interface{}) int {
|
||||
if a == nil && b == nil {
|
||||
return 0
|
||||
}
|
||||
if a == nil {
|
||||
return -1
|
||||
}
|
||||
if b == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
va := reflect.ValueOf(a)
|
||||
vb := reflect.ValueOf(b)
|
||||
|
||||
// Handle different types
|
||||
switch va.Kind() {
|
||||
case reflect.String:
|
||||
if vb.Kind() == reflect.String {
|
||||
as := va.String()
|
||||
bs := vb.String()
|
||||
if as < bs {
|
||||
return -1
|
||||
} else if as > bs {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if vb.Kind() >= reflect.Int && vb.Kind() <= reflect.Int64 {
|
||||
ai := va.Int()
|
||||
bi := vb.Int()
|
||||
if ai < bi {
|
||||
return -1
|
||||
} else if ai > bi {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if vb.Kind() >= reflect.Uint && vb.Kind() <= reflect.Uint64 {
|
||||
au := va.Uint()
|
||||
bu := vb.Uint()
|
||||
if au < bu {
|
||||
return -1
|
||||
} else if au > bu {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if vb.Kind() == reflect.Float32 || vb.Kind() == reflect.Float64 {
|
||||
af := va.Float()
|
||||
bf := vb.Float()
|
||||
if af < bf {
|
||||
return -1
|
||||
} else if af > bf {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetNestedValue gets a nested value using dot notation path
|
||||
// Example: GetNestedValue(obj, "database.schema.table")
|
||||
func GetNestedValue(m interface{}, path string) interface{} {
|
||||
if path == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
parts := strings.Split(path, ".")
|
||||
current := m
|
||||
|
||||
for _, part := range parts {
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(current)
|
||||
v, ok := Deref(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(part)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if !mapVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
current = mapVal.Interface()
|
||||
|
||||
case reflect.Struct:
|
||||
fieldVal := v.FieldByName(part)
|
||||
if !fieldVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
current = fieldVal.Interface()
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// DeepEqual performs a deep equality check between two values
|
||||
func DeepEqual(a, b interface{}) bool {
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
276
pkg/writers/template/README.md
Normal file
276
pkg/writers/template/README.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Template Writer
|
||||
|
||||
Custom template-based writer for RelSpec that allows users to generate any output format using Go text templates.
|
||||
|
||||
## Overview
|
||||
|
||||
The template writer provides a powerful and flexible way to transform database schemas into any desired format. It supports multiple execution modes and provides 80+ template functions for data transformation.
|
||||
|
||||
**For complete user documentation, see:** [/docs/TEMPLATE_MODE.md](../../../docs/TEMPLATE_MODE.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
pkg/writers/template/
|
||||
├── README.md # This file
|
||||
├── writer.go # Core writer with entrypoint mode logic
|
||||
├── template_data.go # Data structures passed to templates
|
||||
├── funcmap.go # Template function registry
|
||||
├── string_helpers.go # String manipulation functions
|
||||
├── type_mappers.go # SQL type conversion (delegates to commontypes)
|
||||
├── filters.go # Database object filtering
|
||||
├── formatters.go # JSON/YAML formatting utilities
|
||||
├── loop_helpers.go # Iteration and collection utilities
|
||||
├── safe_access.go # Safe map/array access functions
|
||||
└── errors.go # Custom error types
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **`pkg/commontypes`** - Centralized type mappings for Go, TypeScript, Java, Python, Rust, C#, PHP
|
||||
- **`pkg/reflectutil`** - Reflection utilities for safe type manipulation
|
||||
- **`pkg/models`** - Database schema models
|
||||
|
||||
## Writer Interface Implementation
|
||||
|
||||
Implements the standard `writers.Writer` interface:
|
||||
|
||||
```go
|
||||
type Writer interface {
|
||||
WriteDatabase(db *models.Database) error
|
||||
WriteSchema(schema *models.Schema) error
|
||||
WriteTable(table *models.Table) error
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The writer supports four execution modes via `WriterOptions.Metadata["mode"]`:
|
||||
|
||||
| Mode | Data Passed | Output | Use Case |
|
||||
|------|-------------|--------|----------|
|
||||
| `database` | Full database | Single file | Reports, documentation |
|
||||
| `schema` | One schema at a time | File per schema | Schema-specific docs |
|
||||
| `script` | One script at a time | File per script | Script processing |
|
||||
| `table` | One table at a time | File per table | Model generation |
|
||||
|
||||
## Configuration
|
||||
|
||||
Writer is configured via `WriterOptions.Metadata`:
|
||||
|
||||
```go
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": "/path/to/template.tmpl", // Required
|
||||
"mode": "table", // Default: "database"
|
||||
"filename_pattern": "{{.Name}}.ts", // Default: "{{.Name}}.txt"
|
||||
}
|
||||
```
|
||||
|
||||
## Template Data Structure
|
||||
|
||||
Templates receive a `TemplateData` struct:
|
||||
|
||||
```go
|
||||
type TemplateData struct {
|
||||
// Primary data (one populated based on mode)
|
||||
Database *models.Database
|
||||
Schema *models.Schema
|
||||
Script *models.Script
|
||||
Table *models.Table
|
||||
|
||||
// Parent context
|
||||
ParentSchema *models.Schema
|
||||
ParentDatabase *models.Database
|
||||
|
||||
// Pre-computed views
|
||||
FlatColumns []*models.FlatColumn
|
||||
FlatTables []*models.FlatTable
|
||||
FlatConstraints []*models.FlatConstraint
|
||||
FlatRelationships []*models.FlatRelationship
|
||||
Summary *models.DatabaseSummary
|
||||
|
||||
// User metadata
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
```
|
||||
|
||||
## Function Categories
|
||||
|
||||
### String Utilities (string_helpers.go)
|
||||
Case conversion, pluralization, trimming, splitting, joining
|
||||
|
||||
### Type Mappers (type_mappers.go)
|
||||
SQL type conversion to 7+ programming languages (delegates to `pkg/commontypes`)
|
||||
|
||||
### Filters (filters.go)
|
||||
Database object filtering by pattern, type, constraints
|
||||
|
||||
### Formatters (formatters.go)
|
||||
JSON/YAML serialization, indentation, escaping, commenting
|
||||
|
||||
### Loop Helpers (loop_helpers.go)
|
||||
Enumeration, batching, reversing, sorting, grouping (uses `pkg/reflectutil`)
|
||||
|
||||
### Safe Access (safe_access.go)
|
||||
Safe map/array access without panics (uses `pkg/reflectutil`)
|
||||
|
||||
## Adding New Functions
|
||||
|
||||
To add a new template function:
|
||||
|
||||
1. **Implement the function** in the appropriate file:
|
||||
```go
|
||||
// string_helpers.go
|
||||
func ToScreamingSnakeCase(s string) string {
|
||||
return strings.ToUpper(ToSnakeCase(s))
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in funcmap.go:**
|
||||
```go
|
||||
func BuildFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
// ... existing functions
|
||||
"toScreamingSnakeCase": ToScreamingSnakeCase,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Document in /docs/TEMPLATE_MODE.md**
|
||||
|
||||
## Error Handling
|
||||
|
||||
Custom error types in `errors.go`:
|
||||
|
||||
- `TemplateLoadError` - Template file not found or unreadable
|
||||
- `TemplateParseError` - Invalid template syntax
|
||||
- `TemplateExecuteError` - Error during template execution
|
||||
|
||||
All errors wrap the underlying error for context.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
go test ./pkg/writers/template/...
|
||||
|
||||
# Test with example data
|
||||
cat > test.tmpl << 'EOF'
|
||||
{{ range .Database.Schemas }}
|
||||
Schema: {{ .Name }} ({{ len .Tables }} tables)
|
||||
{{ end }}
|
||||
EOF
|
||||
|
||||
relspec templ --from json --from-path schema.json --template test.tmpl
|
||||
```
|
||||
|
||||
## Multi-file Output
|
||||
|
||||
For multi-file modes, the writer:
|
||||
|
||||
1. Iterates through items (schemas/scripts/tables)
|
||||
2. Creates `TemplateData` for each item
|
||||
3. Executes template with item data
|
||||
4. Generates filename using `filename_pattern` template
|
||||
5. Writes output to generated filename
|
||||
|
||||
Output directory is created automatically if it doesn't exist.
|
||||
|
||||
## Filename Pattern Execution
|
||||
|
||||
The filename pattern is itself a template:
|
||||
|
||||
```go
|
||||
// Pattern: "{{.Schema}}/{{.Name | toCamelCase}}.ts"
|
||||
// For table "user_profile" in schema "public"
|
||||
// Generates: "public/userProfile.ts"
|
||||
```
|
||||
|
||||
Available in pattern template:
|
||||
- `.Name` - Item name (schema/script/table)
|
||||
- `.Schema` - Schema name (for scripts/tables)
|
||||
- All template functions
|
||||
|
||||
## Example Usage
|
||||
|
||||
### As a Library
|
||||
|
||||
```go
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers/template"
|
||||
)
|
||||
|
||||
// Create writer
|
||||
metadata := map[string]interface{}{
|
||||
"template_path": "model.tmpl",
|
||||
"mode": "table",
|
||||
"filename_pattern": "{{.Name}}.go",
|
||||
}
|
||||
|
||||
opts := &writers.WriterOptions{
|
||||
OutputPath: "./models/",
|
||||
PackageName: "models",
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
writer, err := template.NewWriter(opts)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Write database
|
||||
err = writer.WriteDatabase(db)
|
||||
```
|
||||
|
||||
### Via CLI
|
||||
|
||||
```bash
|
||||
relspec templ \
|
||||
--from pgsql \
|
||||
--from-conn "postgres://localhost/mydb" \
|
||||
--template model.tmpl \
|
||||
--mode table \
|
||||
--output ./models/ \
|
||||
--filename-pattern "{{.Name | toPascalCase}}.go"
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Template Parsing** - Template is parsed once in `NewWriter()`, not per execution
|
||||
2. **Reflection** - Loop and safe access helpers use reflection; cached where possible
|
||||
3. **Pre-computed Views** - `FlatColumns`, `FlatTables`, etc. computed once per data item
|
||||
4. **File I/O** - Multi-file mode creates directories as needed
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
- [ ] Template caching for filename patterns
|
||||
- [ ] Parallel template execution for multi-file mode
|
||||
- [ ] Template function plugins
|
||||
- [ ] Custom function injection via metadata
|
||||
- [ ] Template includes/partials support
|
||||
- [ ] Dry-run mode to preview filenames
|
||||
- [ ] Progress reporting for large schemas
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. Follow existing patterns (see similar functions)
|
||||
2. Add to appropriate category file
|
||||
3. Register in `funcmap.go`
|
||||
4. Update `/docs/TEMPLATE_MODE.md`
|
||||
5. Add tests
|
||||
6. Consider edge cases (nil, empty, invalid input)
|
||||
|
||||
## See Also
|
||||
|
||||
- [User Documentation](/docs/TEMPLATE_MODE.md) - Complete template function reference
|
||||
- [Common Types Package](../../commontypes/) - Centralized type mappings
|
||||
- [Reflect Utilities](../../reflectutil/) - Reflection helpers
|
||||
- [Models Package](../../models/) - Database schema models
|
||||
- [Go Template Docs](https://pkg.go.dev/text/template) - Official Go template documentation
|
||||
50
pkg/writers/template/errors.go
Normal file
50
pkg/writers/template/errors.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package template
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TemplateError represents an error that occurred during template operations
|
||||
type TemplateError struct {
|
||||
Phase string // "load", "parse", "execute"
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *TemplateError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("template %s error: %s: %v", e.Phase, e.Message, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("template %s error: %s", e.Phase, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error
|
||||
func (e *TemplateError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewTemplateLoadError creates a new template load error
|
||||
func NewTemplateLoadError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "load",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTemplateParseError creates a new template parse error
|
||||
func NewTemplateParseError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "parse",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTemplateExecuteError creates a new template execution error
|
||||
func NewTemplateExecuteError(msg string, err error) *TemplateError {
|
||||
return &TemplateError{
|
||||
Phase: "execute",
|
||||
Message: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
144
pkg/writers/template/filters.go
Normal file
144
pkg/writers/template/filters.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/commontypes"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// FilterTables filters tables using a predicate function
|
||||
// Usage: {{ $filtered := filterTables .Schema.Tables (func $t) { return hasPrefix $t.Name "user_" } }}
|
||||
// Note: Template functions can't pass Go funcs, so this is primarily for internal use
|
||||
func FilterTables(tables []*models.Table, pattern string) []*models.Table {
|
||||
if pattern == "" {
|
||||
return tables
|
||||
}
|
||||
|
||||
result := make([]*models.Table, 0)
|
||||
for _, table := range tables {
|
||||
if matchPattern(table.Name, pattern) {
|
||||
result = append(result, table)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterTablesByPattern filters tables by name pattern (glob-style)
|
||||
// Usage: {{ $userTables := filterTablesByPattern .Schema.Tables "user_*" }}
|
||||
func FilterTablesByPattern(tables []*models.Table, pattern string) []*models.Table {
|
||||
return FilterTables(tables, pattern)
|
||||
}
|
||||
|
||||
// FilterColumns filters columns from a map using a pattern
|
||||
// Usage: {{ $filtered := filterColumns .Table.Columns "*_id" }}
|
||||
func FilterColumns(columns map[string]*models.Column, pattern string) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if pattern == "" || matchPattern(col.Name, pattern) {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterColumnsByType filters columns by SQL type
|
||||
// Usage: {{ $stringCols := filterColumnsByType .Table.Columns "varchar" }}
|
||||
func FilterColumnsByType(columns map[string]*models.Column, sqlType string) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
baseType := commontypes.ExtractBaseType(sqlType)
|
||||
|
||||
for _, col := range columns {
|
||||
colBaseType := commontypes.ExtractBaseType(col.Type)
|
||||
if colBaseType == baseType {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterPrimaryKeys returns only columns that are primary keys
|
||||
// Usage: {{ $pks := filterPrimaryKeys .Table.Columns }}
|
||||
func FilterPrimaryKeys(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if col.IsPrimaryKey {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterForeignKeys returns only foreign key constraints
|
||||
// Usage: {{ $fks := filterForeignKeys .Table.Constraints }}
|
||||
func FilterForeignKeys(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterUniqueConstraints returns only unique constraints
|
||||
// Usage: {{ $uniques := filterUniqueConstraints .Table.Constraints }}
|
||||
func FilterUniqueConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.UniqueConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterCheckConstraints returns only check constraints
|
||||
// Usage: {{ $checks := filterCheckConstraints .Table.Constraints }}
|
||||
func FilterCheckConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0)
|
||||
for _, constraint := range constraints {
|
||||
if constraint.Type == models.CheckConstraint {
|
||||
result = append(result, constraint)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterNullable returns only nullable columns
|
||||
// Usage: {{ $nullables := filterNullable .Table.Columns }}
|
||||
func FilterNullable(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if !col.NotNull {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterNotNull returns only non-nullable columns
|
||||
// Usage: {{ $required := filterNotNull .Table.Columns }}
|
||||
func FilterNotNull(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0)
|
||||
for _, col := range columns {
|
||||
if col.NotNull {
|
||||
result = append(result, col)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// matchPattern performs simple glob-style pattern matching
|
||||
// Supports: * (any characters), ? (single character)
|
||||
// Examples: "user_*" matches "user_profile", "user_settings"
|
||||
func matchPattern(s, pattern string) bool {
|
||||
// Use filepath.Match for glob-style pattern matching
|
||||
matched, err := filepath.Match(pattern, s)
|
||||
if err != nil {
|
||||
// If pattern is invalid, do exact match
|
||||
return strings.EqualFold(s, pattern)
|
||||
}
|
||||
return matched
|
||||
}
|
||||
157
pkg/writers/template/formatters.go
Normal file
157
pkg/writers/template/formatters.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ToJSON converts a value to JSON string
|
||||
// Usage: {{ .Database | toJSON }}
|
||||
func ToJSON(v interface{}) string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ToJSONPretty converts a value to pretty-printed JSON string
|
||||
// Usage: {{ .Database | toJSONPretty " " }}
|
||||
func ToJSONPretty(v interface{}, indent string) string {
|
||||
data, err := json.MarshalIndent(v, "", indent)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("{\"error\": \"failed to marshal: %v\"}", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ToYAML converts a value to YAML string
|
||||
// Usage: {{ .Database | toYAML }}
|
||||
func ToYAML(v interface{}) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: failed to marshal: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// Indent indents each line of a string by the specified number of spaces
|
||||
// Usage: {{ .Column.Description | indent 4 }}
|
||||
func Indent(s string, spaces int) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
prefix := strings.Repeat(" ", spaces)
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// IndentWith indents each line of a string with a custom prefix
|
||||
// Usage: {{ .Column.Description | indentWith " " }}
|
||||
func IndentWith(s string, prefix string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// Escape escapes special characters in a string for use in code
|
||||
// Usage: {{ .Column.Default | escape }}
|
||||
func Escape(s string) string {
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||
s = strings.ReplaceAll(s, "\n", "\\n")
|
||||
s = strings.ReplaceAll(s, "\r", "\\r")
|
||||
s = strings.ReplaceAll(s, "\t", "\\t")
|
||||
return s
|
||||
}
|
||||
|
||||
// EscapeQuotes escapes only quote characters
|
||||
// Usage: {{ .Column.Comment | escapeQuotes }}
|
||||
func EscapeQuotes(s string) string {
|
||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||
s = strings.ReplaceAll(s, "'", "\\'")
|
||||
return s
|
||||
}
|
||||
|
||||
// Comment adds comment prefix to a string
|
||||
// Supports: "//" (Go, C++, etc.), "#" (Python, shell), "--" (SQL), "/* */" (block)
|
||||
// Usage: {{ .Table.Description | comment "//" }}
|
||||
func Comment(s string, style string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
|
||||
switch style {
|
||||
case "//":
|
||||
for i, line := range lines {
|
||||
lines[i] = "// " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "#":
|
||||
for i, line := range lines {
|
||||
lines[i] = "# " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "--":
|
||||
for i, line := range lines {
|
||||
lines[i] = "-- " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
case "/* */", "/**/":
|
||||
if len(lines) == 1 {
|
||||
return "/* " + lines[0] + " */"
|
||||
}
|
||||
result := "/*\n"
|
||||
for _, line := range lines {
|
||||
result += " * " + line + "\n"
|
||||
}
|
||||
result += " */"
|
||||
return result
|
||||
|
||||
default:
|
||||
// Default to // style
|
||||
for i, line := range lines {
|
||||
lines[i] = "// " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// QuoteString adds quotes around a string
|
||||
// Usage: {{ .Column.Default | quoteString }}
|
||||
func QuoteString(s string) string {
|
||||
return "\"" + s + "\""
|
||||
}
|
||||
|
||||
// UnquoteString removes quotes from a string
|
||||
// Usage: {{ .Value | unquoteString }}
|
||||
func UnquoteString(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
171
pkg/writers/template/funcmap.go
Normal file
171
pkg/writers/template/funcmap.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// BuildFuncMap creates a template.FuncMap with all available helper functions
|
||||
func BuildFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
// String manipulation functions
|
||||
"toUpper": ToUpper,
|
||||
"toLower": ToLower,
|
||||
"toCamelCase": ToCamelCase,
|
||||
"toPascalCase": ToPascalCase,
|
||||
"toSnakeCase": ToSnakeCase,
|
||||
"toKebabCase": ToKebabCase,
|
||||
"pluralize": Pluralize,
|
||||
"singularize": Singularize,
|
||||
"title": Title,
|
||||
"trim": Trim,
|
||||
"trimPrefix": TrimPrefix,
|
||||
"trimSuffix": TrimSuffix,
|
||||
"replace": Replace,
|
||||
"stringContains": StringContains, // Avoid conflict with slice contains
|
||||
"hasPrefix": HasPrefix,
|
||||
"hasSuffix": HasSuffix,
|
||||
"split": Split,
|
||||
"join": Join,
|
||||
|
||||
// Type conversion functions
|
||||
"sqlToGo": SQLToGo,
|
||||
"sqlToTypeScript": SQLToTypeScript,
|
||||
"sqlToJava": SQLToJava,
|
||||
"sqlToPython": SQLToPython,
|
||||
"sqlToRust": SQLToRust,
|
||||
"sqlToCSharp": SQLToCSharp,
|
||||
"sqlToPhp": SQLToPhp,
|
||||
|
||||
// Filtering functions
|
||||
"filterTables": FilterTables,
|
||||
"filterTablesByPattern": FilterTablesByPattern,
|
||||
"filterColumns": FilterColumns,
|
||||
"filterColumnsByType": FilterColumnsByType,
|
||||
"filterPrimaryKeys": FilterPrimaryKeys,
|
||||
"filterForeignKeys": FilterForeignKeys,
|
||||
"filterUniqueConstraints": FilterUniqueConstraints,
|
||||
"filterCheckConstraints": FilterCheckConstraints,
|
||||
"filterNullable": FilterNullable,
|
||||
"filterNotNull": FilterNotNull,
|
||||
|
||||
// Formatting functions
|
||||
"toJSON": ToJSON,
|
||||
"toJSONPretty": ToJSONPretty,
|
||||
"toYAML": ToYAML,
|
||||
"indent": Indent,
|
||||
"indentWith": IndentWith,
|
||||
"escape": Escape,
|
||||
"escapeQuotes": EscapeQuotes,
|
||||
"comment": Comment,
|
||||
"quoteString": QuoteString,
|
||||
"unquoteString": UnquoteString,
|
||||
|
||||
// Loop/iteration helper functions
|
||||
"enumerate": Enumerate,
|
||||
"batch": Batch,
|
||||
"chunk": Chunk,
|
||||
"reverse": Reverse,
|
||||
"first": First,
|
||||
"last": Last,
|
||||
"skip": Skip,
|
||||
"take": Take,
|
||||
"concat": Concat,
|
||||
"unique": Unique,
|
||||
"sortBy": SortBy,
|
||||
"groupBy": GroupBy,
|
||||
|
||||
// Safe access functions
|
||||
"get": Get,
|
||||
"getOr": GetOr,
|
||||
"getPath": GetPath,
|
||||
"getPathOr": GetPathOr,
|
||||
"safeIndex": SafeIndex,
|
||||
"safeIndexOr": SafeIndexOr,
|
||||
"has": Has,
|
||||
"hasPath": HasPath,
|
||||
"keys": Keys,
|
||||
"values": Values,
|
||||
"merge": Merge,
|
||||
"pick": Pick,
|
||||
"omit": Omit,
|
||||
"sliceContains": SliceContains,
|
||||
"indexOf": IndexOf,
|
||||
"pluck": Pluck,
|
||||
|
||||
// Sorting functions
|
||||
"sortSchemasByName": models.SortSchemasByName,
|
||||
"sortSchemasBySequence": models.SortSchemasBySequence,
|
||||
"sortTablesByName": models.SortTablesByName,
|
||||
"sortTablesBySequence": models.SortTablesBySequence,
|
||||
"sortColumnsByName": models.SortColumnsByName,
|
||||
"sortColumnsBySequence": models.SortColumnsBySequence,
|
||||
"sortColumnsMapByName": models.SortColumnsMapByName,
|
||||
"sortColumnsMapBySequence": models.SortColumnsMapBySequence,
|
||||
"sortViewsByName": models.SortViewsByName,
|
||||
"sortViewsBySequence": models.SortViewsBySequence,
|
||||
"sortSequencesByName": models.SortSequencesByName,
|
||||
"sortSequencesBySequence": models.SortSequencesBySequence,
|
||||
"sortIndexesByName": models.SortIndexesByName,
|
||||
"sortIndexesBySequence": models.SortIndexesBySequence,
|
||||
"sortIndexesMapByName": models.SortIndexesMapByName,
|
||||
"sortIndexesMapBySequence": models.SortIndexesMapBySequence,
|
||||
"sortConstraintsByName": models.SortConstraintsByName,
|
||||
"sortConstraintsMapByName": models.SortConstraintsMapByName,
|
||||
"sortRelationshipsByName": models.SortRelationshipsByName,
|
||||
"sortRelationshipsMapByName": models.SortRelationshipsMapByName,
|
||||
"sortScriptsByName": models.SortScriptsByName,
|
||||
"sortEnumsByName": models.SortEnumsByName,
|
||||
|
||||
// Utility functions (built-in Go template helpers + custom)
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"div": func(a, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
},
|
||||
"mod": func(a, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a % b
|
||||
},
|
||||
"default": func(defaultVal, val interface{}) interface{} {
|
||||
if val == nil {
|
||||
return defaultVal
|
||||
}
|
||||
return val
|
||||
},
|
||||
"dict": func(values ...interface{}) map[string]interface{} {
|
||||
if len(values)%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict
|
||||
},
|
||||
"list": func(values ...interface{}) []interface{} {
|
||||
return values
|
||||
},
|
||||
"seq": func(start, end int) []int {
|
||||
if start > end {
|
||||
return []int{}
|
||||
}
|
||||
result := make([]int, end-start+1)
|
||||
for i := range result {
|
||||
result[i] = start + i
|
||||
}
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
282
pkg/writers/template/loop_helpers.go
Normal file
282
pkg/writers/template/loop_helpers.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
|
||||
)
|
||||
|
||||
// EnumeratedItem represents an item with its index
|
||||
type EnumeratedItem struct {
|
||||
Index int
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Enumerate returns a slice with index-value pairs
|
||||
// Usage: {{ range enumerate .Tables }}{{ .Index }}: {{ .Value.Name }}{{ end }}
|
||||
func Enumerate(slice interface{}) []EnumeratedItem {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []EnumeratedItem{}
|
||||
}
|
||||
|
||||
result := make([]EnumeratedItem, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = EnumeratedItem{
|
||||
Index: i,
|
||||
Value: v.Index(i).Interface(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Batch splits a slice into chunks of specified size
|
||||
// Usage: {{ range batch .Columns 3 }}...{{ end }}
|
||||
func Batch(slice interface{}, size int) [][]interface{} {
|
||||
if size <= 0 {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if length == 0 {
|
||||
return [][]interface{}{}
|
||||
}
|
||||
|
||||
numBatches := (length + size - 1) / size
|
||||
result := make([][]interface{}, numBatches)
|
||||
|
||||
for i := 0; i < numBatches; i++ {
|
||||
start := i * size
|
||||
end := start + size
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
batch := make([]interface{}, end-start)
|
||||
for j := start; j < end; j++ {
|
||||
batch[j-start] = v.Index(j).Interface()
|
||||
}
|
||||
result[i] = batch
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Chunk is an alias for Batch
|
||||
func Chunk(slice interface{}, size int) [][]interface{} {
|
||||
return Batch(slice, size)
|
||||
}
|
||||
|
||||
// Reverse reverses a slice
|
||||
// Usage: {{ range reverse .Tables }}...{{ end }}
|
||||
func Reverse(slice interface{}) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
result := make([]interface{}, length)
|
||||
for i := 0; i < length; i++ {
|
||||
result[length-1-i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// First returns the first N items from a slice
|
||||
// Usage: {{ range first .Tables 5 }}...{{ end }}
|
||||
func First(slice interface{}, n int) []interface{} {
|
||||
if n <= 0 {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n > length {
|
||||
n = length
|
||||
}
|
||||
|
||||
result := make([]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Last returns the last N items from a slice
|
||||
// Usage: {{ range last .Tables 5 }}...{{ end }}
|
||||
func Last(slice interface{}, n int) []interface{} {
|
||||
if n <= 0 {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n > length {
|
||||
n = length
|
||||
}
|
||||
|
||||
result := make([]interface{}, n)
|
||||
start := length - n
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = v.Index(start + i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Skip skips the first N items and returns the rest
|
||||
// Usage: {{ range skip .Tables 2 }}...{{ end }}
|
||||
func Skip(slice interface{}, n int) []interface{} {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
length := v.Len()
|
||||
if n >= length {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, length-n)
|
||||
for i := n; i < length; i++ {
|
||||
result[i-n] = v.Index(i).Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Take returns the first N items (alias for First)
|
||||
// Usage: {{ range take .Tables 5 }}...{{ end }}
|
||||
func Take(slice interface{}, n int) []interface{} {
|
||||
return First(slice, n)
|
||||
}
|
||||
|
||||
// Concat concatenates multiple slices
|
||||
// Usage: {{ $all := concat .Schema1.Tables .Schema2.Tables }}
|
||||
func Concat(slices ...interface{}) []interface{} {
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for _, slice := range slices {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result = append(result, v.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Unique removes duplicates from a slice (compares by string representation)
|
||||
// Usage: {{ $unique := unique .Items }}
|
||||
func Unique(slice interface{}) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
seen := make(map[interface{}]bool)
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SortBy sorts a slice by a field name (for structs) or key (for maps)
|
||||
// Usage: {{ $sorted := sortBy .Tables "Name" }}
|
||||
// Note: This is a basic implementation that works for simple cases
|
||||
func SortBy(slice interface{}, field string) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// Convert to interface slice
|
||||
result := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
result[i] = v.Index(i).Interface()
|
||||
}
|
||||
|
||||
// Sort by field
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
vi := getFieldValue(result[i], field)
|
||||
vj := getFieldValue(result[j], field)
|
||||
return compareValues(vi, vj) < 0
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupBy groups a slice by a field value
|
||||
// Usage: {{ $grouped := groupBy .Tables "Schema" }}
|
||||
func GroupBy(slice interface{}, field string) map[interface{}][]interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return map[interface{}][]interface{}{}
|
||||
}
|
||||
|
||||
result := make(map[interface{}][]interface{})
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
key := getFieldValue(item, field)
|
||||
result[key] = append(result[key], item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CountIf counts items matching a condition
|
||||
// Note: Since templates can't pass functions, this is limited
|
||||
// Usage in code (not directly in templates)
|
||||
func CountIf(slice interface{}, matches func(interface{}) bool) int {
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if matches(v.Index(i).Interface()) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getFieldValue extracts a field value from a struct, map, or pointer
|
||||
func getFieldValue(item interface{}, field string) interface{} {
|
||||
return reflectutil.GetFieldValue(item, field)
|
||||
}
|
||||
|
||||
// compareValues compares two values for sorting
|
||||
func compareValues(a, b interface{}) int {
|
||||
return reflectutil.CompareValues(a, b)
|
||||
}
|
||||
288
pkg/writers/template/safe_access.go
Normal file
288
pkg/writers/template/safe_access.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/reflectutil"
|
||||
)
|
||||
|
||||
// Get safely gets a value from a map by key
|
||||
// Usage: {{ get .Metadata "key" }}
|
||||
func Get(m interface{}, key interface{}) interface{} {
|
||||
return reflectutil.MapGet(m, key)
|
||||
}
|
||||
|
||||
// GetOr safely gets a value from a map with a default fallback
|
||||
// Usage: {{ getOr .Metadata "key" "default" }}
|
||||
func GetOr(m interface{}, key interface{}, defaultValue interface{}) interface{} {
|
||||
result := Get(m, key)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPath safely gets a nested value using dot notation
|
||||
// Usage: {{ getPath .Config "database.connection.host" }}
|
||||
func GetPath(m interface{}, path string) interface{} {
|
||||
return reflectutil.GetNestedValue(m, path)
|
||||
}
|
||||
|
||||
// GetPathOr safely gets a nested value with a default fallback
|
||||
// Usage: {{ getPathOr .Config "database.connection.host" "localhost" }}
|
||||
func GetPathOr(m interface{}, path string, defaultValue interface{}) interface{} {
|
||||
result := GetPath(m, path)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SafeIndex safely gets an element from a slice by index
|
||||
// Usage: {{ safeIndex .Tables 0 }}
|
||||
func SafeIndex(slice interface{}, index int) interface{} {
|
||||
return reflectutil.SliceIndex(slice, index)
|
||||
}
|
||||
|
||||
// SafeIndexOr safely gets an element from a slice with a default fallback
|
||||
// Usage: {{ safeIndexOr .Tables 0 "default" }}
|
||||
func SafeIndexOr(slice interface{}, index int, defaultValue interface{}) interface{} {
|
||||
result := SafeIndex(slice, index)
|
||||
if result == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Has checks if a key exists in a map
|
||||
// Usage: {{ if has .Metadata "key" }}...{{ end }}
|
||||
func Has(m interface{}, key interface{}) bool {
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return false
|
||||
}
|
||||
|
||||
keyVal := reflect.ValueOf(key)
|
||||
return v.MapIndex(keyVal).IsValid()
|
||||
}
|
||||
|
||||
// HasPath checks if a nested path exists
|
||||
// Usage: {{ if hasPath .Config "database.connection.host" }}...{{ end }}
|
||||
func HasPath(m interface{}, path string) bool {
|
||||
return GetPath(m, path) != nil
|
||||
}
|
||||
|
||||
// Keys returns all keys from a map
|
||||
// Usage: {{ range keys .Metadata }}...{{ end }}
|
||||
func Keys(m interface{}) []interface{} {
|
||||
return reflectutil.MapKeys(m)
|
||||
}
|
||||
|
||||
// Values returns all values from a map
|
||||
// Usage: {{ range values .Table.Columns }}...{{ end }}
|
||||
func Values(m interface{}) []interface{} {
|
||||
return reflectutil.MapValues(m)
|
||||
}
|
||||
|
||||
// Merge merges multiple maps into a new map
|
||||
// Usage: {{ $merged := merge .Map1 .Map2 }}
|
||||
func Merge(maps ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
|
||||
for _, m := range maps {
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
continue
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
continue
|
||||
}
|
||||
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
result[iter.Key().Interface()] = iter.Value().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Pick returns a new map with only the specified keys
|
||||
// Usage: {{ $subset := pick .Metadata "name" "description" }}
|
||||
func Pick(m interface{}, keys ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return result
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
keyVal := reflect.ValueOf(key)
|
||||
mapVal := v.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
result[key] = mapVal.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Omit returns a new map without the specified keys
|
||||
// Usage: {{ $filtered := omit .Metadata "internal" "private" }}
|
||||
func Omit(m interface{}, keys ...interface{}) map[interface{}]interface{} {
|
||||
result := make(map[interface{}]interface{})
|
||||
v := reflect.ValueOf(m)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return result
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Map {
|
||||
return result
|
||||
}
|
||||
|
||||
// Create a set of keys to omit
|
||||
omitSet := make(map[interface{}]bool)
|
||||
for _, key := range keys {
|
||||
omitSet[key] = true
|
||||
}
|
||||
|
||||
// Add all keys that are not in the omit set
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key().Interface()
|
||||
if !omitSet[key] {
|
||||
result[key] = iter.Value().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SliceContains checks if a slice contains a value
|
||||
// Usage: {{ if sliceContains .Names "admin" }}...{{ end }}
|
||||
func SliceContains(slice interface{}, value interface{}) bool {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := reflectutil.Deref(v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IndexOf returns the index of a value in a slice, or -1 if not found
|
||||
// Usage: {{ $idx := indexOf .Names "admin" }}
|
||||
func IndexOf(slice interface{}, value interface{}) int {
|
||||
v := reflect.ValueOf(slice)
|
||||
v, ok := reflectutil.Deref(v)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if reflectutil.DeepEqual(v.Index(i).Interface(), value) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// Pluck extracts a field from each element in a slice
|
||||
// Usage: {{ $names := pluck .Tables "Name" }}
|
||||
func Pluck(slice interface{}, field string) []interface{} {
|
||||
v := reflect.ValueOf(slice)
|
||||
|
||||
// Dereference pointers
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return []interface{}{}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
result := make([]interface{}, 0, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i)
|
||||
|
||||
// Dereference item pointers
|
||||
for item.Kind() == reflect.Ptr {
|
||||
if item.IsNil() {
|
||||
result = append(result, nil)
|
||||
continue
|
||||
}
|
||||
item = item.Elem()
|
||||
}
|
||||
|
||||
switch item.Kind() {
|
||||
case reflect.Struct:
|
||||
fieldVal := item.FieldByName(field)
|
||||
if fieldVal.IsValid() {
|
||||
result = append(result, fieldVal.Interface())
|
||||
} else {
|
||||
result = append(result, nil)
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
keyVal := reflect.ValueOf(field)
|
||||
mapVal := item.MapIndex(keyVal)
|
||||
if mapVal.IsValid() {
|
||||
result = append(result, mapVal.Interface())
|
||||
} else {
|
||||
result = append(result, nil)
|
||||
}
|
||||
|
||||
default:
|
||||
result = append(result, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
316
pkg/writers/template/string_helpers.go
Normal file
316
pkg/writers/template/string_helpers.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// ToUpper converts a string to uppercase
|
||||
func ToUpper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// ToLower converts a string to lowercase
|
||||
func ToLower(s string) string {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// ToCamelCase converts snake_case to camelCase
|
||||
// Examples: user_name → userName, http_request → httpRequest
|
||||
func ToCamelCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(s, "_")
|
||||
for i, part := range parts {
|
||||
if i == 0 {
|
||||
parts[i] = strings.ToLower(part)
|
||||
} else {
|
||||
parts[i] = capitalize(part)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// ToPascalCase converts snake_case to PascalCase
|
||||
// Examples: user_name → UserName, http_request → HTTPRequest
|
||||
func ToPascalCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(s, "_")
|
||||
for i, part := range parts {
|
||||
parts[i] = capitalize(part)
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
// ToSnakeCase converts PascalCase/camelCase to snake_case
|
||||
// Examples: UserName → user_name, HTTPRequest → http_request
|
||||
func ToSnakeCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
var prevUpper bool
|
||||
var nextUpper bool
|
||||
|
||||
runes := []rune(s)
|
||||
for i, r := range runes {
|
||||
isUpper := unicode.IsUpper(r)
|
||||
|
||||
if i+1 < len(runes) {
|
||||
nextUpper = unicode.IsUpper(runes[i+1])
|
||||
} else {
|
||||
nextUpper = false
|
||||
}
|
||||
|
||||
if i > 0 && isUpper {
|
||||
// Add underscore before uppercase letter if:
|
||||
// 1. Previous char was lowercase, OR
|
||||
// 2. Next char is lowercase (end of acronym)
|
||||
if !prevUpper || (!nextUpper && i+1 < len(runes)) {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
prevUpper = isUpper
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ToKebabCase converts snake_case or PascalCase/camelCase to kebab-case
|
||||
// Examples: user_name → user-name, UserName → user-name
|
||||
func ToKebabCase(s string) string {
|
||||
// First convert to snake_case, then replace underscores with hyphens
|
||||
snakeCase := ToSnakeCase(s)
|
||||
return strings.ReplaceAll(snakeCase, "_", "-")
|
||||
}
|
||||
|
||||
// Title capitalizes the first letter of each word
|
||||
func Title(s string) string {
|
||||
caser := cases.Title(language.English)
|
||||
src := []byte(s)
|
||||
dest := []byte(s)
|
||||
_, _, _ = caser.Transform(dest, src, true)
|
||||
return string(dest)
|
||||
}
|
||||
|
||||
// Pluralize converts a singular word to plural
|
||||
// Basic implementation with common English rules
|
||||
func Pluralize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Special cases
|
||||
irregular := map[string]string{
|
||||
"person": "people",
|
||||
"child": "children",
|
||||
"tooth": "teeth",
|
||||
"foot": "feet",
|
||||
"man": "men",
|
||||
"woman": "women",
|
||||
"mouse": "mice",
|
||||
"goose": "geese",
|
||||
"ox": "oxen",
|
||||
"datum": "data",
|
||||
"medium": "media",
|
||||
"analysis": "analyses",
|
||||
"crisis": "crises",
|
||||
"status": "statuses",
|
||||
}
|
||||
|
||||
if plural, ok := irregular[strings.ToLower(s)]; ok {
|
||||
return plural
|
||||
}
|
||||
|
||||
// Already plural (ends in 's' but not 'ss' or 'us')
|
||||
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") && !strings.HasSuffix(s, "us") {
|
||||
return s
|
||||
}
|
||||
|
||||
// Words ending in s, x, z, ch, sh
|
||||
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "x") ||
|
||||
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
|
||||
strings.HasSuffix(s, "sh") {
|
||||
return s + "es"
|
||||
}
|
||||
|
||||
// Words ending in consonant + y
|
||||
if len(s) >= 2 && strings.HasSuffix(s, "y") {
|
||||
prevChar := s[len(s)-2]
|
||||
if !isVowel(prevChar) {
|
||||
return s[:len(s)-1] + "ies"
|
||||
}
|
||||
}
|
||||
|
||||
// Words ending in f or fe
|
||||
if strings.HasSuffix(s, "f") {
|
||||
return s[:len(s)-1] + "ves"
|
||||
}
|
||||
if strings.HasSuffix(s, "fe") {
|
||||
return s[:len(s)-2] + "ves"
|
||||
}
|
||||
|
||||
// Words ending in consonant + o
|
||||
if len(s) >= 2 && strings.HasSuffix(s, "o") {
|
||||
prevChar := s[len(s)-2]
|
||||
if !isVowel(prevChar) {
|
||||
return s + "es"
|
||||
}
|
||||
}
|
||||
|
||||
// Default: add 's'
|
||||
return s + "s"
|
||||
}
|
||||
|
||||
// Singularize converts a plural word to singular
|
||||
// Basic implementation with common English rules
|
||||
func Singularize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Special cases
|
||||
irregular := map[string]string{
|
||||
"people": "person",
|
||||
"children": "child",
|
||||
"teeth": "tooth",
|
||||
"feet": "foot",
|
||||
"men": "man",
|
||||
"women": "woman",
|
||||
"mice": "mouse",
|
||||
"geese": "goose",
|
||||
"oxen": "ox",
|
||||
"data": "datum",
|
||||
"media": "medium",
|
||||
"analyses": "analysis",
|
||||
"crises": "crisis",
|
||||
"statuses": "status",
|
||||
}
|
||||
|
||||
if singular, ok := irregular[strings.ToLower(s)]; ok {
|
||||
return singular
|
||||
}
|
||||
|
||||
// Words ending in ies
|
||||
if strings.HasSuffix(s, "ies") && len(s) > 3 {
|
||||
return s[:len(s)-3] + "y"
|
||||
}
|
||||
|
||||
// Words ending in ves
|
||||
if strings.HasSuffix(s, "ves") {
|
||||
return s[:len(s)-3] + "f"
|
||||
}
|
||||
|
||||
// Words ending in ses, xes, zes, ches, shes
|
||||
if strings.HasSuffix(s, "ses") || strings.HasSuffix(s, "xes") ||
|
||||
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
|
||||
strings.HasSuffix(s, "shes") {
|
||||
return s[:len(s)-2]
|
||||
}
|
||||
|
||||
// Words ending in s (not ss)
|
||||
if strings.HasSuffix(s, "s") && !strings.HasSuffix(s, "ss") {
|
||||
return s[:len(s)-1]
|
||||
}
|
||||
|
||||
// Already singular
|
||||
return s
|
||||
}
|
||||
|
||||
// Trim trims whitespace from both ends
|
||||
func Trim(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// TrimPrefix removes the prefix from the string if present
|
||||
func TrimPrefix(s, prefix string) string {
|
||||
return strings.TrimPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// TrimSuffix removes the suffix from the string if present
|
||||
func TrimSuffix(s, suffix string) string {
|
||||
return strings.TrimSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// Replace replaces occurrences of old with new (n times, or all if n < 0)
|
||||
func Replace(s, old, newstr string, n int) string {
|
||||
return strings.Replace(s, old, newstr, n)
|
||||
}
|
||||
|
||||
// StringContains checks if substr is within s
|
||||
func StringContains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
// HasPrefix checks if string starts with prefix
|
||||
func HasPrefix(s, prefix string) bool {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// HasSuffix checks if string ends with suffix
|
||||
func HasSuffix(s, suffix string) bool {
|
||||
return strings.HasSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// Split splits string by separator
|
||||
func Split(s, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
// Join joins string slice with separator
|
||||
func Join(parts []string, sep string) string {
|
||||
return strings.Join(parts, sep)
|
||||
}
|
||||
|
||||
// capitalize capitalizes the first letter and handles common acronyms
|
||||
func capitalize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
upper := strings.ToUpper(s)
|
||||
|
||||
// Handle common acronyms
|
||||
acronyms := map[string]bool{
|
||||
"ID": true,
|
||||
"UUID": true,
|
||||
"GUID": true,
|
||||
"URL": true,
|
||||
"URI": true,
|
||||
"HTTP": true,
|
||||
"HTTPS": true,
|
||||
"API": true,
|
||||
"JSON": true,
|
||||
"XML": true,
|
||||
"SQL": true,
|
||||
"HTML": true,
|
||||
"CSS": true,
|
||||
"RID": true,
|
||||
}
|
||||
|
||||
if acronyms[upper] {
|
||||
return upper
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// isVowel checks if a byte is a vowel
|
||||
func isVowel(c byte) bool {
|
||||
c = byte(unicode.ToLower(rune(c)))
|
||||
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
|
||||
}
|
||||
95
pkg/writers/template/template_data.go
Normal file
95
pkg/writers/template/template_data.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package template
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// TemplateData wraps the model data with additional context for template execution
|
||||
type TemplateData struct {
|
||||
// One of these will be populated based on execution mode
|
||||
Database *models.Database
|
||||
Schema *models.Schema
|
||||
Script *models.Script
|
||||
Table *models.Table
|
||||
|
||||
// Context information (parent references)
|
||||
ParentSchema *models.Schema // Set for table/script modes
|
||||
ParentDatabase *models.Database // Always set for full database context
|
||||
|
||||
// Pre-computed views for convenience
|
||||
FlatColumns []*models.FlatColumn
|
||||
FlatTables []*models.FlatTable
|
||||
FlatConstraints []*models.FlatConstraint
|
||||
FlatRelationships []*models.FlatRelationship
|
||||
Summary *models.DatabaseSummary
|
||||
|
||||
// User metadata from WriterOptions
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// NewDatabaseData creates template data for database mode
|
||||
func NewDatabaseData(db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Database: db,
|
||||
ParentDatabase: db,
|
||||
FlatColumns: db.ToFlatColumns(),
|
||||
FlatTables: db.ToFlatTables(),
|
||||
FlatConstraints: db.ToFlatConstraints(),
|
||||
FlatRelationships: db.ToFlatRelationships(),
|
||||
Summary: db.ToSummary(),
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSchemaData creates template data for schema mode
|
||||
func NewSchemaData(schema *models.Schema, metadata map[string]interface{}) *TemplateData {
|
||||
// Create a temporary database with just this schema for context
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return &TemplateData{
|
||||
Schema: schema,
|
||||
ParentDatabase: db,
|
||||
FlatColumns: db.ToFlatColumns(),
|
||||
FlatTables: db.ToFlatTables(),
|
||||
FlatConstraints: db.ToFlatConstraints(),
|
||||
FlatRelationships: db.ToFlatRelationships(),
|
||||
Summary: db.ToSummary(),
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScriptData creates template data for script mode
|
||||
func NewScriptData(script *models.Script, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Script: script,
|
||||
ParentSchema: schema,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTableData creates template data for table mode
|
||||
func NewTableData(table *models.Table, schema *models.Schema, db *models.Database, metadata map[string]interface{}) *TemplateData {
|
||||
return &TemplateData{
|
||||
Table: table,
|
||||
ParentSchema: schema,
|
||||
ParentDatabase: db,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the primary name for the current template data (used for filename generation)
|
||||
func (td *TemplateData) Name() string {
|
||||
if td.Database != nil {
|
||||
return td.Database.Name
|
||||
}
|
||||
if td.Schema != nil {
|
||||
return td.Schema.Name
|
||||
}
|
||||
if td.Script != nil {
|
||||
return td.Script.Name
|
||||
}
|
||||
if td.Table != nil {
|
||||
return td.Table.Name
|
||||
}
|
||||
return "output"
|
||||
}
|
||||
38
pkg/writers/template/type_mappers.go
Normal file
38
pkg/writers/template/type_mappers.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package template
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/commontypes"
|
||||
|
||||
// SQLToGo converts SQL types to Go types
|
||||
func SQLToGo(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToGo(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToTypeScript converts SQL types to TypeScript types
|
||||
func SQLToTypeScript(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToTypeScript(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToJava converts SQL types to Java types
|
||||
func SQLToJava(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToJava(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToPython converts SQL types to Python types
|
||||
func SQLToPython(sqlType string) string {
|
||||
return commontypes.SQLToPython(sqlType)
|
||||
}
|
||||
|
||||
// SQLToRust converts SQL types to Rust types
|
||||
func SQLToRust(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToRust(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToCSharp converts SQL types to C# types
|
||||
func SQLToCSharp(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToCSharp(sqlType, nullable)
|
||||
}
|
||||
|
||||
// SQLToPhp converts SQL types to PHP types
|
||||
func SQLToPhp(sqlType string, nullable bool) string {
|
||||
return commontypes.SQLToPhp(sqlType, nullable)
|
||||
}
|
||||
283
pkg/writers/template/writer.go
Normal file
283
pkg/writers/template/writer.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// EntrypointMode defines how the template is executed
|
||||
type EntrypointMode string
|
||||
|
||||
const (
|
||||
// DatabaseMode executes the template once for the entire database (single output)
|
||||
DatabaseMode EntrypointMode = "database"
|
||||
// SchemaMode executes the template once per schema (multi-file output)
|
||||
SchemaMode EntrypointMode = "schema"
|
||||
// ScriptMode executes the template once per script (multi-file output)
|
||||
ScriptMode EntrypointMode = "script"
|
||||
// TableMode executes the template once per table (multi-file output)
|
||||
TableMode EntrypointMode = "table"
|
||||
)
|
||||
|
||||
// Writer implements the writers.Writer interface for template-based output
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
templatePath string
|
||||
funcMap template.FuncMap
|
||||
tmpl *template.Template
|
||||
mode EntrypointMode
|
||||
filenamePattern string
|
||||
}
|
||||
|
||||
// NewWriter creates a new template writer with the given options
|
||||
func NewWriter(options *writers.WriterOptions) (*Writer, error) {
|
||||
w := &Writer{
|
||||
options: options,
|
||||
funcMap: BuildFuncMap(),
|
||||
mode: DatabaseMode, // default mode
|
||||
filenamePattern: "{{.Name}}.txt",
|
||||
}
|
||||
|
||||
// Extract template path from metadata
|
||||
if options.Metadata != nil {
|
||||
if path, ok := options.Metadata["template_path"].(string); ok {
|
||||
w.templatePath = path
|
||||
}
|
||||
|
||||
if mode, ok := options.Metadata["mode"].(string); ok {
|
||||
w.mode = EntrypointMode(mode)
|
||||
}
|
||||
|
||||
if pattern, ok := options.Metadata["filename_pattern"].(string); ok {
|
||||
w.filenamePattern = pattern
|
||||
}
|
||||
}
|
||||
|
||||
// Validate template path
|
||||
if w.templatePath == "" {
|
||||
return nil, NewTemplateLoadError("template path is required", nil)
|
||||
}
|
||||
|
||||
// Load and parse template
|
||||
if err := w.loadTemplate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WriteDatabase writes a database using the template
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
switch w.mode {
|
||||
case DatabaseMode:
|
||||
return w.executeDatabaseMode(db)
|
||||
case SchemaMode:
|
||||
return w.executeSchemaMode(db)
|
||||
case ScriptMode:
|
||||
return w.executeScriptMode(db)
|
||||
case TableMode:
|
||||
return w.executeTableMode(db)
|
||||
default:
|
||||
return fmt.Errorf("unknown entrypoint mode: %s", w.mode)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSchema writes a schema using the template
|
||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
// Create a temporary database with just this schema
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// WriteTable writes a single table using the template
|
||||
func (w *Writer) WriteTable(table *models.Table) error {
|
||||
// Create a temporary schema and database
|
||||
schema := models.InitSchema(table.Schema)
|
||||
schema.Tables = []*models.Table{table}
|
||||
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// executeDatabaseMode executes the template once for the entire database
|
||||
func (w *Writer) executeDatabaseMode(db *models.Database) error {
|
||||
data := NewDatabaseData(db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.writeOutput(output, w.options.OutputPath)
|
||||
}
|
||||
|
||||
// executeSchemaMode executes the template once per schema
|
||||
func (w *Writer) executeSchemaMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
data := NewSchemaData(schema, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for schema %s: %w", schema.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeScriptMode executes the template once per script
|
||||
func (w *Writer) executeScriptMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
for _, script := range schema.Scripts {
|
||||
data := NewScriptData(script, schema, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for script %s: %w", script.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for script %s: %w", script.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for script %s: %w", script.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeTableMode executes the template once per table
|
||||
func (w *Writer) executeTableMode(db *models.Database) error {
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
data := NewTableData(table, schema, db, w.options.Metadata)
|
||||
output, err := w.executeTemplate(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute template for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
|
||||
filename, err := w.generateFilename(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate filename for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
|
||||
if err := w.writeOutput(output, filename); err != nil {
|
||||
return fmt.Errorf("failed to write output for table %s.%s: %w", schema.Name, table.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTemplate loads and parses the template file
|
||||
func (w *Writer) loadTemplate() error {
|
||||
// Read template file
|
||||
content, err := os.ReadFile(w.templatePath)
|
||||
if err != nil {
|
||||
return NewTemplateLoadError(fmt.Sprintf("failed to read template file: %s", w.templatePath), err)
|
||||
}
|
||||
|
||||
// Parse template with function map
|
||||
tmpl, err := template.New(filepath.Base(w.templatePath)).Funcs(w.funcMap).Parse(string(content))
|
||||
if err != nil {
|
||||
return NewTemplateParseError(fmt.Sprintf("failed to parse template: %s", w.templatePath), err)
|
||||
}
|
||||
|
||||
w.tmpl = tmpl
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeTemplate executes the template with the given data
|
||||
func (w *Writer) executeTemplate(data *TemplateData) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := w.tmpl.Execute(&buf, data); err != nil {
|
||||
return "", NewTemplateExecuteError("failed to execute template", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// generateFilename generates a filename from the filename pattern
|
||||
func (w *Writer) generateFilename(data *TemplateData) (string, error) {
|
||||
// Parse filename pattern as a template
|
||||
tmpl, err := template.New("filename").Funcs(w.funcMap).Parse(w.filenamePattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid filename pattern: %w", err)
|
||||
}
|
||||
|
||||
// Execute filename template
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to generate filename: %w", err)
|
||||
}
|
||||
|
||||
filename := buf.String()
|
||||
|
||||
// If output path is a directory, join with generated filename
|
||||
if w.options.OutputPath != "" {
|
||||
// Check if output path is a directory
|
||||
info, err := os.Stat(w.options.OutputPath)
|
||||
if err == nil && info.IsDir() {
|
||||
filename = filepath.Join(w.options.OutputPath, filename)
|
||||
} else {
|
||||
// If it doesn't exist, check if it looks like a directory (ends with /)
|
||||
if strings.HasSuffix(w.options.OutputPath, string(filepath.Separator)) {
|
||||
filename = filepath.Join(w.options.OutputPath, filename)
|
||||
} else {
|
||||
// Use output path as base directory
|
||||
dir := filepath.Dir(w.options.OutputPath)
|
||||
if dir != "." {
|
||||
filename = filepath.Join(dir, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// writeOutput writes the output to a file or stdout
|
||||
func (w *Writer) writeOutput(content string, outputPath string) error {
|
||||
// If output path is empty, write to stdout
|
||||
if outputPath == "" {
|
||||
fmt.Print(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(outputPath)
|
||||
if dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user