Compare commits

..

3 Commits

Author SHA1 Message Date
3d9de744e7
ongoing 2024-03-09 11:04:07 +00:00
dcadfe73e8
squash 2024-03-03 11:04:46 +00:00
61b1891472
OpenAPI RESTful API code generator
This patch includes and uses the oapi-codegen tool to auto-generate go
boilerplate code, based on the OpenAPI spec.
2024-02-25 11:15:55 +00:00
4 changed files with 58 additions and 78 deletions

View File

@ -1,34 +0,0 @@
package api
import (
"database/sql"
"log"
"reflect"
)
func convert[T any](rows *sql.Rows) []T {
var ans []T
for rows.Next() {
var r T
s := reflect.ValueOf(&r).Elem()
log.Println(s)
numCols := s.NumField()
columns := make([]interface{}, numCols)
log.Println(columns)
for i := 0; i < numCols; i++ {
field := s.Field(i)
log.Println(field)
columns[i] = field.Addr().Interface()
}
log.Println(columns)
if err := rows.Scan(columns...); err != nil {
log.Fatal(err)
}
ans = append(ans, r)
}
return ans
}

View File

@ -1,29 +1,31 @@
package api package api
//go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=api.cfg.yaml ../../docs/openapi.yaml //go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=api.cfg.yaml ../../docs/openapi.yaml
import ( import (
"database/sql" "database/sql"
"errors"
"log" "log"
"net/http" "net/http"
"reflect"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
openapi_types "github.com/oapi-codegen/runtime/types"
) )
type ServerImpl struct { type PersonalFinanceImpl struct {
db *sql.DB db *sql.DB
} }
func (*ServerImpl) GetBanks(ctx echo.Context) error { func (*PersonalFinanceImpl) GetBanks(ctx echo.Context) error {
return echo.NewHTTPError(http.StatusNotImplemented) return echo.NewHTTPError(http.StatusNotImplemented)
} }
func (*ServerImpl) GetBanksById(ctx echo.Context, bankId int64) error { func (*PersonalFinanceImpl) GetBanksById(ctx echo.Context, bankId int64) error {
return echo.NewHTTPError(http.StatusNotImplemented) return echo.NewHTTPError(http.StatusNotImplemented)
} }
func (pf *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error { func (pf *PersonalFinanceImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error {
rows, err := pf.db.Query("SELECT t.category, t.date, t.description, t.id, tc.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id") rows, err := pf.db.Query("SELECT t.id, t.date, t.description, t.amount, tc.category FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -31,6 +33,39 @@ func (pf *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsPa
return ctx.JSON(http.StatusOK, convert[Transaction](rows)) return ctx.JSON(http.StatusOK, convert[Transaction](rows))
} }
func (*ServerImpl) GetTransactionsById(ctx echo.Context, transactionId int64) error { func (*PersonalFinanceImpl) GetTransactionsById(ctx echo.Context, transactionId int64) error {
return echo.NewHTTPError(http.StatusNotImplemented) return echo.NewHTTPError(http.StatusNotImplemented)
} }
func convert[T any](rows *sql.Rows) []T {
var ans []T
for rows.Next() {
var r T
s := reflect.ValueOf(&r).Elem()
log.Println(s)
numCols := s.NumField()
columns := make([]interface{}, numCols)
log.Println(columns)
for i := 0; i < numCols; i++ {
field := s.Field(i)
columns[i] = field.Addr().Interface()
}
if err := rows.Scan(columns...); err != nil {
log.Fatal(err)
}
ans = append(ans, r)
}
return ans
}
func (d *openapi_types.Date) Scan(value interface{}) error {
if value == nil {
return errors.New("Null date")
}
// if bv, err := driver.
}

View File

@ -1,10 +1,8 @@
package api package api
import ( import (
"log"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
@ -19,7 +17,7 @@ func TestGetTransactions(t *testing.T) {
} }
e := echo.New() e := echo.New()
handlers := ServerImpl{db} handlers := PersonalFinanceImpl{db}
RegisterHandlers(e, &handlers) RegisterHandlers(e, &handlers)
t.Run("when successful", func(t *testing.T) { t.Run("when successful", func(t *testing.T) {
@ -27,10 +25,10 @@ func TestGetTransactions(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/transactions", nil) req := httptest.NewRequest(http.MethodGet, "/transactions", nil)
date := time.Now() date := time.Now()
rows := mock.NewRows([]string{"category", "date", "description", "id", "amount"}). rows := mock.NewRows([]string{"id", "date", "description", "amount", "category"}).
AddRow(nil, date, "#1", 1, 1000). AddRow(1, date, "#1", 1000, nil).
AddRow("expense", date, "#2", 2, -1000) AddRow(2, date, "#2", -1000, "expense")
mock.ExpectQuery("SELECT t.category, t.date, t.description, t.id, tc.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id").WillReturnRows(rows) mock.ExpectQuery("SELECT t.id, t.date, t.description, t.amount, tc.category FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id").WillReturnRows(rows)
ctx := e.NewContext(req, rec) ctx := e.NewContext(req, rec)
err := handlers.GetTransactions(ctx, GetTransactionsParams{}) err := handlers.GetTransactions(ctx, GetTransactionsParams{})
@ -43,23 +41,21 @@ func TestGetTransactions(t *testing.T) {
expected := `[ expected := `[
{ {
"date":"` + date.Format(time.DateOnly) + `",
"description": "#1",
"id":1, "id":1,
"value":1000 "date":"` + date.Format(time.DateOnly) + `",
description": "#1",
"amount":1000
}, },
{ {
"category": "expense",
"date":"` + date.Format(time.DateOnly) + `",
"description": "#2",
"id":2, "id":2,
"value":-1000 "date":"` + date.Format(time.DateOnly) + `",
} description": "#2",
"amount":-1000,
"category": "expense"
},
]` ]`
expected = strings.Join(strings.Fields(expected), "") if ret := rec.Body.String(); ret != expected {
log.Println(expected) t.Error(ret)
if ret := strings.TrimRight(rec.Body.String(), "\n"); ret != expected {
t.Error(ret, expected)
} }
}) })
} }

View File

@ -7,13 +7,11 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"strings" "strings"
"time"
"github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -31,25 +29,10 @@ type Bank struct {
// Banks defines model for Banks. // Banks defines model for Banks.
type Banks = []Bank type Banks = []Bank
// Manually added date time that implements the Scanner interface
type ScannableDate struct {
openapi_types.Date
}
func (d *ScannableDate) Scan(src any) error {
switch s := src.(type) {
case time.Time:
d.Time = s
return nil
}
return errors.New("column is not convertible to date")
}
// Transaction defines model for Transaction. // Transaction defines model for Transaction.
type Transaction struct { type Transaction struct {
Category *string `json:"category,omitempty"` Category *string `json:"category,omitempty"`
Date ScannableDate `json:"date"` Date openapi_types.Date `json:"date"`
Description string `json:"description"` Description string `json:"description"`
Id int64 `json:"id"` Id int64 `json:"id"`
Value float32 `json:"value"` Value float32 `json:"value"`