danielgtaylor / huma

Huma REST/HTTP API Framework for Golang with OpenAPI 3.1
https://huma.rocks/
MIT License
2.2k stars 153 forks source link

Allow anonymous fields to be used as request input parameters #617

Open lsdch opened 1 month ago

lsdch commented 1 month ago

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))
danielgtaylor commented 1 month ago

@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?

lsdch commented 3 weeks ago

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