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
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 93 additions & 16 deletions autopatch/autopatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@
}
}

// Get the schema from the PUT operation
putSchema := put.RequestBody.Content["application/json"].Schema
if putSchema.Ref != "" {
putSchema = oapi.Components.Schemas.SchemaFromRef(putSchema.Ref)
}

// Create an optional version of the PUT schema
optionalPutSchema := makeOptionalSchema(putSchema)

// Manually register the operation so it shows up in the generated OpenAPI.
op := &huma.Operation{
OperationID: "patch-" + name,
Expand All @@ -145,18 +154,10 @@
Required: true,
Content: map[string]*huma.MediaType{
"application/merge-patch+json": {
Schema: &huma.Schema{
Type: huma.TypeObject,
Description: "JSON merge patch object, see PUT operation for schema. All fields are optional.",
AdditionalProperties: true,
},
Schema: optionalPutSchema,
},
"application/merge-patch+shorthand": {
Schema: &huma.Schema{
Type: huma.TypeObject,
Description: "Shorthand merge patch object, see PUT operation for schema. All fields are optional.",
AdditionalProperties: true,
},
Schema: optionalPutSchema,
},
"application/json-patch+json": {
Schema: jsonPatchSchema,
Expand All @@ -171,9 +172,7 @@
}
oapi.AddOperation(op)

// Manually register the handler with the router. This bypasses the normal
// Huma API since this is easier and we are just calling the other pre-existing
// operations.
// Manually register the handler with the router.
adapter := api.Adapter()
adapter.Handle(op, func(ctx huma.Context) {
patchData, err := io.ReadAll(ctx.BodyReader())
Expand Down Expand Up @@ -208,9 +207,8 @@
})

// Accept JSON for the patches.
// TODO: could we accept other stuff here...?
ctx.SetHeader("Accept", "application/json")
ctx.SetHeader("Accept-Encoding", "")
origReq.Header.Set("Accept", "application/json")
origReq.Header.Set("Accept-Encoding", "")

origWriter := httptest.NewRecorder()
adapter.ServeHTTP(origWriter, origReq)
Expand Down Expand Up @@ -323,3 +321,82 @@
io.Copy(ctx.BodyWriter(), putWriter.Body)
})
}

func makeOptionalSchema(s *huma.Schema) *huma.Schema {
if s == nil {
return nil

Check warning on line 327 in autopatch/autopatch.go

View check run for this annotation

Codecov / codecov/patch

autopatch/autopatch.go#L327

Added line #L327 was not covered by tests
}

optionalSchema := &huma.Schema{
Type: s.Type,
Title: s.Title,
Description: s.Description,
Format: s.Format,
ContentEncoding: s.ContentEncoding,
Default: s.Default,
Examples: s.Examples,
AdditionalProperties: s.AdditionalProperties,
Enum: s.Enum,
Minimum: s.Minimum,
ExclusiveMinimum: s.ExclusiveMinimum,
Maximum: s.Maximum,
ExclusiveMaximum: s.ExclusiveMaximum,
MultipleOf: s.MultipleOf,
MinLength: s.MinLength,
MaxLength: s.MaxLength,
Pattern: s.Pattern,
PatternDescription: s.PatternDescription,
MinItems: s.MinItems,
MaxItems: s.MaxItems,
UniqueItems: s.UniqueItems,
MinProperties: s.MinProperties,
MaxProperties: s.MaxProperties,
ReadOnly: s.ReadOnly,
WriteOnly: s.WriteOnly,
Deprecated: s.Deprecated,
Extensions: s.Extensions,
DependentRequired: s.DependentRequired,
Discriminator: s.Discriminator,
}

if s.Items != nil {
optionalSchema.Items = makeOptionalSchema(s.Items)
}

if s.Properties != nil {
optionalSchema.Properties = make(map[string]*huma.Schema)
for k, v := range s.Properties {
optionalSchema.Properties[k] = makeOptionalSchema(v)
}
}

if s.OneOf != nil {
optionalSchema.OneOf = make([]*huma.Schema, len(s.OneOf))
for i, schema := range s.OneOf {
optionalSchema.OneOf[i] = makeOptionalSchema(schema)

Check warning on line 376 in autopatch/autopatch.go

View check run for this annotation

Codecov / codecov/patch

autopatch/autopatch.go#L374-L376

Added lines #L374 - L376 were not covered by tests
}
}

if s.AnyOf != nil {
optionalSchema.AnyOf = make([]*huma.Schema, len(s.AnyOf))
for i, schema := range s.AnyOf {
optionalSchema.AnyOf[i] = makeOptionalSchema(schema)

Check warning on line 383 in autopatch/autopatch.go

View check run for this annotation

Codecov / codecov/patch

autopatch/autopatch.go#L381-L383

Added lines #L381 - L383 were not covered by tests
}
}

if s.AllOf != nil {
optionalSchema.AllOf = make([]*huma.Schema, len(s.AllOf))
for i, schema := range s.AllOf {
optionalSchema.AllOf[i] = makeOptionalSchema(schema)

Check warning on line 390 in autopatch/autopatch.go

View check run for this annotation

Codecov / codecov/patch

autopatch/autopatch.go#L388-L390

Added lines #L388 - L390 were not covered by tests
}
}

if s.Not != nil {
optionalSchema.Not = makeOptionalSchema(s.Not)

Check warning on line 395 in autopatch/autopatch.go

View check run for this annotation

Codecov / codecov/patch

autopatch/autopatch.go#L395

Added line #L395 was not covered by tests
}

// Make all properties optional
optionalSchema.Required = nil

return optionalSchema
}
62 changes: 62 additions & 0 deletions autopatch/autopatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,65 @@ func TestDeprecatedPatch(t *testing.T) {

assert.True(t, api.OpenAPI().Paths["/things/{thing-id}"].Patch.Deprecated)
}
func TestAutoPatchOptionalSchema(t *testing.T) {
_, api := humatest.New(t)

type TestModel struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Optional *string `json:"optional,omitempty"`
}

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) {
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) {
return &struct{ Body *TestModel }{&TestModel{}}, nil
})

AutoPatch(api)

// Check if PATCH operation was generated
patchOp := api.OpenAPI().Paths["/test/{id}"].Patch
assert.NotNil(t, patchOp, "PATCH operation should be generated")

// Check if the generated PATCH operation has the correct schema
patchSchema := patchOp.RequestBody.Content["application/merge-patch+json"].Schema
assert.NotNil(t, patchSchema, "PATCH schema should be present")

// Verify that all fields in the schema are optional
assert.Empty(t, patchSchema.Required, "All fields should be optional in PATCH schema")

// Check if all fields from the original schema are present
properties := patchSchema.Properties
assert.Contains(t, properties, "id")
assert.Contains(t, properties, "name")
assert.Contains(t, properties, "age")
assert.Contains(t, properties, "optional")

// Test the generated PATCH operation
w := api.Patch("/test/123",
"Content-Type: application/merge-patch+json",
strings.NewReader(`{"name": "New Name", "age": 30}`),
)
assert.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
Loading