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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ GLOBAL OPTIONS:
--sqlnullstr Adds a Null{{ENUM}} type for marshalling a nullable string value to sql. If sqlnullint is specified too, it will be Null{{ENUM}}Str (default: false)
--template value, -t value [ --template value, -t value ] Additional template file(s) to generate enums. Use more than one flag for more files. Templates will be executed in alphabetical order.
--alias value, -a value [ --alias value, -a value ] Adds or replaces aliases for a non alphanumeric value that needs to be accounted for. [Format should be "key:value,key2:value2", or specify multiple entries, or both!]
--initialism value [ --initialism value ] Initialism(s) to keep fully uppercased in generated const names (e.g., HTTP,URL,ID). Repeatable.
--mustparse Adds a Must version of the Parse that will panic on failure. (default: false)
--forcelower Forces a camel cased comment to generate lowercased names. (default: false)
--forceupper Forces a camel cased comment to generate uppercased names. (default: false)
Expand All @@ -344,6 +345,23 @@ GLOBAL OPTIONS:
--version, -v print the version
```

### Initialism notes

- `--initialism` affects generated const identifiers only. It does not modify enum string values.
- `--forcelower` and `--forceupper` control enum string values; they are separate from `--initialism`.
- Initialism rewriting runs after `--alias` replacements and after snake_case to CamelCase conversion.
- With `--nocamel`, initialisms in underscore-separated segments may not be rewritten because CamelCase conversion is skipped.
- Rewriting currently runs on the full identifier (including prefix/type-derived segments), not only on the enum value segment.

Example:

```go
// ENUM(created)
type UserId int
```

With `--initialism ID`, the const becomes `UserIDCreated` (the `Id` in the type-derived prefix is rewritten too).

### Syntax

The parser looks for comments on your type defs and parse the enum declarations from it.
Expand Down
115 changes: 111 additions & 4 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ type Generator struct {
BuildDate string
BuiltBy string
GeneratorConfig
t *template.Template
knownTemplates map[string]*template.Template
fileSet *token.FileSet
userTemplateNames []string
t *template.Template
knownTemplates map[string]*template.Template
fileSet *token.FileSet
userTemplateNames []string
initialismReplacements map[string]string
}

// Enum holds data for a discovered enum in the parsed source
Expand Down Expand Up @@ -85,6 +86,15 @@ func NewGeneratorWithConfig(config GeneratorConfig) *Generator {
GeneratorConfig: config,
}

// Precompute initialism replacements from title-cased token to uppercased token.
if len(config.Initialisms) > 0 {
title := cases.Title(language.Und, cases.NoLower)
g.initialismReplacements = make(map[string]string, len(config.Initialisms))
for _, initialism := range config.Initialisms {
g.initialismReplacements[title.String(strings.ToLower(initialism))] = initialism
}
}

funcs := sprig.TxtFuncMap()

funcs["stringify"] = Stringify
Expand Down Expand Up @@ -142,6 +152,102 @@ func ParseAliases(aliases []string) (map[string]string, error) {
return aliasMap, nil
}

// ParseInitialisms parses and validates initialism entries from CLI input.
// Each entry can be comma-separated. Initialisms must be all uppercase ASCII letters.
func ParseInitialisms(entries []string) ([]string, error) {
seen := make(map[string]struct{})
var result []string

for _, entry := range entries {
parts := strings.Split(entry, ",")
for _, part := range parts {
initialism := strings.TrimSpace(part)
if initialism == "" {
continue
}
for _, r := range initialism {
if r < 'A' || r > 'Z' {
return nil, fmt.Errorf("invalid initialism %q: must be all uppercase ASCII letters", initialism)
}
}
if _, ok := seen[initialism]; !ok {
seen[initialism] = struct{}{}
result = append(result, initialism)
}
}
}

return result, nil
}

// applyInitialisms rewrites identifier tokens that match configured initialisms.
// For example, with initialism "HTTP", token "Http" becomes "HTTP".
func (g *Generator) applyInitialisms(name string) string {
if len(g.initialismReplacements) == 0 {
return name
}

tokens := splitIdentifierTokens(name)
if len(tokens) == 0 {
return name
}

var builder strings.Builder
builder.Grow(len(name))
for _, token := range tokens {
if replacement, ok := g.initialismReplacements[token]; ok {
builder.WriteString(replacement)
continue
}
builder.WriteString(token)
}

return builder.String()
}

func splitIdentifierTokens(value string) []string {
if value == "" {
return nil
}

runes := []rune(value)
start := 0
tokens := make([]string, 0, len(runes))

for i := 1; i < len(runes); i++ {
if shouldSplitToken(runes, i) {
tokens = append(tokens, string(runes[start:i]))
start = i
}
}

tokens = append(tokens, string(runes[start:]))
return tokens
}

func shouldSplitToken(runes []rune, index int) bool {
prev := runes[index-1]
curr := runes[index]

if prev == '_' || curr == '_' {
return true
}
if unicode.IsDigit(prev) && !unicode.IsDigit(curr) {
return true
}
if !unicode.IsDigit(prev) && unicode.IsDigit(curr) {
return true
}
if unicode.IsLower(prev) && unicode.IsUpper(curr) {
return true
}
if unicode.IsUpper(prev) && unicode.IsUpper(curr) && index+1 < len(runes) && unicode.IsLower(runes[index+1]) {
return true
}

return false
}

// GenerateFromFile is responsible for orchestrating the Code generation. It results in a byte array
// that can be written to any file desired. It has already had goimports run on the code before being returned.
func (g *Generator) GenerateFromFile(inputFile string) ([]byte, error) {
Expand Down Expand Up @@ -378,6 +484,7 @@ func (g *Generator) parseEnum(ts *ast.TypeSpec) (*Enum, error) {
if !g.LeaveSnakeCase {
prefixedName = snakeToCamelCase(prefixedName)
}
prefixedName = g.applyInitialisms(prefixedName)
}

ev := EnumValue{Name: name, RawName: rawName, PrefixedName: prefixedName, ValueStr: valueStr, ValueInt: data, Comment: comment}
Expand Down
Loading