Skip to content
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

Merged
merged 17 commits into from
Dec 23, 2024

Conversation

igaryo0506
Copy link
Contributor

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

@Yamashou
Copy link
Owner

It would be great if you could add a simple example in the example directory to verify that the current changes are working correctly.

Copy link
Owner

@Yamashou Yamashou left a 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!

for _, field := range rs {
if field.IsFragmentSpread {
fragmentChildrenFields = append(fragmentChildrenFields, field.ResponseFields...)
removeTypeNames = append(removeTypeNames, field.FieldTypeString())
Copy link
Owner

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.

Copy link
Contributor Author

@igaryo0506 igaryo0506 Dec 19, 2024

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

for _, preMergedTypeName := range preMergedTypeNames {
for i, source := range r.StructSources {
if source.Name == preMergedTypeName {
r.StructSources = append(r.StructSources[:i], r.StructSources[i+1:]...)
Copy link
Owner

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.

Copy link
Contributor Author

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

@@ -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()
Copy link
Owner

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.

}

// MergeFragmentFields returns merged ResponseFieldList, post-merged ResponseFieldList, and remove type names
func (rs ResponseFieldList) MergeFragmentFields() (ResponseFieldList, map[string]*ResponseFieldList, []string) {
Copy link
Owner

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.

@igaryo0506
Copy link
Contributor Author

Maybe It will be confusing for you, I used StructGenerator and simplified the logic.
And I make it possible to handle same field name recursively.

@igaryo0506 igaryo0506 requested a review from Yamashou December 19, 2024 13:55
Comment on lines 219 to 227
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
}
}
}
Copy link
Owner

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:

Suggested change
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)

Copy link
Contributor Author

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!

@igaryo0506
Copy link
Contributor Author

I did

  • using MergeStructSource
  • fix bug of merging
  • changed to better query

}
// if there is no same name field, append it
if !sameNameFieldFlag {
targetFields = append(targetFields, sourceField)
Copy link
Owner

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okey, I fixed it

for _, sourceField := range sourceFields {
sameNameFieldFlag := false
for _, targetField := range targetFields {
if sourceField.Name == targetField.Name && !targetField.ResponseFields.IsBasicType() {
Copy link
Owner

@Yamashou Yamashou Dec 21, 2024

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.

Copy link
Contributor Author

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.

@@ -76,6 +69,97 @@ func (rs ResponseFieldList) IsStructType() bool {
return len(rs) > 0 && !rs.IsFragment()
}

type StructGenerator struct {
currentResponseFieldList *ResponseFieldList
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion

Suggested change
currentResponseFieldList *ResponseFieldList
currentResponseFieldList ResponseFieldList

Comment on lines +217 to +225
// 子フィールドに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()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

for _, field := range targetFieldsMap {
res = append(res, field)
}
res = res.SortByName()
Copy link
Contributor Author

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.

}
}

func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged, postMerged *[]*StructSource) ResponseFieldList {
Copy link
Owner

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.

Suggested change
func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged, postMerged *[]*StructSource) ResponseFieldList {
func mergeFieldsRecursively(targetFields ResponseFieldList, sourceFields ResponseFieldList, preMerged []*StructSource) (ResponseFieldList, []*StructSource, []*StructSource) {

Copy link
Contributor Author

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.

Comment on lines 89 to 91
currentResponseFieldList ResponseFieldList
preMergedStructSources []*StructSource
postMergedStructSources []*StructSource
Copy link
Owner

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okey, I did.

Comment on lines 158 to 175
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
}
Copy link
Owner

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.

Suggested change
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
}

Copy link
Contributor Author

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.

Comment on lines 105 to 115
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(),
})
}
}
Copy link
Owner

@Yamashou Yamashou Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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(),
})
}
}

}
}

currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, postMergedStructSources)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, postMergedStructSources)
currentFields, preMergedStructSources, postMergedStructSources = mergeFieldsRecursively(currentFields, fragmentChildrenFields, preMergedStructSources, nil)

@igaryo0506 igaryo0506 requested a review from Yamashou December 23, 2024 10:05
@Yamashou
Copy link
Owner

@igaryo0506 LGTM!

@Yamashou Yamashou merged commit 8fece25 into Yamashou:master Dec 23, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants