Skip to content

Commit

Permalink
Add flag --capitalization, for registering custom capitalizations.
Browse files Browse the repository at this point in the history
  • Loading branch information
atombender committed Oct 1, 2018
1 parent 29ff417 commit cd8720c
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 106 deletions.
20 changes: 15 additions & 5 deletions cmd/gojsonschema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
schemaPackages []string
schemaOutputs []string
schemaRootTypes []string
capitalizations []string
)

var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -47,7 +48,15 @@ var rootCmd = &cobra.Command{
abortWithErr(err)
}

schemaMappings := []generator.SchemaMapping{}
cfg := generator.Config{
Warner: func(message string) {
log("Warning: %s", message)
},
Capitalizations: capitalizations,
DefaultOutputName: defaultOutput,
DefaultPackageName: defaultPackage,
SchemaMappings: []generator.SchemaMapping{},
}
for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
mapping := generator.SchemaMapping{SchemaID: id}
if s, ok := schemaPackageMap[id]; ok {
Expand All @@ -63,12 +72,10 @@ var rootCmd = &cobra.Command{
if s, ok := schemaRootTypeMap[id]; ok {
mapping.RootType = s
}
schemaMappings = append(schemaMappings, mapping)
cfg.SchemaMappings = append(cfg.SchemaMappings, mapping)
}

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

abortWithErr(rootCmd.Execute())
}
Expand Down
36 changes: 0 additions & 36 deletions pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package codegen

import (
"fmt"
"path/filepath"
"strings"

"github.com/atombender/go-jsonschema/pkg/schemas"
)
Expand All @@ -23,37 +21,3 @@ func PrimitiveTypeFromJSONSchemaType(jsType string) (Type, error) {
}
return nil, fmt.Errorf("unknown JSON Schema type %q", jsType)
}

func IdentifierFromFileName(fileName string) string {
s := filepath.Base(fileName)
return Identifierize(strings.TrimSuffix(strings.TrimSuffix(s, ".json"), ".schema"))
}

func Identifierize(s string) string {
// FIXME: Better handling of non-identifier chars
var sb strings.Builder
seps := "_-. \t"
for {
i := strings.IndexAny(s, seps)
if i == -1 {
sb.WriteString(capitalize(s))
break
}
sb.WriteString(capitalize(s[0:i]))
for i < len(s) && strings.ContainsRune(seps, rune(s[i])) {
i++
}
if i >= len(s) {
break
}
s = s[i:]
}
return sb.String()
}

func capitalize(s string) string {
if len(s) == 0 {
return ""
}
return strings.ToUpper(s[0:1]) + s[1:]
}
99 changes: 56 additions & 43 deletions pkg/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import (
"github.com/atombender/go-jsonschema/pkg/schemas"
)

type Config struct {
SchemaMappings []SchemaMapping
Capitalizations []string
DefaultPackageName string
DefaultOutputName string
Warner func(string)
}

type SchemaMapping struct {
SchemaID string
PackageName string
Expand All @@ -21,25 +29,15 @@ type SchemaMapping struct {
}

type Generator struct {
config Config
emitter *codegen.Emitter
defaultPackageName string
defaultOutputName string
schemaMappings []SchemaMapping
warner func(string)
outputs map[string]*output
schemaCacheByFileName map[string]*schemas.Schema
}

func New(
schemaMappings []SchemaMapping,
defaultPackageName string,
defaultOutputName string,
warner func(string)) (*Generator, error) {
func New(config Config) (*Generator, error) {
return &Generator{
warner: warner,
schemaMappings: schemaMappings,
defaultPackageName: defaultPackageName,
defaultOutputName: defaultOutputName,
config: config,
outputs: map[string]*output{},
schemaCacheByFileName: map[string]*schemas.Schema{},
}, nil
Expand Down Expand Up @@ -123,25 +121,25 @@ func (g *Generator) loadSchemaFromFile(fileName, parentFileName string) (*schema
}

func (g *Generator) getRootTypeName(schema *schemas.Schema, fileName string) string {
for _, m := range g.schemaMappings {
for _, m := range g.config.SchemaMappings {
if m.SchemaID == schema.ID && m.RootType != "" {
return m.RootType
}
}
return codegen.IdentifierFromFileName(fileName)
return g.identifierFromFileName(fileName)
}

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

for _, m := range g.schemaMappings {
for _, m := range g.config.SchemaMappings {
if m.SchemaID == id {
return g.beginOutput(id, m.OutputName, m.PackageName)
}
}
return g.beginOutput(id, g.defaultOutputName, g.defaultPackageName)
return g.beginOutput(id, g.config.DefaultOutputName, g.config.DefaultPackageName)
}

func (g *Generator) beginOutput(
Expand Down Expand Up @@ -170,7 +168,7 @@ func (g *Generator) beginOutput(
}

output := &output{
warner: g.warner,
warner: g.config.Warner,
file: &codegen.File{
FileName: outputName,
Package: pkg,
Expand All @@ -183,6 +181,39 @@ func (g *Generator) beginOutput(
return output, nil
}

func (g *Generator) makeEnumConstantName(typeName, value string) string {
if strings.ContainsAny(typeName[len(typeName)-1:], "0123456789") {
return typeName + "_" + g.identifierize(value)
}
return typeName + g.identifierize(value)
}

func (g *Generator) identifierFromFileName(fileName string) string {
s := filepath.Base(fileName)
return g.identifierize(strings.TrimSuffix(strings.TrimSuffix(s, ".json"), ".schema"))
}

func (g *Generator) identifierize(s string) string {
// FIXME: Better handling of non-identifier chars
var sb strings.Builder
for _, part := range splitIdentifierByCaseAndSeparators(s) {
_, _ = sb.WriteString(g.capitalize(part))
}
return sb.String()
}

func (g *Generator) capitalize(s string) string {
if len(s) == 0 {
return ""
}
for _, c := range g.config.Capitalizations {
if strings.ToLower(c) == strings.ToLower(s) {
return c
}
}
return strings.ToUpper(s[0:1]) + s[1:]
}

type schemaGenerator struct {
*Generator
output *output
Expand All @@ -197,7 +228,7 @@ func (g *schemaGenerator) generateRootType() error {

if g.schema.Type.Type == "" {
for name, def := range g.schema.Definitions {
_, err := g.generateDeclaredType(def, newNameScope(codegen.Identifierize(name)))
_, err := g.generateDeclaredType(def, newNameScope(g.identifierize(name)))
if err != nil {
return err
}
Expand Down Expand Up @@ -250,7 +281,7 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro
}
// Minor hack to make definitions default to being objects
def.Type = schemas.TypeNameObject
defName = codegen.Identifierize(defName)
defName = g.identifierize(defName)
} else {
def = schema.Type
defName = g.getRootTypeName(schema, fileName)
Expand Down Expand Up @@ -423,7 +454,7 @@ func (g *schemaGenerator) generateStructType(
scope nameScope) (codegen.Type, error) {
if len(t.Properties) == 0 {
if len(t.Required) > 0 {
g.warner("object type with no properties has required fields; " +
g.config.Warner("object type with no properties has required fields; " +
"skipping validation code for them since we don't know their types")
}
return &codegen.MapType{
Expand All @@ -450,11 +481,11 @@ func (g *schemaGenerator) generateStructType(
prop := t.Properties[name]
isRequired := requiredNames[name]

fieldName := codegen.Identifierize(name)
fieldName := g.identifierize(name)
if count, ok := uniqueNames[fieldName]; ok {
uniqueNames[fieldName] = count + 1
fieldName = fmt.Sprintf("%s_%d", fieldName, count+1)
g.warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
g.config.Warner(fmt.Sprintf("field %q maps to a field by the same name declared "+
"in the same struct; it will be declared as %s", name, fieldName))
} else {
uniqueNames[fieldName] = 1
Expand Down Expand Up @@ -575,7 +606,7 @@ func (g *schemaGenerator) generateEnumType(
enumType = codegen.PrimitiveType{primitiveType}
}
if wrapInStruct {
g.warner("Enum field wrapped in struct in order to store values of multiple types")
g.config.Warner("Enum field wrapped in struct in order to store values of multiple types")
enumType = &codegen.StructType{
Fields: []codegen.StructField{
{
Expand Down Expand Up @@ -659,7 +690,7 @@ func (g *schemaGenerator) generateEnumType(
if s, ok := v.(string); ok {
// TODO: Make sure the name is unique across scope
g.output.file.Package.AddDecl(&codegen.Constant{
Name: makeEnumConstantName(enumDecl.Name, s),
Name: g.makeEnumConstantName(enumDecl.Name, s),
Type: &codegen.NamedType{Decl: &enumDecl},
Value: s,
})
Expand All @@ -670,24 +701,6 @@ func (g *schemaGenerator) generateEnumType(
return &codegen.NamedType{Decl: &enumDecl}, nil
}

// func (g *schemaGenerator) generateAnonymousType(
// t *schemas.Type, name string) (codegen.Type, error) {
// if t.Type == schemas.TypeNameObject {
// if len(t.Properties) == 0 {
// return codegen.MapType{
// KeyType: codegen.PrimitiveType{"string"},
// ValueType: codegen.EmptyInterfaceType{},
// }, nil
// }
// s, err := g.generateStructDecl(t, name, "", false)
// if err != nil {
// return nil, err
// }
// return &codegen.NamedType{Decl: s}, nil
// }
// return nil, fmt.Errorf("unexpected type %q", t.Type)
// }

type output struct {
file *codegen.File
enums map[string]cachedEnum
Expand Down
52 changes: 45 additions & 7 deletions pkg/generator/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import (
"crypto/sha256"
"fmt"
"sort"
"strings"

"github.com/atombender/go-jsonschema/pkg/codegen"
"unicode"
)

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

func makeEnumConstantName(typeName, value string) string {
if strings.ContainsAny(typeName[len(typeName)-1:], "0123456789") {
return typeName + "_" + codegen.Identifierize(value)
func splitIdentifierByCaseAndSeparators(s string) []string {
if len(s) == 0 {
return nil
}

type state int
const (
stateNothing state = iota
stateLower
stateUpper
stateNumber
stateDelimiter
)

var result []string
currState, j := stateNothing, 0
for i := 0; i < len(s); i++ {
var nextState state
c := rune(s[i])
switch {
case unicode.IsLower(c):
nextState = stateLower
case unicode.IsUpper(c):
nextState = stateUpper
case unicode.IsNumber(c):
nextState = stateNumber
default:
nextState = stateDelimiter
}
if nextState != currState {
if currState == stateDelimiter {
j = i
} else if !(currState == stateUpper && nextState == stateLower) {
if i > j {
result = append(result, s[j:i])
}
j = i
}
currState = nextState
}
}
if currState != stateDelimiter && len(s)-j > 0 {
result = append(result, s[j:])
}
return typeName + codegen.Identifierize(value)
return result
}
Loading

0 comments on commit cd8720c

Please sign in to comment.