Conjector is built around concept that application state is represented as singletree-like structure (map of maps), where each component can be accessed using path which is a sequence of keys into subsequent maps. This document will dissect sample application state along other structures support it and explain core concepts using this as example.
{:frob frobnication-cycle-fn
:db {
:main main-db-conn-pool
}
:web {
:server http-server-object
:server-handler main-server-handler-wrapped
:handlers {
"/foo" foo-handler-fn
"/bar" bar-handler-fn
}
}
}
Various object in above structure are addressed by paths, for example: [:db :main]
, [:web :handlers "/foo]
and
each part can be accessed using (get-in app-state [:path :to :component])
. Application state can store constructed
objects (eg. database connections, http server object) or configured functions (closures), for example ring handlers.
Note that various objects in application state can depend on other objects, for example request handlers depend on
database connection, main request handler depends on path handlers, server depends on main request handler.
Application state is constructed from two data structures: configuration data and another structure that describes how application state should be built. In Conjector this structure is called application definition. Both configuration data and system definition structures generally resemble application state in that they share some parts of structure, for example configuration would look like this:
{:frob {:frob-mode :twiddle}
:db {
:main {
:url "jdbc:h2:mem:test"
:classname "org.h2.Driver"}
}
:web {
:server {
:port 3000
:join? false
}
:server-handler {
:wrappers [:wrap-cookies :wrap-session :wrap-params :wrap-keyword-params]
}
:handlers {
"/foo" {:roles #{:viewer :admin}}
"/bar" {:roles #{:admin}}
}
}
}
Application definition is even more interesting. It contains constructors that will create desired parts of application state and additional information that will help determine dependencies between constructed components. For example:
{:frob {:init frob-init-fn, :requires [[:db :main]]}
:db {
:main {:init db-init-fn}
}
:web {
:server {:init web-server-init-fn, :requires [[:web :server-handler]]}
:server-handler {:init web-handler-init-fn :rquires [[:web :handlers]]}
:handlers {
"/foo" {:init foo-handler-init-fn :requires [[:db :main] [:frob]]}
"/bar" {:init bar-handler-init-fn :requires [[:db :main]]}
}
}
}
Note that all above structures share common subset:
{:frob ...
:db {
:main ...
}
:web {
:server ...
:server-handler ...
:handlers {
"/foo" ...
"/bar" ...
}
}
}
Leaf nodes of above subset are components of your application. Each subtree under such a node is component definition.
Note that now components can be decoupled using only dependency information (:requires
) for binding them together.
Also, additional attributes can be added to application definition extending this mechanism in various ways, for example
:config-schema
can provide schema for configuration data of individual components, so application configuration can be
validated after binding parts of schema from all components into single data structure. Conjector provides binding/extract
function for such cases.
Paths are internally represented by vectors but it is not always convenient to use, so alternative forms are available
for developer, where path is represented by keyword, symbol or string where character :
is used path separator. For
example [:web :wrappers :auth :oauth2]
is the same as :web:wrappers:auth:oauth2
or web:wrappers:auth:oauth2
or
"web:wrappers:auth:oauth2"
. In various parts those alterantive representations can be used, for example keywrods
are commonly used in configuration data (if it has to refer to components) and symbol form is used in component definitio
macros (see COMPONENTS).
Maintaining application state in single structure makes live reload possible and safe. In such cases in addition to application definition and configuration data, old application state and old configuration will be passed to constructor functions. This allows constructor functions control reload process, for example database connections can be recreated if something in their configuration has changed and left unmodified when configuration is exactly the same.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close