Skip to content

Commit cd8720c

Browse files
committed
Add flag --capitalization, for registering custom capitalizations.
1 parent 29ff417 commit cd8720c

16 files changed

+278
-106
lines changed

cmd/gojsonschema/main.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
schemaPackages []string
1919
schemaOutputs []string
2020
schemaRootTypes []string
21+
capitalizations []string
2122
)
2223

2324
var rootCmd = &cobra.Command{
@@ -47,7 +48,15 @@ var rootCmd = &cobra.Command{
4748
abortWithErr(err)
4849
}
4950

50-
schemaMappings := []generator.SchemaMapping{}
51+
cfg := generator.Config{
52+
Warner: func(message string) {
53+
log("Warning: %s", message)
54+
},
55+
Capitalizations: capitalizations,
56+
DefaultOutputName: defaultOutput,
57+
DefaultPackageName: defaultPackage,
58+
SchemaMappings: []generator.SchemaMapping{},
59+
}
5160
for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
5261
mapping := generator.SchemaMapping{SchemaID: id}
5362
if s, ok := schemaPackageMap[id]; ok {
@@ -63,12 +72,10 @@ var rootCmd = &cobra.Command{
6372
if s, ok := schemaRootTypeMap[id]; ok {
6473
mapping.RootType = s
6574
}
66-
schemaMappings = append(schemaMappings, mapping)
75+
cfg.SchemaMappings = append(cfg.SchemaMappings, mapping)
6776
}
6877

69-
generator, err := generator.New(schemaMappings, defaultPackage, defaultOutput, func(message string) {
70-
log("Warning: %s", message)
71-
})
78+
generator, err := generator.New(cfg)
7279
if err != nil {
7380
abortWithErr(err)
7481
}
@@ -129,6 +136,9 @@ func main() {
129136
rootCmd.PersistentFlags().StringSliceVar(&schemaRootTypes, "schema-root-type", nil,
130137
"Override name to use for the root type of a specific schema ID; "+
131138
"must be in the format URI=PACKAGE. By default, it is derived from the file name.")
139+
rootCmd.PersistentFlags().StringSliceVar(&capitalizations, "capitalization", nil,
140+
"Specify a preferred Go capitalization for a string. For example, by default a field "+
141+
"named 'id' becomes 'Id'. With --capitalization ID, it will be generated as 'ID'.")
132142

133143
abortWithErr(rootCmd.Execute())
134144
}

pkg/codegen/utils.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package codegen
22

33
import (
44
"fmt"
5-
"path/filepath"
6-
"strings"
75

86
"github.com/atombender/go-jsonschema/pkg/schemas"
97
)
@@ -23,37 +21,3 @@ func PrimitiveTypeFromJSONSchemaType(jsType string) (Type, error) {
2321
}
2422
return nil, fmt.Errorf("unknown JSON Schema type %q", jsType)
2523
}
26-
27-
func IdentifierFromFileName(fileName string) string {
28-
s := filepath.Base(fileName)
29-
return Identifierize(strings.TrimSuffix(strings.TrimSuffix(s, ".json"), ".schema"))
30-
}
31-
32-
func Identifierize(s string) string {
33-
// FIXME: Better handling of non-identifier chars
34-
var sb strings.Builder
35-
seps := "_-. \t"
36-
for {
37-
i := strings.IndexAny(s, seps)
38-
if i == -1 {
39-
sb.WriteString(capitalize(s))
40-
break
41-
}
42-
sb.WriteString(capitalize(s[0:i]))
43-
for i < len(s) && strings.ContainsRune(seps, rune(s[i])) {
44-
i++
45-
}
46-
if i >= len(s) {
47-
break
48-
}
49-
s = s[i:]
50-
}
51-
return sb.String()
52-
}
53-
54-
func capitalize(s string) string {
55-
if len(s) == 0 {
56-
return ""
57-
}
58-
return strings.ToUpper(s[0:1]) + s[1:]
59-
}

pkg/generator/generate.go

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import (
1313
"github.com/atombender/go-jsonschema/pkg/schemas"
1414
)
1515

16+
type Config struct {
17+
SchemaMappings []SchemaMapping
18+
Capitalizations []string
19+
DefaultPackageName string
20+
DefaultOutputName string
21+
Warner func(string)
22+
}
23+
1624
type SchemaMapping struct {
1725
SchemaID string
1826
PackageName string
@@ -21,25 +29,15 @@ type SchemaMapping struct {
2129
}
2230

2331
type Generator struct {
32+
config Config
2433
emitter *codegen.Emitter
25-
defaultPackageName string
26-
defaultOutputName string
27-
schemaMappings []SchemaMapping
28-
warner func(string)
2934
outputs map[string]*output
3035
schemaCacheByFileName map[string]*schemas.Schema
3136
}
3237

33-
func New(
34-
schemaMappings []SchemaMapping,
35-
defaultPackageName string,
36-
defaultOutputName string,
37-
warner func(string)) (*Generator, error) {
38+
func New(config Config) (*Generator, error) {
3839
return &Generator{
39-
warner: warner,
40-
schemaMappings: schemaMappings,
41-
defaultPackageName: defaultPackageName,
42-
defaultOutputName: defaultOutputName,
40+
config: config,
4341
outputs: map[string]*output{},
4442
schemaCacheByFileName: map[string]*schemas.Schema{},
4543
}, nil
@@ -123,25 +121,25 @@ func (g *Generator) loadSchemaFromFile(fileName, parentFileName string) (*schema
123121
}
124122

125123
func (g *Generator) getRootTypeName(schema *schemas.Schema, fileName string) string {
126-
for _, m := range g.schemaMappings {
124+
for _, m := range g.config.SchemaMappings {
127125
if m.SchemaID == schema.ID && m.RootType != "" {
128126
return m.RootType
129127
}
130128
}
131-
return codegen.IdentifierFromFileName(fileName)
129+
return g.identifierFromFileName(fileName)
132130
}
133131

134132
func (g *Generator) findOutputFileForSchemaID(id string) (*output, error) {
135133
if o, ok := g.outputs[id]; ok {
136134
return o, nil
137135
}
138136

139-
for _, m := range g.schemaMappings {
137+
for _, m := range g.config.SchemaMappings {
140138
if m.SchemaID == id {
141139
return g.beginOutput(id, m.OutputName, m.PackageName)
142140
}
143141
}
144-
return g.beginOutput(id, g.defaultOutputName, g.defaultPackageName)
142+
return g.beginOutput(id, g.config.DefaultOutputName, g.config.DefaultPackageName)
145143
}
146144

147145
func (g *Generator) beginOutput(
@@ -170,7 +168,7 @@ func (g *Generator) beginOutput(
170168
}
171169

172170
output := &output{
173-
warner: g.warner,
171+
warner: g.config.Warner,
174172
file: &codegen.File{
175173
FileName: outputName,
176174
Package: pkg,
@@ -183,6 +181,39 @@ func (g *Generator) beginOutput(
183181
return output, nil
184182
}
185183

184+
func (g *Generator) makeEnumConstantName(typeName, value string) string {
185+
if strings.ContainsAny(typeName[len(typeName)-1:], "0123456789") {
186+
return typeName + "_" + g.identifierize(value)
187+
}
188+
return typeName + g.identifierize(value)
189+
}
190+
191+
func (g *Generator) identifierFromFileName(fileName string) string {
192+
s := filepath.Base(fileName)
193+
return g.identifierize(strings.TrimSuffix(strings.TrimSuffix(s, ".json"), ".schema"))
194+
}
195+
196+
func (g *Generator) identifierize(s string) string {
197+
// FIXME: Better handling of non-identifier chars
198+
var sb strings.Builder
199+
for _, part := range splitIdentifierByCaseAndSeparators(s) {
200+
_, _ = sb.WriteString(g.capitalize(part))
201+
}
202+
return sb.String()
203+
}
204+
205+
func (g *Generator) capitalize(s string) string {
206+
if len(s) == 0 {
207+
return ""
208+
}
209+
for _, c := range g.config.Capitalizations {
210+
if strings.ToLower(c) == strings.ToLower(s) {
211+
return c
212+
}
213+
}
214+
return strings.ToUpper(s[0:1]) + s[1:]
215+
}
216+
186217
type schemaGenerator struct {
187218
*Generator
188219
output *output
@@ -197,7 +228,7 @@ func (g *schemaGenerator) generateRootType() error {
197228

198229
if g.schema.Type.Type == "" {
199230
for name, def := range g.schema.Definitions {
200-
_, err := g.generateDeclaredType(def, newNameScope(codegen.Identifierize(name)))
231+
_, err := g.generateDeclaredType(def, newNameScope(g.identifierize(name)))
201232
if err != nil {
202233
return err
203234
}
@@ -250,7 +281,7 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro
250281
}
251282
// Minor hack to make definitions default to being objects
252283
def.Type = schemas.TypeNameObject
253-
defName = codegen.Identifierize(defName)
284+
defName = g.identifierize(defName)
254285
} else {
255286
def = schema.Type
256287
defName = g.getRootTypeName(schema, fileName)
@@ -423,7 +454,7 @@ func (g *schemaGenerator) generateStructType(
423454
scope nameScope) (codegen.Type, error) {
424455
if len(t.Properties) == 0 {
425456
if len(t.Required) > 0 {
426-
g.warner("object type with no properties has required fields; " +
457+
g.config.Warner("object type with no properties has required fields; " +
427458
"skipping validation code for them since we don't know their types")
428459
}
429460
return &codegen.MapType{
@@ -450,11 +481,11 @@ func (g *schemaGenerator) generateStructType(
450481
prop := t.Properties[name]
451482
isRequired := requiredNames[name]
452483

453-
fieldName := codegen.Identifierize(name)
484+
fieldName := g.identifierize(name)
454485
if count, ok := uniqueNames[fieldName]; ok {
455486
uniqueNames[fieldName] = count + 1
456487
fieldName = fmt.Sprintf("%s_%d", fieldName, count+1)
457-
g.warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
488+
g.config.Warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
458489
"in the same struct; it will be declared as %s", name, fieldName))
459490
} else {
460491
uniqueNames[fieldName] = 1
@@ -575,7 +606,7 @@ func (g *schemaGenerator) generateEnumType(
575606
enumType = codegen.PrimitiveType{primitiveType}
576607
}
577608
if wrapInStruct {
578-
g.warner("Enum field wrapped in struct in order to store values of multiple types")
609+
g.config.Warner("Enum field wrapped in struct in order to store values of multiple types")
579610
enumType = &codegen.StructType{
580611
Fields: []codegen.StructField{
581612
{
@@ -659,7 +690,7 @@ func (g *schemaGenerator) generateEnumType(
659690
if s, ok := v.(string); ok {
660691
// TODO: Make sure the name is unique across scope
661692
g.output.file.Package.AddDecl(&codegen.Constant{
662-
Name: makeEnumConstantName(enumDecl.Name, s),
693+
Name: g.makeEnumConstantName(enumDecl.Name, s),
663694
Type: &codegen.NamedType{Decl: &enumDecl},
664695
Value: s,
665696
})
@@ -670,24 +701,6 @@ func (g *schemaGenerator) generateEnumType(
670701
return &codegen.NamedType{Decl: &enumDecl}, nil
671702
}
672703

673-
// func (g *schemaGenerator) generateAnonymousType(
674-
// t *schemas.Type, name string) (codegen.Type, error) {
675-
// if t.Type == schemas.TypeNameObject {
676-
// if len(t.Properties) == 0 {
677-
// return codegen.MapType{
678-
// KeyType: codegen.PrimitiveType{"string"},
679-
// ValueType: codegen.EmptyInterfaceType{},
680-
// }, nil
681-
// }
682-
// s, err := g.generateStructDecl(t, name, "", false)
683-
// if err != nil {
684-
// return nil, err
685-
// }
686-
// return &codegen.NamedType{Decl: s}, nil
687-
// }
688-
// return nil, fmt.Errorf("unexpected type %q", t.Type)
689-
// }
690-
691704
type output struct {
692705
file *codegen.File
693706
enums map[string]cachedEnum

pkg/generator/utils.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import (
44
"crypto/sha256"
55
"fmt"
66
"sort"
7-
"strings"
8-
9-
"github.com/atombender/go-jsonschema/pkg/codegen"
7+
"unicode"
108
)
119

1210
func hashArrayOfValues(values []interface{}) string {
@@ -23,9 +21,49 @@ func hashArrayOfValues(values []interface{}) string {
2321
return fmt.Sprintf("%x", h.Sum(nil))
2422
}
2523

26-
func makeEnumConstantName(typeName, value string) string {
27-
if strings.ContainsAny(typeName[len(typeName)-1:], "0123456789") {
28-
return typeName + "_" + codegen.Identifierize(value)
24+
func splitIdentifierByCaseAndSeparators(s string) []string {
25+
if len(s) == 0 {
26+
return nil
27+
}
28+
29+
type state int
30+
const (
31+
stateNothing state = iota
32+
stateLower
33+
stateUpper
34+
stateNumber
35+
stateDelimiter
36+
)
37+
38+
var result []string
39+
currState, j := stateNothing, 0
40+
for i := 0; i < len(s); i++ {
41+
var nextState state
42+
c := rune(s[i])
43+
switch {
44+
case unicode.IsLower(c):
45+
nextState = stateLower
46+
case unicode.IsUpper(c):
47+
nextState = stateUpper
48+
case unicode.IsNumber(c):
49+
nextState = stateNumber
50+
default:
51+
nextState = stateDelimiter
52+
}
53+
if nextState != currState {
54+
if currState == stateDelimiter {
55+
j = i
56+
} else if !(currState == stateUpper && nextState == stateLower) {
57+
if i > j {
58+
result = append(result, s[j:i])
59+
}
60+
j = i
61+
}
62+
currState = nextState
63+
}
64+
}
65+
if currState != stateDelimiter && len(s)-j > 0 {
66+
result = append(result, s[j:])
2967
}
30-
return typeName + codegen.Identifierize(value)
68+
return result
3169
}

0 commit comments

Comments
 (0)