Skip to content

Commit 5a303f1

Browse files
Implement crud ops for generation recitals
1 parent 501992a commit 5a303f1

File tree

11 files changed

+211
-75
lines changed

11 files changed

+211
-75
lines changed

cmd/internal/config/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package config
22

33
import (
4-
"log"
54
"time"
65

6+
"github.com/simondanielsson/recite/cmd/internal/logging"
77
"github.com/simondanielsson/recite/pkg/env"
88
)
99

1010
// Load loads a config.
11-
func Load(getenv func(string) string, logger *log.Logger) (Config, error) {
11+
func Load(getenv func(string) string, logger logging.Logger) (Config, error) {
1212
if err := env.Load(); err != nil {
13-
logger.Fatal("failed loading .env")
13+
logger.Err.Fatal("failed loading .env")
1414
}
1515
// TODO: read these values from yaml config
1616
cfg := Config{

cmd/internal/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ const DBConnPool DBConnPoolKey = "db_pool_key"
1212
type StatusCodeKeyType string
1313

1414
const StatusCodeKey StatusCodeKeyType = "status_code_key"
15+
16+
const DateFormat string = "Mon Jan 2 15:04:05 MST 2006"

cmd/internal/db/middleware.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ package db
33
import (
44
"context"
55
"fmt"
6-
"log"
76
"net/http"
87

98
"github.com/jackc/pgx/v5"
109
"github.com/jackc/pgx/v5/pgxpool"
1110
constants "github.com/simondanielsson/recite/cmd/internal"
11+
"github.com/simondanielsson/recite/cmd/internal/logging"
1212
"github.com/simondanielsson/recite/cmd/internal/queries"
1313
)
1414

15-
func AddDatabaseMiddleware(handler http.Handler, pool *pgxpool.Pool, logger *log.Logger) http.Handler {
15+
func AddDatabaseMiddleware(handler http.Handler, pool *pgxpool.Pool, logger logging.Logger) http.Handler {
1616
return http.HandlerFunc(
1717
func(w http.ResponseWriter, r *http.Request) {
1818
ctx := r.Context()
@@ -24,11 +24,13 @@ func AddDatabaseMiddleware(handler http.Handler, pool *pgxpool.Pool, logger *log
2424
// Attach transaction-scoped repository, and pool for transactions to be launched outside request context
2525
ctx = context.WithValue(ctx, constants.RepositoryKey, repository)
2626
ctx = context.WithValue(ctx, constants.DBConnPool, pool)
27+
r = r.WithContext(ctx)
2728

28-
handler.ServeHTTP(w, r.WithContext(ctx))
29+
handler.ServeHTTP(w, r)
2930

30-
status, ok := ctx.Value(constants.StatusCodeKey).(int)
31+
status, ok := r.Context().Value(constants.StatusCodeKey).(int)
3132
if !ok {
33+
logger.Err.Println("No status code found in context, rolling back tx")
3234
_ = tx.Rollback(ctx)
3335
return
3436
}
@@ -37,7 +39,7 @@ func AddDatabaseMiddleware(handler http.Handler, pool *pgxpool.Pool, logger *log
3739
_ = tx.Rollback(ctx)
3840
} else if err := tx.Commit(ctx); err != nil {
3941
// Response has already been sent - just log
40-
logger.Printf("tx commit failed: %v", err)
42+
logger.Err.Printf("tx commit failed: %v", err)
4143
}
4244
},
4345
)

cmd/internal/dto/recitals.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dto
2+
3+
import "time"
4+
5+
type Recital struct {
6+
ID int32 `json:"id"`
7+
Url string `json:"url"`
8+
Title string `json:"title"`
9+
Description string `json:"description"`
10+
Status string `json:"status"`
11+
Path string `json:"path"`
12+
CreatedAt time.Time `json:"created_at"`
13+
}

cmd/internal/logging/logging.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@ import (
77

88
const DateFormat string = "Mon Jan 2 15:04:05 MST 2006"
99

10-
func NewLogger(w io.Writer) *log.Logger {
11-
logger := log.Logger{}
12-
logger.SetOutput(w)
13-
return &logger
10+
// Logger holds a out and err logger
11+
type Logger struct {
12+
Out *log.Logger
13+
Err *log.Logger
14+
}
15+
16+
func NewLogger(outWriter io.Writer, errWriter io.Writer) Logger {
17+
outLogger := log.Logger{}
18+
outLogger.SetOutput(outWriter)
19+
20+
errLogger := log.Logger{}
21+
errLogger.SetOutput(errWriter)
22+
return Logger{
23+
Out: &outLogger,
24+
Err: &errLogger,
25+
}
1426
}

cmd/internal/logging/middleware.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
package logging
22

33
import (
4-
"log"
54
"net/http"
65
"time"
76

87
constants "github.com/simondanielsson/recite/cmd/internal"
98
)
109

11-
func AddLoggingMiddleware(handler http.Handler, logger *log.Logger) http.Handler {
10+
func AddLoggingMiddleware(handler http.Handler, logger Logger) http.Handler {
1211
return http.HandlerFunc(
1312
func(w http.ResponseWriter, r *http.Request) {
1413
handler.ServeHTTP(w, r)
1514
status, ok := r.Context().Value(constants.StatusCodeKey).(int)
1615
if !ok {
17-
// TODO: write proper error, or assume it failed
18-
panic("could not get status")
16+
status = -1
17+
logger.Err.Println("Could not get status code from context")
1918
}
2019

21-
logger.Printf("%s | %s %s - %d\n", time.Now().Format(DateFormat), r.Method, r.URL, status)
20+
logger.Out.Printf("%s | %s %s - %d\n", time.Now().Format(constants.DateFormat), r.Method, r.URL, status)
2221
},
2322
)
2423
}

cmd/internal/marshal/marshal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010
)
1111

1212
func Encode[T any](ctx context.Context, w http.ResponseWriter, r *http.Request, status int, v T) error {
13-
w.Header().Set("Content-Type", "application/json")
1413
w.WriteHeader(status)
1514

1615
// A bit hacky way to write override the existing context
1716
ctx = context.WithValue(ctx, constants.StatusCodeKey, status)
1817
*r = *(r.WithContext(ctx))
1918

19+
w.Header().Set("Content-Type", "application/json")
2020
if err := json.NewEncoder(w).Encode(v); err != nil {
2121
return fmt.Errorf("encode json: %w", err)
2222
}

cmd/internal/routes/routes.go

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package routes
33
import (
44
"context"
55
"fmt"
6-
"log"
76
"net/http"
87
"strconv"
98

109
"github.com/jackc/pgx/v5/pgxpool"
1110
constants "github.com/simondanielsson/recite/cmd/internal"
11+
"github.com/simondanielsson/recite/cmd/internal/logging"
1212
"github.com/simondanielsson/recite/cmd/internal/marshal"
1313
"github.com/simondanielsson/recite/cmd/internal/queries"
1414
"github.com/simondanielsson/recite/cmd/internal/services"
@@ -18,26 +18,28 @@ type messageResponse struct {
1818
Message string `json:"message"`
1919
}
2020

21-
func RegisterRoutes(mux *http.ServeMux, logger *log.Logger) {
21+
func RegisterRoutes(mux *http.ServeMux, logger logging.Logger) {
2222
mux.Handle("GET /api/v1", rootGetHandler(logger))
2323
mux.Handle("GET /api/v1/health", rootGetHandler(logger))
24-
mux.Handle("POST /api/v1/recitals", recitalPostHandler(logger))
25-
mux.Handle("GET /api/v1/recitals/{id}", recitalGetHandler(logger))
24+
mux.Handle("POST /api/v1/recitals", createRecitalHandler(logger))
25+
mux.Handle("GET /api/v1/recitals", listRecitalsHandler(logger))
26+
mux.Handle("GET /api/v1/recitals/{id}", getRecitalHandler(logger))
27+
mux.Handle("DELETE /api/v1/recitals/{id}", deleteRecitalHandler(logger))
2628
}
2729

28-
func rootGetHandler(logger *log.Logger) http.Handler {
30+
func rootGetHandler(logger logging.Logger) http.Handler {
2931
return http.HandlerFunc(
3032
func(w http.ResponseWriter, r *http.Request) {
3133
w.WriteHeader(http.StatusOK)
3234
if _, err := w.Write([]byte("ok\n")); err != nil {
33-
logger.Println(err)
35+
logger.Err.Println(err)
3436
w.WriteHeader(http.StatusInternalServerError)
3537
}
3638
},
3739
)
3840
}
3941

40-
func recitalPostHandler(logger *log.Logger) http.Handler {
42+
func createRecitalHandler(logger logging.Logger) http.Handler {
4143
type request struct {
4244
Url string `json:"url"`
4345
}
@@ -49,12 +51,8 @@ func recitalPostHandler(logger *log.Logger) http.Handler {
4951
func(w http.ResponseWriter, r *http.Request) {
5052
req, err := marshal.Decode[request](r)
5153
if err != nil || req.Url == "" {
52-
res := messageResponse{
53-
Message: fmt.Sprintf("bad request, should contain url. Got %s", req.Url),
54-
}
55-
if err := marshal.Encode(r.Context(), w, r, http.StatusBadRequest, res); err != nil {
56-
writeErrHeader(w, err, logger)
57-
}
54+
message := fmt.Sprintf("bad request, should contain url. Got %s", req.Url)
55+
repondWithErrorMessage(w, r, message, http.StatusBadRequest, logger)
5856
return
5957
}
6058

@@ -67,24 +65,25 @@ func recitalPostHandler(logger *log.Logger) http.Handler {
6765
ctx := r.Context()
6866
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
6967
if !ok {
70-
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
68+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
7169
return
7270
}
7371
pool, ok := ctx.Value(constants.DBConnPool).(*pgxpool.Pool)
7472
if !ok {
75-
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading db connection pool"), logger)
73+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading db connection pool"), logger)
7674
return
7775
}
7876

7977
id, err := services.CreateRecital(ctx, req.Url, repository, pool, logger)
8078
if err != nil {
81-
logGenericInternalServiceError(ctx, w, r, err, logger)
79+
respondWithOpaqueMessage(ctx, w, r, err, logger)
8280
return
8381
}
8482

8583
res := response{
8684
Id: id,
8785
}
86+
w.Header().Add("Location", fmt.Sprintf("/api/v1/recitals/%d", id))
8887
if err := marshal.Encode(ctx, w, r, http.StatusCreated, res); err != nil {
8988
writeErrHeader(w, err, logger)
9089
return
@@ -93,10 +92,40 @@ func recitalPostHandler(logger *log.Logger) http.Handler {
9392
)
9493
}
9594

96-
func recitalGetHandler(logger *log.Logger) http.Handler {
97-
type response struct {
98-
queries.Recital
99-
}
95+
func listRecitalsHandler(logger logging.Logger) http.Handler {
96+
type response []queries.Recital
97+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
offset, err := readIntQueryParam("offset", 0, w, r, logger)
99+
if err != nil {
100+
return
101+
}
102+
103+
limit, err := readIntQueryParam("limit", 30, w, r, logger)
104+
if err != nil {
105+
return
106+
}
107+
108+
ctx := r.Context()
109+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
110+
if !ok {
111+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
112+
return
113+
}
114+
115+
recitals, err := services.ListRecitals(ctx, int32(limit), int32(offset), repository, logger)
116+
if err != nil {
117+
respondWithOpaqueMessage(ctx, w, r, err, logger)
118+
return
119+
}
120+
121+
if err := marshal.Encode(ctx, w, r, int(http.StatusOK), recitals); err != nil {
122+
writeErrHeader(w, err, logger)
123+
return
124+
}
125+
})
126+
}
127+
128+
func getRecitalHandler(logger logging.Logger) http.Handler {
100129
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101130
id, err := strconv.Atoi(r.PathValue("id"))
102131
if err != nil {
@@ -110,7 +139,7 @@ func recitalGetHandler(logger *log.Logger) http.Handler {
110139
ctx := r.Context()
111140
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
112141
if !ok {
113-
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
142+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
114143
}
115144

116145
recital, err := services.GetRecital(ctx, int32(id), repository, logger)
@@ -122,35 +151,82 @@ func recitalGetHandler(logger *log.Logger) http.Handler {
122151
return
123152
}
124153

125-
res := response{
126-
Recital: recital,
127-
}
128-
if err := marshal.Encode(ctx, w, r, http.StatusOK, res); err != nil {
154+
if err := marshal.Encode(ctx, w, r, http.StatusOK, recital); err != nil {
129155
logFailedEncodingResponse(ctx, w, r, err, logger)
130156
}
131157
})
132158
}
133159

134-
func writeErrHeader(w http.ResponseWriter, err error, logger *log.Logger) {
135-
w.WriteHeader(http.StatusInternalServerError)
136-
if _, err := w.Write([]byte(err.Error())); err != nil {
137-
logger.Print("failed writing error")
138-
}
160+
func deleteRecitalHandler(logger logging.Logger) http.Handler {
161+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162+
id, err := strconv.Atoi(r.PathValue("id"))
163+
if err != nil {
164+
res := messageResponse{Message: "invalid id"}
165+
if err := marshal.Encode(r.Context(), w, r, http.StatusBadRequest, res); err != nil {
166+
writeErrHeader(w, err, logger)
167+
}
168+
return
169+
}
170+
171+
ctx := r.Context()
172+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
173+
if !ok {
174+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
175+
}
176+
if err := services.DeleteRecital(ctx, int32(id), repository, logger); err != nil {
177+
res := messageResponse{Message: fmt.Sprintf("Could not find recital with id %d", id)}
178+
if err := marshal.Encode(ctx, w, r, http.StatusNotFound, res); err != nil {
179+
logFailedEncodingResponse(ctx, w, r, err, logger)
180+
}
181+
return
182+
}
183+
status := http.StatusNoContent
184+
w.WriteHeader(status)
185+
// A bit hacky way towrite override the existing context
186+
ctx = context.WithValue(ctx, constants.StatusCodeKey, status)
187+
*r = *(r.WithContext(ctx))
188+
})
139189
}
140190

141-
func logGenericInternalServiceError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger *log.Logger) {
142-
res := messageResponse{Message: "Something went wrong."}
143-
logger.Print(err)
144-
if err := marshal.Encode(ctx, w, r, int(http.StatusInternalServerError), res); err != nil {
191+
func repondWithErrorMessage(w http.ResponseWriter, r *http.Request, message string, code int, logger logging.Logger) {
192+
res := messageResponse{
193+
Message: message,
194+
}
195+
if err := marshal.Encode(r.Context(), w, r, code, res); err != nil {
145196
writeErrHeader(w, err, logger)
146-
return
147197
}
148198
}
149199

150-
func logFailedEncodingResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger *log.Logger) {
151-
logger.Println(err)
200+
func respondWithOpaqueMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger logging.Logger) {
201+
logger.Err.Println(err)
202+
repondWithErrorMessage(w, r, "Something went wrong.", int(http.StatusInternalServerError), logger)
203+
}
204+
205+
func writeErrHeader(w http.ResponseWriter, err error, logger logging.Logger) {
206+
w.WriteHeader(http.StatusInternalServerError)
207+
if _, err := w.Write([]byte(err.Error())); err != nil {
208+
logger.Err.Println("failed writing error")
209+
}
210+
}
211+
212+
func logFailedEncodingResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger logging.Logger) {
213+
logger.Err.Println(err)
152214
res := messageResponse{"failed to encode response"}
153215
if err := marshal.Encode(ctx, w, r, http.StatusInternalServerError, res); err != nil {
154216
writeErrHeader(w, err, logger)
155217
}
156218
}
219+
220+
func readIntQueryParam(name string, otherwise int, w http.ResponseWriter, r *http.Request, logger logging.Logger) (int, error) {
221+
valueString := r.URL.Query().Get(name)
222+
if valueString == "" {
223+
return otherwise, nil
224+
} else {
225+
value, err := strconv.Atoi(valueString)
226+
if err != nil {
227+
repondWithErrorMessage(w, r, fmt.Sprintf("Invalid %s: expected integer", name), http.StatusBadRequest, logger)
228+
return 0, err
229+
}
230+
return value, nil
231+
}
232+
}

0 commit comments

Comments
 (0)