summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2026-08-13 20:53:24 +0200
committerxengineering <me@xengineering.eu>2026-08-15 19:56:51 +0200
commit53a5b271eae38913307793fa9884856709c4f822 (patch)
tree41cc0f119c25b581f0b3739e0c90faa4f0776c69
parent99cff9a3102c64cd146d524524001bd6ecb246c4 (diff)
downloadceres-53a5b271eae38913307793fa9884856709c4f822.tar
ceres-53a5b271eae38913307793fa9884856709c4f822.tar.zst
ceres-53a5b271eae38913307793fa9884856709c4f822.zip
Add JSON-based API under `/api`
This commit provides an unstable JSON API for communication with an upcoming Dart frontend. Later it might be part of the public interface for integration into other services. For this step it has to be ensured the semantics are not changing a lot in the future which is currently out of scope.
-rw-r--r--model/object.go1
-rw-r--r--model/recipe.go14
-rw-r--r--model/recipes.go14
-rw-r--r--server.go9
-rw-r--r--view/json.go107
5 files changed, 142 insertions, 3 deletions
diff --git a/model/object.go b/model/object.go
index 63ef419..0b1919b 100644
--- a/model/object.go
+++ b/model/object.go
@@ -9,4 +9,5 @@ type Object interface {
Read(tx *sql.Tx) error
Update(tx *sql.Tx) error
Delete(tx *sql.Tx) error
+ SetID(id int)
}
diff --git a/model/recipe.go b/model/recipe.go
index 684b158..adcafa1 100644
--- a/model/recipe.go
+++ b/model/recipe.go
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
+ "log"
+ "strconv"
)
type Recipe struct {
@@ -19,11 +21,23 @@ type Recipe struct {
IsFavorite bool `json:"is_favorite"`
}
+func NewRecipe() Object {
+ return &Recipe{}
+}
+
func (r Recipe) String() string {
b, _ := json.MarshalIndent(r, "", " ")
return string(b)
}
+func (r *Recipe) SetID(id int) {
+ if r == nil {
+ log.Println("Got nil as recipe!")
+ }
+
+ r.Id = strconv.Itoa(id)
+}
+
func (r *Recipe) Validate() error {
var err error
diff --git a/model/recipes.go b/model/recipes.go
index e4fd1b8..09b51e7 100644
--- a/model/recipes.go
+++ b/model/recipes.go
@@ -7,13 +7,21 @@ import (
)
type RecipesElement struct {
- Id int64 // TODO change to string
- Title string
- IsFavorite bool
+ Id int64 `json:"id"` // TODO change to string
+ Title string `json:"title"`
+ IsFavorite bool `json:"is_favorite"`
}
type Recipes []RecipesElement
+func NewRecipes() Object {
+ return &Recipes{}
+}
+
+func (r *Recipes) SetID(id int) {
+ // singleton object, ignoring ID
+}
+
func (r *Recipes) Create(tx *sql.Tx) error {
return fmt.Errorf("Impossible to create a recipe list")
}
diff --git a/server.go b/server.go
index 3b17227..03c8251 100644
--- a/server.go
+++ b/server.go
@@ -26,12 +26,21 @@ func NewServer(addr string, db *model.DB) *Server {
mux.Handle("GET /version", view.VersionRead(version))
+ mux.Handle("GET /api/recipes", view.ReadJSON[*model.Recipes](db, model.NewRecipes))
mux.Handle("GET /recipes", view.RecipesRead(db))
mux.Handle("GET /recipe/create", view.RecipeCreate())
+
+ mux.Handle("POST /api/recipe", view.CreateJSON[*model.Recipe](db, model.NewRecipe))
mux.Handle("POST /recipe", controller.RecipeCreate(db))
+
+ mux.Handle("GET /api/recipe/{id}", view.ReadJSON[*model.Recipe](db, model.NewRecipe))
mux.Handle("GET /recipe/{id}", view.RecipeRead(db))
+
+ mux.Handle("POST /api/recipe/{id}", view.UpdateJSON[*model.Recipe](db, model.NewRecipe))
mux.Handle("POST /recipe/{id}", controller.RecipeUpdate(db))
+
+ mux.Handle("DELETE /api/recipe/{id}", view.DeleteJSON[*model.Recipe](db, model.NewRecipe))
mux.Handle("DELETE /recipe/{id}", controller.RecipeDelete(db))
mux.Handle("GET /favicon.ico", view.FaviconRead())
diff --git a/view/json.go b/view/json.go
new file mode 100644
index 0000000..deaece2
--- /dev/null
+++ b/view/json.go
@@ -0,0 +1,107 @@
+package view
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "strconv"
+
+ "xengineering.eu/ceres/model"
+)
+
+func CreateJSON[O model.Object](db *model.DB, constructor func() model.Object) http.Handler {
+ return http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ buf, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Request body could not be read: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ object := constructor()
+ err = json.Unmarshal(buf, &object)
+ if err != nil {
+ http.Error(w, "JSON could not be parsed: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ err = db.Transaction(object.Create)
+ if err != nil {
+ http.Error(w, "Object could not be saved to database: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ },
+ )
+}
+
+func ReadJSON[O model.Object](db *model.DB, constructor func() model.Object) http.Handler {
+ return http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ object := constructor()
+
+ id, err := strconv.Atoi(r.PathValue("id"))
+ if err == nil {
+ object.SetID(id)
+ }
+
+ err = db.Transaction(object.Read)
+ if err != nil {
+ http.Error(w, "Object could not be read from database: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ err = json.NewEncoder(w).Encode(object)
+ if err != nil {
+ http.Error(w, "Object could not be encoded to JSON: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ },
+ )
+}
+
+func UpdateJSON[O model.Object](db *model.DB, constructor func() model.Object) http.Handler {
+ return http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ buf, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Request body could not be read: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ object := constructor()
+ err = json.Unmarshal(buf, &object)
+ if err != nil {
+ http.Error(w, "JSON could not be parsed: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ err = db.Transaction(object.Update)
+ if err != nil {
+ http.Error(w, "Object could not be updated in database: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ },
+ )
+}
+
+func DeleteJSON[O model.Object](db *model.DB, constructor func() model.Object) http.Handler {
+ return http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ object := constructor()
+
+ id, err := strconv.Atoi(r.PathValue("id"))
+ if err != nil {
+ http.Error(w, "ID of object could not be parsed to an integer: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ object.SetID(id)
+
+ err = db.Transaction(object.Delete)
+ if err != nil {
+ http.Error(w, "Object could not be deleted from database: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ },
+ )
+}