Skip to content

REST API code generation for Go

Marcelo Cantos edited this page Jan 18, 2019 · 20 revisions

From a testability perspective, here’s a somewhat radical idea around fully encapsulating the business logic from both its dependants and its dependencies. Consider the following Sysl design:

Backend1:
    GET /txs/{crn<:string}:
        return <: set of Tx

Backend2:
    FetchTrans(custid<:string):
        return <: set of Tx

TransactionService:
    GET /transactions/{crn<:string}:
        Backend1 <- GET /tx
        Backend2 <- FetchTrans
        return <: set of Transaction

Clients

The generated interfaces for Backend[12] and TransactionService would be fairly straightforward, something like:

type Backend1 interface {
    GetTxs(ctx context.Context, crn string) ([]Tx, error)
}

type Backend2 interface {
    FetchTrans(ctx context.Context, custid string) ([]Tx, error)
}

type TransactionService interface {
    GetTransactions(ctx context.Context, crn string) ([]Transaction, error)
}

Server

The TransactionService, however, would have an additional interface, the purpose of which is to define the protocol between the front-end service that implements the endpoint and the implementation of the endpoint:

type TransactionServiceServer interface {
    GetTransactions(
        ctx context.Context,
        crn string,
        Backend1GetTxs func(ctx context.Context, crn string) ([]Tx, error),
        Backend2FetchTrans func(ctx context.Context, custid string) ([]Tx, error),
    ) ([]Transaction, error)
}

Generating the clients

Client-side logic will usually be generated:

type backend1Client struct {
    client *http.Client
}

func NewBackend1Client(client *http.Client) Backend1 {
    return &backend1Client{client}
}

func (c *backend1Client) GetTxs(ctx context.Context, crn string) (result []Tx, err error) {
    err = sysl.Get(ctx, c.client, &result, "/txs/"+url.PathEscape(crn))
    return
}

Wiring up the server

The generated frontend TransactionService logic will instantiate code-generated Backend[12] clients and pass their bounds methods to the delegated TransactionService interface. Here’s an oversimplified hypothetical generated API frontend:

func HandleTransactionService(
    r chi.Router,
    ts TransactionServiceProtocol,
    backend1 Backend1,
    backend2 Backend2,
) {
    r.Get("/transactions/{crn}", func(w http.ResponseWriter, r *http.Request) {
        crn := chi.URLParam(r, "crn")
        requestID := sysl.RequestID(r)
        err, result := ts.GetTransactions(
            r.Context(),
            crn,
            func(ctx context.Context, crn string) ([]Tx, error) {
                if !sysl.RequestIDMatches(r, requestID) {
                    return nil, err
                }
                return backend1.GetTxs(ctx, crn)
            }),
            func(ctx context.Context, custid string) ([]Tx, error) {
                if !sysl.RequestIDMatches(r, requestID) {
                    return nil, err
                }
                return backend2.GetTxs(ctx, custid)
            }),
            backend2.FetchTrans,
        )
        if err != nil {
            http.Error(w, http.StatusText(500) + ": " + err.Error(), 500)
        } else {
            json.NewEncoder(w).Encode(result)
        }
    })
}

Developer does their bit

With all the machinery in place, the developer can now implement the core business logic.

type TransactionServiceServerImpl struct {}

func (*TransactionServiceServerImpl) GetTransactions(
    ctx context.Context,
    crn string,
    Backend1GetTxs func(ctx context.Context, crn string) ([]Tx, error),
    Backend2FetchTrans func(ctx context.Context, custid string) ([]Tx, error),
) ([]Transaction, error) {
    ctx, cancel := context.WithCancel(ctx)

    var b []Tx
    async := sysl.Async(func() (err error) {
        b, err = Backend2FechTrans(ctx, "crn:"+crn)
        return
    })

    a, err := Backend1GetTxs(ctx, crn)
    if err != nil {
        cancel()
        return nil, err
    }

    result := make([]Transaction, 0, len(a))
    seen := map[string]struct{}

    for _, tx := range a {
        result = append(result, TxToTransaction(tx))
        seen[tx.TxID] = struct{}{}
    }

    if err := async.Wait(); err != nil {
        return nil, err
    }
    for _, tx := range b {
        if _, found := seen[tx.TxID]; !found {
            result = append(result, TxToTransaction(tx))
        }
    }

    return result, nil
}

func TxToTransaction(tx Tx) Transaction {
    ⋮
}
Clone this wiki locally