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 all 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
}

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)
}
}

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

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

// Make all properties optional
optionalSchema.Required = nil

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

assert.True(t, api.OpenAPI().Paths["/things/{thing-id}"].Patch.Deprecated)
}
func TestMakeOptionalSchemaBasicProperties(t *testing.T) {
originalSchema := &huma.Schema{
Type: "object",
Properties: map[string]*huma.Schema{
"id": {Type: "string"},
"name": {Type: "string"},
},
Required: []string{"id", "name"},
}

optionalSchema := makeOptionalSchema(originalSchema)

assert.Equal(t, "object", optionalSchema.Type)
assert.Contains(t, optionalSchema.Properties, "id")
assert.Contains(t, optionalSchema.Properties, "name")
assert.Empty(t, optionalSchema.Required)
}

func TestMakeOptionalSchemaAnyOf(t *testing.T) {
originalSchema := &huma.Schema{
AnyOf: []*huma.Schema{
{Type: "string"},
{Type: "number"},
},
}

optionalSchema := makeOptionalSchema(originalSchema)

assert.Len(t, optionalSchema.AnyOf, 2)
assert.Equal(t, "string", optionalSchema.AnyOf[0].Type)
assert.Equal(t, "number", optionalSchema.AnyOf[1].Type)
}

func TestMakeOptionalSchemaAllOf(t *testing.T) {
minLength := 1
maxLength := 100
originalSchema := &huma.Schema{
AllOf: []*huma.Schema{
{MinLength: &minLength},
{MaxLength: &maxLength},
},
}

optionalSchema := makeOptionalSchema(originalSchema)

assert.Len(t, optionalSchema.AllOf, 2)
assert.Equal(t, 1, *optionalSchema.AllOf[0].MinLength)
assert.Equal(t, 100, *optionalSchema.AllOf[1].MaxLength)
}

func TestMakeOptionalSchemaNot(t *testing.T) {
originalSchema := &huma.Schema{
Not: &huma.Schema{
Type: "null",
},
}

optionalSchema := makeOptionalSchema(originalSchema)

assert.NotNil(t, optionalSchema.Not)
assert.Equal(t, "null", optionalSchema.Not.Type)
}

func TestMakeOptionalSchemaNilInput(t *testing.T) {
assert.Nil(t, makeOptionalSchema(nil))
}

func TestMakeOptionalSchemaNestedSchemas(t *testing.T) {
nestedSchema := &huma.Schema{
Type: "object",
Properties: map[string]*huma.Schema{
"nested": {
Type: "object",
Properties: map[string]*huma.Schema{
"deeplyNested": {Type: "string"},
},
Required: []string{"deeplyNested"},
},
},
Required: []string{"nested"},
}

optionalNestedSchema := makeOptionalSchema(nestedSchema)

assert.Empty(t, optionalNestedSchema.Required)
assert.Empty(t, optionalNestedSchema.Properties["nested"].Required)
}
Loading