api.spaceplanner.app

Spaceplanner API
git clone git://jacobedwards.org/api.spaceplanner.app
Log | Files | Refs

commit dfc9b0e0d9bc0ffdd81191b418a9164ac45d6163
parent 2ea7d10e53506e1984da716fac3e92d8d2dfd4bf
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon,  2 Sep 2024 19:29:32 -0700

Try and reduce code duplication in floorplan patching functions

I'v implemented an interface (DBObject) with certain operations
database objects should support (Create, Update, Delete) and made
Points and PointMaps implement it. It does reduce duplication but
it's still not very elegant, likely due to my lack of understanding
of go.  Nonetheless I think it's an improvement.

Diffstat:
Minternal/backend/floorplan_data.go | 255+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
1 file changed, 144 insertions(+), 111 deletions(-)

diff --git a/internal/backend/floorplan_data.go b/internal/backend/floorplan_data.go @@ -11,14 +11,14 @@ import ( type Point struct { id int64 `json:"id"` - OldId *int64 `json:"old_id,omitempty"` + OldID *int64 `json:"old_id,omitempty"` X int `json:"x" binding:"required"` Y int `json:"y" binding:"required"` } type PointMap struct { id int64 - OldId *int64 `json:"old_id,omitempty"` + OldID *int64 `json:"old_id,omitempty"` Type string `json:"type" binding:"required"` A int64 `json:"a" binding:"required"` B int64 `json:"b" binding:"required"` @@ -41,6 +41,24 @@ type PatchError struct { extra error } +type DBObject interface { + Ref() rowReference + updateRefs(rowReference, map[rowReference]rowReference) DBObject + SetOldID(int64) DBObject + + /* + * NOTE: these all take user and floorplan arguments solely for authorization, but + * I want a cleaner method + */ + Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) + Update(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) + Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) +} + +type Mappable interface { + Key() any +} + func (e *Env) GetFloorplanData(tx *sql.Tx, user string, floorplan string) (FloorplanData, error) { var data FloorplanData var err error @@ -119,7 +137,7 @@ func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan string, da for id, point := range data.Points { patch := Patch{ Op: "new", - Path: rowReference{table: "points", id: id}.String(), + Path: newRef("points", id).String(), Value: point, } patches = append(patches, patch) @@ -127,7 +145,7 @@ func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan string, da for id, pointmap := range data.Pointmaps { patch := Patch{ Op: "new", - Path: rowReference{table: "pointmaps", id: id}.String(), + Path: newRef("pointmaps", id).String(), Value: pointmap, } patches = append(patches, patch) @@ -169,8 +187,7 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc mytx = true } - new_point_ids := make(map[int64]int64) - new_pointmap_ids := make(map[int64]int64) + newRefs := make(map[rowReference]rowReference) data := FloorplanData{} data.Points = make(map[int64]Point) @@ -189,20 +206,20 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc } if (ref.table == "points") { - point, err := e.patchPoint(tx, user, floorplan, &patch, &ref, new_point_ids) + point, err := applyPatch[Point](e, tx, user, floorplan, &patch, ref, newRefs) if err != nil { return data, ref.Error("", err) } - if point != nil { - data.Points[point.id] = *point + if patch.Op != "delete" { + data.Points[point.id] = point } } else if (ref.table == "pointmaps") { - pointmap, err := e.patchPointMap(tx, user, floorplan, &patch, &ref, new_pointmap_ids, new_point_ids) + pointmap, err := applyPatch[PointMap](e, tx, user, floorplan, &patch, ref, newRefs) if err != nil { return data, ref.Error("", err) } - if pointmap != nil { - data.Pointmaps[pointmap.id] = *pointmap + if patch.Op != "delete" { + data.Pointmaps[pointmap.id] = pointmap } } else { return data, ref.Error("Path does not exist", nil) @@ -219,98 +236,52 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc return data, nil } -func (e *Env) patchPoint(tx *sql.Tx, user string, floorplan string, patch *Patch, - ref *rowReference, idmap map[int64]int64) (*Point, error) { - if ref.table != "points" { - return nil, ref.Error("Expected points", nil) - } +func applyPatch[T DBObject](e *Env, tx *sql.Tx, user, floorplan string, patch *Patch, ref rowReference, + newRefs map[rowReference]rowReference) (T, error) { + inputRef := ref - id := ref.id - if newid, exists := idmap[ref.id]; exists { - id = newid + ref, exists := newRefs[inputRef] + if !exists { + ref = inputRef } - if (patch.Op == "remove") { - if _, err := e.deletePoint(tx, user, floorplan, id); err != nil { - return nil, ref.Error("", err) - } - return nil, nil + var thing T + if err := remarshal(patch.Value, &thing); err != nil { + return thing, inputRef.Error("Invalid object", err) } - var point Point - if err := remarshal(patch.Value, &point); err != nil { - return nil, ref.Error("parse value", err) - } + // Could do with adding a ref.table check + + // Tried to figure out the correct solution to this, but + // can't figure it out right now. I'll come back to it later + // but for now a few type assertions + thing = thing.updateRefs(ref, newRefs).(T) var err error - if (patch.Op == "new") { - point, err = e.addPoint(tx, user, floorplan, point) - if point.id != id { - idmap[id] = point.id - point.OldId = &id - } - } else if (patch.Op == "replace") { - point.id = id - point, err = e.replacePoint(tx, user, floorplan, point) - } else { - return nil, ref.Error("Unsupported operation", nil) + var dbo DBObject + switch patch.Op { + case "new": + dbo, err = thing.Create(e, tx, user, floorplan) + case "replace": + dbo, err = thing.Update(e, tx, user, floorplan) + case "delete": + dbo, err = thing.Delete(e, tx, user, floorplan) + default: + return thing, inputRef.Error("Unsupported operation", nil) } if err != nil { - return nil, err - } - - return &point, nil -} - -func (e *Env) patchPointMap(tx *sql.Tx, user string, floorplan string, patch *Patch, - ref *rowReference, idmap map[int64]int64, pointidmap map[int64]int64) (*PointMap, error) { - if ref.table != "pointmaps" { - return nil, ref.Error("Expected pointmaps", nil) - } - - id := ref.id - if newid, exists := idmap[ref.id]; exists { - id = newid + return thing, inputRef.Error("Unable to perform operation", err) } - if (patch.Op == "remove") { - if _, err := e.deletePointMap(tx, user, floorplan, id); err != nil { - return nil, ref.Error("", err) - } - return nil, nil - } - - var pointmap PointMap - if err := remarshal(patch.Value, &pointmap); err != nil { - return nil, ref.Error("", err) - } - if newid, exists := pointidmap[pointmap.A]; exists { - pointmap.A = newid - } - if newid, exists := pointidmap[pointmap.B]; exists { - pointmap.B = newid + thing = dbo.(T) + ref = thing.Ref() + if inputRef == ref { + return thing, nil } - var err error - if (patch.Op == "new") { - pointmap, err = e.addPointMap(tx, user, floorplan, pointmap) - if pointmap.id != id { - idmap[id] = pointmap.id - pointmap.OldId = &id - } - } else if (patch.Op == "replace") { - pointmap.id = id - pointmap, err = e.replacePointMap(tx, user, floorplan, pointmap) - } else { - return nil, ref.Error("Unsupported operation", nil) - } - - if err != nil { - return nil, ref.Error("", err) - } - - return &pointmap, nil + newRefs[inputRef] = ref + return thing.SetOldID(inputRef.id).(T), nil } func remarshal[T any](value interface{}, result *T) (error) { @@ -321,27 +292,36 @@ func remarshal[T any](value interface{}, result *T) (error) { return json.Unmarshal(s, result) } -func (e *Env) addPoint(tx *sql.Tx, user string, floorplan string, point Point) (Point, error) { +/* + * NOTE: Tried getting this right for a while, settled on having + * these functions return an interface instead of their value + * Not sure exactly how this is suppost to be done. + */ +func (p Point) Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "add_point",`INSERT INTO spaceplanner.floorplan_points (floorplan, x, y) VALUES (spaceplanner.floorplan_id($1, $2), $3, $4) RETURNING id, x, y`) if err != nil { return Point{}, err } - return scanPoint(stmt.QueryRow(user, floorplan, point.X, point.Y)) + return scanPoint(stmt.QueryRow(user, floorplan, p.X, p.Y)) } -func (e *Env) replacePoint(tx *sql.Tx, user string, floorplan string, point Point) (Point, error) { +func (p Point) Update(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "repl_point", `UPDATE spaceplanner.floorplan_points SET (x, y) = ($4, $5) WHERE floorplan = floorplan_id($1, $2) AND id = $3 RETURNING id, x, y`) if err != nil { return Point{}, err } - return scanPoint(stmt.QueryRow(user, floorplan, point.id, point.X, point.Y)) + return scanPoint(stmt.QueryRow(user, floorplan, p.id, p.X, p.Y)) +} + +func (p Point) Delete(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { + return e.DeletePoint(tx, user, floorplan, p.id) } -func (e *Env) deletePoint(tx *sql.Tx, user string, floorplan string, id int64) (Point, error) { +func (e *Env) DeletePoint(tx *sql.Tx, user string, floorplan string, id int64) (Point, error) { stmt, err := e.CacheTxStmt(tx, "dele_point", `DELETE FROM spaceplanner.floorplan_points WHERE floorplan = floorplan_id($1, $2) AND id = $3 RETURNING id, x, y`) @@ -352,7 +332,7 @@ func (e *Env) deletePoint(tx *sql.Tx, user string, floorplan string, id int64) ( return scanPoint(stmt.QueryRow(user, floorplan, id)) } -func (e *Env) addPointMap(tx *sql.Tx, user string, floorplan string, pointmap PointMap) (PointMap, error) { +func (pm PointMap) Create(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "add_pointmap", `INSERT INTO spaceplanner.floorplan_pointmaps (floorplan, type, a, b) VALUES ( floorplan_id($1, $2), $3, $4, $5 @@ -361,10 +341,10 @@ func (e *Env) addPointMap(tx *sql.Tx, user string, floorplan string, pointmap Po return PointMap{}, err } - return scanPointMap(stmt.QueryRow(user, floorplan, pointmap.Type, pointmap.A, pointmap.B)) + return scanPointMap(stmt.QueryRow(user, floorplan, pm.Type, pm.A, pm.B)) } -func (e *Env) replacePointMap(tx *sql.Tx, user string, floorplan string, pointmap PointMap) (PointMap, error) { +func (pm PointMap) Update(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "repl_pointmap", `UPDATE spaceplanner.floorplan_pointmaps SET (type, a, b) = ($4, $5, $6) WHERE floorplan = floorplan_id($1, $2) AND id = $3 @@ -373,10 +353,14 @@ func (e *Env) replacePointMap(tx *sql.Tx, user string, floorplan string, pointma return PointMap{}, err } - return scanPointMap(stmt.QueryRow(user, floorplan, pointmap.id, pointmap.Type, pointmap.A, pointmap.B)) + return scanPointMap(stmt.QueryRow(user, floorplan, pm.id, pm.Type, pm.A, pm.B)) } -func (e *Env) deletePointMap(tx *sql.Tx, user string, floorplan string, id int64) (PointMap, error) { +func (pm PointMap) Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { + return e.DeletePointMap(tx, user, floorplan, pm.id) +} + +func (e *Env) DeletePointMap(tx *sql.Tx, user string, floorplan string, id int64) (PointMap, error) { stmt, err := e.CacheTxStmt(tx, "dele_pointmap", `DELETE FROM spaceplanner.floorplan_pointmaps WHERE floorplan = floorplan_id($1, $2) AND id = $3 @@ -388,6 +372,55 @@ func (e *Env) deletePointMap(tx *sql.Tx, user string, floorplan string, id int64 return scanPointMap(stmt.QueryRow(user, floorplan, id)) } +func (p Point) Ref() rowReference { + return newRef("points", p.id) +} + +func (p Point) updateRefs(ref rowReference, newRefs map[rowReference]rowReference) DBObject { + if n, exists := newRefs[ref]; exists { + p.id = n.id + } else { + p.id = ref.id + } + return p +} + +func (p Point) SetOldID(id int64) DBObject { + if id != p.id { + p.OldID = &id + } + return p +} + +func (m PointMap) Ref() rowReference { + return newRef("pointmaps", m.id) +} + +func (m PointMap) updateRefs(ref rowReference, newRefs map[rowReference]rowReference) DBObject { + if n, exists := newRefs[ref]; exists { + m.id = n.id + } else { + m.id = ref.id + } + if n, exists := newRefs[newRef("points", m.A)]; exists { + m.A = n.id + } + if n, exists := newRefs[newRef("points", m.B)]; exists { + m.B = n.id + } + return m +} + +func (m PointMap) SetOldID(id int64) DBObject { + if id != m.id { + m.OldID = &id + } + return m +} + +func newRef(table string, id int64) rowReference { + return rowReference{table: table, id: id} +} func parseRowReference(ref string) (rowReference, error) { // Ref should look like this: /type/id so three @@ -402,21 +435,21 @@ func parseRowReference(ref string) (rowReference, error) { return rowReference{}, errors.New(ref + ": Invalid id") } - return rowReference{table: parts[1], id: id}, nil + return newRef(parts[1], id), nil } -func scanPoint(row Scanner) (Point, error) { - var point Point +func scanPoint(s Scanner) (Point, error) { + var p Point - err := row.Scan(&point.id, &point.X, &point.Y) - return point, err + err := s.Scan(&p.id, &p.X, &p.Y) + return p, err } -func scanPointMap(row Scanner) (PointMap, error) { - var pointmap PointMap +func scanPointMap(s Scanner) (PointMap, error) { + var pm PointMap - err := row.Scan(&pointmap.id, &pointmap.Type, &pointmap.A, &pointmap.B) - return pointmap, err + err := s.Scan(&pm.id, &pm.Type, &pm.A, &pm.B) + return pm, err } func mapArray[K comparable, V any](array []V, mapper func(V) (K, error)) (map[K]V, error) { @@ -436,8 +469,8 @@ func mapPoint(p Point) (int64, error) { return p.id, nil } -func mapPointMap(p PointMap) (int64, error) { - return p.id, nil +func mapPointMap(pm PointMap) (int64, error) { + return pm.id, nil } func (ref rowReference) String() string {