A Clojure library for all things hypermedia.
- Create hypermedia resources
- Marshal to and from JSON, or a map
- Navigate JSON+HAL APIs
- Thanks to a great contribution from @danielzurawski, we now support clj-http. In version 6, this is now the default.
With Halboy you can create resources, and pull information from them.
(require '[halboy.resource :as hal])
(def my-resource
(-> (hal/new-resource "/orders/123")
(hal/add-link :creator "/users/rob")
(hal/add-resource :discount (-> (hal/new-resource "/discounts/1256")
(hal/add-property :discount-percentage 10)))
(hal/add-resource :items [(-> (hal/new-resource "/items/534")
(hal/add-property :price 25.48))])
(hal/add-property :state :dispatching)))
(hal/get-link my-resource :self)
; { :href "/orders/123" }
(hal/get-href my-resource :creator)
; "/users/rob"
(hal/get-property my-resource :state)
; :dispatching
(-> (hal/get-resource my-resource :discount)
(hal/get-property :discount-percentage))
; 10
(-> (hal/get-resource my-resource :items)
(first)
(hal/get-property :price))
; 25.48
You can also marshal your hal resources to and from maps, or JSON.
(require '[halboy.resource :as hal])
(require '[halboy.json :as haljson])
(def my-resource
(-> (hal/new-resource "/orders/123")
(hal/add-link :creator "/users/rob")
(hal/add-resource :items (-> (hal/new-resource "/items/534")
(hal/add-property :price 25.48)))
(hal/add-property :state :dispatching)))
(haljson/resource->map my-resource)
; {:_links {:self {:href "/orders/123"},
; :creator {:href "/users/rob"}},
; :_embedded {:items {:_links {:self {:href "/items/534"}},
; :price 25.48}},
; :state :dispatching}
(haljson/resource->json my-resource)
; Formatted in these docs only.
;
; {
; \"_links\": {
; \"self\": {
; \"href\": \"/orders/123\"
; },
; \"creator\": {
; \"href\": \"/users/rob\"
; }
; },
; \"_embedded\": {
; \"items\": {
; \"_links\": {
; \"self\": {
; \"href\": \"/items/534\"
; }
; },
; \"price\": 25.48
; }
; },
; \"state\": \"dispatching\"
; }
(-> (haljson/resource->json my-resource)
(haljson/json->resource)
(hal/get-href :self))
; "/orders/123"
Provided you're calling a HAL+JSON API, you can discover the API and navigate
through its links. When you've found what you want, you
call navigator/resource
and you get a plain old HAL resource, which you can
inspect using any of the methods above.
(require '[halboy.resource :as hal])
(require '[halboy.navigator :as navigator])
; GET / - 200 OK
;{
; "_links": {
; "self": {
; "href": "/"
; },
; "users": {
; "href": "/users"
; },
; "user": {
; "href": "/users/{id}",
; "templated": true
; }
; }
;}
;
; GET /users - 200 OK
;{
; "_links": {
; "self": {
; "href": "/users"
; },
; "_embedded": {
; "users": [{
; "_links": {
; "self": {
; "href": "/users/rob"
; },
; "id": "rob"
; }
; }]
; }
; }
;}
(def users-result
(-> (navigator/discover "https://api.example.com/")
(navigator/get :users)))
(navigator/status users-result)
; 200
(navigator/location users-result)
; "https://api.example.com/users"
(-> (navigator/focus users-result [:users 0])
(navigator/resource)
(hal/get-property :id))
; "rob"
(-> (navigator/discover "https://api.example.com/")
(navigator/get :user {:id "rob"})
(navigator/location))
; "https://api.example.com/users/rob"
(def sue-result
(-> (navigator/discover "https://api.example.com/")
(navigator/post :users {:id "sue" :name "Sue" :title "Dev"})))
(navigator/location sue-result)
; "https://api.example.com/users/sue"
(-> (navigator/resource sue-result)
(hal/get-property :title))
; "Dev"
Halboy offers an out-of-the-box HTTP client which, as of 6.0.0, uses clj-http.
You can pass an HTTP client into Halboy using the :client
key of the settings.
It must adhere to the halboy.http.protocol.HttpClient
protocol.
There is an alternative HTTP client, which uses HTTPKit. You can use it by passing it into the navigator settings:
(navigator/discover "https://api.example.com"
{:client (halboy.http.http-kit/new-http-client)
:follow-redirects true
:http {:headers {}}})
Halboy also offers an HTTP client with caching support which is an in-memory
cache. It uses clojure.core.cache TTLCache
for caching with the default TTL of
2000 milliseconds which can be overridden to a different type of cache of choice
or a different TTL.
; default cachable client
(navigator/discover "https://api.example.com"
{:client (halboy.http.cachable/new-http-client)
:follow-redirects true
:http {:headers {}}})
; cachable client with an hour TTL
(require '[clojure.core.cache :as cache])
(def in-memory-cache (atom (cache/ttl-cache-factory {} :ttl 3600000)))
(navigator/discover "https://api.example.com"
{:client (halboy.http.cachable/new-http-client in-memory-cache)
:follow-redirects true
:http {:headers {}}})
All settings under the :http
key are passed into the HTTP client. These are
deep merged into each request, with keys on the request taking priority.
The request will always fill in the keys :method
, :url
, :body
, and
:query-params
.
Headers specified in HTTP settings will be merged with headers defined by
set-header
. If they share the same key, the set-header
call wins.
To run the tests:
./go library:test