-
Notifications
You must be signed in to change notification settings - Fork 42
REST API code generation for Go
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
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)
}
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)
}
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
}
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)
}
})
}
With all the machinery in place, the developer can now implement the core business logic.
type TransactionServiceServerImpl struct {}
// GetTransactions calls Backend1/2 and combines the results into a single set.
// Only one of each transaction will appear in the result. If either backend
// call fails, the other call is cancelled and the operation fails.
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 a []Tx
var aWaiter chan error
go func() {
var err error
a, err = Backend1GetTxs(ctx, crn)
aWaiter <- err
}()
var b []Tx
var bWaiter chan error
go func() {
var err error
b, err = Backend2FechTrans(ctx, "crn:"+crn)
bWaiter <- err
})
result := []Transaction{}
seen := map[string]struct{}
addTxes := func(err error, txes []Tx) error {
if err != nil {
cancel()
return err
}
for _, tx := range txes {
if _, found := seen[tx.TxID]; !found {
result = append(result, TxToTransaction(tx))
seen[tx.TxID] = struct{}{}
}
}
return nil
}
select {
case err := <-aWaiter:
if err := addTxes(err, a); err != nil {
return nil, err
}
if err := addTxes(<- bWaiter, b); err != nil {
return nil, err
}
case err := <-bWaiter:
if err := addTxes(err, b); err != nil {
return nil, err
}
if err := addTxes(<- aWaiter, a); err != nil {
return nil, err
}
}
return result, nil
}
func TxToTransaction(tx Tx) Transaction {
⋮
}