- Golden example: todo-go/ (6/6 testit läpi) - todo-go.md golden reference - prompts/code-go.md koodigenerointi-prompti - Dockerfile.go-test (golang:1.23-alpine) - benchmark.mjs: LANG_CONFIG, parseTestOutput, prompt/golden-valinta Go:lle - Käyttö: node benchmark.mjs --lang go --models qwen2.5-coder:32b
467 lines
12 KiB
Markdown
467 lines
12 KiB
Markdown
# Todo — reference implementation (Go + Chi + SQLite)
|
|
|
|
This is a complete example. Generate equivalent structure for the given project.
|
|
Use ONLY the fields from the JSON spec — do not add extras.
|
|
|
|
## go.mod
|
|
|
|
Chi v5 router, modernc.org/sqlite (pure Go, no CGO).
|
|
|
|
```
|
|
module todo-go
|
|
|
|
go 1.23.0
|
|
|
|
toolchain go1.23.12
|
|
|
|
require (
|
|
github.com/go-chi/chi/v5 v5.2.1
|
|
modernc.org/sqlite v1.37.1
|
|
)
|
|
|
|
require (
|
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
github.com/google/uuid v1.6.0 // indirect
|
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
|
golang.org/x/sys v0.33.0 // indirect
|
|
modernc.org/libc v1.65.7 // indirect
|
|
modernc.org/mathutil v1.7.1 // indirect
|
|
modernc.org/memory v1.11.0 // indirect
|
|
)
|
|
```
|
|
|
|
## models.go
|
|
|
|
Data structs: Todo (full row), CreateTodo (POST), UpdateTodo (PUT, all fields optional pointers).
|
|
|
|
```go
|
|
package main
|
|
|
|
// Todo represents a task with priority and status tracking.
|
|
type Todo struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description,omitempty"`
|
|
DueDate *string `json:"due_date,omitempty"`
|
|
Priority int64 `json:"priority"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// CreateTodo is the request body for creating a new todo.
|
|
type CreateTodo struct {
|
|
Title string `json:"title"`
|
|
Description *string `json:"description,omitempty"`
|
|
DueDate *string `json:"due_date,omitempty"`
|
|
Priority *int64 `json:"priority,omitempty"`
|
|
Status *string `json:"status,omitempty"`
|
|
}
|
|
|
|
// UpdateTodo is the request body for updating an existing todo.
|
|
type UpdateTodo struct {
|
|
Title *string `json:"title,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
DueDate *string `json:"due_date,omitempty"`
|
|
Priority *int64 `json:"priority,omitempty"`
|
|
Status *string `json:"status,omitempty"`
|
|
}
|
|
```
|
|
|
|
## handlers.go
|
|
|
|
CRUD handlers as closures taking *sql.DB. INSERT/UPDATE use RETURNING, sql.ErrNoRows for 404.
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func createTodo(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var input CreateTodo
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
priority := int64(1)
|
|
if input.Priority != nil {
|
|
priority = *input.Priority
|
|
}
|
|
status := "pending"
|
|
if input.Status != nil {
|
|
status = *input.Status
|
|
}
|
|
var todo Todo
|
|
err := db.QueryRow(
|
|
`INSERT INTO todos (title, description, due_date, priority, status)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
RETURNING id, title, description, due_date, priority, status`,
|
|
input.Title, input.Description, input.DueDate, priority, status,
|
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(todo)
|
|
}
|
|
}
|
|
|
|
func listTodos(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos")
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
var todos []Todo
|
|
for rows.Next() {
|
|
var t Todo
|
|
if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
todos = append(todos, t)
|
|
}
|
|
if todos == nil {
|
|
todos = []Todo{}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(todos)
|
|
}
|
|
}
|
|
|
|
func getTodo(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
var todo Todo
|
|
err := db.QueryRow(
|
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
|
).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(todo)
|
|
}
|
|
}
|
|
|
|
func updateTodo(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
var existing Todo
|
|
err := db.QueryRow(
|
|
"SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id,
|
|
).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var input UpdateTodo
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if input.Title != nil {
|
|
existing.Title = *input.Title
|
|
}
|
|
if input.Description != nil {
|
|
existing.Description = input.Description
|
|
}
|
|
if input.DueDate != nil {
|
|
existing.DueDate = input.DueDate
|
|
}
|
|
if input.Priority != nil {
|
|
existing.Priority = *input.Priority
|
|
}
|
|
if input.Status != nil {
|
|
existing.Status = *input.Status
|
|
}
|
|
var updated Todo
|
|
err = db.QueryRow(
|
|
`UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ?
|
|
WHERE id = ?
|
|
RETURNING id, title, description, due_date, priority, status`,
|
|
existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id,
|
|
).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(updated)
|
|
}
|
|
}
|
|
|
|
func deleteTodo(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
|
result, err := db.Exec("DELETE FROM todos WHERE id = ?", id)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
```
|
|
|
|
## main.go
|
|
|
|
Entry point: SQLite connection, table init, Chi router on port 3000.
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// InitDB creates tables if they don't exist.
|
|
func InitDB(db *sql.DB) {
|
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
due_date TEXT,
|
|
priority INTEGER NOT NULL DEFAULT 1,
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
)`)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// NewRouter creates a chi router with all routes.
|
|
func NewRouter(db *sql.DB) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Post("/todos", createTodo(db))
|
|
r.Get("/todos", listTodos(db))
|
|
r.Get("/todos/{id}", getTodo(db))
|
|
r.Put("/todos/{id}", updateTodo(db))
|
|
r.Delete("/todos/{id}", deleteTodo(db))
|
|
return r
|
|
}
|
|
|
|
func main() {
|
|
db, err := sql.Open("sqlite", "file:app.db?mode=rwc")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
InitDB(db)
|
|
|
|
fmt.Println("Server running: http://127.0.0.1:3000")
|
|
log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db)))
|
|
}
|
|
```
|
|
|
|
## handlers_test.go
|
|
|
|
Integration tests: setupTestServer with httptest.NewServer + :memory: SQLite, unique data per test.
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
InitDB(db)
|
|
return httptest.NewServer(NewRouter(db)), db
|
|
}
|
|
|
|
func TestCreateTodo(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
resp, err := http.Post(ts.URL+"/todos", "application/json",
|
|
strings.NewReader(`{"title":"Buy groceries","priority":2}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
var body map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&body)
|
|
if body["title"] != "Buy groceries" {
|
|
t.Fatalf("expected title 'Buy groceries', got %v", body["title"])
|
|
}
|
|
if body["id"] == nil {
|
|
t.Fatal("expected id to be present")
|
|
}
|
|
}
|
|
|
|
func TestListTodos(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
http.Post(ts.URL+"/todos", "application/json",
|
|
strings.NewReader(`{"title":"Listable task"}`))
|
|
|
|
resp, err := http.Get(ts.URL + "/todos")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
var body []map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&body)
|
|
if len(body) < 1 {
|
|
t.Fatal("expected at least 1 todo")
|
|
}
|
|
}
|
|
|
|
func TestGetTodoByID(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
|
strings.NewReader(`{"title":"Fetchable task"}`))
|
|
var created map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
id := created["id"].(float64)
|
|
resp, err := http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
var body map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&body)
|
|
if body["id"] != id {
|
|
t.Fatalf("expected id %.0f, got %v", id, body["id"])
|
|
}
|
|
}
|
|
|
|
func TestGetTodoNotFound(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/todos/99999")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestUpdateTodo(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
|
strings.NewReader(`{"title":"Old title"}`))
|
|
var created map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
id := created["id"].(float64)
|
|
req, _ := http.NewRequest(http.MethodPut, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id),
|
|
strings.NewReader(`{"title":"New title"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
var body map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&body)
|
|
if body["title"] != "New title" {
|
|
t.Fatalf("expected 'New title', got %v", body["title"])
|
|
}
|
|
}
|
|
|
|
func TestDeleteTodo(t *testing.T) {
|
|
ts, db := setupTestServer(t)
|
|
defer ts.Close()
|
|
defer db.Close()
|
|
|
|
resp, _ := http.Post(ts.URL+"/todos", "application/json",
|
|
strings.NewReader(`{"title":"Deletable task"}`))
|
|
var created map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
id := created["id"].(float64)
|
|
req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Fatalf("expected 204, got %d", resp.StatusCode)
|
|
}
|
|
|
|
resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id))
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404 after delete, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
```
|