Skip to content

Latest commit

 

History

History
221 lines (171 loc) · 5.85 KB

README.md

File metadata and controls

221 lines (171 loc) · 5.85 KB

halboy

Current Version

A Clojure library for all things hypermedia.

  • Create hypermedia resources
  • Marshal to and from JSON, or a map
  • Navigate JSON+HAL APIs

New in version 6

  • Thanks to a great contribution from @danielzurawski, we now support clj-http. In version 6, this is now the default.

API

Resources

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

Marshalling

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"

Navigation

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
;    }
;  }
;}

(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/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"

Customisation

Custom HTTP clients

Halboy offers an out-of-the-box HTTP client which, as of 6.0.0, uses clj-http. You can pass a HTTP client into Halboy using the :client key of the settings. It must adhere to the halboy.http.protocol.HttpClient protocol.

HTTP Kit

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 {}}})

CachableHttpClient

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 miliseconds 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 {}}})

HTTP settings

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.

Contributing

I'm happy to receive and go through feedback, bug reports, and pull requests.

If you need to contact me, my email is jimmy[at]jimmythompson.co.uk.

Development

To run the tests:

$ lein eftest