-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve Handling of Duplicate Fields in ResponseField Conversion and Ensure Backward Compatibility #254
Conversation
It would be great if you could add a simple example in the example directory to verify that the current changes are working correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Looking forward to your reply!
clientgenv2/source_generator.go
Outdated
for _, field := range rs { | ||
if field.IsFragmentSpread { | ||
fragmentChildrenFields = append(fragmentChildrenFields, field.ResponseFields...) | ||
removeTypeNames = append(removeTypeNames, field.FieldTypeString()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to separate the process of adding fields to FragmentChildrenFields and adding field.FieldTypeString() to removeTypeNames.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done with this commit.
b1cbb79
clientgenv2/source_generator.go
Outdated
for _, preMergedTypeName := range preMergedTypeNames { | ||
for i, source := range r.StructSources { | ||
if source.Name == preMergedTypeName { | ||
r.StructSources = append(r.StructSources[:i], r.StructSources[i+1:]...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the process is not very intuitive, so I’d like to either add a comment or extract it into a function with a descriptive name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks add comment with this commit
0ff4a3f
clientgenv2/source_generator.go
Outdated
@@ -130,6 +152,24 @@ func (r *SourceGenerator) NewResponseField(selection ast.Selection, typeName str | |||
// if a child field is fragment, this field type became fragment. | |||
baseType = fieldsResponseFields[0].Type | |||
case fieldsResponseFields.IsStructType(): | |||
fieldsResponseFields, postMergedResponseFields, preMergedTypeNames := fieldsResponseFields.MergeFragmentFields() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable fieldsResponseFields being both the return value and having the same name makes it a bit confusing, so I’d like to use a clearer name.
clientgenv2/source_generator.go
Outdated
} | ||
|
||
// MergeFragmentFields returns merged ResponseFieldList, post-merged ResponseFieldList, and remove type names | ||
func (rs ResponseFieldList) MergeFragmentFields() (ResponseFieldList, map[string]*ResponseFieldList, []string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does my understanding of what MergeFragmentFields is doing match the actual implementation?
1. Retrieving a list of fields for structs that are not IsFragmentSpread.
2. Generating a map for fragment types to prevent duplicates.
3. Creating a list of type string names to ignore duplicate fields.
The function name seems slightly disconnected from these operations, so I wanted to confirm.
e0e8b04
to
f43b1b6
Compare
Maybe It will be confusing for you, I used StructGenerator and simplified the logic. |
clientgenv2/source_generator.go
Outdated
for _, preMergedTypeName := range generator.GetPreMergedStructSources() { | ||
for i, source := range r.StructSources { | ||
// when name is same, remove it | ||
if source.Name == preMergedTypeName.Name { | ||
r.StructSources = append(r.StructSources[:i], r.StructSources[i+1:]...) | ||
break | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty good Fix! In my opinion, the best implementation would be something like this:
for _, preMergedTypeName := range generator.GetPreMergedStructSources() { | |
for i, source := range r.StructSources { | |
// when name is same, remove it | |
if source.Name == preMergedTypeName.Name { | |
r.StructSources = append(r.StructSources[:i], r.StructSources[i+1:]...) | |
break | |
} | |
} | |
} | |
r.StructSources = generator.MargedStructSources(r.StructSources) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty nice advice. I will try!
I did
|
clientgenv2/source_generator.go
Outdated
} | ||
// if there is no same name field, append it | ||
if !sameNameFieldFlag { | ||
targetFields = append(targetFields, sourceField) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s not ideal to cause side effects on arguments, so it would be better to define a type for the return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okey, I fixed it
clientgenv2/source_generator.go
Outdated
for _, sourceField := range sourceFields { | ||
sameNameFieldFlag := false | ||
for _, targetField := range targetFields { | ||
if sourceField.Name == targetField.Name && !targetField.ResponseFields.IsBasicType() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation, it seems like there are unnecessary loops, and the nesting feels a bit excessive. I’d like to simplify this by using the following implementation with a map:
func (rs ResponseFieldList) MapByName() map[string]*ResponseField {
m := make(map[string]*ResponseField)
for _, v := range rs {
m[v.Name] = v
}
return m
}
This approach should make the logic cleaner and more efficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true, I fixed this.
clientgenv2/source_generator.go
Outdated
@@ -76,6 +69,97 @@ func (rs ResponseFieldList) IsStructType() bool { | |||
return len(rs) > 0 && !rs.IsFragment() | |||
} | |||
|
|||
type StructGenerator struct { | |||
currentResponseFieldList *ResponseFieldList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion
currentResponseFieldList *ResponseFieldList | |
currentResponseFieldList ResponseFieldList |
// 子フィールドにFragmentがある場合は、現在のフィールドとマージする | ||
// if there is a fragment in child fields, merge it with the current field | ||
generator := NewStructGenerator(fieldsResponseFields) | ||
|
||
// restruct struct sources | ||
r.StructSources = generator.MergedStructSources(r.StructSources) | ||
|
||
// append current struct | ||
structType := generator.GetCurrentResponseFieldList().StructType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
clientgenv2/source_generator.go
Outdated
for _, field := range targetFieldsMap { | ||
res = append(res, field) | ||
} | ||
res = res.SortByName() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a sorting step. Because the code uses a map, the field order in client.go could change randomly. This update ensures that the field order remains consistent.
clientgenv2/source_generator.go
Outdated
} | ||
} | ||
|
||
func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged, postMerged *[]*StructSource) ResponseFieldList { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s generally not a good practice to implicitly cause side effects by taking a pointer as an argument and modifying its value. I think it would be better to return the value explicitly as a return value.
func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged, postMerged *[]*StructSource) ResponseFieldList { | |
func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged []*StructSource) (ResponseFieldList, []*StructSource, []*StructSource) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okey, I will follow you.
clientgenv2/source_generator.go
Outdated
currentResponseFieldList ResponseFieldList | ||
preMergedStructSources []*StructSource | ||
postMergedStructSources []*StructSource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, it would be helpful to include comments explaining each field of the struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okey, I did.
clientgenv2/source_generator.go
Outdated
func (g *StructGenerator) MergedStructSources(sources []*StructSource) []*StructSource { | ||
res := sources | ||
// remove pre-merged struct | ||
for _, preMergedTypeName := range g.preMergedStructSources { | ||
for i, source := range res { | ||
// when name is same, remove it | ||
if source.Name == preMergedTypeName.Name { | ||
res = append(res[:i], res[i+1:]...) | ||
break | ||
} | ||
} | ||
} | ||
|
||
// append post-merged struct | ||
res = append(res, g.postMergedStructSources...) | ||
|
||
return res | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion. I prefer this approach of creating a map first because it results in shallower nesting during lookups, making the code cleaner and easier to follow.
func (g *StructGenerator) MergedStructSources(sources []*StructSource) []*StructSource { | |
res := sources | |
// remove pre-merged struct | |
for _, preMergedTypeName := range g.preMergedStructSources { | |
for i, source := range res { | |
// when name is same, remove it | |
if source.Name == preMergedTypeName.Name { | |
res = append(res[:i], res[i+1:]...) | |
break | |
} | |
} | |
} | |
// append post-merged struct | |
res = append(res, g.postMergedStructSources...) | |
return res | |
} | |
func structSourcesMapByTypeName(sources []*StructSource) map[string]*StructSource { | |
structSourceMapByTypeName := make(map[string]*StructSource) | |
for _, source := range sources { | |
structSourceMapTypeName[source.Name] = source | |
} | |
return structSourceMapByTypeName | |
} | |
func (g *StructGenerator) MergedStructSources(sources []*StructSource) []*StructSource { | |
preMargedStructSourceMapByTypeName := structSourcesMapByTypeName(g.preMergedStructSources) | |
res := sources | |
// remove pre-merged struct | |
for i, source := range res { | |
// when name is same, remove it | |
if _, ok := preMargedStructSourceMapByTypeName[source.Name]; ok { | |
res = append(res[:i], res[i+1:]...) | |
break | |
} | |
} | |
// append post-merged struct | |
res = append(res, g.postMergedStructSources...) | |
return res | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true. I use map to avoid double loop.
clientgenv2/source_generator.go
Outdated
preMergedStructSources := make([]*StructSource, 0) | ||
postMergedStructSources := make([]*StructSource, 0) | ||
|
||
for _, field := range responseFieldList { | ||
if field.IsFragmentSpread { | ||
preMergedStructSources = append(preMergedStructSources, &StructSource{ | ||
Name: field.FieldTypeString(), | ||
Type: field.ResponseFields.StructType(), | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preMergedStructSources := make([]*StructSource, 0) | |
postMergedStructSources := make([]*StructSource, 0) | |
for _, field := range responseFieldList { | |
if field.IsFragmentSpread { | |
preMergedStructSources = append(preMergedStructSources, &StructSource{ | |
Name: field.FieldTypeString(), | |
Type: field.ResponseFields.StructType(), | |
}) | |
} | |
} | |
preMergedStructSources := make([]*StructSource, 0) | |
for _, field := range responseFieldList { | |
if field.IsFragmentSpread { | |
preMergedStructSources = append(preMergedStructSources, &StructSource{ | |
Name: field.FieldTypeString(), | |
Type: field.ResponseFields.StructType(), | |
}) | |
} | |
} |
clientgenv2/source_generator.go
Outdated
} | ||
} | ||
|
||
currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, postMergedStructSources) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, postMergedStructSources) | |
currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, nil) |
@igaryo0506 LGTM! |
abstraction
Previously, when fragments and fields coexisted within a single field, the StructType function—used for generating Go structs from ResponseFields—attempted to handle them directly. This approach occasionally triggered a panic due to the presence of identical fields originating from both fragments and the field itself.
To resolve this, I have consolidated the fields during the conversion from AST objects to ResponseFields. This enhancement not only prevents the panic and ensures a more reliable result, but also maintains backward compatibility.
next to do
Looking ahead, we will also need to implement a GetFragment function.
related issue
#249