diff options
| author | mo khan <mo@mokhan.ca> | 2025-04-10 17:35:46 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-04-10 17:35:46 -0600 |
| commit | d8fd76b1c0c0bd86b2114dcf6af0b6e34d4783f3 (patch) | |
| tree | 32d71c0e6a9a0c25789ab90a29ef742001898afc /pkg | |
| parent | a722bd0ef4f69547229921eb0fe2eefce3ed6397 (diff) | |
feat: add a single API endpoint to return a list of sparkles
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/db/repository.go | 45 | ||||
| -rw-r--r-- | pkg/db/repository_test.go | 41 | ||||
| -rw-r--r-- | pkg/domain/sparkle.go | 50 | ||||
| -rw-r--r-- | pkg/domain/sparkle_test.go | 51 | ||||
| -rw-r--r-- | pkg/pls/ulid.go | 16 | ||||
| -rw-r--r-- | pkg/web/server.go | 33 | ||||
| -rw-r--r-- | pkg/web/server_test.go | 37 |
7 files changed, 273 insertions, 0 deletions
diff --git a/pkg/db/repository.go b/pkg/db/repository.go new file mode 100644 index 0000000..d8a5990 --- /dev/null +++ b/pkg/db/repository.go @@ -0,0 +1,45 @@ +package db + +import ( + "gitlab.com/mokhax/sparkled/pkg/domain" + "gitlab.com/mokhax/sparkled/pkg/pls" +) + +type Repository interface { + All() []*domain.Sparkle + Each(func(*domain.Sparkle)) + Save(*domain.Sparkle) error +} + +type inMemoryRepository struct { + sparkles []*domain.Sparkle +} + +func NewRepository() Repository { + return &inMemoryRepository{ + sparkles: []*domain.Sparkle{}, + } +} + +func (r *inMemoryRepository) All() []*domain.Sparkle { + return r.sparkles +} + +func (r *inMemoryRepository) Each(visitor func(item *domain.Sparkle)) { + for _, item := range r.All() { + visitor(item) + } +} + +func (r *inMemoryRepository) Save(item *domain.Sparkle) error { + if err := item.Validate(); err != nil { + return err + } + + if item.ID == "" { + item.ID = pls.GenerateULID() + } + + r.sparkles = append(r.sparkles, item) + return nil +} diff --git a/pkg/db/repository_test.go b/pkg/db/repository_test.go new file mode 100644 index 0000000..7a9938c --- /dev/null +++ b/pkg/db/repository_test.go @@ -0,0 +1,41 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/mokhax/sparkled/pkg/domain" +) + +func TestRepository(t *testing.T) { + storage := NewRepository() + + t.Run("Save", func(t *testing.T) { + t.Run("an invalid Sparkle", func(t *testing.T) { + err := storage.Save(&domain.Sparkle{Reason: "because"}) + + counter := 0 + storage.Each(func(item *domain.Sparkle) { + counter++ + }) + + assert.NotNil(t, err) + assert.Equal(t, 0, counter) + }) + + t.Run("a valid Sparkle", func(t *testing.T) { + sparkle := &domain.Sparkle{Sparklee: "@tanuki", Reason: "because"} + require.NoError(t, storage.Save(sparkle)) + + sparkles := []*domain.Sparkle{} + storage.Each(func(item *domain.Sparkle) { + sparkles = append(sparkles, item) + }) + assert.Equal(t, 1, len(sparkles)) + assert.NotEmpty(t, sparkles[0].ID) + assert.Equal(t, "@tanuki", sparkles[0].Sparklee) + assert.Equal(t, "because", sparkles[0].Reason) + }) + }) +} diff --git a/pkg/domain/sparkle.go b/pkg/domain/sparkle.go new file mode 100644 index 0000000..54ccb13 --- /dev/null +++ b/pkg/domain/sparkle.go @@ -0,0 +1,50 @@ +package domain + +import ( + "errors" + "regexp" + + "gitlab.com/mokhax/sparkled/pkg/pls" +) + +type Sparkle struct { + ID string `json:"id" jsonapi:"primary,sparkles"` + Sparklee string `json:"sparklee" jsonapi:"attr,sparklee"` + Reason string `json:"reason" jsonapi:"attr,reason"` +} + +var SparkleRegex = regexp.MustCompile(`\A\s*(?P<sparklee>@\w+)\s+(?P<reason>.+)\z`) +var SparkleeIndex = SparkleRegex.SubexpIndex("sparklee") +var ReasonIndex = SparkleRegex.SubexpIndex("reason") + +var ReasonIsRequired = errors.New("Reason is required") +var SparkleIsEmpty = errors.New("Sparkle is empty") +var SparkleIsInvalid = errors.New("Sparkle is invalid") +var SparkleeIsRequired = errors.New("Sparklee is required") + +func NewSparkle(text string) (*Sparkle, error) { + if len(text) == 0 { + return nil, SparkleIsEmpty + } + + matches := SparkleRegex.FindStringSubmatch(text) + if len(matches) == 0 { + return nil, SparkleIsInvalid + } + + return &Sparkle{ + ID: pls.GenerateULID(), + Sparklee: matches[SparkleeIndex], + Reason: matches[ReasonIndex], + }, nil +} + +func (s *Sparkle) Validate() error { + if s.Sparklee == "" { + return SparkleeIsRequired + } + if s.Reason == "" { + return ReasonIsRequired + } + return nil +} diff --git a/pkg/domain/sparkle_test.go b/pkg/domain/sparkle_test.go new file mode 100644 index 0000000..8d81afd --- /dev/null +++ b/pkg/domain/sparkle_test.go @@ -0,0 +1,51 @@ +package domain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSparkle(t *testing.T) { + t.Run("NewSparkle", func(t *testing.T) { + t.Run("with a valid body", func(t *testing.T) { + sparkle, err := NewSparkle("@tanuki for helping me with my homework!") + + assert.Nil(t, err) + if err != nil { + assert.Equal(t, "@tanuki", sparkle.Sparklee) + assert.Equal(t, "for helping me with my homework!", sparkle.Reason) + } + }) + + t.Run("with an empty body", func(t *testing.T) { + sparkle, err := NewSparkle("") + + assert.Nil(t, sparkle) + assert.NotNil(t, err) + if err != nil { + assert.Equal(t, "Sparkle is empty", err.Error()) + } + }) + + t.Run("without a reason", func(t *testing.T) { + sparkle, err := NewSparkle("@tanuki") + + assert.Nil(t, sparkle) + assert.NotNil(t, err) + if err != nil { + assert.Equal(t, "Sparkle is invalid", err.Error()) + } + }) + + t.Run("without a username", func(t *testing.T) { + sparkle, err := NewSparkle("for helping me with my homework") + + assert.Nil(t, sparkle) + assert.NotNil(t, err) + if err != nil { + assert.Equal(t, "Sparkle is invalid", err.Error()) + } + }) + }) +} diff --git a/pkg/pls/ulid.go b/pkg/pls/ulid.go new file mode 100644 index 0000000..27e7d6c --- /dev/null +++ b/pkg/pls/ulid.go @@ -0,0 +1,16 @@ +package pls + +import ( + "math/rand" + "time" + + "github.com/oklog/ulid" +) + +func GenerateULID() string { + seed := time.Now().UnixNano() + source := rand.NewSource(seed) + entropy := rand.New(source) + id, _ := ulid.New(ulid.Timestamp(time.Now()), entropy) + return id.String() +} diff --git a/pkg/web/server.go b/pkg/web/server.go new file mode 100644 index 0000000..02f3a41 --- /dev/null +++ b/pkg/web/server.go @@ -0,0 +1,33 @@ +package web + +import ( + "net/http" + + "github.com/google/jsonapi" + "github.com/xlgmokha/x/pkg/serde" + "gitlab.com/mokhax/sparkled/pkg/db" +) + +type Server struct { + db db.Repository + fileserver http.Handler +} + +func NewServer(storage db.Repository) *Server { + return &Server{ + db: storage, + fileserver: http.FileServer(http.Dir("public")), + } +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.URL.String() { + case "/sparkles.json": + switch r.Method { + case "GET": + w.Header().Set("Content-Type", jsonapi.MediaType) + serde.ToJSONAPI(w, s.db.All()) + } + break + } +} diff --git a/pkg/web/server_test.go b/pkg/web/server_test.go new file mode 100644 index 0000000..0281867 --- /dev/null +++ b/pkg/web/server_test.go @@ -0,0 +1,37 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xlgmokha/x/pkg/serde" + "gitlab.com/mokhax/sparkled/pkg/db" + "gitlab.com/mokhax/sparkled/pkg/domain" +) + +func TestServer(t *testing.T) { + t.Run("GET /sparkles.json", func(t *testing.T) { + t.Run("returns the list of sparkles", func(t *testing.T) { + sparkle, _ := domain.NewSparkle("@tanuki for helping me") + store := db.NewRepository() + store.Save(sparkle) + + response := httptest.NewRecorder() + request, err := http.NewRequest("GET", "/sparkles.json", nil) + require.NoError(t, err) + NewServer(store).ServeHTTP(response, request) + + assert.Equal(t, http.StatusOK, response.Code) + + items, err := serde.FromJSONAPI[[]*domain.Sparkle](response.Body) + require.NoError(t, err) + + assert.Equal(t, 1, len(items)) + assert.Equal(t, "@tanuki", items[0].Sparklee) + assert.Equal(t, "for helping me", items[0].Reason) + }) + }) +} |
