package httpapi import ( "database/sql" "net/url" "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 RegisterFilmRoutes(r *gin.Engine, db *sql.DB) { // List films r.GET("/films", 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")) langName := strings.TrimSpace(c.Query("language.name")) catName := strings.TrimSpace(c.Query("category.name")) q := sq.New(db) var ( films []sq.Film total int64 err error ) if langName != "" { films, err = q.ListFilmsByLanguageName(c, sq.ListFilmsByLanguageNameParams{UPPER: langName, Limit: int64(limit), Offset: int64(offset)}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } total, err = q.CountFilmsByLanguageName(c, langName) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } } else if catName != "" { films, err = q.ListFilmsByCategoryName(c, sq.ListFilmsByCategoryNameParams{UPPER: catName, Limit: int64(limit), Offset: int64(offset)}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } total, err = q.CountFilmsByCategoryName(c, catName) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } } else { films, err = q.ListFilms(c, sq.ListFilmsParams{Limit: int64(limit), Offset: int64(offset)}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } total, err = q.CountFilms(c) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } } selfURL := buildURL("/films", limit, offset, order, c.Query("expand")) if langName != "" { sep := "?" if strings.Contains(selfURL, "?") { sep = "&" } selfURL = selfURL + sep + "language.name=" + url.QueryEscape(langName) } if catName != "" { sep := "?" if strings.Contains(selfURL, "?") { sep = "&" } selfURL = selfURL + sep + "category.name=" + url.QueryEscape(catName) } res := hres.New(selfURL) res.AddCurie() res.AddLink("self", halgo.Link{Href: selfURL}) if int64(offset+limit) < total { nextURL := buildURL("/films", limit, offset+limit, order, c.Query("expand")) if langName != "" { sep := "?" if strings.Contains(nextURL, "?") { sep = "&" } nextURL += sep + "language.name=" + url.QueryEscape(langName) } if catName != "" { sep := "?" if strings.Contains(nextURL, "?") { sep = "&" } nextURL += sep + "category.name=" + url.QueryEscape(catName) } res.AddLink("next", halgo.Link{Href: nextURL}) } if offset > 0 { prev := offset - limit if prev < 0 { prev = 0 } prevURL := buildURL("/films", limit, prev, order, c.Query("expand")) if langName != "" { sep := "?" if strings.Contains(prevURL, "?") { sep = "&" } prevURL += sep + "language.name=" + url.QueryEscape(langName) } if catName != "" { sep := "?" if strings.Contains(prevURL, "?") { sep = "&" } prevURL += sep + "category.name=" + url.QueryEscape(catName) } res.AddLink("prev", halgo.Link{Href: prevURL}) } var embedded []interface{} for _, f := range films { itemURL := "/films/" + strconv.FormatInt(f.FilmID, 10) relActorsURL := itemURL + "/actors" item := hres.New(itemURL) item.SetState(f) item.AddLink("self", halgo.Link{Href: itemURL}) item.AddLink("sakila:actors", halgo.Link{Href: relActorsURL}) // language relations item.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)}) if f.OriginalLanguageID.Valid { item.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)}) } // optional embeds if expand["language"] { lang, err := q.GetLanguage(c, f.LanguageID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10)) lr.SetState(lang) lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)}) item.Embed("language", lr) } if expand["original_language"] && f.OriginalLanguageID.Valid { olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10)) olr.SetState(olang) olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)}) item.Embed("original_language", olr) } if expand["actors"] { actors, err := q.ListActorsByFilm(c, f.FilmID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } var actorEmbeds []interface{} for _, a := range actors { actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10) ar := hres.New(actorURL) ar.SetState(a) ar.AddLink("self", halgo.Link{Href: actorURL}) ar.AddLink("films", halgo.Link{Href: actorURL + "/films"}) actorEmbeds = append(actorEmbeds, ar) } item.Embed("actors", actorEmbeds) } embedded = append(embedded, item) } res.Embed("films", embedded) res.SetState(gin.H{"count": len(films), "total": total, "limit": limit, "offset": offset}) c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Get film r.GET("/films/: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) f, err := q.GetFilm(c, id) if err != nil { if err == sql.ErrNoRows { c.JSON(404, gin.H{"error": "not found"}) return } c.JSON(500, gin.H{"error": err.Error()}) return } self := "/films/" + strconv.FormatInt(f.FilmID, 10) relActors := self + "/actors" res := hres.New(self) res.AddCurie() res.SetState(f) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:actors", halgo.Link{Href: relActors}) // language relations res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)}) if f.OriginalLanguageID.Valid { res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)}) } // optional embeds if expand["language"] { lang, err := q.GetLanguage(c, f.LanguageID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10)) lr.SetState(lang) lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)}) res.Embed("language", lr) } if expand["original_language"] && f.OriginalLanguageID.Valid { olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10)) olr.SetState(olang) olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)}) res.Embed("original_language", olr) } if expand["actors"] { actors, err := q.ListActorsByFilm(c, f.FilmID) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } var actorEmbeds []interface{} for _, a := range actors { actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10) ar := hres.New(actorURL) ar.SetState(a) ar.AddLink("self", halgo.Link{Href: actorURL}) ar.AddLink("films", halgo.Link{Href: actorURL + "/films"}) actorEmbeds = append(actorEmbeds, ar) } res.Embed("actors", actorEmbeds) } c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Create film r.POST("/films", func(c *gin.Context) { var in struct { FilmID *int64 `json:"film_id"` Title string `json:"title"` Description *string `json:"description"` ReleaseYear *string `json:"release_year"` LanguageID *int64 `json:"language_id"` OriginalLanguageID *int64 `json:"original_language_id"` RentalDuration *int64 `json:"rental_duration"` RentalRate *float64 `json:"rental_rate"` Length *int64 `json:"length"` ReplacementCost *float64 `json:"replacement_cost"` Rating *string `json:"rating"` SpecialFeatures *string `json:"special_features"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if strings.TrimSpace(in.Title) == "" || in.LanguageID == nil { c.JSON(400, gin.H{"error": "title and language_id are required"}) return } q := sq.New(db) var id int64 if in.FilmID != nil { id = *in.FilmID } else { var err error id, err = q.NextFilmID(c) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } } toNullString := func(p *string) sql.NullString { if p == nil { return sql.NullString{Valid: false} } return sql.NullString{String: *p, Valid: true} } toNullInt := func(p *int64) sql.NullInt64 { if p == nil { return sql.NullInt64{Valid: false} } return sql.NullInt64{Int64: *p, Valid: true} } var desc interface{} if in.Description != nil { desc = *in.Description } else { desc = nil } rentalDuration := int64(3) if in.RentalDuration != nil { rentalDuration = *in.RentalDuration } rentalRate := 4.99 if in.RentalRate != nil { rentalRate = *in.RentalRate } replacementCost := 19.99 if in.ReplacementCost != nil { replacementCost = *in.ReplacementCost } length := toNullInt(in.Length) err := q.InsertFilm(c, sq.InsertFilmParams{ FilmID: id, Title: in.Title, Description: desc, ReleaseYear: toNullString(in.ReleaseYear), LanguageID: *in.LanguageID, OriginalLanguageID: toNullInt(in.OriginalLanguageID), RentalDuration: rentalDuration, RentalRate: rentalRate, Length: length, ReplacementCost: replacementCost, Rating: toNullString(in.Rating), SpecialFeatures: toNullString(in.SpecialFeatures), }) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } created, err := q.GetFilm(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/films/" + strconv.FormatInt(created.FilmID, 10) res := hres.New(self) res.AddCurie() res.SetState(created) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"}) res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(created.LanguageID, 10)}) if created.OriginalLanguageID.Valid { res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(created.OriginalLanguageID.Int64, 10)}) } c.Header("Content-Type", "application/hal+json") c.Header("Location", self) c.JSON(201, res) }) // Replace film (PUT) r.PUT("/films/: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 { Title string `json:"title"` Description *string `json:"description"` ReleaseYear *string `json:"release_year"` LanguageID int64 `json:"language_id"` OriginalLanguageID *int64 `json:"original_language_id"` RentalDuration int64 `json:"rental_duration"` RentalRate float64 `json:"rental_rate"` Length *int64 `json:"length"` ReplacementCost float64 `json:"replacement_cost"` Rating *string `json:"rating"` SpecialFeatures *string `json:"special_features"` } if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if strings.TrimSpace(in.Title) == "" { c.JSON(400, gin.H{"error": "title is required"}) 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} } toNullInt := func(p *int64) sql.NullInt64 { if p == nil { return sql.NullInt64{Valid: false} } return sql.NullInt64{Int64: *p, Valid: true} } var desc interface{} if in.Description != nil { desc = *in.Description } else { desc = nil } aff, err := q.UpdateFilmPut(c, sq.UpdateFilmPutParams{ Title: in.Title, Description: desc, ReleaseYear: toNullString(in.ReleaseYear), LanguageID: in.LanguageID, OriginalLanguageID: toNullInt(in.OriginalLanguageID), RentalDuration: in.RentalDuration, RentalRate: in.RentalRate, Length: toNullInt(in.Length), ReplacementCost: in.ReplacementCost, Rating: toNullString(in.Rating), SpecialFeatures: toNullString(in.SpecialFeatures), FilmID: 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.GetFilm(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/films/" + strconv.FormatInt(updated.FilmID, 10) res := hres.New(self) res.AddCurie() res.SetState(updated) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"}) res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.LanguageID, 10)}) if updated.OriginalLanguageID.Valid { res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.OriginalLanguageID.Int64, 10)}) } c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Patch film r.PATCH("/films/: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 { Title *string `json:"title"` Description *string `json:"description"` ReleaseYear *string `json:"release_year"` LanguageID *int64 `json:"language_id"` OriginalLanguageID *int64 `json:"original_language_id"` RentalDuration *int64 `json:"rental_duration"` RentalRate *float64 `json:"rental_rate"` Length *int64 `json:"length"` ReplacementCost *float64 `json:"replacement_cost"` Rating *string `json:"rating"` SpecialFeatures *string `json:"special_features"` } 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} } toNullInt := func(p *int64) sql.NullInt64 { if p == nil { return sql.NullInt64{Valid: false} } return sql.NullInt64{Int64: *p, Valid: true} } toNullFloat := func(p *float64) sql.NullFloat64 { if p == nil { return sql.NullFloat64{Valid: false} } return sql.NullFloat64{Float64: *p, Valid: true} } var desc interface{} if in.Description != nil { desc = *in.Description } else { desc = nil } aff, err := q.PatchFilm(c, sq.PatchFilmParams{ Title: toNullString(in.Title), Description: desc, ReleaseYear: toNullString(in.ReleaseYear), LanguageID: toNullInt(in.LanguageID), OriginalLanguageID: toNullInt(in.OriginalLanguageID), RentalDuration: toNullInt(in.RentalDuration), RentalRate: toNullFloat(in.RentalRate), Length: toNullInt(in.Length), ReplacementCost: toNullFloat(in.ReplacementCost), Rating: toNullString(in.Rating), SpecialFeatures: toNullString(in.SpecialFeatures), FilmID: 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.GetFilm(c, id) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } self := "/films/" + strconv.FormatInt(updated.FilmID, 10) res := hres.New(self) res.AddCurie() res.SetState(updated) res.AddLink("self", halgo.Link{Href: self}) res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"}) res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.LanguageID, 10)}) if updated.OriginalLanguageID.Valid { res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.OriginalLanguageID.Int64, 10)}) } c.Header("Content-Type", "application/hal+json") c.JSON(200, res) }) // Delete film r.DELETE("/films/: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.DeleteFilm(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) }) }