Nanyx

Todo app: server

A server whose store is a context, so it runs over a database or an in-memory map.

module todo.server

import (
  todo.shared
  nanyx/time
)

-- A server for the todo protocol. The store is a context, so the same
-- implementation runs over a database in production and over an in-memory
-- map in tests.

context TodoStore = (
  findAll: () -> list(Todo)
  find: TodoId -> #some(Todo) | #notFound
  insert: Todo -> #some(Todo) | #error(#insertNotSuccessful)
  update: Todo -> #some(Todo) | #error(#updateNotSuccessful)
  delete: TodoId -> #some(()) | #error(#deleteNotSuccessful)
)

-- The store assigns the id
def newTodo: [Clock] string -> Todo = { description ->
  Todo(id = 0, description = description, completed = false, dateAdded = Clock.now)
}

def exists: (#some(Todo) | #notFound) -> #some(Todo) | #error(#todoDoesNotExist) = {
  | #some(todo) -> #some(todo)
  | #notFound -> #error(#todoDoesNotExist)
}

export def createProtocol: [TodoStore & Clock] () -> TodoProtocol = {
  TodoProtocol(
    allTodos = { async { findAll() } }

    addTodo = { description -> async { insert(newTodo(description)) } }

    toggleCompleted = { id -> async {
      proc {
        def existing = try exists(find(id))
        return try update(existing with completed = not existing.completed)
      }
      \match
        | #some(todo) -> #updated(todo)
        | #error(e) -> #updateError(e)
    }}

    deleteTodo = { id -> async {
      proc {
        try exists(find(id))
        try delete(id)
        return ()
      }
      \match
        | #some(()) -> #deleted
        | #error(e) -> #deleteError(e)
    }}
  )
}

-- An in-memory store that lives for the duration of `action`. The mutable
-- cells never leave the `memory` scope; only plain values come out.
export def withInMemoryStore: [Clock] ([TodoStore] () -> a) -> a = { action ->
  memory {
    def todos: map(TodoId, Todo) = mut []
    def nextId = mut 1

    use TodoStore(
      findAll = { todos \Map.values }

      find = { id -> todos(id) }

      insert = { todo ->
        def saved = todo with id = nextId
        todos := todos \Map.insert(saved.id, saved)
        nextId := nextId + 1
        #some(saved)
      }

      update = { todo ->
        todos := todos \Map.insert(todo.id, todo)
        #some(todo)
      }

      delete = { id ->
        todos := todos \Map.remove(id)
        #some(())
      }
    ) in action()
  }
}

export def seedInitialData: [TodoStore & Clock] () -> () = {
  for ["Learn Nanyx", "Write a web app", "Ship it"] | description ->
    insert(newTodo(description))
}

This program lives in the repository as examples/todo-web-app.nyx.