From 10bd39d915c7b069020d3e1ca546b6fd45d56571 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Sat, 13 May 2023 13:03:48 +0200 Subject: [PATCH] handler index --- handler/handler.go | 47 ++++++++++++++++++++++++++ handler/server.go | 78 +++++++++++++++++++++++++++++++++++++++++++ handler/server.go].go | 1 - service.go | 15 ++++++++- 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 handler/server.go delete mode 100644 handler/server.go].go diff --git a/handler/handler.go b/handler/handler.go index abeebd1..287a80a 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1 +1,48 @@ package handler + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func Index(w http.ResponseWriter) { + Message(w, http.StatusOK, "yogai index") +} + +func Message(w http.ResponseWriter, status int, message string, details ...any) { + w.WriteHeader(status) + response := struct { + Message string `json:"message"` + Details []any `json:"details,omitempty"` + }{ + Message: message, + Details: details, + } + body, marshalErr := json.Marshal(response) + if marshalErr != nil { + fmt.Fprintf(w, fmt.Sprintf(`{"message": %q, "details":%q}`, message, marshalErr.Error())) + return + } + fmt.Fprintf(w, string(body)) +} + +func Error(w http.ResponseWriter, status int, message string, err error, details ...any) { + w.WriteHeader(status) + response := struct { + Message string `json:"message"` + Error string `json:"error"` + Details []any `json:"details,omitempty"` + }{ + Message: message, + Error: err.Error(), + Details: details, + } + body, marshalErr := json.Marshal(response) + if marshalErr != nil { + fmt.Fprintf(w, fmt.Sprintf(`{"message": %q, "error": %q, "details":%q}`, message, err.Error(), marshalErr.Error())) + return + } + + fmt.Fprintf(w, string(body)) +} diff --git a/handler/server.go b/handler/server.go new file mode 100644 index 0000000..b0e583f --- /dev/null +++ b/handler/server.go @@ -0,0 +1,78 @@ +package handler + +import ( + "fmt" + "golang.org/x/exp/slog" + "miniflux.app/logger" + "net/http" + "net/http/httptest" + "path" + "strings" +) + +type Server struct { + apis map[string]http.Handler + logger *slog.Logger +} + +func NewServer(logger *slog.Logger) *Server { + return &Server{ + apis: map[string]http.Handler{}, + logger: logger, + } +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + originalPath := r.URL.Path + rec := httptest.NewRecorder() // records the response to be able to mix writing headers and content + + w.Header().Add("Content-Type", "application/json") + + // route to api + head, tail := ShiftPath(r.URL.Path) + if len(head) == 0 { + Index(rec) + returnResponse(w, rec) + return + } + api, ok := s.apis[head] + if !ok { + Error(rec, http.StatusNotFound, "Not found", fmt.Errorf("%s is not a valid path", r.URL.Path)) + } else { + r.URL.Path = tail + api.ServeHTTP(rec, r) + } + + returnResponse(w, rec) + logger.Info("request served", "path", originalPath, "status", rec.Code) +} + +func returnResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder) { + w.WriteHeader(rec.Code) + for k, v := range rec.Header() { + w.Header()[k] = v + } + w.Write(rec.Body.Bytes()) +} + +// ShiftPath splits off the first component of p, which will be cleaned of +// relative components before processing. head will never contain a slash and +// tail will always be a rooted path without trailing slash. +// See https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/ +func ShiftPath(p string) (string, string) { + p = path.Clean("/" + p) + + // restore iri prefixes that might be mangled by path.Clean + for k, v := range map[string]string{ + "http:/": "http://", + "https:/": "https://", + } { + p = strings.Replace(p, k, v, -1) + } + + i := strings.Index(p[1:], "/") + 1 + if i <= 0 { + return p[1:], "/" + } + return p[1:i], p[i:] +} diff --git a/handler/server.go].go b/handler/server.go].go deleted file mode 100644 index abeebd1..0000000 --- a/handler/server.go].go +++ /dev/null @@ -1 +0,0 @@ -package handler diff --git a/service.go b/service.go index aa9ca97..a04e7a7 100644 --- a/service.go +++ b/service.go @@ -3,18 +3,23 @@ package main import ( "context" "ewintr.nl/yogai/fetcher" + "ewintr.nl/yogai/handler" "ewintr.nl/yogai/storage" + "fmt" "golang.org/x/exp/slog" "google.golang.org/api/option" "google.golang.org/api/youtube/v3" + "net/http" "os" "os/signal" + "strconv" "time" ) func main() { ctx := context.Background() logger := slog.New(slog.NewTextHandler(os.Stderr)) + postgres, err := storage.NewPostgres(storage.PostgresInfo{ Host: getParam("POSTGRES_HOST", "localhost"), Port: getParam("POSTGRES_PORT", "5432"), @@ -50,7 +55,15 @@ func main() { fetcher := fetcher.NewFetch(videoRepo, mflx, fetchInterval, yt, openAIClient, logger) go fetcher.Run() - logger.Info("service started") + logger.Info("fetch service started") + + port, err := strconv.Atoi(getParam("API_PORT", "8080")) + if err != nil { + logger.Error("invalid port", err) + os.Exit(1) + } + go http.ListenAndServe(fmt.Sprintf(":%d", port), handler.NewServer(logger)) + logger.Info("http server started") done := make(chan os.Signal) signal.Notify(done, os.Interrupt)