Skip to content

Commit

Permalink
Merge pull request #496 from ScriptType/Autopatch-Body-optional
Browse files Browse the repository at this point in the history
Improvement Autopatch (adding a body)
  • Loading branch information
danielgtaylor authored Jul 15, 2024
2 parents 6ff17c6 + 1b351ea commit 77ffe69
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 16 deletions.
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 @@ func PatchResource(api huma.API, path *huma.PathItem) {
}
}

// 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 @@ func PatchResource(api huma.API, path *huma.PathItem) {
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 @@ func PatchResource(api huma.API, path *huma.PathItem) {
}
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 @@ func PatchResource(api huma.API, path *huma.PathItem) {
})

// 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 @@ func PatchResource(api huma.API, path *huma.PathItem) {
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)
}
}

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

0 comments on commit 77ffe69

Please sign in to comment.