From e5e5d9f26b41111eea18409c17cabaa1610789e7 Mon Sep 17 00:00:00 2001 From: Sanath Kumar Date: Fri, 26 Jan 2024 15:04:09 -0700 Subject: [PATCH] feat: initial service and api impl for handling creation of TinyURLs (#8) * feat: initial service and api impl for handling creation of TinyURLs * fix: rework the ci files to check for migrations only on pull_request and not on merge * fix: split jobs and fix names --- .../workflows/{makefile.yml => base-ci.yml} | 36 ++++---- .github/workflows/migrations-ci.yml | 24 ++++++ cmd/tiny/main.go | 37 ++++++--- devenv/docker-compose.yaml | 3 + internal/tiny/api.go | 83 +++++++++++++++++++ internal/tiny/service.go | 45 ++++++++++ internal/tiny/tiny.go | 31 ------- pkg/apiserver/echo.go | 18 ++++ pkg/postgres/postgres.go | 4 + 9 files changed, 222 insertions(+), 59 deletions(-) rename .github/workflows/{makefile.yml => base-ci.yml} (55%) create mode 100644 .github/workflows/migrations-ci.yml create mode 100644 internal/tiny/api.go create mode 100644 internal/tiny/service.go delete mode 100644 internal/tiny/tiny.go create mode 100644 pkg/apiserver/echo.go diff --git a/.github/workflows/makefile.yml b/.github/workflows/base-ci.yml similarity index 55% rename from .github/workflows/makefile.yml rename to .github/workflows/base-ci.yml index 760a6bb..06548ec 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/base-ci.yml @@ -1,4 +1,4 @@ -name: Makefile CI +name: Base CI Jobs on: push: @@ -7,10 +7,9 @@ on: branches: [ "main" ] jobs: - build: - + lint-fmt: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v4 with: @@ -18,28 +17,29 @@ jobs: - uses: actions/setup-go@v4 with: go-version: '^1.21.3' # The Go version to download (if necessary) and use. - - - name: Run OAPI-Generate - run: make gen - name: Run Lint Go run: make lint-go - - name: Find modified migrations - run: | - modified_migrations=$(git diff --name-only origin/$GITHUB_BASE_REF...origin/$GITHUB_HEAD_REF 'migrations/*.sql') - echo "$modified_migrations" - echo "::set-output name=file_names::$modified_migrations" - id: modified-migrations - - uses: sbdchd/squawk-action@v1 - with: - pattern: ${{ steps.modified-migrations.outputs.file_names }} - - name: Run Format run: make fmt + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v4 + with: + go-version: '^1.21.3' # The Go version to download (if necessary) and use. + + - name: Run OAPI-Generate + run: make gen + - name: Make Build Go run: make build-go - name: Make Build Docker Images - run: make build-image + run: make build-image \ No newline at end of file diff --git a/.github/workflows/migrations-ci.yml b/.github/workflows/migrations-ci.yml new file mode 100644 index 0000000..8278c0f --- /dev/null +++ b/.github/workflows/migrations-ci.yml @@ -0,0 +1,24 @@ +name: Migrations CI Jobs + +on: + pull_request: + branches: [ "main" ] + +jobs: + migrations: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Find modified migrations + run: | + modified_migrations=$(git diff --name-only origin/$GITHUB_BASE_REF...origin/$GITHUB_HEAD_REF 'migrations/*.sql') + echo "$modified_migrations" + echo "::set-output name=file_names::$modified_migrations" + id: modified-migrations + - uses: sbdchd/squawk-action@v1 + with: + pattern: ${{ steps.modified-migrations.outputs.file_names }} \ No newline at end of file diff --git a/cmd/tiny/main.go b/cmd/tiny/main.go index 1d46406..797eb44 100644 --- a/cmd/tiny/main.go +++ b/cmd/tiny/main.go @@ -3,18 +3,28 @@ package main import ( "context" "fmt" + "log" + "log/slog" + "net/http" "net/url" "os" "os/signal" "syscall" - "time" "github.com/spf13/cobra" "sanathk.com/tinyurl/internal/tiny" "sanathk.com/tinyurl/internal/tiny/db" + srvStub "sanathk.com/tinyurl/pkg/api/services/v1/tiny" + "sanathk.com/tinyurl/pkg/apiserver" "sanathk.com/tinyurl/pkg/postgres" ) +const ( + // TODO: this should be definied and configured via env var/flags + servicePort = "8080" + defaultAPIPath = "/api/v1" +) + func main() { if err := cmd(); err != nil { os.Exit(1) @@ -33,6 +43,7 @@ func cmd() error { } func run() error { + slog.Info("running TinyURL service") ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) defer cancel() @@ -58,14 +69,20 @@ func run() error { return err } - ticker := time.NewTicker(10 * time.Second) - for { - select { - case <-ticker.C: - svc.Hello() - case <-time.After(5 * time.Minute): - ticker.Stop() - return nil - } + api, err := tiny.NewAPIHandler(ctx, svc) + if err != nil { + return err + } + + srv, err := apiserver.NewEchoServer() + if err != nil { + return err + } + srv.Group(defaultAPIPath) + srvStub.RegisterHandlersWithBaseURL(srv, api, defaultAPIPath) + + if err := srv.Start(fmt.Sprintf(":%s", servicePort)); err != http.ErrServerClosed { + log.Fatal(err) } + return nil } diff --git a/devenv/docker-compose.yaml b/devenv/docker-compose.yaml index bea4c82..6f0a510 100644 --- a/devenv/docker-compose.yaml +++ b/devenv/docker-compose.yaml @@ -7,6 +7,8 @@ services: depends_on: postgres: condition: service_healthy + ports: + - "8080:8080" postgres: image: postgres:16.1 healthcheck: @@ -20,6 +22,7 @@ services: - postgres-persisted:/var/lib/postgresql/data environment: POSTGRES_USER: postgres + PGUSER: postgres POSTGRES_PASSWORD: admin POSTGRES_DB: tiny ports: diff --git a/internal/tiny/api.go b/internal/tiny/api.go new file mode 100644 index 0000000..6d12bec --- /dev/null +++ b/internal/tiny/api.go @@ -0,0 +1,83 @@ +package tiny + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/labstack/echo/v4" + openapi_types "github.com/oapi-codegen/runtime/types" + srvType "sanathk.com/tinyurl/pkg/api/services/v1/tiny" +) + +type APIHandler struct { + svc *Service +} + +// TODO: potentially maintain this type on the OpenAPI Spec as it is for responding back to the API? +type APIError struct { + Message string `json:"message"` + Timestamp string `json:"timestamp"` // in UTC +} + +type Error struct { + APIError +} + +func (e Error) Error() string { + return fmt.Sprintf("[%v] %s", e.Timestamp, e.Message) +} + +func NewAPIHandler(ctx context.Context, svc *Service) (*APIHandler, error) { + slog.Info("initializing a TinyURL API Handler") + + return &APIHandler{ + svc: svc, + }, nil +} + +func (a *APIHandler) CreateTinyURL(ec echo.Context) error { + slog.Info("handling a CreateTinyURL API request") + body := srvType.TinyURLRequest{} + + err := ec.Bind(&body) + if err != nil { + slog.Error("could not process request body to create a TinyURL", slog.Any("error", err.Error())) + ae := APIError{ + Message: "could not process request body to create a TinyURL", + Timestamp: time.Now().UTC().String(), + } + return ec.JSON( + http.StatusBadRequest, Error{ae}, + ) + } + + var resp TinyURLResponse + req := TinyURLRequest{ + Expiry: body.Expiry.Time, + Original: body.Original, + } + resp, err = a.svc.CreateTinyURL(ec, req) + if err != nil { + slog.Error("could not complete creating TinyURL", slog.Any("error", err.Error())) + ae := APIError{ + // What actionable action can we provide to the user here? + // As this error message by itself is not very useful + Message: "could not complete creating TinyURL", + Timestamp: time.Now().UTC().String(), + } + return ec.JSON( + http.StatusInternalServerError, Error{ae}, + ) + } + + return ec.JSON( + http.StatusOK, srvType.TinyURLResponse{ + Expiry: openapi_types.Date{Time: resp.Expiry}, + Original: resp.Original, + Tinyurl: resp.Tinyurl, + }, + ) +} diff --git a/internal/tiny/service.go b/internal/tiny/service.go new file mode 100644 index 0000000..5e947f6 --- /dev/null +++ b/internal/tiny/service.go @@ -0,0 +1,45 @@ +package tiny + +import ( + "context" + "io/fs" + "log/slog" + "time" + + "github.com/labstack/echo/v4" +) + +type DatabaseInterface interface { + Migrate(files fs.FS, path string) error +} + +type TinyURLRequest struct { + Expiry time.Time + Original string +} + +type TinyURLResponse struct { + Expiry time.Time + Original string + Tinyurl string +} + +type Service struct { + db DatabaseInterface +} + +func NewService(ctx context.Context, db DatabaseInterface) (*Service, error) { + slog.Info("init TinyURL internal service") + return &Service{ + db: db, + }, nil +} + +func (s *Service) CreateTinyURL(ec echo.Context, req TinyURLRequest) (TinyURLResponse, error) { + slog.Info("processing a TinyURL request") + return TinyURLResponse{ + Expiry: req.Expiry, + Original: req.Original, + Tinyurl: "sanathk.com/tinyurl123", + }, nil +} diff --git a/internal/tiny/tiny.go b/internal/tiny/tiny.go deleted file mode 100644 index 0c38d39..0000000 --- a/internal/tiny/tiny.go +++ /dev/null @@ -1,31 +0,0 @@ -package tiny - -import ( - "context" - "fmt" - "io/fs" - - "github.com/jackc/pgx/v5" - "sanathk.com/tinyurl/pkg/postgres" -) - -type Postgreser interface { - Conn() *pgx.Conn - Migrate(files fs.FS, path string) error -} - -type Service struct { - pg Postgreser -} - -// URL should include scheme://username:password@address:port/databasename -// example: "postgres://admin:admin@localhost:5432/testdb" -func NewService(ctx context.Context, pg postgres.Postgres) (*Service, error) { - return &Service{ - pg: pg, - }, nil -} - -func (s *Service) Hello() { - fmt.Println("hello") -} diff --git a/pkg/apiserver/echo.go b/pkg/apiserver/echo.go new file mode 100644 index 0000000..379776a --- /dev/null +++ b/pkg/apiserver/echo.go @@ -0,0 +1,18 @@ +package apiserver + +import ( + "log/slog" + + "github.com/labstack/echo/v4" +) + +type EchoServer struct { + *echo.Echo +} + +func NewEchoServer() (*EchoServer, error) { + slog.Info("creating an echo server") + return &EchoServer{ + echo.New(), + }, nil +} diff --git a/pkg/postgres/postgres.go b/pkg/postgres/postgres.go index 9dd68e3..526b1c9 100644 --- a/pkg/postgres/postgres.go +++ b/pkg/postgres/postgres.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/fs" + "log/slog" "net/url" "github.com/golang-migrate/migrate/v4" @@ -35,6 +36,7 @@ func NewPostgres(ctx context.Context, connString *url.URL) (Postgres, error) { } func (pg Postgres) connect(ctx context.Context) (*pgx.Conn, error) { + slog.Info("connecting to postgres") return pgx.Connect(ctx, pg.connString.String()) } @@ -43,6 +45,7 @@ func (pg Postgres) Conn() *pgx.Conn { } func (pg Postgres) Migrate(files fs.FS, path string) error { + slog.Info("migrating database schema to postgres") c, err := pgx.ParseConfig(pg.connString.String()) if err != nil { return fmt.Errorf("parsing postgres connString: %w", err) @@ -76,5 +79,6 @@ func (pg Postgres) Migrate(files fs.FS, path string) error { } return fmt.Errorf("migrating up: %w", err) } + slog.Info("successfully migrated database schema in postgres") return nil }