Created a base to build on

This commit is contained in:
2025-12-22 21:19:31 +01:00
parent a1e33bd403
commit 47a9719eeb
20 changed files with 1590 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package api
import (
"context"
"completed/internal/db"
)
type MockQuerier struct {
CreateGameFunc func(ctx context.Context, arg db.CreateGameParams) (db.Game, error)
CreatePlatformFunc func(ctx context.Context, name string) (db.Platform, error)
CreateUserFunc func(ctx context.Context, arg db.CreateUserParams) (db.User, error)
DeleteGameFunc func(ctx context.Context, id int64) error
DeletePlatformFunc func(ctx context.Context, id int64) error
DeleteUserFunc func(ctx context.Context, id int64) error
GetGameFunc func(ctx context.Context, id int64) (db.Game, error)
GetUserFunc func(ctx context.Context, id int64) (db.User, error)
ListGamesFunc func(ctx context.Context) ([]db.Game, error)
ListPlatformsFunc func(ctx context.Context) ([]db.Platform, error)
ListUsersFunc func(ctx context.Context) ([]db.User, error)
UpdateGameFunc func(ctx context.Context, arg db.UpdateGameParams) (db.Game, error)
UpdatePlatformFunc func(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error)
}
func (m *MockQuerier) CreateGame(ctx context.Context, arg db.CreateGameParams) (db.Game, error) {
return m.CreateGameFunc(ctx, arg)
}
func (m *MockQuerier) CreatePlatform(ctx context.Context, name string) (db.Platform, error) {
return m.CreatePlatformFunc(ctx, name)
}
func (m *MockQuerier) CreateUser(ctx context.Context, arg db.CreateUserParams) (db.User, error) {
return m.CreateUserFunc(ctx, arg)
}
func (m *MockQuerier) DeleteGame(ctx context.Context, id int64) error {
return m.DeleteGameFunc(ctx, id)
}
func (m *MockQuerier) DeletePlatform(ctx context.Context, id int64) error {
return m.DeletePlatformFunc(ctx, id)
}
func (m *MockQuerier) DeleteUser(ctx context.Context, id int64) error {
return m.DeleteUserFunc(ctx, id)
}
func (m *MockQuerier) GetGame(ctx context.Context, id int64) (db.Game, error) {
return m.GetGameFunc(ctx, id)
}
func (m *MockQuerier) GetUser(ctx context.Context, id int64) (db.User, error) {
return m.GetUserFunc(ctx, id)
}
func (m *MockQuerier) ListGames(ctx context.Context) ([]db.Game, error) {
return m.ListGamesFunc(ctx)
}
func (m *MockQuerier) ListPlatforms(ctx context.Context) ([]db.Platform, error) {
return m.ListPlatformsFunc(ctx)
}
func (m *MockQuerier) ListUsers(ctx context.Context) ([]db.User, error) {
return m.ListUsersFunc(ctx)
}
func (m *MockQuerier) UpdateGame(ctx context.Context, arg db.UpdateGameParams) (db.Game, error) {
return m.UpdateGameFunc(ctx, arg)
}
func (m *MockQuerier) UpdatePlatform(ctx context.Context, arg db.UpdatePlatformParams) (db.Platform, error) {
return m.UpdatePlatformFunc(ctx, arg)
}

257
internal/api/routes.go Normal file
View File

@@ -0,0 +1,257 @@
package api
import (
"context"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/jackc/pgx/v5/pgtype"
"completed/internal/db"
)
func RegisterRoutes(api huma.API, queries db.Querier) {
// Root/Hello Route
huma.Register(api, huma.Operation{
OperationID: "get-hello",
Method: http.MethodGet,
Path: "/hello",
Summary: "Say Hello",
Description: "Returns a friendly hello message.",
}, func(ctx context.Context, input *struct{}) (*struct {
Body struct {
Message string `json:"message"`
}
}, error) {
resp := &struct {
Body struct {
Message string `json:"message"`
}
}{}
resp.Body.Message = "Hello from Huma + Echo!"
return resp, nil
})
registerPlatformRoutes(api, queries)
registerGameRoutes(api, queries)
}
// --- Platform Routes ---
// PlatformDTO ensures consistent JSON output
type PlatformDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type PlatformInput struct {
Name string `json:"name" doc:"Name of the platform"`
}
func registerPlatformRoutes(api huma.API, queries db.Querier) {
huma.Register(api, huma.Operation{
OperationID: "list-platforms",
Method: http.MethodGet,
Path: "/platforms",
Summary: "List Platforms",
}, func(ctx context.Context, input *struct{}) (*struct{ Body []PlatformDTO }, error) {
platforms, err := queries.ListPlatforms(ctx)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
dtos := make([]PlatformDTO, len(platforms))
for i, p := range platforms {
dtos[i] = PlatformDTO{ID: p.ID, Name: p.Name}
}
return &struct{ Body []PlatformDTO }{Body: dtos}, nil
})
huma.Register(api, huma.Operation{
OperationID: "create-platform",
Method: http.MethodPost,
Path: "/platforms",
Summary: "Create Platform",
}, func(ctx context.Context, input *struct{ Body PlatformInput }) (*struct{ Body PlatformDTO }, error) {
platform, err := queries.CreatePlatform(ctx, input.Body.Name)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
})
huma.Register(api, huma.Operation{
OperationID: "update-platform",
Method: http.MethodPut,
Path: "/platforms/{id}",
Summary: "Update Platform",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
Body PlatformInput
}) (*struct{ Body PlatformDTO }, error) {
platform, err := queries.UpdatePlatform(ctx, db.UpdatePlatformParams{
ID: input.ID,
Name: input.Body.Name,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body PlatformDTO }{Body: PlatformDTO{ID: platform.ID, Name: platform.Name}}, nil
})
huma.Register(api, huma.Operation{
OperationID: "delete-platform",
Method: http.MethodDelete,
Path: "/platforms/{id}",
Summary: "Delete Platform",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
}) (*struct{}, error) {
err := queries.DeletePlatform(ctx, input.ID)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return nil, nil
})
}
// --- Game Routes ---
// GameDTO for API communication (Clean JSON)
type GameDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
PlatformID int64 `json:"platform_id"`
Score int32 `json:"score"`
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
}
type GameInput struct {
Name string `json:"name"`
PlatformID int64 `json:"platform_id"`
Score int32 `json:"score"`
ReleaseYear string `json:"release_year" format:"date" doc:"YYYY-MM-DD"`
Finished string `json:"finished,omitempty" format:"date" doc:"YYYY-MM-DD (optional)"`
}
// Helper to convert DB Game to DTO
func toGameDTO(g db.Game) GameDTO {
dto := GameDTO{
ID: g.ID,
Name: g.Name,
PlatformID: g.PlatformID,
Score: g.Score,
ReleaseYear: g.ReleaseYear.Time.Format("2006-01-02"),
}
if g.Finished.Valid {
dto.Finished = g.Finished.Time.Format("2006-01-02")
}
return dto
}
// Helper to parse date string to pgtype.Date
func parseDate(s string) (pgtype.Date, error) {
if s == "" {
return pgtype.Date{}, nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return pgtype.Date{}, err
}
return pgtype.Date{Time: t, Valid: true}, nil
}
func registerGameRoutes(api huma.API, queries db.Querier) {
huma.Register(api, huma.Operation{
OperationID: "list-games",
Method: http.MethodGet,
Path: "/games",
Summary: "List Games",
}, func(ctx context.Context, input *struct{}) (*struct{ Body []GameDTO }, error) {
games, err := queries.ListGames(ctx)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
dtos := make([]GameDTO, len(games))
for i, g := range games {
dtos[i] = toGameDTO(g)
}
return &struct{ Body []GameDTO }{Body: dtos}, nil
})
huma.Register(api, huma.Operation{
OperationID: "create-game",
Method: http.MethodPost,
Path: "/games",
Summary: "Create Game",
}, func(ctx context.Context, input *struct{ Body GameInput }) (*struct{ Body GameDTO }, error) {
ry, err := parseDate(input.Body.ReleaseYear)
if err != nil {
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
}
fin, err := parseDate(input.Body.Finished)
if err != nil {
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
}
game, err := queries.CreateGame(ctx, db.CreateGameParams{
Name: input.Body.Name,
PlatformID: input.Body.PlatformID,
Score: input.Body.Score,
ReleaseYear: ry,
Finished: fin,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
})
huma.Register(api, huma.Operation{
OperationID: "update-game",
Method: http.MethodPut,
Path: "/games/{id}",
Summary: "Update Game",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
Body GameInput
}) (*struct{ Body GameDTO }, error) {
ry, err := parseDate(input.Body.ReleaseYear)
if err != nil {
return nil, huma.Error400BadRequest("Invalid release_year format (expected YYYY-MM-DD)")
}
fin, err := parseDate(input.Body.Finished)
if err != nil {
return nil, huma.Error400BadRequest("Invalid finished format (expected YYYY-MM-DD)")
}
game, err := queries.UpdateGame(ctx, db.UpdateGameParams{
ID: input.ID,
Name: input.Body.Name,
PlatformID: input.Body.PlatformID,
Score: input.Body.Score,
ReleaseYear: ry,
Finished: fin,
})
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return &struct{ Body GameDTO }{Body: toGameDTO(game)}, nil
})
huma.Register(api, huma.Operation{
OperationID: "delete-game",
Method: http.MethodDelete,
Path: "/games/{id}",
Summary: "Delete Game",
}, func(ctx context.Context, input *struct {
ID int64 `path:"id"`
}) (*struct{}, error) {
err := queries.DeleteGame(ctx, input.ID)
if err != nil {
return nil, huma.Error500InternalServerError("Database error: " + err.Error())
}
return nil, nil
})
}

View File

@@ -0,0 +1,66 @@
package api
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humaecho"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"completed/internal/db"
)
func TestListGames(t *testing.T) {
// 1. Setup Mock
mockDB := &MockQuerier{
ListGamesFunc: func(ctx context.Context) ([]db.Game, error) {
return []db.Game{
{ID: 1, Name: "Test Game", Score: 90},
}, nil
},
}
// 2. Setup API
e := echo.New()
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
RegisterRoutes(api, mockDB)
// 3. Execute Request
req := httptest.NewRequest(http.MethodGet, "/games", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// 4. Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Test Game")
}
func TestCreatePlatform(t *testing.T) {
// 1. Setup Mock
mockDB := &MockQuerier{
CreatePlatformFunc: func(ctx context.Context, name string) (db.Platform, error) {
return db.Platform{ID: 1, Name: name}, nil
},
}
// 2. Setup API
e := echo.New()
api := humaecho.New(e, huma.DefaultConfig("Test API", "1.0.0"))
RegisterRoutes(api, mockDB)
// 3. Execute Request
req := httptest.NewRequest(http.MethodPost, "/platforms", strings.NewReader(`{"name": "Sega Genesis"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// 4. Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Sega Genesis")
assert.Contains(t, rec.Body.String(), `"id":1`)
}