REST Client
A Clojure library which converts a REST specification into functions that emit clj-http
request maps.
Very light-weight.
Quick start
This library requires clojure 1.9.0 or higher.
Add to dependencies:
[clj-rest-client "1.0.0-rc4"]
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.
You can make the http call immediate by setting it as client function to API definition:
(defrest {"http://example.com" {"person" {GET (get-person-by-id [id pos-int?])}}} :client client/request)
(get-person-by-id 3)
Usage
A description of features, options and solutions for common needs.
Definition format
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:
- a symbol, name of function to be declared
- an optional argument vector, defaults to
[]
, must contain alternating parameter names and parameter specs. - an optional non-vector expression that evaluates to a map, defaults to
{}
, contains additional properties for returned request map, it can use parameters in definition
Due to optionals the following definition is also legal:
{GET (get-all)}
Parameter Specs
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])
Date formatting
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)])}})
->json, ->json*
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.
->json?, ->json?*
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?)])}})
Query parameters
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.
Path parameters
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.
Parameter annotations
Parameters in parameter vector can be annotated.
[id pos-int? ^:+ password string? ^:body report bytes?]
:body
Adds body param to request. See options for specifics.
:form
Add this parameter to form params. The name of actual form parameter is calculated using same transformation as query parameters.
:form-map
Adds this parameter as a map of form params.
:+
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.
defrest options
The macro support options as varargs key-values.
Here's the options with defaults
(defrest {} :param-transform identity :json-responses true :jsonify-bodies :smart :client identity)
client
This option specifies function that is invoked with clj-http maps generated by api functions. Defaults to identity. This is a good place to put your http client function if you want requests to be executed immediately.
There are benefits to separating request generation and request execution, thus the default being identity function.
param-transform
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.
val-transform
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.
json-responses
If true then all requests specify {:as :json}
and all responses are expected to be json responses. Default true.
jsonify-bodies
Set to :always
, :smart
, :never
. Body params will be ran through serializer if set to :always
.
Option :smart
will not run string bodies through JSON serializer. Defaults to :smart.
Loading definition
So far, defrest
macro was used with a map literal.
Actually the defrest
macro supports three ways of loading definitions:
- map literal
- symbol, loads definition map from var named by symbol
- string, loads definition map from URL in EDN format
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.
Dealing with endpoints that break pattern
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.
Using definitions with relative paths only
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"))
License
Copyright © 2018 Rok Lenarčič
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.