Skip to content

Commit

Permalink
Merge pull request #58 from bldg14/kf/functional-core-imperative-shell
Browse files Browse the repository at this point in the history
functional core imperative shell
  • Loading branch information
kevinfalting authored Aug 11, 2024
2 parents 0c3ccec + fe7d8ad commit 65e1305
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 130 deletions.
83 changes: 29 additions & 54 deletions cmd/eventual/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,60 @@ package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/kevinfalting/mux"
"github.com/kevinfalting/structconf"
"github.com/bldg14/eventual/internal/shell/http"
"github.com/bldg14/eventual/internal/shell/storage"

"github.com/bldg14/eventual/internal/event"
"github.com/bldg14/eventual/internal/event/stub"
"github.com/bldg14/eventual/internal/middleware"
"github.com/kevinfalting/structconf"
)

func main() {
if err := run(); err != nil {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

if err := run(ctx, os.Args[1:]); err != nil {
log.Fatal(err)
}
}

func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

flagEnv := flag.String("env", EnvLocal, "environment this server is running in")
flag.Parse()
func run(ctx context.Context, args []string) error {
fset := flag.NewFlagSet("eventual", flag.ExitOnError)
flagEnv := fset.String("env", EnvLocal, "environment this server is running in")
if err := fset.Parse(args); err != nil {
return fmt.Errorf("failed to Parse flags: %w", err)
}

cfg := Config(*flagEnv)
if err := structconf.Parse(ctx, &cfg); err != nil {
return fmt.Errorf("failed to Parse config: %w", err)
}

pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
dbURL, err := url.Parse(cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("failed to get new pool: %w", err)
}

if err := pool.Ping(ctx); err != nil {
return fmt.Errorf("failed to Ping: %w", err)
return fmt.Errorf("failed to Parse DatabaseURL: %w", err)
}

api := mux.New(
middleware.CORS(strings.Split(cfg.AllowedOrigins, ",")...),
)

eh := mux.ErrorHandler{
ErrWriter: os.Stderr,
ErrFunc: http.Error,
pool, err := storage.NewPool(ctx, storage.Config{
DatabaseURL: dbURL,
})
if err != nil {
return fmt.Errorf("failed to NewPool: %w", err)
}

api.Handle("/api/v1/events", mux.Methods(
mux.WithGET(eh.Err(HandleGetAllEvents)),
))

server := http.Server{
Handler: api,
Addr: fmt.Sprintf(":%d", cfg.Port),
server, err := http.NewServer(http.Config{
Port: cfg.Port,
AllowedOrigins: strings.Split(cfg.AllowedOrigins, ","),
Pool: pool,
})
if err != nil {
return fmt.Errorf("failed to NewServer: %w", err)
}

serverError := make(chan error, 1)
Expand All @@ -74,6 +67,7 @@ func run() error {
select {
case err := <-serverError:
return fmt.Errorf("failed to ListenAndServe: %w", err)

case <-ctx.Done():
if err := server.Shutdown(context.Background()); err != nil {
return fmt.Errorf("failed to Shutdown: %w", err)
Expand All @@ -82,22 +76,3 @@ func run() error {

return nil
}

func HandleGetAllEvents(w http.ResponseWriter, r *http.Request) error {
var eventStoreStub stub.Stub
events, err := event.GetAll(eventStoreStub)
if err != nil {
return mux.Error(fmt.Errorf("HandleGetAllEvents failed to GetAll: %w", err), http.StatusInternalServerError)
}

result, err := json.Marshal(events)
if err != nil {
return mux.Error(fmt.Errorf("HandleGetAllEvents failed to Marshal: %w", err), http.StatusInternalServerError)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(result)

return nil
}
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kevinfalting/mux v0.1.0 h1:MaIw4vBERtpuieXsBDctxFXa0MH7cvenVuuxhaG1FSk=
github.com/kevinfalting/mux v0.1.0/go.mod h1:CtyQSnYs4qrELIxoCuZMKx0Diox29jxYcBmoawzsReU=
github.com/kevinfalting/mux v0.2.0 h1:oPZLFksusZ17vpECAyraw6dgg0w1cxskZ2FBR0or1Pk=
github.com/kevinfalting/mux v0.2.0/go.mod h1:CtyQSnYs4qrELIxoCuZMKx0Diox29jxYcBmoawzsReU=
github.com/kevinfalting/structconf v0.1.0 h1:0wmv2eja3khzGBYTOLZ9t7+aP1EIQvrDZUmGYQAxyZA=
Expand Down
28 changes: 0 additions & 28 deletions internal/event/event.go

This file was deleted.

41 changes: 26 additions & 15 deletions internal/middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package middleware

import (
"errors"
"net/http"
"strings"

Expand All @@ -17,28 +18,17 @@ import (
// must include the full url, including the scheme. If you need a wildcarded
// subdomain, prepend the allowed origin with "*." but do not include a scheme.
// Schemes for wildcarded subdomains are not supported (yet).
func CORS(allowedOrigins ...string) mux.Middleware {
origins := make(map[string]bool, len(allowedOrigins))
for _, allowedOrigin := range allowedOrigins {
if allowedOrigin != "" {
origins[allowedOrigin] = true
}
}

if len(origins) == 0 {
panic("allowedOrigins must not be empty")
}

func CORS(allowedOrigins AllowedOrigins) mux.Middleware {
isOriginAllowed := func(origin string) bool {
if _, ok := origins["*"]; ok {
if _, ok := allowedOrigins.origins["*"]; ok {
return true
}

if _, ok := origins[origin]; ok {
if _, ok := allowedOrigins.origins[origin]; ok {
return true
}

for allowedOrigin := range origins {
for allowedOrigin := range allowedOrigins.origins {
if !strings.HasPrefix(allowedOrigin, "*.") {
continue
}
Expand All @@ -62,3 +52,24 @@ func CORS(allowedOrigins ...string) mux.Middleware {
})
}
}

type AllowedOrigins struct {
origins map[string]struct{}
}

func ParseAllowedOrigins(allowedOrigins ...string) (AllowedOrigins, error) {
origins := map[string]struct{}{}
for _, origin := range allowedOrigins {
if origin == "" {
continue
}

origins[origin] = struct{}{}
}

if len(origins) == 0 {
return AllowedOrigins{}, errors.New("allowedOrigins must not be empty")
}

return AllowedOrigins{origins: origins}, nil
}
48 changes: 22 additions & 26 deletions internal/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,44 @@ import (

func TestCORS(t *testing.T) {
tests := []struct {
name string
allowedOrigins []string
origin string
expectHeader string
expectPanic bool
name string
allowedOrigins []string
origin string
expectHeader string
expectBadOrigins bool
}{
{
name: "single allowedOrigins",
allowedOrigins: []string{"https://test.com"},
origin: "https://test.com",
expectHeader: "https://test.com",
expectPanic: false,
},
{
name: "multiple allowedOrigins",
allowedOrigins: []string{"https://test.com", "https://test2.com"},
origin: "https://test2.com",
expectHeader: "https://test2.com",
expectPanic: false,
},
{
name: "empty allowedOrigins",
allowedOrigins: []string{"https://test.com", "", "https://test2.com"},
origin: "https://test.com",
expectHeader: "https://test.com",
expectPanic: false,
},
{
name: "no allowedOrigins",
allowedOrigins: []string{},
expectPanic: true,
name: "no allowedOrigins",
allowedOrigins: []string{},
expectBadOrigins: true,
},
{
name: "nil allowedOrigins",
allowedOrigins: nil,
expectPanic: true,
name: "nil allowedOrigins",
allowedOrigins: nil,
expectBadOrigins: true,
},
{
name: "all empty allowedOrigins",
allowedOrigins: []string{"", "", ""},
expectPanic: true,
name: "all empty allowedOrigins",
allowedOrigins: []string{"", "", ""},
expectBadOrigins: true,
},
{
name: "wildcard allowedOrigins",
Expand Down Expand Up @@ -86,15 +83,16 @@ func TestCORS(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
isPanicking := true
defer func() {
r := recover()
if isPanicking && !test.expectPanic {
t.Errorf("expected no panic, got %v", r)
allowedOrigins, err := middleware.ParseAllowedOrigins(test.allowedOrigins...)
if test.expectBadOrigins {
if err != nil {
return
}
}()

mw := middleware.CORS(test.allowedOrigins...)
t.Fatalf("expected error but got nothing")
}

mw := middleware.CORS(allowedOrigins)

h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand All @@ -120,8 +118,6 @@ func TestCORS(t *testing.T) {
if w.Header().Get("Access-Control-Allow-Headers") != "Content-Type, Authorization" {
t.Errorf("expected %q, got %q", "Content-Type, Authorization", w.Header().Get("Access-Control-Allow-Headers"))
}

isPanicking = false
})
}
}
39 changes: 39 additions & 0 deletions internal/shell/app/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package app

import (
"fmt"
"time"

"github.com/bldg14/eventual/internal/shell/storage"
"github.com/jackc/pgx/v5/pgxpool"
)

type Event struct {
Title string `json:"title"`
Start string `json:"start"`
End string `json:"end"`
Description string `json:"description"`
URL string `json:"url"`
Email string `json:"email"`
}

func GetAllEvents(pool *pgxpool.Pool) ([]Event, error) {
storageEvents, err := storage.GetAll(pool)
if err != nil {
return nil, fmt.Errorf("HandleGetAllEvents failed to GetAll: %w", err)
}

events := make([]Event, len(storageEvents))
for i, storageEvent := range storageEvents {
events[i] = Event{
Title: storageEvent.Title,
Start: storageEvent.Start.Format(time.RFC3339),
End: storageEvent.End.Format(time.RFC3339),
Description: storageEvent.Description,
URL: storageEvent.URL,
Email: storageEvent.Email,
}
}

return events, nil
}
31 changes: 31 additions & 0 deletions internal/shell/http/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package http

import (
"encoding/json"
"fmt"
"net/http"

"github.com/bldg14/eventual/internal/shell/app"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/kevinfalting/mux"
)

func HandleGetAllEvents(pool *pgxpool.Pool) mux.ErrHandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
events, err := app.GetAllEvents(pool)
if err != nil {
return mux.Error(fmt.Errorf("HandleGetAllEvents failed to GetAllEvents: %w", err), http.StatusInternalServerError)
}

result, err := json.Marshal(events)
if err != nil {
return mux.Error(fmt.Errorf("HandleGetAllEvents failed to Marshal: %w", err), http.StatusInternalServerError)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(result)

return nil
}
}
Loading

0 comments on commit 65e1305

Please sign in to comment.