diff --git a/app/movie.go b/app/movie.go index fc9e1ea..b8ff5ff 100644 --- a/app/movie.go +++ b/app/movie.go @@ -9,25 +9,16 @@ import ( "log/slog" "net/http" + "ewintr.nl/emdb/movie" "github.com/google/uuid" ) -type Movie struct { - ID string `json:"id"` - Title string `json:"title"` - Year int `json:"year"` - IMDBID string `json:"imdb_id"` - WatchedOn string `json:"watched_on"` - Rating int `json:"rating"` - Comment string `json:"comment"` -} - type MovieAPI struct { - repo *SQLite + repo movie.MovieRepository logger *slog.Logger } -func NewMovieAPI(repo *SQLite, logger *slog.Logger) *MovieAPI { +func NewMovieAPI(repo movie.MovieRepository, logger *slog.Logger) *MovieAPI { return &MovieAPI{ repo: repo, logger: logger.With("api", "movie"), @@ -37,16 +28,20 @@ func NewMovieAPI(repo *SQLite, logger *slog.Logger) *MovieAPI { func (api *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger := api.logger.With("method", "serveHTTP") - movieID, _ := ShiftPath(r.URL.Path) + subPath, _ := ShiftPath(r.URL.Path) switch { - case r.Method == http.MethodGet && movieID != "": - api.Read(w, r, movieID) - case r.Method == http.MethodGet && movieID == "": + case r.Method == http.MethodGet && subPath != "": + api.Read(w, r, subPath) + case r.Method == http.MethodPut && subPath != "": + api.Store(w, r, subPath) + case r.Method == http.MethodPost && subPath == "": + api.Store(w, r, "") + case r.Method == http.MethodDelete && subPath != "": + api.Delete(w, r, subPath) + case r.Method == http.MethodGet && subPath == "": api.List(w, r) - case r.Method == http.MethodPost: - api.Create(w, r) default: - Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, movieID), logger) + Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, subPath), logger) } } @@ -73,7 +68,7 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string fmt.Fprint(w, string(resJson)) } -func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) { +func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) { logger := api.logger.With("method", "create") body, err := io.ReadAll(r.Body) @@ -83,14 +78,23 @@ func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - var movie *Movie + var movie *movie.Movie if err := json.Unmarshal(body, &movie); err != nil { Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger) return } - movie.ID = uuid.New().String() - if err := api.repo.StoreMovie(movie); err != nil { + switch { + case urlID == "" && movie.ID == "": + movie.ID = uuid.New().String() + case urlID != "" && movie.ID == "": + movie.ID = urlID + case urlID != "" && movie.ID != "" && urlID != movie.ID: + Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger) + return + } + + if err := api.repo.Store(movie); err != nil { Error(w, http.StatusInternalServerError, "could not store movie", err, logger) return } @@ -104,6 +108,17 @@ func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(resBody)) } +func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) { + logger := api.logger.With("method", "delete") + + if err := api.repo.Delete(urlID); err != nil { + Error(w, http.StatusInternalServerError, "could not delete movie", err, logger) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) { logger := api.logger.With("method", "list") @@ -120,5 +135,4 @@ func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) { } fmt.Fprint(w, string(resBody)) - } diff --git a/app/sqlite.go b/app/sqlite.go index f6b2c96..8cfcf7c 100644 --- a/app/sqlite.go +++ b/app/sqlite.go @@ -4,14 +4,27 @@ import ( "database/sql" "errors" "fmt" + "strings" + "ewintr.nl/emdb/movie" + "github.com/google/uuid" _ "modernc.org/sqlite" ) type sqliteMigration string var sqliteMigrations = []sqliteMigration{ - `CREATE TABLE movie ("id" TEXT UNIQUE, "title" TEXT, "year" INTEGER, "imdb_id" TEXT, "watched_on" TEXT, "rating" INTEGER, "comment" TEXT)`, + `CREATE TABLE movie ( + "id" TEXT UNIQUE NOT NULL, + "imdb_id" TEXT NOT NULL DEFAULT "", + "title" TEXT NOT NULL DEFAULT "", + "english_title" TEXT NOT NULL DEFAULT "", + "year" INTEGER NOT NULL DEFAULT 0, + "directors" TEXT NOT NULL DEFAULT "", + "watched_on" TEXT NOT NULL DEFAULT "", + "rating" INTEGER NOT NULL DEFAULT 0, + "comment" TEXT NOT NULL DEFAULT "" + )`, `CREATE TABLE system ("latest_sync" INTEGER)`, `INSERT INTO system (latest_sync) VALUES (0)`, } @@ -44,16 +57,21 @@ func NewSQLite(dbPath string) (*SQLite, error) { return s, nil } -func (s *SQLite) StoreMovie(movie *Movie) error { +func (s *SQLite) Store(m *movie.Movie) error { + if m.ID == "" { + m.ID = uuid.New().String() + } + tx, err := s.db.Begin() if err != nil { return fmt.Errorf("%w: %v", ErrSqliteFailure, err) } defer tx.Rollback() - if _, err := s.db.Exec(`REPLACE INTO movie (id, title, year, imdb_id, watched_on, rating, comment) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - movie.ID, movie.Title, movie.Year, movie.IMDBID, movie.WatchedOn, movie.Rating, movie.Comment); err != nil { + directors := strings.Join(m.Directors, ",") + if _, err := s.db.Exec(`REPLACE INTO movie (id, imdb_id, title, english_title, year, directors, watched_on, rating, comment) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + m.ID, m.IMDBID, m.Title, m.EnglishTitle, m.Year, directors, m.WatchedOn, m.Rating, m.Comment); err != nil { return fmt.Errorf("%w: %v", ErrSqliteFailure, err) } @@ -64,50 +82,53 @@ func (s *SQLite) StoreMovie(movie *Movie) error { return nil } -func (s *SQLite) FindOne(id string) (*Movie, error) { +func (s *SQLite) Delete(id string) error { + if _, err := s.db.Exec(`DELETE FROM movie WHERE id=?`, id); err != nil { + return fmt.Errorf("%w: %v", ErrSqliteFailure, err) + } + + return nil +} + +func (s *SQLite) FindOne(id string) (*movie.Movie, error) { row := s.db.QueryRow(` -SELECT title, year, imdb_id, watched_on, rating, comment +SELECT imdb_id, title, english_title, year, directors, watched_on, rating, comment FROM movie WHERE id=?`, id) if row.Err() != nil { return nil, row.Err() } - movie := &Movie{ + m := &movie.Movie{ ID: id, } - if err := row.Scan(&movie.Title, &movie.Year, &movie.IMDBID, &movie.WatchedOn, &movie.Rating, &movie.Comment); err != nil { - return nil, err + var directors string + if err := row.Scan(&m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.WatchedOn, &m.Rating, &m.Comment); err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) } + m.Directors = strings.Split(directors, ",") - return movie, nil + return m, nil } -func (s *SQLite) FindAll() ([]*Movie, error) { +func (s *SQLite) FindAll() ([]*movie.Movie, error) { rows, err := s.db.Query(` -SELECT id, title, year, imdb_id, watched_on, rating, comment +SELECT imdb_id, title, english_title, year, directors, watched_on, rating, comment FROM movie`) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) } - movies := make([]*Movie, 0) + movies := make([]*movie.Movie, 0) defer rows.Close() for rows.Next() { - var id, title, imdbID, watchedOn, comment string - var year, rating int - if err := rows.Scan(&id, &title, &year, &imdbID, &watchedOn, &rating, &comment); err != nil { + m := &movie.Movie{} + var directors string + if err := rows.Scan(&m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.WatchedOn, &m.Rating, &m.Comment); err != nil { return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) } - movies = append(movies, &Movie{ - ID: id, - Title: title, - Year: year, - IMDBID: imdbID, - WatchedOn: watchedOn, - Rating: rating, - Comment: comment, - }) + m.Directors = strings.Split(directors, ",") + movies = append(movies, m) } return movies, nil diff --git a/movie/movie.go b/movie/movie.go new file mode 100644 index 0000000..eb43b7a --- /dev/null +++ b/movie/movie.go @@ -0,0 +1,20 @@ +package movie + +type Movie struct { + ID string `json:"id"` + IMDBID string `json:"imdbID"` + Title string `json:"title"` + EnglishTitle string `json:"englishTitle"` + Year int `json:"year"` + Directors []string `json:"directors"` + WatchedOn string `json:"watchedOn"` + Rating int `json:"rating"` + Comment string `json:"comment"` +} + +type MovieRepository interface { + Store(movie *Movie) error + FindOne(id string) (*Movie, error) + FindAll() ([]*Movie, error) + Delete(id string) error +}