Skip to content

Commit

Permalink
feat: support anonymous structs (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
byashimov authored Oct 4, 2024
1 parent 91c282a commit 5f73522
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 360 deletions.
27 changes: 16 additions & 11 deletions generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,15 @@ func exec() error {
return client.Save(cfg.ClientFile)
}

// reMakesSense sometimes there are invalid enums, for instance just a comma ","
// reMakesSense sometimes there are invalid enums, for instance, just a comma ","
var reMakesSense = regexp.MustCompile(`\w`)

//nolint:funlen // It's a generator, it's supposed to be long, and we won't expand it.
func writeStruct(f *jen.File, s *Schema) error {
if s.isAnonymous() {
return nil
}

if s.isEnum() {
kind := getScalarType(s)
o := f.Type().Id(s.CamelName)
Expand Down Expand Up @@ -499,16 +503,24 @@ func writeStruct(f *jen.File, s *Schema) error {
}

o.Line().Const().Defs(enums...)

o.Line().Func().Id(s.CamelName + "Choices").Params().Index().Add(kind).Block(
jen.Return(jen.Index().Add(kind).Values(values...)),
)

return nil
}

fields := make([]jen.Code, 0, len(s.Properties))
if s.Description != "" {
f.Comment(fmt.Sprintf("%s %s", s.CamelName, fmtComment(s.Description)))
}

f.Type().Id(s.CamelName).Add(fmtStruct(s))
return nil
}

// fmtStruct returns anynonous struct
func fmtStruct(s *Schema) *jen.Statement {
fields := make([]jen.Code, 0, len(s.Properties))
for _, k := range s.propertyNames {
p := s.Properties[k]
field := jen.Id(strcase.ToCamel(k)).Add(getType(p))
Expand All @@ -527,14 +539,7 @@ func writeStruct(f *jen.File, s *Schema) error {

fields = append(fields, field)
}

if s.Description != "" {
f.Comment(fmt.Sprintf("%s %s", s.CamelName, fmtComment(s.Description)))
}

f.Type().Id(s.CamelName).Struct(fields...)

return nil
return jen.Struct(fields...)
}

func getResponse(s *Schema) *Schema {
Expand Down
134 changes: 77 additions & 57 deletions generator/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ type Schema struct {
name string
propertyNames []string
parent *Schema
in, out bool // Request or Response DTO
in, out bool // Request or Response DTO
hasCollision bool // Means this struct has a collision with another one with different type of fields
duplicates []*Schema // Refs to structs with exactly the same fields
}

//nolint:funlen,gocognit,gocyclo // It is easy to maintain and read, we don't need to split it
Expand Down Expand Up @@ -194,13 +196,17 @@ func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) {
}
}

// Adds suffix to reduce name collision
suffix := ""
switch {
case s.isIn():
suffix = "In"
case s.isOut():
suffix = "Out"
}

if s.isObject() {
switch {
case s.isIn():
s.CamelName += "In"
case s.isOut():
s.CamelName += "Out"
}
s.CamelName += suffix
}

// Cleans duplicates like StatusStatus
Expand Down Expand Up @@ -233,9 +239,9 @@ func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) {

if s.Type == SchemaTypeString {
parts := strings.Split(s.name, "_")
suffix := parts[len(parts)-1]
suffx := parts[len(parts)-1]

if len(parts) > 1 && (suffix == "at" || suffix == "time") {
if len(parts) > 1 && (suffx == "at" || suffx == "time") {
s.Type = SchemaTypeTime
}
}
Expand All @@ -261,31 +267,55 @@ func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) {
}
}

if s.isObject() || s.isEnum() {
if s.isObject() || s.isEnum() { //nolint:nestif
for s.parent != nil {
v, ok := scope[s.CamelName]
if !ok {
break
}

if v.hash() == s.hash() {
// This is a duplicate
v.duplicates = append(v.duplicates, s)
return
}

s.CamelName += "Alt"
// Resolves name collision
// Takes parent's name as prefix or uses parent's name
parent := s.parent
if s.parent.isArray() {
parent = parent.parent
}

if parent.isPrivate() {
s.CamelName = strcase.ToCamel(parent.CamelName)
} else {
s.CamelName = strings.TrimSuffix(strcase.ToCamel(parent.CamelName), suffix) + s.CamelName
}

// Marks all have collision
// We don't know in the beginning that there will be a collision
// That's why we need this "duplicates" field
v.hasCollision = true
for _, d := range v.duplicates {
d.hasCollision = true
}
s.hasCollision = true
}

scope[s.CamelName] = s
}
}

// isPrivate returns true when a struct is just a wrapper for one field,
// so we can just return the field value making things less nested
func (s *Schema) isPrivate() bool {
return s.parent == nil && s.out && len(s.Properties) == 1
}

// hash is for comparison
func (s *Schema) hash() string {
if s.isEnum() {
// Compares enums by values
return mustMarshal(s.Enum)
}

Expand All @@ -300,19 +330,6 @@ func (s *Schema) isArray() bool {
return s.Type == SchemaTypeArray
}

func (s *Schema) isNestedArray() bool {
return s.isArray() && s.Items.isArray()
}

func (s *Schema) isScalar() bool {
switch s.Type {
case SchemaTypeString, SchemaTypeInteger, SchemaTypeNumber, SchemaTypeBoolean, SchemaTypeTime:
return true
}

return false
}

// isMap schemaless map
func (s *Schema) isMap() bool {
return s.Type == SchemaTypeObject && len(s.Properties) == 0
Expand Down Expand Up @@ -344,6 +361,21 @@ func (s *Schema) lowerCamel() string {
return strcase.ToLowerCamel(s.CamelName)
}

func (s *Schema) level() int {
level := 0
p := s.parent
for p != nil {
level++
p = p.parent
}
return level
}

// isAnonymous returns true when a struct should be rendered anonymous to reduce scope noise
func (s *Schema) isAnonymous() bool {
return s.hasCollision && s.isObject() && s.level() > 3
}

func getScalarType(s *Schema) *jen.Statement {
switch s.Type {
case SchemaTypeString:
Expand All @@ -363,48 +395,29 @@ func getScalarType(s *Schema) *jen.Statement {

// getType returns go type with/wo a pointer
func getType(s *Schema) *jen.Statement {
switch {
case s.Type == SchemaTypeAny:
return jen.Any()
case s.isEnum():
if s.isEnum() {
return jen.Id(s.CamelName)
case s.isScalar():
scalar := getScalarType(s)
if s.required {
return scalar
}
}

return jen.Op("*").Add(scalar)
switch s.Type {
case SchemaTypeAny:
return jen.Any()
case SchemaTypeString, SchemaTypeInteger, SchemaTypeNumber, SchemaTypeBoolean, SchemaTypeTime:
return withPointer(getScalarType(s), s.required)
}

switch {
case s.isArray():
a := jen.Index()
if !(s.required || s.isOut()) {
a = jen.Op("*").Index()
}

// No pointers for complex objects
switch {
case s.isNestedArray():
// but not nested array
case s.Items.isObject() || s.Items.isArray():
return a.Id(s.Items.CamelName)
}

return a.Add(getType(s.Items))
return withPointer(jen.Index(), s.required || s.isOut()).Add(getType(s.Items))
case s.isObject():
if !s.required {
return jen.Id("*" + s.CamelName)
o := jen.Id(s.CamelName)
if s.isAnonymous() {
o = fmtStruct(s)
}

return jen.Id(s.CamelName)
return withPointer(o, s.required)
case s.isMap():
a := jen.Map(jen.String())
if !(s.required || s.isOut()) {
a = jen.Op("*").Map(jen.String())
}

a := withPointer(jen.Map(jen.String()), s.required || s.isOut())
if s.AdditionalProperties != nil {
s.AdditionalProperties.required = true
return a.Add(getType(s.AdditionalProperties))
Expand All @@ -419,6 +432,13 @@ func getType(s *Schema) *jen.Statement {
}
}

func withPointer(j *jen.Statement, required bool) *jen.Statement {
if required {
return j
}
return jen.Op("*").Add(j)
}

func mustMarshal(s any) string {
b, err := json.Marshal(s)
if err != nil {
Expand Down
18 changes: 9 additions & 9 deletions handler/account/account.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions handler/clickhouse/clickhouse.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5f73522

Please sign in to comment.