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

feat(fields): read/set all the fields from the given struct #11

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

bkrukowski
Copy link
Member

@bkrukowski bkrukowski commented Oct 1, 2024

Summary by Sourcery

Implement a new feature for iterating over struct fields with options for setting and getting values, including type conversion and recursive handling. Refactor existing code to utilize a new method for reducing value chains, and add extensive tests to ensure functionality. Update build dependencies to include a new testing library.

New Features:

  • Introduce a new feature to iterate over fields in a struct, allowing for setting and getting field values with options for type conversion and recursive traversal.

Enhancements:

  • Refactor the Set function to use a new ReducedValueOf method for handling value chains, simplifying the logic for setting fields in structs.

Build:

  • Add a new dependency for testing purposes: github.com/davecgh/go-spew v1.1.1.

Tests:

  • Add comprehensive tests for the new field iteration feature, covering various scenarios including setting values, handling unexported fields, and recursive struct traversal.

Copy link

sourcery-ai bot commented Oct 1, 2024

Reviewer's Guide by Sourcery

This pull request implements a new feature for iterating over struct fields with options for setting and getting values, including type conversion and recursive handling. It also refactors existing code to use a new helper function for reflect value processing and adds comprehensive tests for the new functionality.

No diagrams generated as the changes look simple and do not need a visual representation.

File-Level Changes

Change Details Files
Implement new field iteration functionality
  • Add new Iterate function to traverse struct fields
  • Implement options for setting and getting field values
  • Add support for type conversion and recursive struct traversal
  • Implement PrefillNilStructs option to automatically initialize nil struct pointers
fields/iterate.go
fields/iterate_test.go
fields/examples_test.go
Refactor existing reflect value processing
  • Add ReducedValueOf function to simplify handling of pointer and interface chains
  • Update Set function to use the new ReducedValueOf helper
internal/reflect/common.go
internal/reflect/get_set.go
Add comprehensive tests for new field iteration functionality
  • Add tests for setting values on struct fields
  • Add tests for handling unexported fields
  • Add tests for recursive struct traversal
  • Add examples demonstrating various use cases of the new functionality
fields/iterate_test.go
fields/examples_test.go
Implement Path type for field traversal
  • Add Path type to represent the path of fields during traversal
  • Implement methods for working with Path, such as Names, HasSuffix, and EqualNames
fields/path.go
Add new dependency for testing
  • Add github.com/davecgh/go-spew dependency for improved test output
go.mod

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @bkrukowski - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 2 issues found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

}
}

func Iterate(strct any, opts ...Option) (err error) {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the 'Iterate' function into smaller, more focused functions.

While the Iterate function provides flexibility, it can be simplified to improve readability and maintainability. Consider refactoring it into smaller, more focused functions:

  1. Extract the setter logic into a separate function:
func applySetterAndPrefill(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (any, bool) {
    // Call setter
    if cfg.setter != nil {
        if newVal, ok := cfg.setter(append(path, f), value); ok {
            return newVal, true
        }
    }

    // Set pointer to a zero-value struct
    if cfg.prefillNilStructs &&
        f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct &&
        reflect.ValueOf(value).IsZero() {
        return reflect.New(f.Type.Elem()).Interface(), true
    }

    return value, false
}
  1. Extract the recursive iteration logic:
func recursiveIterate(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (any, bool, error) {
    if cfg.recursive && isStructOrNonNilStructPtr(f.Type, value) {
        original := value
        if err := iterate(&value, cfg, append(path, f)); err != nil {
            return nil, false, fmt.Errorf("%s: %w", f.Name, err)
        }
        if !reflect.DeepEqual(original, value) {
            return value, true, nil
        }
    }
    return value, false, nil
}
  1. Simplify the main iterate function:
func iterate(strct any, cfg *config, path []reflect.StructField) error {
    var finalErr error

    fn := func(f reflect.StructField, value any) (any, bool) {
        if finalErr != nil {
            return nil, false
        }

        if cfg.getter != nil {
            cfg.getter(append(path, f), value)
        }

        value, set := applySetterAndPrefill(f, value, cfg, path)
        if set {
            return value, true
        }

        value, set, err := recursiveIterate(f, value, cfg, path)
        if err != nil {
            finalErr = err
            return nil, false
        }
        return value, set
    }

    err := intReflect.IterateFields(
        strct,
        fn,
        cfg.convertTypes,
        cfg.convertToPtr,
    )

    if err != nil {
        return err
    }

    return finalErr
}

These changes maintain the existing functionality while making the code more modular and easier to understand. Each function now has a more focused responsibility, which should improve maintainability and make future modifications easier.

}

//nolint:gocognit,goconst,lll
func TestIterate(t *testing.T) {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider restructuring the TestIterate function to improve readability and maintainability.

The TestIterate function is indeed complex and could benefit from restructuring. While it's comprehensive, its current structure makes it difficult to read and maintain. We suggest using Go's subtests feature more effectively to group related scenarios. This approach will improve readability and maintainability without losing any functionality.

Here's an example of how you could restructure a portion of the test:

func TestIterate(t *testing.T) {
    t.Parallel()

    t.Run("Setter", func(t *testing.T) {
        t.Run("Person", func(t *testing.T) {
            t.Run("OK", func(t *testing.T) {
                runScenario(t, Person{}, Person{Name: "Jane"}, "", fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) {
                    return "Jane", true
                }))
            })

            t.Run("OK (convert types)", func(t *testing.T) {
                runScenario(t, Person{}, Person{Name: "Jane"}, "", 
                    fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) {
                        return CustomString("Jane"), true
                    }),
                    fields.ConvertTypes(true),
                )
            })

            t.Run("Error (convert types)", func(t *testing.T) {
                runScenario(t, Person{}, Person{},
                    "fields.Iterate: IterateFields: *interface {}: IterateFields: fields_test.Person: field 0 \"Name\": value of type fields_test.CustomString is not assignable to type string",
                    fields.Setter(func(_ fields.Path, value any) (_ any, set bool) {
                        return CustomString("Jane"), true
                    }),
                )
            })
        })

        // More grouped scenarios...
    })
}

func runScenario(t *testing.T, input, expectedOutput interface{}, expectedError string, options ...fields.Option) {
    t.Helper()
    err := fields.Iterate(&input, options...)

    if expectedError != "" {
        require.EqualError(t, err, expectedError)
        return
    }

    require.NoError(t, err)
    assert.Equal(t, expectedOutput, input)
}

This structure:

  1. Groups related scenarios under descriptive subtest names.
  2. Uses a helper function runScenario to reduce repetition.
  3. Improves readability by clearly separating different test cases.
  4. Maintains the relationship between tests while reducing nesting.

Apply this pattern throughout the test function to significantly improve its structure and readability.

@coveralls
Copy link

Pull Request Test Coverage Report for Build 11134063116

Details

  • 180 of 226 (79.65%) changed or added relevant lines in 5 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-5.0%) to 92.317%

Changes Missing Coverage Covered Lines Changed/Added Lines %
fields/iterate.go 103 105 98.1%
fields/path.go 9 27 33.33%
internal/reflect/iterate.go 46 72 63.89%
Totals Coverage Status
Change from base Build 11133485394: -5.0%
Covered Lines: 745
Relevant Lines: 807

💛 - Coveralls

@coveralls
Copy link

coveralls commented Oct 1, 2024

Pull Request Test Coverage Report for Build 11135243522

Details

  • 236 of 304 (77.63%) changed or added relevant lines in 6 files are covered.
  • 5 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-7.4%) to 89.932%

Changes Missing Coverage Covered Lines Changed/Added Lines %
fields/path.go 9 27 33.33%
internal/reflect/iterate.go 96 146 65.75%
Files with Coverage Reduction New Missed Lines %
caller/internal/caller/call.go 5 81.08%
Totals Coverage Status
Change from base Build 11133485394: -7.4%
Covered Lines: 795
Relevant Lines: 884

💛 - Coveralls

@bkrukowski
Copy link
Member Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @bkrukowski - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

}

//nolint:wrapcheck
func iterate(strct any, cfg *config, path []reflect.StructField) error {
Copy link

Choose a reason for hiding this comment

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

suggestion: Add more comments to explain the logic flow

The iterate function and its helper trySetValue handle complex logic. Consider adding more inline comments to explain the flow and decision points, especially in the trySetValue function. This will improve maintainability and make it easier for other developers to understand and modify the code if needed.

}

//nolint:gocognit,goconst,lll
func TestIterate(t *testing.T) {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the TestIterate function to improve its structure and readability.

The TestIterate function is comprehensive but overly complex. To improve readability and maintainability without sacrificing test coverage, consider the following refactoring:

  1. Group test cases by functionality and create separate sub-tests for each group.
  2. Extract the test case definitions into separate variables or functions.
  3. Create helper functions for common operations.

Here's an example of how you could start refactoring:

func TestIterate(t *testing.T) {
    t.Parallel()

    t.Run("Setter", func(t *testing.T) {
        t.Run("Simple cases", testSimpleSetterCases)
        t.Run("Nested structures", testNestedSetterCases)
        t.Run("Embedded structures", testEmbeddedSetterCases)
        t.Run("Edge cases", testSetterEdgeCases)
    })
}

func testSimpleSetterCases(t *testing.T) {
    cases := []struct {
        name    string
        options []fields.Option
        input   any
        output  any
        error   string
    }{
        // Define simple setter test cases here
    }
    runSetterTestCases(t, cases)
}

func testNestedSetterCases(t *testing.T) {
    // Similar structure to testSimpleSetterCases, but for nested structures
}

func testEmbeddedSetterCases(t *testing.T) {
    // Similar structure to testSimpleSetterCases, but for embedded structures
}

func testSetterEdgeCases(t *testing.T) {
    // Similar structure to testSimpleSetterCases, but for edge cases
}

func runSetterTestCases(t *testing.T, cases []struct {
    name    string
    options []fields.Option
    input   any
    output  any
    error   string
}) {
    for _, c := range cases {
        c := c
        t.Run(c.name, func(t *testing.T) {
            t.Parallel()
            input := c.input
            err := fields.Iterate(&input, c.options...)

            if c.error != "" {
                require.EqualError(t, err, c.error)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, c.output, input)
        })
    }
}

This refactoring maintains test coverage while improving readability and maintainability. It groups related test cases, reduces nesting, and makes it easier to add or modify test cases in the future.

@bkrukowski
Copy link
Member Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @bkrukowski - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding benchmarks to measure performance, especially for deeply nested structures.
  • Consider introducing more specific error types for better error differentiation.
Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

fields/any.go Show resolved Hide resolved
}

//nolint:wrapcheck
func iterate(strct any, cfg *config, path []reflect.StructField) error {
Copy link

Choose a reason for hiding this comment

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

suggestion: Add explanatory comments to iterate function

This function seems to handle multiple complex operations. Consider adding comments to explain the different steps it's performing. This will improve readability and help future maintainers understand its logic.

// iterate traverses the given struct recursively, applying validation rules
// defined in the config. It uses reflection to inspect struct fields and
// their tags, building a path of nested fields as it goes.
func iterate(strct any, cfg *config, path []reflect.StructField) error {

}

//nolint:gocognit,goconst,lll
func TestIterate(t *testing.T) {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the TestIterate function to use table-driven tests with subtests.

While the TestIterate function provides comprehensive coverage, its nested structure and numerous scenarios can be challenging to maintain and understand. Consider refactoring to use table-driven tests with subtests. This approach will improve readability and maintainability while retaining thorough test coverage. Here's an example of how you could restructure the test:

func TestIterate(t *testing.T) {
    t.Parallel()

    tests := []struct {
        name    string
        options []fields.Option
        input   any
        output  any
        error   string
    }{
        {
            name: "Person OK",
            options: []fields.Option{
                fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) {
                    return "Jane", true
                }),
            },
            input: Person{},
            output: Person{
                Name: "Jane",
            },
            error: "",
        },
        // Add other test cases here...
    }

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()

            input := tt.input
            err := fields.Iterate(&input, tt.options...)

            if tt.error != "" {
                require.EqualError(t, err, tt.error)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tt.output, input)
        })
    }
}

This structure:

  1. Keeps all test cases in a single slice, making it easy to add or modify cases.
  2. Uses subtests to maintain separation between test cases.
  3. Reduces nesting and improves readability.
  4. Maintains parallel execution of tests.

Consider breaking down very large test cases into separate functions if they require additional setup or assertions. This approach will make your tests more modular and easier to maintain while preserving the comprehensive coverage you've established.

Copy link

sonarqubecloud bot commented Oct 1, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
3.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarCloud

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