The goal of this library is to facilitate using data described by realms in web applications contexts, i.e. when transpoting data via http.
There are two main facilities included in this library. One part is for defining translations of your data in Transit/EDN form. The second part is to simplify defining and using server endpoints used by fat web clients.
Based on active-data-translate, there is a "batteries included" transit format that can translate between most of your data and a transit compatible form automatically:
active.data.http.formats.transit/transit-format
Use this only if the coupling introduced by that is not an issue, or can be mitigated by other means.
The coupling can for example be
between producer and consumer code of the transit values, if they are developed independently, or
between past and future versions of the code, if transit values are written to some kind of databases, or if producer and consumer can have different versions of the code.
In those situations, you should define explicit translations for the realms that are defined in your code and thus are subject to potential change over time. You can use
active.data.http.formats.transit/basic-formatters
as a base for that, which includes formatters for things that are unlikely to change, like formatting strings as strings, numbers as numbers, and sequences as vectors, etc.
A reitit coercion based on realms and a format is available in the
active.data.http.reitit
namespace. This allows you to declare
parameters and response bodies as realms, and have them automatically
translated via the format.
(require '[active.data.http.reitit :as http-reitit])
(def-record user [...])
(def my-format ...)
["/api/public/get-user/:id"
{:get {:handler (fn [request]
{:status 200
:body (db/get-user-from-db (:id (:path (:parameters request))))})
:parameters {:path {:id realm/integer}}
:responses {200 {:body user}}
:coercion (http-reitit/realm-coercion my-format)}}]
See active-data-translate on how to define formats.
Note that to get this working you'll need a few middlewares for these routes. Namely the coersion middlewares, parameters-middleware and some transit middleware like muuntaja:
{:middleware
[reitit.ring.coercion/coerce-exceptions-middleware
reitit.ring.coercion/coerce-request-middleware
reitit.ring.coercion/coerce-response-middleware
reitit.ring.middleware.parameters/parameters-middlware]
:muuntaja muuntaja.core/instance}
See Reitit documentation for more details on how this can be set up.
To make endpoints for a webclient served by the same server and the usage of them even easier to set up, there is an "RPC"-like facility included in this library.
It is intended for "internal apis" and shared code (cljc) only, where the coupling between the server and the client code is not an issue.
To use this, you would first define a so called context and the api in some shared code (cljc file):
(require '[active.data.http.rpc :as rpc #?(:cljs :include-macros true)])
(def internal-api (rpc/context "/api/internal"))
(rpc/defn-rpc get-user! internal-api :- user [id :- realm/integer])
And define implementations for those RPCs in some server code, for example with reitit:
(require '[active.data.http.rpc.reitit :as rpc-reitit])
(def routes
(rpc-reitit/context-routes
internal-api
[(rpc-reitit/impl get-user! db/get-user-from-db]))
And finally to "call" those RPCs from the client side, for example with the reacl-c library, you would modify the shared api code to add a "caller" to the context like so:
(require '[active.data.http.rpc :as rpc #?(:cljs :include-macros true)])
#?(:cljs (require '[active.data.http.rpc.reacl-c :as rpc-reacl-c]))
(def internal-api (-> (rpc/context "/api/internal")
#?(:cljs (rpc/set-context-caller rpc-reacl-c/caller))))
Which then enables you to call the names defined by defn-rpc
as a
function to get a reacl-c request, which can then be executed in various
ways:
(require '[reacl-c-basics.ajax :as ajax])
(ajax/fetch (get-user! 4711))
See reacl-c-basics for more details on that.
Signals are (small) pieces of data sent by the server to your fat webclient, for example to inform it that new data is available. It is not recommended to send that data itself, but to fetch that data separately after receiving a signal to do so.
To add that to your shared internal api, you can write something like this:
(require '[active.data.http.signals :as signals])
(def wakeup-signal ::wakeup)
(defonce signals
(signals/context "/api/internal-signals"
(realm/enum wakeup-signal)))
Note: the context has some internal state, so it is recommended to use
defonce
for it.
To get the necessary reitit routes use
(require '[active.data.http.signals.reitit :as signals.reitit])
(def routes
(signals.reitit/context-routes api/signals))
To actually signal something
(require '[active.data.http.signals :as signals])
(signals/broadcast! api/signals api/wakeup-signal)
And to receive signals when using reacl-c:
(require '[active.data.http.signals.reacl-c :as signals])
;; an item that emits the current timestamp when a wakeup is received
(signals/receive api/signals api/wakeup-signals)
Note that signals use websockets under the hood, so you need a recent version of ring and a ring-adapter that supports websockets.
Copyright © 2024 Active Group GmbH
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close