foxcpp / go-jmap

:email: JMAP client & server library (WIP)
MIT License
34 stars 5 forks source link

JMAP Core server implementation #4

Open foxcpp opened 5 years ago

foxcpp commented 5 years ago

Things to consider

Below are things I believe would make library more useful.

foxcpp commented 5 years ago

Here is some basic outline for the sake of discussion:

JMAP Core server logic:

// implements http.Handler so it could be simply attached to /.well-known/jmap endpoint on net/http server.
type Server struct {}

type SessionManager interface {
  // value is the value of Authentication header field.
  CheckAuth(value string) (bool, error)
  Accounts(value string) (allAccts map[ID]Account, primaryAccts map[string]ID, err error)
}

type FuncHandler func(Invocation) (Invocation, error)

type CapabilityBackend struct {
  Handlers map[string]FuncHandler
  ArgUnmarshallers map[string]FuncArgsUnmarshal
}

func NewServer(auth SessionManager, cb ...CapabilityBackend) (Server, error) {}

Here is the typical flow:

  1. Server receives the HTTP request. Authentication header is checked using SessionManager object. Request is then deserialized from JSON, inner Invocation objects are deserialized using callbacks provided by CapabilityBackend's. Additionally, JSON Pointers ("back references") are resolved at this point too (I have no thoughts on how to implement it though, any ideas are welcome). Side note: This way we free backend implementations from having to implement JSON-related boilerplate. They receive arguments as concrete structures.
  2. Each Invocation is passed to callbacks provided by CapabilityBackend's. Returned Invocations are added to the Response object and then serialized to JSON and returned as HTTP response.

JMAP Mail, JMAP Calendar, etc implementations provide factory functions that return CapabilityBackend with handlers bound to user-provided backend (database) implementation.

Here is the example of how hypothetical JMAP Todo implementation would look like (some parts are left out for brevity):

// Implementation is provided by user.
type Backend interface {
  Query(args *TodoQueryArgs) (*TodoQueryResp, error)
}

func NewTodoServer(be Backend) jmap.CapabilityBackend {
  return jmap.CapabilityBackend {
    Handlers: map[string]jmap.FuncHandler{
      "Todo/query": func(i jmap.Invocation) (Invocation, error) {
        resp, err := be.Query(i.Args.(*TodoQueryArgs))
        return Invocation{Name: i.Name, CallID: i.Name, Args: resp}, err
      },
    },
    Unmarshallers: map[string]jmap.FuncArgsUnmarshal{
      "Todo/query": unmarshalTodoQueryArgs,
    },
  },
}
foxcpp commented 5 years ago

Here is the example of main function implementation using interfaces proposed above:

func main() {
  sessionMngr := SessionThingy{} // perhaps something OAuth-based, whatever
  todoBackend := TodoBackend{} // Our implementation of Todo DB.

  jmapSrv := jmap.NewServer(&sessionMngr, todo.NewTodoServer(todoBackend))
  http.Handle("/.well-known/jmap", &jmapSrv)
  return http.ListenAndServeTLS(...);
}