diff --git a/cmd/parser/main.go b/cmd/parser/main.go index acdd64b..e32e62a 100644 --- a/cmd/parser/main.go +++ b/cmd/parser/main.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "recipes/internal/config" + "recipes/internal/lib/logger/sl" "recipes/internal/media_storage/minio" "recipes/internal/parser" "recipes/internal/storage/postgresql" @@ -32,7 +33,7 @@ func main() { cfg.Postgresql.DBName, ) if err != nil { - log.Error("failed to init storage", "err", err) + log.Error("failed to init storage", sl.Err(err)) os.Exit(1) } // init media storage @@ -43,13 +44,13 @@ func main() { cfg.Minio.Password, ) if err != nil { - log.Error("failed to init media storage", "err", err) + log.Error("failed to init media storage", sl.Err(err)) os.Exit(1) } // run parser _, err = parser.SavePage(log, 1, mstorage, storage, storage) if err != nil { - log.Error("Parse failed", "err", err) + log.Error("Parse failed", sl.Err(err)) os.Exit(1) } log.Info("parsing was completed successfully") diff --git a/cmd/recipes/main.go b/cmd/recipes/main.go index 5a439b0..57c2894 100644 --- a/cmd/recipes/main.go +++ b/cmd/recipes/main.go @@ -4,7 +4,9 @@ import ( "fmt" "log/slog" "os" + "os/signal" "recipes/internal/config" + "syscall" prettyLogger "github.com/charmbracelet/log" ) @@ -15,6 +17,13 @@ const ( envProd = "prod" ) +//TODO +// cache +// http server +// app +// graceful sd +// tests + func main() { // load config cfg := config.MustLoad() @@ -23,15 +32,20 @@ func main() { log.Info("starting application", slog.String("env", cfg.Env)) log.Debug("debug messages are enabled") log.Debug("Application config", slog.Any("config", fmt.Sprintf("%+v", *cfg))) - // init storage - // init cache - - // init media storage - - // init app + // init app (storage, cache, media storage) // graceful shutdown + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + + <-stop + + log.Info("stopping application...") + + // application.GRPCSrv.Stop() + + log.Info("application stopped") } func setupLogger(env string) *slog.Logger { diff --git a/go.mod b/go.mod index 5ca3047..569fa89 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect + github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect @@ -21,8 +22,14 @@ require ( github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/go-chi/render v1.0.3 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect github.com/google/uuid v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -34,6 +41,7 @@ require ( github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect diff --git a/go.sum b/go.sum index 74d1d26..a05d15d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -40,10 +42,22 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= @@ -80,6 +94,8 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -135,8 +151,13 @@ github.com/s32x/httpclient v0.0.0-20220217184346-6df4d4d51c14/go.mod h1:FqlhGa3u github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/http-server/handlers/recipe/recipe.go b/internal/http-server/handlers/recipe/recipe.go new file mode 100644 index 0000000..2318945 --- /dev/null +++ b/internal/http-server/handlers/recipe/recipe.go @@ -0,0 +1,76 @@ +package recipe + +import ( + "context" + "errors" + "log/slog" + "net/http" + "recipes/internal/domain/models" + resp "recipes/internal/lib/api/response" + "recipes/internal/lib/logger/sl" + "recipes/internal/storage" + + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" + "github.com/go-playground/validator/v10" +) + +type Request struct { + RecipeId uint `json:"recipe_id" validate:"required"` +} + +type Response struct { + resp.Response + Recipe models.Recipe `json:"recipe"` +} + +type RecipeProvider interface { + GetRecipe(ctx context.Context, r_id uint) (models.Recipe, error) +} + +func New(log *slog.Logger, recipeProvider RecipeProvider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + const op = "http-server.handlers.recipe.New" + + log = log.With( + slog.String("op", op), + slog.String("request_id", middleware.GetReqID(r.Context())), + ) + + var req Request + // decode request + err := render.DecodeJSON(r.Body, &req) + if err != nil { + log.Error("failed to decode request body", sl.Err(err)) + render.JSON(w, r, resp.Error("failed to decode request")) + return + } + log.Debug("request body decoded", slog.Any("request", req)) + // validate request + if err := validator.New().Struct(req); err != nil { + validateErr := err.(validator.ValidationErrors) + + log.Error("invalid request", sl.Err(err)) + render.JSON(w, r, resp.ValidationError(validateErr)) + return + } + // get from storage + recipe, err := recipeProvider.GetRecipe(r.Context(), req.RecipeId) + if err != nil { + log.Error("failed to get recipe from storage", sl.Err(err)) + if errors.Is(err, storage.ErrRecipeNotFound) { + render.JSON(w, r, resp.Error("recipe not found")) + return + } + render.JSON(w, r, resp.Error("failed to get recipe")) + return + } + // render response + var resp Response = Response{ + Response: resp.OK(), + Recipe: recipe, + } + log.Debug("response", slog.Any("resp", resp)) + render.JSON(w, r, resp) + } +} diff --git a/internal/http-server/middleware/logger.go b/internal/http-server/middleware/logger.go new file mode 100644 index 0000000..04b33aa --- /dev/null +++ b/internal/http-server/middleware/logger.go @@ -0,0 +1,42 @@ +package middleware + +import ( + "log/slog" + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" +) + +func New(log *slog.Logger) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + log = log.With( + slog.String("component", "midddleware/logger"), + ) + + log.Info("logger middleware enabled") + + fn := func(w http.ResponseWriter, r *http.Request) { + entry := log.With( + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + slog.String("remote_addr", r.RemoteAddr), + slog.String("user_agent", r.UserAgent()), + slog.String("request_id", middleware.GetReqID(r.Context())), + ) + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + t1 := time.Now() + defer func() { + entry.Info("request completed", + slog.Int("status", ww.Status()), + slog.Int("bytes", ww.BytesWritten()), + slog.String("duration", time.Since(t1).String()), + ) + }() + + next.ServeHTTP(ww, r) + } + return http.HandlerFunc(fn) + } +} diff --git a/internal/lib/api/response/response.go b/internal/lib/api/response/response.go new file mode 100644 index 0000000..27e97ba --- /dev/null +++ b/internal/lib/api/response/response.go @@ -0,0 +1,49 @@ +package response + +import ( + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +type Response struct { + Status string `json:"status"` + Error string `json:"error,omitempty"` +} + +const ( + StatusOK = "OK" + StatusError = "Error" +) + +func OK() Response { + return Response{ + Status: StatusOK, + } +} + +func Error(msg string) Response { + return Response{ + Status: StatusError, + Error: msg, + } +} + +func ValidationError(errs validator.ValidationErrors) Response { + var errMsgs []string + + for _, err := range errs { + switch err.ActualTag() { + case "required": + errMsgs = append(errMsgs, fmt.Sprintf("field %s is a required field", err.Field())) + default: + errMsgs = append(errMsgs, fmt.Sprintf("field %s is not valid", err.Field())) + } + } + + return Response{ + Status: StatusError, + Error: strings.Join(errMsgs, ", "), + } +} diff --git a/internal/lib/logger/sl/sl.go b/internal/lib/logger/sl/sl.go new file mode 100644 index 0000000..ac19308 --- /dev/null +++ b/internal/lib/logger/sl/sl.go @@ -0,0 +1,10 @@ +package sl + +import "log/slog" + +func Err(err error) slog.Attr { + return slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + } +}