Search code examples
gogo-gin

Bind Query inside middleware


I'm trying to write a "Binder" middleware that will validate any request query using a struct type with gin bindings/validators

So for example, let's say I have an endpoint group called /api/subject which requires the query string to have a subject code and an ID that will be validated using the following struct (called entity.Subject):

type Subject struct {
    Code string `binding:"required,alphanum"`
    ID   string `binding:"required,alphanum,len=4"`
}

That's just one example, but I'd like to be able to pass any struct type to this middleware, because I'd like to access the query data on future handlers without worrying about query validation.

So I tried something like this:

func Binder(t reflect.Type) gin.HandlerFunc {
    return func(c *gin.Context) {
        obj := reflect.New(t).Elem().Interface()
        if err := c.BindQuery(&obj); err != nil {
            c.AbortWithStatus(http.StatusBadRequest)
            return
        }

        c.Set(t.Name(), obj)
    }
}

And added this middleware like so:

apiGroup := router.Group("/api")
{
    // other subgroups/endpoints
    // ...
    subjectGroup := apiGroup.Group("/subject", middleware.Binder(reflect.TypeOf(entity.Subject{})))
}

And later on, in another handler function, let's say GetSubject, I want to access the subject data passed by doing c.MustGet("Subject").(entity.Subject)

But this isn't working =(, when I print obj, it's just an empty interface, how would I do this?


Solution

  • I managed to do something similar!

    I created the following middleware

    var allowedTypes = []binding.Binding{
        binding.Query,
        binding.Form,
        binding.FormPost,
        binding.FormMultipart,
    }
    
    func Bind(name string, data interface{}, bindingType binding.Binding) gin.HandlerFunc {
        return func(ctx *gin.Context) {
            ok := false
            for _, b := range allowedTypes {
                if b == bindingType {
                    ok = true
                }
            }
            if !ok {
                ctx.AbortWithError(
                    http.StatusInternalServerError,
                    fmt.Errorf("Bind function only allows %v\n", allowedTypes),
                )
            }
            _ = ctx.MustBindWith(data, bindingType)
            ctx.Set(name, data)
        }
    }
    

    Remember to pass a pointer to your desired type in the call, like so:

    router.GET("/something", Bind("Object", &myObject, binding.Query))

    I restricted only to a few binding types because they allow ShouldBind to be called multiple times, whereas JSON, XML and others consume the Request body.

    This way you can pass multiple Bind middlewares and if the validation fails it automatically aborts with http.StatusBadRequest