diff --git a/.gitignore b/.gitignore index 4774115..11c378f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build bin host/host +.DS_Store diff --git a/README.org b/README.org index c8e5900..53617d5 100644 --- a/README.org +++ b/README.org @@ -9,20 +9,18 @@ If you are looking for the original event-driven GTD sample check out * Folder Structure -Solution is composed from the core infrastructure and actual domain implementation. +# core - event-driven infrastructure and specs -*Infrastructure*: +This folder contains core infrastructure for prototyping event-driven +back-ends. You can import it in your go and move from there. -- =bin= - temporary folder with compiled binaries -- =core= - event-driven infrastructure and spec runners -- =etc= - misc files (no source code) -- =host= - entry point for the process +- =root= - binary-sortable UUID and a definition of an event +- =api= - logic for hosting a simple JSON API (with some helpers) +- =bus= - event bus and an in-memory implementation +- =log= - helpers to setup logging +- =env= - environment for defining modules and specs (contracts) +- =specs= - express, verify and print event-driven specifications +- =hosting= - wire and run modules in a process -*Domain*: - -- =lang= - domain language (value objects and events) -- =task= - module for editing and managing tasks -- =views= - module with basic views for the client -- /all the rest/ - event-driven modules with features diff --git a/core/api/error-response.go b/api/error-response.go similarity index 100% rename from core/api/error-response.go rename to api/error-response.go diff --git a/core/api/error.go b/api/error.go similarity index 100% rename from core/api/error.go rename to api/error.go diff --git a/core/api/interface.go b/api/interface.go similarity index 100% rename from core/api/interface.go rename to api/interface.go diff --git a/core/api/json_response.go b/api/json_response.go similarity index 100% rename from core/api/json_response.go rename to api/json_response.go diff --git a/core/api/request.go b/api/request.go similarity index 100% rename from core/api/request.go rename to api/request.go diff --git a/core/api/web.go b/api/web.go similarity index 100% rename from core/api/web.go rename to api/web.go diff --git a/core/bus/log.go b/bus/log.go similarity index 100% rename from core/bus/log.go rename to bus/log.go diff --git a/core/bus/memory.go b/bus/memory.go similarity index 100% rename from core/bus/memory.go rename to bus/memory.go diff --git a/core/readme.md b/core/readme.md deleted file mode 100644 index 732c6d7..0000000 --- a/core/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -# core - event-driven infrastructure and specs - -This folder contains core infrastructure for prototyping event-driven -back-ends. You can import it in your go and move from there. - -* `root` - binary-sortable UUID and a definition of an event -* `api` - logic for hosting a simple JSON API (with some helpers) -* `bus` - event bus and an in-memory implementation -* `log` - helpers to setup logging -* `env` - environment for defining modules and specs (contracts) -* `specs` - express, verify and print event-driven specifications -* `hosting` - wire and run modules in a process - - - diff --git a/core/env/container.go b/env/container.go similarity index 100% rename from core/env/container.go rename to env/container.go diff --git a/core/env/interface.go b/env/interface.go similarity index 100% rename from core/env/interface.go rename to env/interface.go diff --git a/core/event.go b/event.go similarity index 100% rename from core/event.go rename to event.go diff --git a/host/main.go b/host/main.go deleted file mode 100644 index 216bbfe..0000000 --- a/host/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gorilla/mux" - - "github.com/abdullin/omni/core/bus" - "github.com/abdullin/omni/core/env" - "github.com/abdullin/omni/task" - "github.com/abdullin/omni/views" - - "github.com/abdullin/omni/core/hosting" - - "github.com/op/go-logging" -) - -var l = logging.MustGetLogger("main") - -func main() { - - router := mux.NewRouter() - memBus := bus.NewMem() - - var wrap = bus.WrapWithLogging(memBus) - modules := factory(wrap) - var host = hosting.New(modules) - - host.WireHttp(router) - host.WireHandlers(memBus) - - memBus.Start() - - bind := ":8001" - l.Info("Listening at %v", bind) - l.Panic(http.ListenAndServe(bind, router)) -} - -func factory(pub env.Publisher) []env.Module { - return []env.Module{ - views.NewModule(pub), - task.NewModule(pub), - } -} diff --git a/core/hosting/context.go b/hosting/context.go similarity index 100% rename from core/hosting/context.go rename to hosting/context.go diff --git a/core/hosting/wire.go b/hosting/wire.go similarity index 100% rename from core/hosting/wire.go rename to hosting/wire.go diff --git a/core/id.go b/id.go similarity index 100% rename from core/id.go rename to id.go diff --git a/lang/events.go b/lang/events.go deleted file mode 100644 index 1ad5933..0000000 --- a/lang/events.go +++ /dev/null @@ -1,92 +0,0 @@ -package lang - -import . "github.com/abdullin/omni/core" - -func i(contract string, eventId EventId) *Info { - return NewInfo(contract, eventId.Id) -} - -type TaskAdded struct { - EventId EventId `json:"eventId,omitempty"` - TaskId TaskId `json:"taskId,omitempty"` - Name string `json:"name"` -} - -func NewTaskAdded(event EventId, task TaskId, name string) *TaskAdded { - return &TaskAdded{event, task, name} -} - -type TaskMovedToInbox struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskMovedToInbox(event EventId, task TaskId) *TaskMovedToInbox { - return &TaskMovedToInbox{event, task} -} - -func (e *TaskMovedToInbox) Meta() *Info { return i("TaskMovedToInbox", e.EventId) } - -type TaskRemoved struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskRemoved(event EventId, task TaskId) *TaskRemoved { - return &TaskRemoved{event, task} -} - -type TaskChecked struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskChecked(event EventId, task TaskId) *TaskChecked { - return &TaskChecked{event, task} -} - -type TaskUnchecked struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskUnhecked(event EventId, task TaskId) *TaskUnchecked { - return &TaskUnchecked{event, task} -} - -type ContextCreated struct { - EventId EventId `json:"eventId"` - ContextId ContextId `json:"contextId"` - Name string `json:"name"` -} - -func NewContextCreated(event EventId, context ContextId, name string) *ContextCreated { - return &ContextCreated{event, context, name} -} - -type TaskStarred struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskStarred(event EventId, task TaskId) *TaskStarred { - return &TaskStarred{event, task} -} - -type TaskUnstarred struct { - EventId EventId `json:"eventId"` - TaskId TaskId `json:"taskId"` -} - -func NewTaskUnstarred(event EventId, task TaskId) *TaskUnstarred { - return &TaskUnstarred{event, task} -} - -func (e *TaskAdded) Meta() *Info { return i("TaskAdded", e.EventId) } -func (e *TaskChecked) Meta() *Info { return i("TaskChecked", e.EventId) } -func (e *TaskStarred) Meta() *Info { return i("TaskStarred", e.EventId) } -func (e *TaskUnstarred) Meta() *Info { return i("TaskUnstarred", e.EventId) } - -func (e *TaskRemoved) Meta() *Info { return i("TaskRemoved", e.EventId) } - -func (e *ContextCreated) Meta() *Info { return i("ContextCreated", e.EventId) } diff --git a/lang/ids.go b/lang/ids.go deleted file mode 100644 index 0f62a85..0000000 --- a/lang/ids.go +++ /dev/null @@ -1,22 +0,0 @@ -package lang - -import . "github.com/abdullin/omni/core" - -type TaskId struct{ Id } -type EventId struct{ Id } -type ContextId struct{ Id } - -func NewTaskId() TaskId { - return TaskId{NewId()} -} - -func NewEventId() EventId { - return EventId{NewId()} -} - -func NewContextId() ContextId { - return ContextId{NewId()} -} - -var NoEventId = EventId{} -var NoTaskId = TaskId{} diff --git a/lang/readme.md b/lang/readme.md deleted file mode 100644 index 64c8dd2..0000000 --- a/lang/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# Package lang - -This package contains domain language for this backend: - -* `ids.go` - strongly-typed identities -* `events.go` - events (part of the interchange context) diff --git a/core/log/setup.go b/log/setup.go similarity index 100% rename from core/log/setup.go rename to log/setup.go diff --git a/core/spec/Makefile b/spec/Makefile similarity index 100% rename from core/spec/Makefile rename to spec/Makefile diff --git a/core/spec/context.go b/spec/context.go similarity index 100% rename from core/spec/context.go rename to spec/context.go diff --git a/core/spec/marshal.go b/spec/marshal.go similarity index 100% rename from core/spec/marshal.go rename to spec/marshal.go diff --git a/core/spec/publisher.go b/spec/publisher.go similarity index 100% rename from core/spec/publisher.go rename to spec/publisher.go diff --git a/core/spec/report.go b/spec/report.go similarity index 100% rename from core/spec/report.go rename to spec/report.go diff --git a/core/spec/sanity.go b/spec/sanity.go similarity index 100% rename from core/spec/sanity.go rename to spec/sanity.go diff --git a/core/spec/syntax_then_events.go b/spec/syntax_then_events.go similarity index 100% rename from core/spec/syntax_then_events.go rename to spec/syntax_then_events.go diff --git a/core/spec/syntax_then_response.go b/spec/syntax_then_response.go similarity index 100% rename from core/spec/syntax_then_response.go rename to spec/syntax_then_response.go diff --git a/core/spec/syntax_when_request.go b/spec/syntax_when_request.go similarity index 100% rename from core/spec/syntax_when_request.go rename to spec/syntax_when_request.go diff --git a/core/spec/syntax_where.go b/spec/syntax_where.go similarity index 100% rename from core/spec/syntax_where.go rename to spec/syntax_where.go diff --git a/core/spec/verify.go b/spec/verify.go similarity index 100% rename from core/spec/verify.go rename to spec/verify.go diff --git a/task/denormalizer.go b/task/denormalizer.go deleted file mode 100644 index e5ac42c..0000000 --- a/task/denormalizer.go +++ /dev/null @@ -1,9 +0,0 @@ -package task - -type denormalizer struct { - store *store -} - -func newDenormalizer(s *store) *denormalizer { - return &denormalizer{s} -} diff --git a/task/module.go b/task/module.go deleted file mode 100644 index 46b208b..0000000 --- a/task/module.go +++ /dev/null @@ -1,27 +0,0 @@ -package task - -import "github.com/abdullin/omni/core/env" - -type Module struct { - pub env.Publisher - d *denormalizer - s *store -} - -func NewModule(pub env.Publisher) *Module { - store := newStore() - denormalizer := newDenormalizer(store) - return &Module{pub, denormalizer, store} -} - -func (m *Module) Register(r env.Registrar) { - r.HandleHttp("POST", "/task", m.postTask) - r.HandleHttp("PUT", "/task", m.putTask) - //r.HandleEvents("views-denormalizer", m.d) - //r.ResetData("store", m.s.reset) -} - -var Spec = &env.Spec{ - Name: "task", - UseCases: useCases, -} diff --git a/task/post-task.go b/task/post-task.go deleted file mode 100644 index 9741d96..0000000 --- a/task/post-task.go +++ /dev/null @@ -1,41 +0,0 @@ -package task - -import ( - "github.com/abdullin/omni/core/api" - "github.com/abdullin/omni/lang" -) - -type postTaskRequest struct { - Name string `json:"name"` - Inbox bool `json:"inbox"` -} -type postTaskResponse struct { - TaskId lang.TaskId `json:"taskId"` - Name string `json:"name"` - Inbox bool `json:"inbox"` -} - -func (m *Module) postTask(req *api.Request) api.Response { - var request postTaskRequest - - if err := req.ParseBody(&request); err != nil { - return api.BadRequestResponse(err.Error()) - } - if request.Name == "" { - return api.BadRequestResponse("Expected name") - } - - // TODO - accept array - taskId := lang.NewTaskId() - m.pub.MustPublish(lang.NewTaskAdded(lang.NewEventId(), taskId, request.Name)) - if request.Inbox { - m.pub.MustPublish(lang.NewTaskMovedToInbox(lang.NewEventId(), taskId)) - } - - // track request ID for idempotency - return api.NewJSON(&postTaskResponse{ - TaskId: taskId, - Name: request.Name, - Inbox: request.Inbox, - }) -} diff --git a/task/put-task.go b/task/put-task.go deleted file mode 100644 index 637ef22..0000000 --- a/task/put-task.go +++ /dev/null @@ -1,7 +0,0 @@ -package task - -import "github.com/abdullin/omni/core/api" - -func (m *Module) putTask(req *api.Request) api.Response { - return api.NotImplementedResponse() -} diff --git a/task/store.go b/task/store.go deleted file mode 100644 index 851b45a..0000000 --- a/task/store.go +++ /dev/null @@ -1,7 +0,0 @@ -package task - -type store struct{} - -func newStore() *store { - return &store{} -} diff --git a/task/usecases.go b/task/usecases.go deleted file mode 100644 index 15d28bc..0000000 --- a/task/usecases.go +++ /dev/null @@ -1,88 +0,0 @@ -package task - -import ( - "github.com/abdullin/omni/core/env" - "github.com/abdullin/omni/core/spec" - "github.com/abdullin/omni/lang" - "github.com/abdullin/seq" -) - -var useCases = []env.UseCaseFactory{ - - given_no_task_when_put_then_not_found, - given_unchecked_task_when_check_then_event, - when_post_inbox_task_then_event_is_published, -} - -func newEventId() lang.EventId { - return lang.NewEventId() -} -func newTaskId() lang.TaskId { - return lang.NewTaskId() -} - -var IgnoreEventId lang.EventId - -func given_unchecked_task_when_check_then_event() *env.UseCase { - - taskId := lang.NewTaskId() - - return &env.UseCase{ - Name: "Given new task, when PUT /task with check, then event", - Given: spec.Events( - lang.NewTaskAdded(newEventId(), taskId, "ho-ho"), - ), - When: spec.PutJSON("/task", seq.Map{ - "checked": true, - "taskId": taskId, - }), - ThenResponse: spec.ReturnJSON(seq.Map{ - "taskId": taskId, - "name": "ho-ho", - "checked": true, - "starred": false, - }), - ThenEvents: spec.Events( - lang.NewTaskChecked(IgnoreEventId, taskId), - ), - Where: spec.Where{IgnoreEventId: "ignore"}, - } -} - -func given_no_task_when_put_then_not_found() *env.UseCase { - return &env.UseCase{ - Name: "Given no task, when PUT /task, then Not Found will be returned", - Given: spec.Events(), - When: spec.PutJSON("/task", seq.Map{ - "taskId": newTaskId(), - "checked": true, - }), - ThenResponse: spec.ReturnErrorJSON(404, "Task doesn't exist."), - } -} - -func when_post_inbox_task_then_event_is_published() *env.UseCase { - - newTaskId := lang.NewTaskId() - - return &env.UseCase{ - Name: "When POST /task for inbox, then 2 events are published", - When: spec.PostJSON("/task", seq.Map{ - "name": "NewTask", - "inbox": true, - }), - ThenResponse: spec.ReturnJSON(seq.Map{ - "name": "NewTask", - "inbox": "true", - "taskId": newTaskId, - }), - ThenEvents: spec.Events( - lang.NewTaskAdded(IgnoreEventId, newTaskId, "NewTask"), - lang.NewTaskMovedToInbox(IgnoreEventId, newTaskId), - ), - Where: spec.Where{ - newTaskId: "sameTaskId", - IgnoreEventId: "ignore", - }, - } -} diff --git a/task/usecases_test.go b/task/usecases_test.go deleted file mode 100644 index 96660c3..0000000 --- a/task/usecases_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package task - -import ( - "testing" - - "github.com/abdullin/omni/core/spec" -) - -func TestUseCases(t *testing.T) { - ctx := spec.NewContext(Spec) - mod := NewModule(ctx.Pub()) - ctx.Verify(mod).ToTesting(t) -} diff --git a/views/denormalizer.go b/views/denormalizer.go deleted file mode 100644 index 4720c91..0000000 --- a/views/denormalizer.go +++ /dev/null @@ -1,36 +0,0 @@ -package views - -import ( - "github.com/abdullin/omni/core" - "github.com/abdullin/omni/lang" -) - -type denormalizer struct { - s *store -} - -func newDenormalizer(s *store) *denormalizer { - return &denormalizer{s} -} - -func (d *denormalizer) HandleEvent(e core.Event) error { - switch t := e.(type) { - case *lang.TaskAdded: - d.s.addTask(t.TaskId, t.Name) - case *lang.TaskRemoved: - d.s.removeTask(t.TaskId) - case *lang.TaskMovedToInbox: - d.s.moveTaskToInbox(t.TaskId) - - } - return nil -} - -func (d *denormalizer) Contracts() []string { - return []string{ - "TaskAdded", - "TaskCreated", - "TaskRemoved", - "TaskMovedToInbox", - } -} diff --git a/views/get_inbox.go b/views/get_inbox.go deleted file mode 100644 index 46e6a1b..0000000 --- a/views/get_inbox.go +++ /dev/null @@ -1,29 +0,0 @@ -package views - -import ( - "fmt" - - "github.com/abdullin/omni/core/api" - "github.com/abdullin/omni/lang" -) - -func (m *Module) getInbox(req *api.Request) api.Response { - - type task struct { - TaskId lang.TaskId `json:"taskId"` - Name string `json:"name"` - } - type response struct { - Tasks []task `json:"tasks"` - } - - var items = []task{} - for k, t := range m.s.all { - fmt.Println("Serve", k, t) - items = append(items, task{t.TaskId, t.Name}) - } - - return api.NewJSON(response{ - Tasks: items, - }) -} diff --git a/views/module.go b/views/module.go deleted file mode 100644 index aacde83..0000000 --- a/views/module.go +++ /dev/null @@ -1,28 +0,0 @@ -package views - -import "github.com/abdullin/omni/core/env" - -// basic module with views that don't fit anywhere else - -type Module struct { - pub env.Publisher - d *denormalizer - s *store -} - -func NewModule(pub env.Publisher) *Module { - store := newStore() - denormalizer := newDenormalizer(store) - return &Module{pub, denormalizer, store} -} - -func (m *Module) Register(r env.Registrar) { - r.HandleHttp("GET", "/views/inbox", m.getInbox) - r.HandleEvents("views-denormalizer", m.d) - r.ResetData("store", m.s.reset) -} - -var Spec = &env.Spec{ - Name: "views", - UseCases: useCases, -} diff --git a/views/readme.md b/views/readme.md deleted file mode 100644 index b9c8fcd..0000000 --- a/views/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# Package views - -This package implements an event-driven module with generic views for -the UI: - -* inbox tasks; -* tasks by project; -* tasks by context; -* starred tasks; -* etc. diff --git a/views/store.go b/views/store.go deleted file mode 100644 index 8fc692d..0000000 --- a/views/store.go +++ /dev/null @@ -1,32 +0,0 @@ -package views - -import "github.com/abdullin/omni/lang" - -type store struct { - all map[lang.TaskId]*taskItem -} - -type taskItem struct { - TaskId lang.TaskId - Name string - Inbox bool -} - -func newStore() *store { - return &store{make(map[lang.TaskId]*taskItem)} -} - -func (s *store) reset() { - s.all = make(map[lang.TaskId]*taskItem) -} - -func (s *store) addTask(id lang.TaskId, name string) { - s.all[id] = &taskItem{id, name, false} -} - -func (s *store) removeTask(id lang.TaskId) { - delete(s.all, id) -} -func (s *store) moveTaskToInbox(id lang.TaskId) { - s.all[id].Inbox = true -} diff --git a/views/usecases.go b/views/usecases.go deleted file mode 100644 index 92737c6..0000000 --- a/views/usecases.go +++ /dev/null @@ -1,71 +0,0 @@ -package views - -import ( - "github.com/abdullin/omni/core/env" - "github.com/abdullin/omni/core/spec" - "github.com/abdullin/omni/lang" - "github.com/abdullin/seq" -) - -var useCases = []env.UseCaseFactory{ - given_no_tasks_get_inbox_returns_empty, - given_inbox_task_get_inbox_returns_it, - given_inbox_task_deleted_inbox_returns_nothing, -} - -func given_no_tasks_get_inbox_returns_empty() *env.UseCase { - - return &env.UseCase{ - Name: "Given no tasks, GET /inbox returns nothing", - When: spec.GetJSON("/views/inbox", nil), - ThenResponse: spec.ReturnJSON(seq.Map{ - "tasks.length": 0, - }), - } -} - -func given_inbox_task_get_inbox_returns_it() *env.UseCase { - - e1 := lang.NewTaskAdded(event(), task(), "Write a use case") - e2 := lang.NewTaskMovedToInbox(event(), e1.TaskId) - - return &env.UseCase{ - Name: "Given inbox task, GET /inbox returns it", - Given: spec.GivenEvents(e1, e2), - When: spec.GetJSON("/views/inbox", nil), - ThenResponse: spec.ReturnJSON(seq.Map{ - "tasks": seq.Map{ - "length": 1, - "[0]": seq.Map{ - "name": e1.Name, - "taskId": e1.TaskId, - }, - }, - }), - } -} - -func given_inbox_task_deleted_inbox_returns_nothing() *env.UseCase { - - e1 := lang.NewTaskAdded(event(), task(), "Write a use case") - e2 := lang.NewTaskMovedToInbox(event(), e1.TaskId) - e3 := lang.NewTaskRemoved(event(), e1.TaskId) - - return &env.UseCase{ - Name: "Given inbox task that is deleted, GET /inbox returns nothing", - Given: spec.GivenEvents(e1, e2, e3), - When: spec.GetJSON("/views/inbox", nil), - ThenResponse: spec.ReturnJSON(seq.Map{ - "tasks": seq.Map{ - "length": 0, - }, - }), - } -} - -func event() lang.EventId { - return lang.NewEventId() -} -func task() lang.TaskId { - return lang.NewTaskId() -} diff --git a/views/usecases_test.go b/views/usecases_test.go deleted file mode 100644 index bb249b4..0000000 --- a/views/usecases_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package views - -import ( - "testing" - - "github.com/abdullin/omni/core/spec" -) - -func TestUseCases(t *testing.T) { - ctx := spec.NewContext(Spec) - mod := NewModule(ctx.Pub()) - ctx.Verify(mod).ToTesting(t) -}