package httpapi import ( "database/sql" "strconv" "strings" hres "form-builder-be-go/internal/hal" sq "form-builder-be-go/internal/sqlc" "github.com/gin-gonic/gin" "github.com/raff/halgo" ) func RegisterActorRoutes(r *gin.Engine, db *sql.DB) { // List actors r.GET("/actors", func(c *gin.Context) { limit := parseIntDefault(c.Query("limit"), 20) if limit > 100 { limit = 100 } offset := parseIntDefault(c.Query("offset"), 0) order := c.Query("order") expand := parseExpand(c.Query("expand")) q := sq.New(db) actors, err := q.ListActors(c, sq.ListActorsParams{Limit: int64(limit), Offset: int64(offset)}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } total, err := q.CountActors(c) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } selfURL := buildURL("/actors", limit, offset, order, c.Query("expand")) res := hres.New(selfURL) res.AddCurie() res.AddLink("self", halgo.Link{Href: selfURL}) if int64(offset+limit) < total { nextURL := buildURL("/actors", limit, offset+limit, order, c.Query("expand")) res.AddLink("next", halgo.Link{Href: nextURL}) } if offset > 0 { prev := offset - limit if prev < 0 { prev = 0 } prevURL := buildURL("/actors", limit, prev, order, c.Query("expand")) res.AddLink("prev", halgo.Link{Href: prevURL}) } var embedded []interface{} for _, a := range actors { itemURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10) relFilmsURL := itemURL + "/films" item := hres.New(itemURL) item.SetState(a) item.AddLink("self", halgo.Link{Href: itemURL}) item.AddLink("sakila:films", halgo.Link{Href: relFilmsURL}) if expand["films"] { films, err := q.ListFilmsByActor(c, int64(a.ActorID)) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } var filmEmbeds []interface{} for _, f := range films { filmURL := "/films/" + strconv.FormatInt(f.FilmID, 10) film := hres.New(filmURL) film.SetState(f) film.AddLink("self", halgo.Link{Href: filmURL}) film.AddLink("sakila:actors", halgo.Link{Href: filmURL + "/actors"}) filmEmbeds = append(filmEmbeds, film) } item.Embed("films", filmEmbeds) } embedded = append(embedded, item) } res.Embed("actors", embedded) res.SetState(gin.H{"count": len(actors), "total": total, "limit": limit, "offset": offset}) c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Get actor r.GET("/actors/:id", func(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(400, gin.H{"error": "invalid id"}) return } expand := parseExpand(c.Query("expand")) q := sq.New(db) aRec, err := q.GetActor(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } // sqlc returns error for not found self := "/actors/" + strconv.FormatInt(int64(aRec.ActorID), 10) relFilms := self + "/films" res := hres.New(self) res.AddCurie() res.SetState(aRec) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:films", halgo.Link{Href: relFilms}) if expand["films"] { films, err := q.ListFilmsByActor(c, int64(aRec.ActorID)) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } var filmEmbeds []interface{} for _, f := range films { filmURL := "/films/" + strconv.FormatInt(f.FilmID, 10) film := hres.New(filmURL) film.SetState(f) film.AddLink("self", halgo.Link{Href: filmURL}) film.AddLink("sakila:actors", halgo.Link{Href: filmURL + "/actors"}) filmEmbeds = append(filmEmbeds, film) } res.Embed("films", filmEmbeds) } c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Create actor r.POST("/actors", func(c *gin.Context) { var in struct { ActorID *int64 `json:"actor_id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if strings.TrimSpace(in.FirstName) == "" || strings.TrimSpace(in.LastName) == "" { c.JSON(400, gin.H{"error": "first_name and last_name are required"}) return } q := sq.New(db) var id int64 if in.ActorID != nil { id = *in.ActorID } else { var err error id, err = q.NextActorID(c) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } } if err := q.InsertActor(c, sq.InsertActorParams{ActorID: id, FirstName: in.FirstName, LastName: in.LastName}); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } created, err := q.GetActor(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/actors/" + strconv.FormatInt(int64(created.ActorID), 10) res := hres.New(self) res.AddCurie() res.SetState(created) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:films", halgo.Link{Href: self + "/films"}) c.Header("Content-Type", "application/hal+json") c.Header("Location", self) c.JSON(201, res) }) // Replace actor r.PUT("/actors/:id", func(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(400, gin.H{"error": "invalid id"}) return } var in struct { FirstName string `json:"first_name"` LastName string `json:"last_name"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if strings.TrimSpace(in.FirstName) == "" || strings.TrimSpace(in.LastName) == "" { c.JSON(400, gin.H{"error": "first_name and last_name are required"}) return } q := sq.New(db) aff, err := q.UpdateActorPut(c, sq.UpdateActorPutParams{FirstName: in.FirstName, LastName: in.LastName, ActorID: id}) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if aff == 0 { c.JSON(404, gin.H{"error": "not found"}) return } updated, err := q.GetActor(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/actors/" + strconv.FormatInt(int64(updated.ActorID), 10) res := hres.New(self) res.AddCurie() res.SetState(updated) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:films", halgo.Link{Href: self + "/films"}) c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Patch actor r.PATCH("/actors/:id", func(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(400, gin.H{"error": "invalid id"}) return } var in struct { FirstName *string `json:"first_name"` LastName *string `json:"last_name"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } q := sq.New(db) toNullString := func(p *string) sql.NullString { if p == nil { return sql.NullString{Valid: false} } return sql.NullString{String: *p, Valid: true} } aff, err := q.PatchActor(c, sq.PatchActorParams{FirstName: toNullString(in.FirstName), LastName: toNullString(in.LastName), ActorID: id}) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if aff == 0 { c.JSON(404, gin.H{"error": "not found"}) return } updated, err := q.GetActor(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/actors/" + strconv.FormatInt(int64(updated.ActorID), 10) res := hres.New(self) res.AddCurie() res.SetState(updated) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:films", halgo.Link{Href: self + "/films"}) c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Delete actor r.DELETE("/actors/:id", func(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(400, gin.H{"error": "invalid id"}) return } q := sq.New(db) aff, err := q.DeleteActor(c, id) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if aff == 0 { c.JSON(404, gin.H{"error": "not found"}) return } c.Status(204) }) }