update: service, handlers, and db separation
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/dryrun Pipeline was successful
ci/woodpecker/push/publish-tag Pipeline was successful
ci/woodpecker/push/publish-latest Pipeline was successful

This commit is contained in:
Michael Thomson 2024-06-14 17:49:56 -04:00
parent 44feca12d2
commit 3d29dadaf3
No known key found for this signature in database
14 changed files with 364 additions and 29 deletions

19
db/db.go Normal file
View File

@ -0,0 +1,19 @@
package db
import "github.com/google/uuid"
type Todo struct {
Id uuid.UUID
Name string
Done bool
}
type TodosStore struct {
Todos []Todo
}
func NewTodoStore() TodosStore {
return TodosStore{
Todos: []Todo{},
}
}

2
go.mod
View File

@ -3,3 +3,5 @@ module michaelthomson.dev/mthomson/go-todos-app
go 1.22.1
require github.com/a-h/templ v0.2.707
require github.com/google/uuid v1.6.0 // indirect

4
go.sum
View File

@ -1,2 +1,6 @@
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

41
handlers/homeHandler.go Normal file
View File

@ -0,0 +1,41 @@
package handlers
import (
"net/http"
"michaelthomson.dev/mthomson/go-todos-app/services"
"michaelthomson.dev/mthomson/go-todos-app/templates/pages"
)
type HomeHandler struct {
ts services.TodoService
}
func NewHomeHandler(ts services.TodoService) HomeHandler {
return HomeHandler{ts: ts}
}
func (h *HomeHandler) Home(w http.ResponseWriter, r *http.Request) {
var err error
todos, err := h.ts.GetTodos()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = pages.Home(todos).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h HomeHandler) Router() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/", h.Home)
return router
}

61
handlers/todoHandler.go Normal file
View File

@ -0,0 +1,61 @@
package handlers
import (
"net/http"
"github.com/google/uuid"
"michaelthomson.dev/mthomson/go-todos-app/services"
"michaelthomson.dev/mthomson/go-todos-app/templates/partials"
)
type TodoHandler struct {
ts services.TodoService
}
func NewTodoHandler(ts services.TodoService) TodoHandler {
return TodoHandler{ts: ts}
}
func (h TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
var err error
err = r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
name := r.FormValue("name")
addedTodo, err := h.ts.AddTodo(name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = partials.Todo(addedTodo).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
var err error
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.ts.DeleteTodoById(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

24
main.go
View File

@ -3,24 +3,28 @@ package main
import (
"log"
"net/http"
"path/filepath"
"github.com/a-h/templ"
"michaelthomson.dev/mthomson/go-todos-app/views"
"michaelthomson.dev/mthomson/go-todos-app/db"
"michaelthomson.dev/mthomson/go-todos-app/handlers"
"michaelthomson.dev/mthomson/go-todos-app/services"
)
func main() {
todosStore := db.NewTodoStore()
ts := services.NewTodoService(&todosStore)
homeHandler := handlers.NewHomeHandler(*ts)
todoHandler := handlers.NewTodoHandler(*ts)
router := http.NewServeMux()
// Serve static files
router.HandleFunc("GET /assets/", func(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path[len("/assets/"):]
fullPath := filepath.Join(".", "assets", filePath)
http.ServeFile(w, r, fullPath)
})
fs := http.FileServer(http.Dir("./assets"))
router.Handle("GET /assets/", http.StripPrefix("/assets/", fs))
home := views.Home()
router.Handle("GET /", templ.Handler(home))
router.HandleFunc("GET /{$}", homeHandler.Home)
router.HandleFunc("POST /todos", todoHandler.Create)
router.HandleFunc("DELETE /todos/{id}", todoHandler.Delete)
server := http.Server{
Addr: "localhost:3000",

61
services/todoService.go Normal file
View File

@ -0,0 +1,61 @@
package services
import (
"fmt"
"github.com/google/uuid"
"michaelthomson.dev/mthomson/go-todos-app/db"
)
type TodoService struct {
store *db.TodosStore
}
func NewTodoService(store *db.TodosStore) *TodoService {
return &TodoService{
store: store,
}
}
func (ts *TodoService) AddTodo(name string) (todo db.Todo, err error) {
addedTodo := db.Todo{
Id: uuid.New(),
Name: name,
Done: false,
}
ts.store.Todos = append(ts.store.Todos, addedTodo)
return addedTodo, nil
}
func (ts *TodoService) GetTodos() (todos []db.Todo, err error) {
return ts.store.Todos, nil
}
func (ts *TodoService) GetTodoById(id uuid.UUID) (todo db.Todo, err error) {
for _, todo := range ts.store.Todos {
if id == todo.Id {
return todo, nil
}
}
return db.Todo{}, fmt.Errorf("Could not find todo by id %q", id)
}
func (ts *TodoService) DeleteTodoById(id uuid.UUID) (err error) {
index := -1
for i, todo := range ts.store.Todos {
if id == todo.Id {
index = i
}
}
if index == -1 {
return fmt.Errorf("Could not find todo by id %q", id)
}
ts.store.Todos = append(ts.store.Todos[:index], ts.store.Todos[index + 1:]...)
return nil
}

View File

@ -0,0 +1,20 @@
package pages
import "michaelthomson.dev/mthomson/go-todos-app/templates/shared"
import "michaelthomson.dev/mthomson/go-todos-app/db"
import "michaelthomson.dev/mthomson/go-todos-app/templates/partials"
templ Home(todos []db.Todo) {
@shared.Page("Todos") {
<div class="text-xl">Todos</div>
<form hx-post="/todos" hx-target="#todos" hx-swap="beforeend">
<input name="name" type="text" />
<button type="submit">Submit</button>
</form>
<div id="todos">
for _, todo := range todos {
@partials.Todo(todo)
}
</div>
}
}

View File

@ -1,7 +1,7 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package views
package pages
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -10,9 +10,11 @@ import "context"
import "io"
import "bytes"
import "michaelthomson.dev/mthomson/go-todos-app/views/template"
import "michaelthomson.dev/mthomson/go-todos-app/templates/shared"
import "michaelthomson.dev/mthomson/go-todos-app/db"
import "michaelthomson.dev/mthomson/go-todos-app/templates/partials"
func Home() templ.Component {
func Home(todos []db.Todo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
@ -31,7 +33,17 @@ func Home() templ.Component {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-xl\">Home page</div>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-xl\">Todos</div><form hx-post=\"/todos\" hx-target=\"#todos\" hx-swap=\"beforeend\"><input name=\"name\" type=\"text\"> <button type=\"submit\">Submit</button></form><div id=\"todos\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, todo := range todos {
templ_7745c5c3_Err = partials.Todo(todo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -40,7 +52,7 @@ func Home() templ.Component {
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = template.Base("Todos app").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = shared.Page("Todos").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -0,0 +1,14 @@
package partials
import "michaelthomson.dev/mthomson/go-todos-app/db"
func todoId(todo db.Todo) string {
return "todo-" + todo.Id.String()
}
templ Todo(todo db.Todo) {
<div id={ todoId(todo) }>
{ todo.Id.String() }: { todo.Name }
<button hx-delete={ "/todos/" + todo.Id.String() } hx-target={ "#" + todoId(todo) } hx-swap="delete">Delete</button>
</div>
}

View File

@ -0,0 +1,106 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package partials
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "michaelthomson.dev/mthomson/go-todos-app/db"
func todoId(todo db.Todo) string {
return "todo-" + todo.Id.String()
}
func Todo(todo db.Todo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(todoId(todo))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 10, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Id.String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(": ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 11, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <button hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/todos/" + todo.Id.String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("#" + todoId(todo))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/partials/todo.templ`, Line: 12, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"delete\">Delete</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -1,6 +1,6 @@
package template
package shared
templ Base(title string) {
templ Page(title string) {
<html>
<head>
<meta charset="UTF-8"></meta>

View File

@ -1,7 +1,7 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.663
package template
package shared
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -10,7 +10,7 @@ import "context"
import "io"
import "bytes"
func Base(title string) templ.Component {
func Page(title string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
@ -30,7 +30,7 @@ func Base(title string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/template/template.templ`, Line: 10, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/shared/page.templ`, Line: 10, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {

View File

@ -1,9 +0,0 @@
package views
import "michaelthomson.dev/mthomson/go-todos-app/views/template"
templ Home() {
@template.Base("Todos app") {
<div class="text-xl">Home page</div>
}
}