A Clojure library which converts a REST specification into functions that emit clj-http
request maps.
Very light-weight.
This library requires clojure 1.9.0 or higher.
Add to dependencies:
[clj-rest-client "1.0.0-rc3"]
In your namespace add dependency:
(ns example.core
(:require [clj-rest-client.core :refer [defrest]
[clj-http.client :as client]]))
Define a rest interface:
(defrest {"http://example.com" {"person" {GET (get-person-by-id [id pos-int?])}}})
This defines a function named get-person-by-id
that you can now call and it will return a clj-http
compatible map.
You can then run the request by using clj-http
:
(client/request (get-person-by-id 3))
The function is instrumented, and will raise an error if parameters aren't valid by spec.
A description of features, options and solutions for common needs.
Definition is a nested map. Symbol or keyword keys define signature for a HTTP method for this subpath. E.g.
(defrest {"http://example.com" {"person" {"{id}" {GET (get-person-by-id [id pos-int? detail-level pos-int?] {:as :bytes})}}}})
Here the GET symbol key is followed by an endpoint definition, which defines a function for GET method for http://example.com/person
subpath.
It is equivalent to use GET
or get
or :get
.
The string keys in the nested definition map are there to denote subpaths. They shouldn't start or end with /
.
In this particular example the root string key also defines protocol, server, port.
This is in no way required. You can define a REST with relative paths only.
It will be demonstated later how to approach using definitions without predefined host.
{GET (get-example [id pos-int? time inst?] {:as :bytes})}
The endpoint definition is a list or vector (in this example a list), which contains the following:
[]
, must contain alternating parameter names and parameter specs.{}
, contains additional properties for returned request map,
it can use parameters in definitionDue to optionals the following definition is also legal:
{GET (get-all)}
Parameter specs are applied to parameters with conform. This enables you to use (s/conforming ...)
in parameter vector to convert types before they are sent.
There are a few premade conformers in clj-rest-client.conform
namespace that you can combine with s/and
to do formatting. Usually you'll use :refer
directive.
(:require [clj-rest-client.conform :refer [->json ->json? ->json* ->json?* ->date-format])
This will format java.util.Date
and java.time
objects using the given formatter.
(defrest {"example" {GET (example [date (->date-format DateTimeFormatter/ISO_DATE_TIME)])}})
Sometimes you need to convert a query parameter or a part of a bigger parameter to JSON. Use this conformer:
(defrest {"example" {GET (example [inline-query-map (s/and map? ->json)])}})
Function ->json*
works the same, but it takes a cheshire opt map.
The previous example spec doesn't take nil
values, but if you changed map?
to (s/nillable map?)
then invoking the function
with nil
would produce query parameter inline-query-map=null
. If you want nil
to stay nil
(and thus be eliminated from
query parameters) then use var with question mark:
(defrest {"example" {GET (example [inline-query-map (s/and (s/nillable map?) ->json?)])}})
All parameters defined default to being sent as query parameters, unless otherwise specified. Query parameters that are nil
are
absent.
Description of other parameter types follows.
String keys (paths) support notation for path parameters e.g. {x}
.
(defrest {"a{x}a" {"b{y}b" {:get (exam [x pos-int? y pos-int? z pos-int?])}}})
This expands into the following code for url construction:
{:url (str "a" x "a/b" y "b"), :query-params (into {} (filter second) {"z" z}), ....
Note that x
and y
are now used as path parameters, but z
is a query parameter.
You can specify format of common parameters in a map key, by using vector instead of string:
So instead of doing:
(defrest {"patient"
{"{id}/{type}"
{GET (get-patient [id pos-int? type string? detail pos-int?])
POST (upsert-patient [id pos-int? type string? patient ::patient])}}})
you can do:
(defrest {"patient"
{["{id}/{type}" pos-int? string?]
{GET (get-patient [detail pos-int?])
POST (upsert-patient [patient ::patient])}}})
Here common path parameter was moved into the path spec, but the generated functions are the same. Every function on that subtree gets that parameter prepended.
Parameters in parameter vector can be annotated.
[id pos-int? ^:+ password string? ^:body report bytes?]
Adds body param to request. See options for specifics.
This removes parameter from query parameter list. This is useful for extra parameters that don't end up in resulting request, but are useful when generating it.
{"dashboard" {GET (get-articles [^:+ password string?] (when password {:basic-auth ["admin" password]}))}}
Annotation ensures password doesn't show up in query params while still being in function signature and useful in its workings.
The macro support options as varargs key-values.
Here's the options with defaults
(defrest {} :param-transform identity :json-responses true :json-bodies true :instrument true)
This option specifies function that is uset to transform query parameter names: parameter (symbol) -> query parameter name (string).
This is useful to transform clojure's kebab-case symbol names to camel case param names.
This option specifies a function that is applied to all arguments after argument spec and conform and before being embedded into request map. It's a function of two arguments: param name symbol and param value, returns new param value.
Default implementation replaces keyword params with their name string. It's available (for delegating purposes) as default-val-transform
in core namespace.
If true then all requests specify {:as :json}
and all responses are expected to be json responses. Default true.
If true then body parameters are sent as to-JSON serialized form params, otherwise body params are simply added to request as :body
.
Default true.
So far, defrest
macro was used with a map literal.
Actually the defrest
macro supports three ways of loading definitions:
The URL can be any valid java.net.URL
string such as http://some/url
or file:my-file.edn
.
Additionally classpath:
urls are supported, such as classpath:my-definition.edn
.
You can add custom protocols via the normal Java custom url handlers and cmd switch -Djava.protocol.handler.pkgs=org.my.protocols
.
Note that loads happen at macro expansion time.
It is common to have an API where 95% of endpoints return JSON (and thus warrants the use of :json-responses option), and yet have 5% of endpoints where that isn't the case.
One way to deal with this is to simply use two defrest
with different defaults. E.g.:
(defrest {"majority" ... define 95% of endpoints here})
(defrest {"special" ... define 5% of endpoints here} :json-bodies false)
Or simply find the minority cases and use the extras map to override the default:
(defrest {"special" {"endpoint" {GET (get-file {:as :bytes})}}})
Here we override the default :as :json
for outstanding endpoints on a case by case basis.
In some cases like GitHub API or some other large vendor, usually the absolute URL of API is static and can be specified in defrest
map.
But when testing other APIs, it's common to specify relative paths, while the host varies.
Here's a couple of ways of dealing with that. First it can be beneficial to use loading by symbol to add host to definition separately.
(def relative-api '{"person" {GET (get-person)}})
(def absolute-api {"http://my-server" relative-api})
(defrest absolute-api)
Simple yet effective solution is to define a client closure such as this:
(ns example.core
(:require [clj-rest-client.core :refer [defrest]
[clj-http.client :as client]]))
(defrest {"person" {GET (get-person)}})
(defn client [url] (fn [req] (client/request (update req :url (partial str url "/")))))
(def c (client "http://my-server"))
; execute request
(c (get-person))
Another helper is prefix-middleware
function in clj-rest-client.core, which returns clj-http
middleware that prefixes
urls with given prefix. Here's an example:
(ns example.core
(:require [clj-rest-client.core :refer [defrest prefix-middleware]
[clj-http.client :as client]]))
(defrest {"person" {GET (get-person)}})
; execute request with extra middleware
(client/with-additional-middleware [(prefix-middleware "http://my-server")]
(client/request (get-person)))
Or simply use set!
or alter-var-root
or binding
to add prefix middleware to client/*current-middleware
.
Or you can make basic host a path param. E.g.
(defrest {"{url}" {"person" {GET (get-person [url string?])}}})
; execute request
(client/request (get-person "http://my-server"))
Copyright © 2018 Rok Lenarčič
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close