- Fix string case operations

- Add assets
This commit is contained in:
Alexander Garin 2023-02-15 17:47:43 +03:00
parent 1fbda33ac3
commit ef05f896b7
11 changed files with 195 additions and 155 deletions

View File

@ -1,3 +1,5 @@
![gotygen](assets/logo.png)
Golang types generator tool
### Configuration
@ -6,13 +8,14 @@ Example `.gotygen`
```yaml
packages:
- name: test
output: ./pkc/test/types.go
- name: "test"
output: "./pkc/test/types.go"
acronyms: ["ID", "JSON"]
inputs:
- name: Response1
path: ./api/test/response1.json
- name: Response2
path: ./api/test/response2.json
- name: "Response"
path: "./api/test/response1.json"
- name: "Response"
path: "./api/test/response2.json"
replacements:
"Response.Context": "ResponseContext"
- "Response.Context:ResponseContext"
```

View File

@ -10,7 +10,8 @@ import (
"strings"
"time"
"githouse.ru/macrox/gotygen/pkg/conv"
"githouse.ru/macrox/gotygen/pkg/strcase"
"githouse.ru/macrox/gotygen/pkg/filler"
"githouse.ru/macrox/gotygen/pkg/utils"
@ -44,7 +45,7 @@ var rootCmd = &cobra.Command{
)
for _, i := range p.Inputs {
if len(i.Name) < 1 {
i.Name = conv.ToCamelCase(utils.TrimmedFilenameFromPath(i.Path))
i.Name = strcase.ToCamel(utils.TrimmedFilenameFromPath(i.Path))
if len(i.Name) < 1 {
slog.Warn("input config constrain empty name")
continue
@ -57,13 +58,13 @@ var rootCmd = &cobra.Command{
}
f := filler.NewJsonFiller(data, i.Name, p.Acronyms...)
if len(p.Replacements) > 0 {
f.UseReplacements(p.Replacements...)
f.UseReplacements(p.Replacements)
}
if len(i.Replacements) > 0 {
f.UseReplacements(i.Replacements...)
f.UseReplacements(i.Replacements)
}
if len(i.Acronyms) > 0 {
f.UseCaps(i.Acronyms...)
f.UseCaps(i.Acronyms)
}
if err = f.Fill(doc); err != nil {
slog.Error("failed fill document", err)

View File

@ -1,83 +0,0 @@
package conv
import (
"regexp"
"strings"
"githouse.ru/macrox/gotygen/pkg/utils"
)
type CamelCaseConverter interface {
UseCaps(caps ...string) CamelCaseConverter
ToCamelCase(s string) string
}
type DefaultCamelCaseConverter struct {
allCaps map[string]bool
converted map[string]string
}
func NewDefaultCamelCaseConverter(allCaps []string) CamelCaseConverter {
return &DefaultCamelCaseConverter{
allCaps: utils.SliceToMap(allCaps, func(s string) (string, bool) {
return s, true
}),
converted: make(map[string]string),
}
}
var (
splitRE = regexp.MustCompile(`_+|-+|\s+`)
allCapsRE = regexp.MustCompile(`^[A-Z]{2,}\d*$`)
capitalizedRE = regexp.MustCompile(`[A-Z][a-z]+\d*`)
ToCamelCase = NewDefaultCamelCaseConverter(nil).ToCamelCase
)
func (c *DefaultCamelCaseConverter) UseCaps(v ...string) CamelCaseConverter {
if c.allCaps == nil {
c.allCaps = make(map[string]bool, len(v))
}
for _, s := range v {
c.allCaps[s] = true
}
return c
}
func (c *DefaultCamelCaseConverter) ToCamelCase(s string) string {
if r, ok := c.converted[s]; ok {
return r
}
words := splitRE.Split(s, -1)
for i, word := range words {
if len(word) != 0 {
if allCapsRE.MatchString(word) {
words[i] = word[:1] + strings.ToLower(word[1:])
} else {
words[i] = strings.ToUpper(word[:1]) + word[1:]
}
}
}
r := capitalizedRE.ReplaceAllStringFunc(strings.Join(words, ""), func(s string) string {
if word := strings.ToUpper(s); c.allCaps[word] {
return word
}
return s
})
c.converted[s] = r
return r
}
var (
followNonCapRE = regexp.MustCompile(`([a-z\d])([A-Z])`)
followCapRE = regexp.MustCompile(`([A-Z])([A-Z][a-z]+)`)
)
func ToSnakeCase(s string) string {
s = followNonCapRE.ReplaceAllString(s, "${1}_$2")
s = followCapRE.ReplaceAllString(s, "${1}_$2")
return strings.ToLower(s)
}

View File

@ -1,27 +0,0 @@
package conv
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestToCamelCase(t *testing.T) {
assert.Equal(t, "", ToCamelCase(""))
assert.Equal(t, "T", ToCamelCase("t"))
assert.Equal(t, "To", ToCamelCase("to"))
assert.Equal(t, "ToCamelCase", ToCamelCase("to camel case "))
assert.Equal(t, "ToCamelCase", ToCamelCase("to_camel__case"))
assert.Equal(t, "ToCamelCase", ToCamelCase("to-camel--case"))
assert.Equal(t, "ToCamelCase", ToCamelCase("to_camel -case"))
assert.Equal(t, "JsonToGo", ToCamelCase("JSON to Go"))
converter := NewDefaultCamelCaseConverter([]string{"ID", "JSON"})
assert.Equal(t, "ID", converter.ToCamelCase("id"))
assert.Equal(t, "JSON", converter.ToCamelCase("json"))
assert.Equal(t, "JSONToGo", converter.ToCamelCase("JSON to Go"))
}
func TestToSnakeCase(t *testing.T) {
assert.Equal(t, "", ToSnakeCase(""))
assert.Equal(t, "json_to_go", ToSnakeCase("JSONToGo"))
}

View File

@ -1,14 +1,13 @@
package filler
import (
"githouse.ru/macrox/gotygen/pkg/conv"
"githouse.ru/macrox/gotygen/pkg/document"
"githouse.ru/macrox/gotygen/pkg/scanner"
)
type Filler interface {
scanner.Scanner
conv.CamelCaseConverter
UseReplacements(replacements ...string) Filler
UseReplacements(a []string) Filler
UseCaps(a []string) Filler
Fill(out *document.Document) error
}

View File

@ -4,18 +4,20 @@ import (
"fmt"
"strings"
"githouse.ru/macrox/gotygen/pkg/strcase"
"githouse.ru/macrox/gotygen/pkg/utils"
"githouse.ru/macrox/gotygen/pkg/token"
"githouse.ru/macrox/gotygen/pkg/conv"
"githouse.ru/macrox/gotygen/pkg/document"
"githouse.ru/macrox/gotygen/pkg/scanner"
)
type jsonFiller struct {
scanner.Scanner
conv.CamelCaseConverter
camelCtx strcase.CamelContext
replacements map[string]string
declaredName string
@ -147,7 +149,7 @@ func (f *jsonFiller) scanObjectType(refOut *document.Document, rootName string)
return nil, err
}
keys = append(keys, key)
if t, err = f.scanType(refOut, f.ToCamelCase(key)); err != nil {
if t, err = f.scanType(refOut, strcase.ToCamel(key, f.camelCtx)); err != nil {
return nil, err
} else {
types = append(types, t)
@ -171,7 +173,7 @@ func (f *jsonFiller) scanObjectType(refOut *document.Document, rootName string)
}
s := document.NewStruct()
for i, key := range keys {
name, omitEmpty := f.ToCamelCase(key), utils.IsType[document.Map](types[i]) || utils.IsType[document.Array](types[i])
name, omitEmpty := strcase.ToCamel(key, f.camelCtx), utils.IsType[document.Map](types[i]) || utils.IsType[document.Array](types[i])
if refOut != nil {
if arr, ok := types[i].(document.Array); ok {
element := arr.Element()
@ -237,23 +239,6 @@ func (f *jsonFiller) scanType(refOut *document.Document, rootName string) (docum
}
}
func (f *jsonFiller) Fill(out *document.Document) error {
if out == nil {
return fmt.Errorf("document is nill")
}
t, err := f.scanType(out, f.declaredName)
if err != nil {
return err
}
out.PutDeclaredType(document.NewDeclaredType(f.declaredName, t), true)
return nil
}
func (f *jsonFiller) UseCaps(caps ...string) conv.CamelCaseConverter {
f.CamelCaseConverter.UseCaps(caps...)
return f
}
func (f *jsonFiller) normalizeId(parts ...string) string {
if len(parts) < 1 {
return ""
@ -269,11 +254,28 @@ func (f *jsonFiller) normalizeId(parts ...string) string {
return key
}
func (f *jsonFiller) UseReplacements(replacements ...string) Filler {
for _, replacement := range replacements {
parts := strings.Split(replacement, ":")
if len(parts) > 1 {
f.replacements[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[len(parts)-1])
func (f *jsonFiller) Fill(out *document.Document) error {
if out == nil {
return fmt.Errorf("document is nill")
}
t, err := f.scanType(out, f.declaredName)
if err != nil {
return err
}
out.PutDeclaredType(document.NewDeclaredType(f.declaredName, t), true)
return nil
}
func (f *jsonFiller) UseCaps(a []string) Filler {
f.camelCtx.UseCaps(a)
return f
}
func (f *jsonFiller) UseReplacements(a []string) Filler {
for _, s := range a {
p := strings.Split(s, ":")
if len(p) > 1 {
f.replacements[strings.TrimSpace(p[0])] = strings.TrimSpace(p[len(p)-1])
}
}
return f
@ -281,9 +283,9 @@ func (f *jsonFiller) UseReplacements(replacements ...string) Filler {
func NewJsonFiller(data []byte, declaredName string, allCaps ...string) Filler {
return &jsonFiller{
CamelCaseConverter: conv.NewDefaultCamelCaseConverter(allCaps),
Scanner: scanner.NewFromBytes(data),
replacements: make(map[string]string),
declaredName: declaredName,
Scanner: scanner.NewFromBytes(data),
camelCtx: strcase.NewCamelContext(allCaps),
replacements: make(map[string]string),
declaredName: declaredName,
}
}

93
pkg/strcase/camel.go Normal file
View File

@ -0,0 +1,93 @@
package strcase
import (
"regexp"
"strings"
)
var (
splitRE = regexp.MustCompile(`_+|-+|\s+`)
allCapsRE = regexp.MustCompile(`^[A-Z]{2,}\d*$`)
capitalizedRE = regexp.MustCompile(`[A-Z][a-z]+\d*`)
)
type CamelContext interface {
UseCaps(a []string) CamelContext
HasCaps(w string) bool
Find(s string) (string, bool)
Save(s, r string)
}
type camelContext struct {
allCaps map[string]bool
cache map[string]string
}
func (c *camelContext) UseCaps(a []string) CamelContext {
if c.allCaps == nil {
c.allCaps = make(map[string]bool, len(a))
}
for _, s := range a {
c.allCaps[s] = true
}
return c
}
func (c *camelContext) HasCaps(w string) (ok bool) {
if c.allCaps != nil {
_, ok = c.allCaps[w]
}
return
}
func (c *camelContext) Find(s string) (r string, ok bool) {
if c.cache != nil {
r, ok = c.cache[s]
}
return
}
func (c *camelContext) Save(s, r string) {
if c.cache == nil {
c.cache = make(map[string]string)
}
c.cache[s] = r
}
func NewCamelContext(caps []string) CamelContext {
return new(camelContext).UseCaps(caps)
}
func ToCamel(s string, c ...CamelContext) (r string) {
var ctx CamelContext
if len(c) > 0 {
ctx = c[0]
}
if ctx != nil {
var ok bool
if r, ok = ctx.Find(s); ok {
return
}
}
words := splitRE.Split(s, -1)
for i, word := range words {
if len(word) != 0 {
if allCapsRE.MatchString(word) {
words[i] = word[:1] + strings.ToLower(word[1:])
} else {
words[i] = strings.ToUpper(word[:1]) + word[1:]
}
}
}
if ctx != nil {
r = capitalizedRE.ReplaceAllStringFunc(strings.Join(words, ""), func(s string) string {
if w := strings.ToUpper(s); ctx.HasCaps(w) {
return w
}
return s
})
ctx.Save(s, r)
} else {
r = strings.Join(words, "")
}
return
}

23
pkg/strcase/camel_test.go Normal file
View File

@ -0,0 +1,23 @@
package strcase
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestToCamel(t *testing.T) {
assert.Equal(t, "", ToCamel(""))
assert.Equal(t, "T", ToCamel("t"))
assert.Equal(t, "To", ToCamel("to"))
assert.Equal(t, "ToCamelCase", ToCamel("to camel case "))
assert.Equal(t, "ToCamelCase", ToCamel("to_camel__case"))
assert.Equal(t, "ToCamelCase", ToCamel("to-camel--case"))
assert.Equal(t, "ToCamelCase", ToCamel("to_camel -case"))
assert.Equal(t, "JsonToGo", ToCamel("JSON to Go"))
ctx := NewCamelContext([]string{"ID", "JSON"})
assert.Equal(t, "ID", ToCamel("id", ctx))
assert.Equal(t, "JSON", ToCamel("json", ctx))
assert.Equal(t, "JSONToGo", ToCamel("JSON to Go", ctx))
}

17
pkg/strcase/snake.go Normal file
View File

@ -0,0 +1,17 @@
package strcase
import (
"regexp"
"strings"
)
var (
followNonCapRE = regexp.MustCompile(`([a-z\d])([A-Z])`)
followCapRE = regexp.MustCompile(`([A-Z])([A-Z][a-z]+)`)
)
func ToSnake(s string) string {
s = followNonCapRE.ReplaceAllString(s, "${1}_$2")
s = followCapRE.ReplaceAllString(s, "${1}_$2")
return strings.ToLower(s)
}

12
pkg/strcase/snake_test.go Normal file
View File

@ -0,0 +1,12 @@
package strcase
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestToSnake(t *testing.T) {
assert.Equal(t, "", ToSnake(""))
assert.Equal(t, "json_to_go", ToSnake("JSONToGo"))
}