huma icon indicating copy to clipboard operation
huma copied to clipboard

Allow anonymous fields to be used as request input parameters

Open lsdch opened this issue 1 year ago • 2 comments

Hi! Would you consider adding support for anonymous fields in request input parameters ?

My use case is the implementation of generic operation handlers such as :

type InputUUID UUID

func (i InputUUID) Identifier() UUID {
	return i
}

func GetHandler[
	OperationInput GetInputInterface[Item, ID],
	Item any,
	ID any,
](
	find models.ItemFinder[ID, Item],
) func(context.Context, OperationInput) (*GetHandlerOutput[Item], error) {
  return func(ctx context.Context, input OperationInput) (*GetHandlerOutput[Item], error) {
      item, err := find(input.DB(), input.Identifier())
      return &GetHandlerOutput[Item]{Body: item}, err
  }
}

router.Register(accountAPI, "GetPendingUserRequest",
          huma.Operation{
	          Path:        "/pending/{uuid}",
	          Method:      http.MethodGet,
	          Summary:     "Get pending user request",
          }, GetHandler[*struct {
	          resolvers.AccessRestricted[resolvers.Admin]
	          InputUUID `path:"uuid" format:"uuid"` // Anonymous field parameter
          }](people.GetPendingUserRequest))

lsdch avatar Oct 18 '24 12:10 lsdch

@lsdch this example seems incomplete (I'm not sure what GetInputInterface is for example). Can you help me understand the advantage of this? Like what is the difference between what you propose and something like:

Identifier InputUUID `path:"uuid" format:"uuid"`

Then rather than input.Identifier() you would use input.Identifier. I'm guessing the difference has to do with that interface type, but what is the reason for trying to do it this way?

danielgtaylor avatar Oct 18 '24 16:10 danielgtaylor

Hey, sorry about the late response !

So I have a generic GetHandler function that returns a Huma handler to fetch an item using an identifier + a function that handles the DB call (in this example people.GetPendingUserRequest). I use it to limit the amount of boilerplate for this kind of operations and lean towards more declarative code.

The challenge is that item identifiers may have different type (e.g. UUID, string) and different input source (e.g. {uuid}, {code}, {email}), so that I need to be able to configure that when declaring the endpoint. This is why I have these interfaces that are indeed missing from my example:

type IdentifierInput[T any] interface {
	Identifier() T
}
type GetInputInterface[Item any, ID any] interface {
	IdentifierInput[ID] // The identifier of the item to get
        // ... some resolvers to handle things like access control
}

Because of how Go is designed, I could not access the identifier from inside GetHandler if I declare it as a named field. However using interfaces + embedded fields I can just get the identifer directly from the operation input.

func GetHandler[
	OperationInput GetInputInterface[Item, ID],
	Item any,
	ID any,
](
	find models.ItemFinder[ID, Item],
) func(context.Context, OperationInput) (*GetHandlerOutput[Item], error) {
        // Huma handler
	return func(ctx context.Context, input OperationInput) (*GetHandlerOutput[Item], error) {
		item, err := find(input.DB(), input.Identifier()) // getting the identifier directly from the input
		// ... boilerplate
		return &GetHandlerOutput[Item]{Body: item}, err
	}
}

lsdch avatar Nov 05 '24 08:11 lsdch