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

Improvement Autopatch (adding a body) #496

Merged
merged 3 commits into from
Jul 15, 2024

Conversation

ScriptType
Copy link

@ScriptType ScriptType commented Jul 6, 2024

Improve AutoPatch to generate optional schema for PATCH operations

Problem

The current AutoPatch functionality generates PATCH operations for resources with GET and PUT operations, but it doesn't create a specific type for the PATCH request that matches the PUT operation with all fields optional.

Solution

This PR modifies the AutoPatch function to create an optional schema for PATCH operations based on the PUT operation's schema.

  1. Creates a new makeOptionalSchema function that creates an optional version of a given schema.
  2. Modifies the PatchResource function to use this new optional schema for PATCH operations.
  3. Ensures all fields in the PATCH schema are nullable and optional.

Changes

  • Added makeOptionalSchema function in autopatch.go
  • Modified PatchResource function to use the new optional schema
  • Updated tests to verify the correctness of the generated PATCH schema

Testing

A new test function TestAutoPatchOptionalSchema has been added to verify that:

  • The PATCH operation is correctly generated
  • All fields from the original schema are present in the PATCH schema
  • All fields in the PATCH schema are optional and nullable
  • The generated PATCH operation works as expected

Its my First contribution in golang would be happy to hear from your review :).

Summary by CodeRabbit

  • New Features

    • Enhanced PATCH operation to support optional schema properties, making all fields nullable for greater flexibility.
  • Tests

    • Added comprehensive test cases to ensure the correct transformation of schemas to optional schemas, covering basic properties, complex schema constructs, and edge cases.

Copy link

coderabbitai bot commented Jul 6, 2024

Walkthrough

The updates enhance the PatchResource function in autopatch/autopatch.go by introducing makeOptionalSchema, which creates schemas with optional fields for more flexible PATCH operations. Corresponding tests in autopatch/autopatch_test.go ensure the correct transformation of schemas. Additional tests validate various schema scenarios, including nested schemas and different schema constructs.

Changes

File Change Summary
autopatch/autopatch.go Added makeOptionalSchema function and updated PatchResource to use optional schemas for PATCH operations.
autopatch/autopatch_test.go Added TestAutoPatchOptionalSchema to test the new optional schema. Included various tests for different schema scenarios.

Poem

In the code where schemas bloom,
Optional fields now make room,
For PATCH requests with ease and grace,
Flexibility in every case. 🌸
Tests ensure no fields resist,
As all turn optional, none are missed.
Hail the new schema's space,
Patched with elegance in this place. 🐇


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Outside diff range and nitpick comments (5)
autopatch/autopatch_test.go (3)

333-333: Consider renaming the test function for clarity.

The function name TestAutoPatchOptionalSchema is clear but could be more descriptive to indicate that it tests the PATCH operation generation.

- func TestAutoPatchOptionalSchema(t *testing.T) {
+ func TestGenerateOptionalPatchSchema(t *testing.T) {

336-341: Consider adding comments to the struct fields.

Adding comments to the TestModel struct fields can improve readability and understanding of the test data.

type TestModel struct {
	ID       string  `json:"id"`               // Unique identifier
	Name     string  `json:"name"`             // Name of the entity
	Age      int     `json:"age"`              // Age of the entity
	Optional *string `json:"optional,omitempty"` // Optional field
}

343-366: Consider adding error handling for the GET and PUT handlers.

The GET and PUT handlers in the test function do not handle errors, which might lead to confusion if the handlers fail. Adding error handling can improve the robustness of the test.

huma.Register(api, huma.Operation{
	OperationID: "get-test",
	Method:      http.MethodGet,
	Path:        "/test/{id}",
}, func(ctx context.Context, input *struct {
	ID string `path:"id"`
}) (*struct {
	Body *TestModel
}, error) {
	if input.ID == "" {
		return nil, fmt.Errorf("ID is required")
	}
	return &struct{ Body *TestModel }{&TestModel{}}, nil
})

huma.Register(api, huma.Operation{
	OperationID: "put-test",
	Method:      http.MethodPut,
	Path:        "/test/{id}",
}, func(ctx context.Context, input *struct {
	ID   string    `path:"id"`
	Body TestModel `json:"body"`
}) (*struct {
	Body *TestModel
}, error) {
	if input.ID == "" {
		return nil, fmt.Errorf("ID is required")
	}
	return &struct{ Body *TestModel }{&TestModel{}}, nil
})
autopatch/autopatch.go (2)

Line range hint 1-1:
Consider adding more comments to explain the logic.

Adding comments to explain the logic in the AutoPatch function can improve readability and maintainability.

// Iterate over paths to find resources with GET and PUT but no PATCH operations
for _, path := range oapi.Paths {
	// Check if both GET and PUT operations exist and PATCH operation is missing
	if path.Get != nil && path.Put != nil && path.Patch == nil {
		// Iterate over GET and PUT operations to check for autopatch metadata
		for _, op := range []*huma.Operation{path.Get, path.Put} {
			if op.Metadata != nil && op.Metadata["autopatch"] != nil {
				if b, ok := op.Metadata["autopatch"].(bool); ok && !b {
					// Skip if autopatch is explicitly disabled
					continue Outer
				}
			}
		}
		// Check if the PUT request body has a JSON schema
		body := path.Put.RequestBody
		if body != nil && body.Content != nil && body.Content["application/json"] != nil {
			ct := body.Content["application/json"]
			if ct.Schema != nil {
				s := ct.Schema
				if s.Ref != "" {
					// Dereference if needed
					s = registry.SchemaFromRef(s.Ref)
				}
				// Only generate PATCH operations for objects
				if s.Type == "object" {
					PatchResource(api, path)
				}
			}
		}
	}
}

175-175: Consider adding comments to explain the handler logic.

Adding comments to explain the logic in the handler function can improve readability and maintainability.

// Manually register the handler with the router.
adapter.Handle(op, func(ctx huma.Context) {
	// Read the request body
	patchData, err := io.ReadAll(ctx.BodyReader())
	if err != nil {
		huma.WriteErr(api, ctx, http.StatusBadRequest, "Unable to read request body", err)
		return
	}

	// Perform the GET request to fetch the original resource
	origReq, err := http.NewRequest(http.MethodGet, ctx.URL().Path, nil)
	if err != nil {
		huma.WriteErr(api, ctx, http.StatusInternalServerError, "Unable to get resource", err)
		return
	}

	// Copy incoming headers
	ctx.EachHeader(func(k, v string) {
		if k == "Accept" || k == "Accept-Encoding" {
			// Force these headers to be JSON for easier handling
			return
		}
		if k == "If-Match" || k == "If-None-Match" || k == "If-Modified-Since" || k == "If-Unmodified-Since" {
			// Ignore conditional request headers here
			return
		}
		if k == "Content-Type" || k == "Content-Length" {
			// GET request will be empty
			return
		}
		origReq.Header.Add(k, v)
	})

	// Accept JSON for the patches
	origReq.Header.Set("Accept", "application/json")
	origReq.Header.Set("Accept-Encoding", "")

	// Perform the GET request to fetch the original resource
	origWriter := httptest.NewRecorder()
	adapter.ServeHTTP(origWriter, origReq)

	if origWriter.Code >= 300 {
		// Handle error on the GET side
		for key, values := range origWriter.Header() {
			for _, value := range values {
				ctx.SetHeader(key, value)
			}
		}
		ctx.SetStatus(origWriter.Code)
		io.Copy(ctx.BodyWriter(), origWriter.Body)
		return
	}

	// Apply the patch
	var patched []byte
	switch strings.Split(ctx.Header("Content-Type"), ";")[0] {
	case "application/json-patch+json":
		patch, err := jsonpatch.DecodePatch(patchData)
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to decode JSON Patch", err)
			return
		}
		patched, err = patch.Apply(origWriter.Body.Bytes())
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to apply patch", err)
			return
		}
	case "application/merge-patch+json", "application/json", "":
		// Assume most cases are merge-patch
		patched, err = jsonpatch.MergePatch(origWriter.Body.Bytes(), patchData)
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to apply patch", err)
			return
		}
	case "application/merge-patch+shorthand":
		// Load the original data as a base
		var tmp any
		if err := json.Unmarshal(origWriter.Body.Bytes(), &tmp); err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to apply patch", err)
			return
		}

		// Unmarshal the shorthand over the existing data
		tmp, err = shorthand.Unmarshal(string(patchData), shorthand.ParseOptions{
			ForceStringKeys: true,
		}, tmp)
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to apply patch", err)
			return
		}

		// Marshal the updated data back for the PUT request
		patched, err = json.Marshal(tmp)
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnprocessableEntity, "Unable to apply patch", err)
			return
		}
	default:
		// Unsupported content type
		huma.WriteErr(api, ctx, http.StatusUnsupportedMediaType, "Content type should be one of application/merge-patch+json or application/json-patch+json")
		return
	}

	if jsonpatch.Equal(patched, origWriter.Body.Bytes()) {
		ctx.SetStatus(http.StatusNotModified)
		return
	}

	// Write the updated data back to the server
	putReq, err := http.NewRequest(http.MethodPut, ctx.URL().Path, bytes.NewReader(patched))
	if err != nil {
		huma.WriteErr(api, ctx, http.StatusInternalServerError, "Unable to put modified resource", err)
		return
	}
	ctx.EachHeader(func(k, v string) {
		if k == "Content-Type" || k == "Content-Length" {
			return
		}
		putReq.Header.Add(k, v)
	})

	putReq.Header.Set("Content-Type", "application/json")

	// Set conditional headers if available
	h := putReq.Header
	if h.Get("If-Match") == "" && h.Get("If-None-Match") == "" && h.Get("If-Unmodified-Since") == "" && h.Get("If-Modified-Since") == "" {
		oh := origWriter.Header()
		if etag := oh.Get("ETag"); etag != "" {
			h.Set("If-Match", etag)
		} else if modified := oh.Get("Last-Modified"); modified != "" {
			h.Set("If-Unmodified-Since", modified)
		}
	}

	// Perform the PUT request to update the resource
	putWriter := httptest.NewRecorder()
	adapter.ServeHTTP(putWriter, putReq)
	for key, values := range putWriter.Header() {
		for _, value := range values {
			ctx.SetHeader(key, value)
		}
	}
	ctx.SetStatus(putWriter.Code)
	io.Copy(ctx.BodyWriter(), putWriter.Body)
})
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 4a07c36 and 8889fb4.

Files selected for processing (2)
  • autopatch/autopatch.go (5 hunks)
  • autopatch/autopatch_test.go (1 hunks)
Additional comments not posted (10)
autopatch/autopatch_test.go (2)

368-391: Ensure that all fields are nullable in the PATCH schema.

The loop verifies that all fields in the PATCH schema are nullable. This is a crucial part of the test, and it looks good.


393-399: The PATCH operation test looks good.

The test for the generated PATCH operation is well-structured and ensures the PATCH operation works as expected.

autopatch/autopatch.go (8)

133-137: Ensure the schema is correctly dereferenced.

The code correctly dereferences the schema if needed.


139-141: The optional schema creation looks good.

The code correctly creates an optional version of the PUT schema.


157-160: Ensure consistency in content types.

The code ensures that the optional schema is used for both application/merge-patch+json and application/merge-patch+shorthand content types.


325-328: Handle nil schema case properly.

The function correctly handles the case where the input schema is nil.


330-361: Ensure all schema attributes are copied.

The function correctly copies all relevant attributes from the original schema to the optional schema.


363-371: Ensure recursive handling of items and properties.

The function correctly handles nested schemas by recursively calling makeOptionalSchema for items and properties.


374-397: Ensure recursive handling of OneOf, AnyOf, AllOf, and Not.

The function correctly handles OneOf, AnyOf, AllOf, and Not by recursively calling makeOptionalSchema.


399-403: Ensure all properties are made optional.

The function correctly sets the Required field to nil to make all properties optional.

removed nullable as it didnt play nicely in the api generation

generating it like this
                  type: string,null
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 8889fb4 and 9ef5fc2.

Files selected for processing (2)
  • autopatch/autopatch.go (5 hunks)
  • autopatch/autopatch_test.go (1 hunks)
Files skipped from review as they are similar to previous changes (2)
  • autopatch/autopatch.go
  • autopatch/autopatch_test.go

Copy link

codecov bot commented Jul 10, 2024

Codecov Report

Attention: Patch coverage is 93.65079% with 4 lines in your changes missing coverage. Please review.

Project coverage is 92.84%. Comparing base (4a07c36) to head (1b351ea).
Report is 4 commits behind head on main.

Files Patch % Lines
autopatch/autopatch.go 93.65% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #496   +/-   ##
=======================================
  Coverage   92.83%   92.84%           
=======================================
  Files          22       22           
  Lines        3784     3842   +58     
=======================================
+ Hits         3513     3567   +54     
- Misses        227      230    +3     
- Partials       44       45    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@danielgtaylor
Copy link
Owner

@ScriptType this looks great, but can we get the test coverage up a little before merging?

@ScriptType
Copy link
Author

Will do it over the weekend :). Glad that you like it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 9ef5fc2 and 1b351ea.

Files selected for processing (1)
  • autopatch/autopatch_test.go (1 hunks)
Additional comments not posted (7)
autopatch/autopatch_test.go (7)

333-349: Test for basic properties in makeOptionalSchema looks good.

The test comprehensively checks that the properties are correctly handled and made optional.


351-364: Test for AnyOf in makeOptionalSchema looks good.

The test correctly verifies that the AnyOf schemas are handled as expected.


366-381: Test for AllOf in makeOptionalSchema looks good.

The test correctly verifies that the AllOf schemas are handled as expected.


383-394: Test for Not in makeOptionalSchema looks good.

The test correctly verifies that the Not schemas are handled as expected.


396-398: Test for nil input in makeOptionalSchema looks good.

The test correctly verifies that the function handles nil input as expected.


400-419: Test for nested schemas in makeOptionalSchema looks good.

The test correctly verifies that nested schemas are handled as expected.


Line range hint 1-332:
Test for AutoPatch with optional schema looks good.

The test comprehensively covers various scenarios to ensure the PATCH operation works as expected with the optional schema.

Copy link
Owner

@danielgtaylor danielgtaylor left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@danielgtaylor danielgtaylor merged commit 77ffe69 into danielgtaylor:main Jul 15, 2024
3 checks 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