Repository: danielgtaylor/huma
Pull request #565 (2024-08-29 09:09:43)

Allow using pointers for non param fields in input structs
This PR allows using pointers fields in input structs, if they are not path/query/header/cookie parameters. This gives more flexibility to huma users, e.g. when defining resolvers

```go
type EndpointInput struct {
  Token string `header:"Authorization"`
  User *User
}

func (i *EndpointInput) Resolve(ctx huma.Context) []error {
  user, token_valid := ValidateToken(i.Token) // user is nil if token is missing or invalid
  i.User = user 
  return nil
}
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Introduced support for pointer types in input structures, enhancing the resolver mechanism.
	- Added a new test to validate the functionality of pointer handling in HTTP operations.

- **Bug Fixes**
	- Adjusted the control flow for pointer type checks to allow for additional logic before triggering a panic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Patch:
diff --git a/huma.go b/huma.go
index a5cafbb..940ebcc 100644
--- a/huma.go
+++ b/huma.go
@@ -97,15 +97,6 @@ func findParams(registry Registry, op *Operation, t reflect.Type) *findResult[*p
 			return nil
 		}
 
-		if f.Type.Kind() == reflect.Pointer {
-			// TODO: support pointers? The problem is that when we dynamically
-			// create an instance of the input struct the `params.Every(...)`
-			// call cannot set them as the value is `reflect.Invalid` unless
-			// dynamically allocated, but we don't know when to allocate until
-			// after the `Every` callback has run. Doable, but a bigger change.
-			panic("pointers are not supported for path/query/header parameters")
-		}
-
 		pfi := &paramFieldInfo{
 			Type: f.Type,
 		}
@@ -146,6 +137,15 @@ func findParams(registry Registry, op *Operation, t reflect.Type) *findResult[*p
 			return nil
 		}
 
+		if f.Type.Kind() == reflect.Pointer {
+			// TODO: support pointers? The problem is that when we dynamically
+			// create an instance of the input struct the `params.Every(...)`
+			// call cannot set them as the value is `reflect.Invalid` unless
+			// dynamically allocated, but we don't know when to allocate until
+			// after the `Every` callback has run. Doable, but a bigger change.
+			panic("pointers are not supported for path/query/header parameters")
+		}
+
 		pfi.Schema = SchemaFromField(registry, f, "")
 
 		var example any


Test patch:
diff --git a/huma_test.go b/huma_test.go
index 9c24812..621aac4 100644
--- a/huma_test.go
+++ b/huma_test.go
@@ -2059,6 +2059,38 @@ func TestResolverCompositionCalledOnce(t *testing.T) {
 	assert.Equal(t, http.StatusNoContent, w.Code, w.Body.String())
 }
 
+type ResolverWithPointer struct {
+	Ptr *string
+}
+
+func (r *ResolverWithPointer) Resolve(ctx huma.Context) []error {
+	r.Ptr = new(string)
+	*r.Ptr = "String"
+	return nil
+}
+
+func TestResolverWithPointer(t *testing.T) {
+	// Allow using pointers in input structs if they are not path/query/header/cookie parameters
+	r, app := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0"))
+	huma.Register(app, huma.Operation{
+		OperationID: "test",
+		Method:      http.MethodPut,
+		Path:        "/test",
+	}, func(ctx context.Context, input *struct {
+		ResolverWithPointer
+	}) (*struct{}, error) {
+		// Exactly one call should have been made to the resolver.
+		assert.Equal(t, "String", *input.Ptr)
+		return nil, nil
+	})
+
+	req, _ := http.NewRequest(http.MethodPut, "/test", strings.NewReader(`{}`))
+	req.Header.Set("Content-Type", "application/json")
+	w := httptest.NewRecorder()
+	r.ServeHTTP(w, req)
+	assert.Equal(t, http.StatusNoContent, w.Code, w.Body.String())
+}
+
 func TestParamPointerPanics(t *testing.T) {
 	// For now, we don't support these, so we panic rather than have subtle
 	// bugs that are hard to track down.