Skip to content

Commit 4977a7a

Browse files
Merge pull request #770 from danielgtaylor/group-convenience
feat: better group+convenience function interop
2 parents ad6a04a + b899b51 commit 4977a7a

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

group.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,34 @@ func (g *Group) UseSimpleModifier(modifier func(o *Operation)) {
127127
// operation, in the order they were added. This is useful for modifying an
128128
// operation before it is registered with the router or OpenAPI document.
129129
func (g *Group) ModifyOperation(op *Operation, next func(*Operation)) {
130-
chain := next
130+
chain := func(op *Operation) {
131+
// If this came from unmodified convenience functions, we may need to
132+
// regenerate the operation ID and summary as they are based on things
133+
// like the path which may have changed.
134+
if op.Metadata != nil {
135+
// Copy so we don't modify the original map.
136+
meta := make(map[string]any, len(op.Metadata))
137+
for k, v := range op.Metadata {
138+
meta[k] = v
139+
}
140+
op.Metadata = meta
141+
142+
// If the conveniences are set, we need to regenerate the operation ID and
143+
// summary based on the new path. We also update the metadata to reflect
144+
// the new generated operation ID and summary so groups of groups can
145+
// continue to modify them as needed.
146+
if op.Metadata["_convenience_id"] == op.OperationID {
147+
op.OperationID = GenerateOperationID(op.Method, op.Path, op.Metadata["_convenience_id_out"])
148+
op.Metadata["_convenience_id"] = op.OperationID
149+
}
150+
if op.Metadata["_convenience_summary"] == op.Summary {
151+
op.Summary = GenerateSummary(op.Method, op.Path, op.Metadata["_convenience_summary_out"])
152+
op.Metadata["_convenience_summary"] = op.Summary
153+
}
154+
}
155+
// Call the final handler.
156+
next(op)
157+
}
131158
for i := len(g.modifiers) - 1; i >= 0; i-- {
132159
// Use an inline func to provide a closure around the index & chain.
133160
func(i int, n func(*Operation)) {

group_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,70 @@ func TestGroupMultiPrefix(t *testing.T) {
5151
assert.Equal(t, http.StatusNoContent, resp.Result().StatusCode)
5252
}
5353

54+
func TestGroupConvenienceEquivalency(t *testing.T) {
55+
_, api := humatest.New(t)
56+
57+
// Register a normal route via convenience function.
58+
huma.Get(api, "/v1/users", func(ctx context.Context, input *struct{}) (*struct{}, error) {
59+
return nil, nil
60+
})
61+
62+
// Upgrade to groups and expect the same behavior.
63+
grp2 := huma.NewGroup(api, "/v2")
64+
65+
huma.Get(grp2, "/users", func(ctx context.Context, input *struct{}) (*struct{}, error) {
66+
return nil, nil
67+
})
68+
69+
// Ensure convenience overrides still work.
70+
grp3 := huma.NewGroup(api, "/v3")
71+
huma.Get(grp3, "/users", func(ctx context.Context, input *struct{}) (*struct{}, error) {
72+
return nil, nil
73+
}, func(o *huma.Operation) {
74+
o.OperationID = "custom-id"
75+
o.Summary = "Custom summary"
76+
})
77+
78+
// Ensure group overrides still work.
79+
grp4 := huma.NewGroup(api, "/v4")
80+
grp4.UseModifier(func(op *huma.Operation, next func(*huma.Operation)) {
81+
op.OperationID = "custom-id"
82+
op.Summary = "Custom summary"
83+
next(op)
84+
})
85+
huma.Get(grp4, "/users", func(ctx context.Context, input *struct{}) (*struct{}, error) {
86+
return nil, nil
87+
})
88+
89+
// Groups of groups should continue to work as well, including both groups in
90+
// the generated ID/summary.
91+
grp5 := huma.NewGroup(api, "/v5")
92+
grp6 := huma.NewGroup(grp5, "/users")
93+
huma.Get(grp6, "/", func(ctx context.Context, input *struct{}) (*struct{}, error) {
94+
return nil, nil
95+
})
96+
97+
oapi := api.OpenAPI()
98+
99+
assert.NotNil(t, oapi.Paths["/v1/users"])
100+
assert.NotNil(t, oapi.Paths["/v2/users"])
101+
assert.NotNil(t, oapi.Paths["/v3/users"])
102+
assert.NotNil(t, oapi.Paths["/v4/users"])
103+
assert.NotNil(t, oapi.Paths["/v5/users/"])
104+
105+
assert.Equal(t, "get-v1-users", oapi.Paths["/v1/users"].Get.OperationID)
106+
assert.Equal(t, "get-v2-users", oapi.Paths["/v2/users"].Get.OperationID)
107+
assert.Equal(t, "custom-id", oapi.Paths["/v3/users"].Get.OperationID)
108+
assert.Equal(t, "custom-id", oapi.Paths["/v4/users"].Get.OperationID)
109+
assert.Equal(t, "get-v5-users", oapi.Paths["/v5/users/"].Get.OperationID)
110+
111+
assert.Equal(t, "Get v1 users", oapi.Paths["/v1/users"].Get.Summary)
112+
assert.Equal(t, "Get v2 users", oapi.Paths["/v2/users"].Get.Summary)
113+
assert.Equal(t, "Custom summary", oapi.Paths["/v3/users"].Get.Summary)
114+
assert.Equal(t, "Custom summary", oapi.Paths["/v4/users"].Get.Summary)
115+
assert.Equal(t, "Get v5 users", oapi.Paths["/v5/users/"].Get.Summary)
116+
}
117+
54118
func TestGroupCustomizations(t *testing.T) {
55119
_, api := humatest.New(t)
56120

huma.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2025,15 +2025,27 @@ func OperationTags(tags ...string) func(o *Operation) {
20252025

20262026
func convenience[I, O any](api API, method, path string, handler func(context.Context, *I) (*O, error), operationHandlers ...func(o *Operation)) {
20272027
var o *O
2028+
opID := GenerateOperationID(method, path, o)
2029+
opSummary := GenerateSummary(method, path, o)
20282030
operation := Operation{
2029-
OperationID: GenerateOperationID(method, path, o),
2030-
Summary: GenerateSummary(method, path, o),
2031+
OperationID: opID,
2032+
Summary: opSummary,
20312033
Method: method,
20322034
Path: path,
2035+
Metadata: map[string]any{},
20332036
}
20342037
for _, oh := range operationHandlers {
20352038
oh(&operation)
20362039
}
2040+
// If not modified, hint that these were auto-generated!
2041+
if operation.OperationID == opID {
2042+
operation.Metadata["_convenience_id"] = opID
2043+
operation.Metadata["_convenience_id_out"] = o
2044+
}
2045+
if operation.Summary == opSummary {
2046+
operation.Metadata["_convenience_summary"] = opSummary
2047+
operation.Metadata["_convenience_summary_out"] = o
2048+
}
20372049
Register(api, operation, handler)
20382050
}
20392051

0 commit comments

Comments
 (0)