Saturday, February 25, 2017

Developing a Go code generator for GoKit using Obeo and Sirius (Part 1)

So I have been reading in Go Programming Blueprints about GoKit and it seems that the process of writing much of the wrapper logic for GoKit is repetitive process.  In fact, the book itself points out that this may be an area where code generation may help.

In previous jobs, i have used the modeling tools from eclipse to abstract out program design for set of microservices into a graphical DSL using Papyrus and Acceleo,   Now it looks like Sirius offers the ability to go beyond the limitations of Papyrus to create different types of diagrams that can help to capture model information more easily.

In my previous work, I have found that the first step to creating a Model for such a an effort is to examine the existing source code and extracting the elements for a "conceptual model".  (A conceptual model captures the important concepts and relationships in some domain.)

First we analyze the service.go file from Chapter 10.

As a first step,  I highlight the code to identify potential candidates for inclusion in the model.

NOTE: for code generation projects, we are concerned with the items that convey "instance" data and not the boilerplate code that will repeat based on the instance data.  Our goal is to extract out the information that will enable us to generate this code.

In the code below, there is great deal of boilerplate code that really is subject to error prone copy paste.  What we want to know is what is the smallest set of information that would enable use to generate this code.

I have highlighted the areas of what I call "instance data"

package vault

import (
                "encoding/json"
                "errors"
                "net/http"

                "github.com/go-kit/kit/endpoint"
                "golang.org/x/crypto/bcrypt"
                "golang.org/x/net/context"
)

// Service provides password hashing capabilities.
type Service interface {
                Hash(ctx context.Context, password string) (string, error)
                Validate(ctx context.Context, password, hash string) (bool, error)
}

// NewService makes a new Service.
func NewService() Service {
                return vaultService{}
}

type vaultService struct{}

func (vaultService) Hash(ctx context.Context, password string) (string, error) {
                hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
                if err != nil {
                                return "", err
                }
                return string(hash), nil
}

func (vaultService) Validate(ctx context.Context, password, hash string) (bool, error) {
                err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
                if err != nil {
                                return false, nil
                }
                return true, nil
}

type hashRequest struct {
                Password string `json:"password"`
}
type hashResponse struct {
                Hash string `json:"hash"`
                Err  string `json:"err,omitempty"`
}
type validateRequest struct {
                Password string `json:"password"`
                Hash     string `json:"hash"`
}
type validateResponse struct {
                Valid bool   `json:"valid"`
                Err   string `json:"err,omitempty"`
}



func decodeHashRequest(ctx context.Context, r *http.Request) (interface{}, error) {
                var req hashRequest
                err := json.NewDecoder(r.Body).Decode(&req)
                if err != nil {
                                return nil, err
                }
                return req, nil
}

func decodeValidateRequest(ctx context.Context, r *http.Request) (interface{}, error) {
                var req validateRequest
                err := json.NewDecoder(r.Body).Decode(&req)
                if err != nil {
                                return nil, err
                }
                return req, nil
}

func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
                return json.NewEncoder(w).Encode(response)
}

func MakeHashEndpoint(srv Service) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                                req := request.(hashRequest)
                                v, err := srv.Hash(ctx, req.Password)
                                if err != nil {
                                                return hashResponse{v, err.Error()}, nil
                                }
                                return hashResponse{v, ""}, nil
                }
}

func MakeValidateEndpoint(srv Service) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                                req := request.(validateRequest)
                                v, err := srv.Validate(ctx, req.Password, req.Hash)
                                if err != nil {
                                                return validateResponse{false, err.Error()}, nil
                                }
                                return validateResponse{v, ""}, nil
                }
}

// Endpoints represents all endpoints for the vault Service.
type Endpoints struct {
                HashEndpoint     endpoint.Endpoint
                ValidateEndpoint endpoint.Endpoint
}

// Hash uses the HashEndpoint to hash a password.
func (e Endpoints) Hash(ctx context.Context, password string) (string, error) {
                req := hashRequest{Password: password}
                resp, err := e.HashEndpoint(ctx, req)
                if err != nil {
                                return "", err
                }
                hashResp := resp.(hashResponse)
                if hashResp.Err != "" {
                                return "", errors.New(hashResp.Err)
                }
                return hashResp.Hash, nil
}

// Validate uses the ValidateEndpoint to validate a password and hash pair.
func (e Endpoints) Validate(ctx context.Context, password,
                hash string) (bool, error) {
                req := validateRequest{Password: password, Hash: hash}
                resp, err := e.ValidateEndpoint(ctx, req)
                if err != nil {
                                return false, err
                }
                validateResp := resp.(validateResponse)
                if validateResp.Err != "" {
                                return false, errors.New(validateResp.Err)
                }
                return validateResp.Valid, nil
}

No comments: