njnygaard
njnygaard

Reputation: 75

Golang handlers handling different types

These are AppHandlers from a pattern I found online while researching gorilla/mux. They part of a struct that satisfies http.Handler. If you notice, the following two blocks are exactly the same. Effectively, they could be passed the 'variant' ("flow" or "process") as a string.

func CreateFlow(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {

    highest, code, err := a.Create("flow", r)
    if code != 200 || err != nil {
        return code, err
    }

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(struct {
        Highest int `json:"id"`
    }{highest})
    w.Header().Set("Content-Type", "application/json")
    w.Write(b.Bytes())
    return 200, nil
}

func CreateProcess(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {

    highest, code, err := a.Create("process", r)
    if code != 200 || err != nil {
        return code, err
    }

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(struct {
        Highest int `json:"id"`
    }{highest})
    w.Header().Set("Content-Type", "application/json")
    w.Write(b.Bytes())
    return 200, nil
}

However, the following two blocks not only need the string, but they need a variable of the associated type ("Flow" and "Process") to successfully Unmarshal the hit I get from ElasticSearch. Other than that, they are Identical code.

func GetFlow(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {

    hit, code, err := a.GetByID("flow", mux.Vars(r)["id"], r)
    if code != 200 {
        return code, err
    }

    var flow Flow

    err = json.Unmarshal(*hit.Source, &flow)
    if err != nil {
        return 500, err
    }

    flow.ESID = hit.Id

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(flow)
    w.Header().Set("Content-Type", "application/json")
    w.Write(b.Bytes())
    return 200, nil
}

func GetProcess(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {

    hit, code, err := a.GetByID("process", mux.Vars(r)["id"], r)
    if code != 200 {
        return code, err
    }

    var process Process

    err = json.Unmarshal(*hit.Source, &process)
    if err != nil {
        return 500, err
    }

    process.ESID = hit.Id

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(process)
    w.Header().Set("Content-Type", "application/json")
    w.Write(b.Bytes())
    return 200, nil
}

I am not sure how to generalize this behavior in golang when there is a declared type involved. These handlers are all in the same package too, as I think that they are all accomplishing a similar task. I am very clearly repeating myself in code but I need advice on how I can improve. I've gone past "a little copying is better than a little dependency." but I am afraid because "reflection is never clear".

Here is an example of the declaration in main using one of these functions.

api.Handle("/flow/{id:[0-9]+}", handlers.AppHandler{context, handlers.GetFlow}).Methods("GET")

Upvotes: 2

Views: 2081

Answers (2)

Mustansir Zia
Mustansir Zia

Reputation: 1004

Alright, I propose a solution that will give you the maximum code reuse and minimum code copying. This, in my opinion, is by far the most generic solution. We will also take into account the answer given by https://stackoverflow.com/users/7426/adrian to complete the solution. You only have to define a single function which will be a higher order function CreateHandler which will return a function of the following signature:
func(*AppContext, http.ResponseWriter, http.Request) (int, error).

This signature is the actual signature of the handler that is to be used as a mux end point. The solution involves defining a Handler type which is a struct having three fields:

handlerType: Think of it as an enum having either a value of "CREATE" or "GET". This will decide which among the two blocks of code that you pasted in your question should we use.

handlerActionName: This will tell the "CREATE" or "GET" which Elastible to use. Value should either be "flow" or "process".

elastible: This will the Interface type Elastible that will have the SetESID function. We will use this to send our Flow or Process types to our Handler. Thus both Flow and Process should satisfy our interface. This will make the solution even more generic and will only calling handler.elastible.SetESID() and we will have inserted the ESID irrespective of that fact the underlying type in 'elastible' can either be 'Flow' or a 'Process'

I also define a sendResponse(response interface{}) function that we will resuse to send the response. It acquires w http.ResponseWriter using closure. response can thus be anything, a

struct {
    Highest int `json:"id"`
}{highest} 

or a Flow or a Process. This will make this function generic too.

The complete solution would now be.

// This is the type that will be used to build our handlers.
type Handler struct {
    handlerType       string    // Can be "CREATE" or "GET"
    handlerActionName string    // Can be "flow" or "process"
    elastible         Elastible // Can be *Flow or *Process
}

// Your ESID Type.
type ESIDType string

// Solution proposed by https://stackoverflow.com/users/7426/adrian.
type Elastible interface {
    SetESID(id ESIDType)
}

// Make the Flow and Process pointers implement the Elastible interface.
func (flow *Flow) SetESID(id ESIDType) {
    flow.ESID = id
}

func (process *Process) SetESID(id ESIDType) {
    process.ESID = id
}

// Create a Higher Order Function which will return the actual handler.
func CreateHandler(handler Handler) func(*AppContext, http.ResponseWriter, http.Request) (int, error) {

    return func(a *AppContext, w http.ResponseWriter, r http.Request) (int, error) {

        // Define a sendResponse function so that we may not need to copy paste it later.
        // It captures w using closure and takes an interface argument that we use to call .Encode() with.

        sendResponse := func(response interface{}) (int, error) {
            b := new(bytes.Buffer)
            json.NewEncoder(b).Encode(response)
            w.Header().Set("Content-Type", "application/json")
            w.Write(b.Bytes())
            return 200, nil
        }

        // Define these variables beforehand since we'll be using them
        // in both the if and else block. Not necessary really.
        var code int
        var err error

        // Check the handlerType. Is it create or get?
        if handler.handlerType == "CREATE" {
            var highest int

            // Creates the thing using handler.handlerActionName which may be "flow" or "process"
            highest, code, err = a.Create(handler.handlerActionName, r)
            if code != 200 || err != nil {
                return code, err
            }

            // Send the response using the above defined function and return.
            return sendResponse(struct {
                Highest int `json:"id"`
            }{highest})

        } else {

            // This is GET handlerType.
            var hit HitType

            // Get the hit using again the handler.handlerActionName which may be "flow" or "process"
            hit, code, err = a.GetByID(handler.handlerActionName, mux.Vars(r)["id"], r)
            if code != 200 || err != nil {
                return code, err
            }

            // Do the un-marshalling.
            err = json.Unmarshal(*hit.Source, ob)
            if err != nil {
                return 500, err
            }

            // We have set the handler.elastible to be an interface type
            // which will have the SetESID function that will set the ESID in the
            // underlying type that will be passed on runtime.
            // So the ESID will be set for both the Flow and the Process types.
            // This interface idea was given inside an earlier answer by
            // https://stackoverflow.com/users/7426/adrian

            handler.elastible.SetESID(hit.id)
            return sendResponse(handler.elastible)
        }
    }
}

And you would setup your mux end points using the following code.

    // This was your first function. "CreateFlow"
    api.Handle("/createFlow/{id:[0-9]+}", handlers.AppHandler{
        context, CreateHandler(Handler{
            elastible:         &Flow{},
            handlerActionName: "flow",
            handlerType:       "CREATE",
        }),
    }).Methods("GET")

    // This was your second function. "CreateProcess"
    api.Handle("/createProcess/{id:[0-9]+}", handlers.AppHandler{
        context, CreateHandler(Handler{
            elastible:         &Process{},
            handlerActionName: "process",
            handlerType:       "CREATE",
        }),
    }).Methods("GET")

    // This was your third function. "GetFlow"
    api.Handle("/getFlow/{id:[0-9]+}", handlers.AppHandler{
        context, CreateHandler(Handler{
            elastible:         &Flow{},
            handlerActionName: "flow",
            handlerType:       "GET",
        }),
    }).Methods("GET")

    // This was your fourth function. "GetProcess"
    api.Handle("/getProcess/{id:[0-9]+}", handlers.AppHandler{
        context, CreateHandler(Handler{
            elastible:         &Process{},
            handlerActionName: "process",
            handlerType:       "GET",
        }),
    }).Methods("GET")


Hope it helps!

Upvotes: 2

Adrian
Adrian

Reputation: 46432

You can do it by passing in an exemplar of the necessary type, the same way that Unmarshal does it:

func GetFlow(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {
    return GetThing(a,w,r,"flow",new(Flow))
}

func GetProcess(a *AppContext, w http.ResponseWriter, r *http.Request) (int, error) {
    return GetThing(a,w,r,"process",new(Process))
}

func GetThing(a *AppContext, w http.ResponseWriter, r *http.Request, t string, ob Elastible{}) (int, error) {
    hit, code, err := a.GetByID(t, mux.Vars(r)["id"], r)
    if code != 200 {
        return code, err
    }

    err = json.Unmarshal(*hit.Source, ob)
    if err != nil {
        return 500, err
    }

    ob.SetESID(hit.Id)

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(ob)
    w.Header().Set("Content-Type", "application/json")
    w.Write(b.Bytes())
    return 200, nil
}

type Elastible interface {
    SetESID(id ESIDType)    // whatever type ESID is, not clear from example
}

func (f *Flow) SetESID(id ESIDType) {
    f.ESID = id
}

This code is untested (because I don't have your struct defs or other dependent code) but I hope it gets the idea across.

Upvotes: 6

Related Questions