The HTTP abstraction library for Clojure/Script and Babashka, supporting OpenAPI/Swagger and many HTTP client libraries.
Calling HTTP endpoints can be complicated. You have to construct the right URL with the right route parameters, remember what the query parameters are, what method to use, how to encode a request body, coerce a response and many other things that leak into your codebase.
Martian takes a description of these details — either from your OpenAPI/Swagger definition, or just as lovely Clojure data — and provides a client interface to the API that abstracts you away from HTTP and lets you simply call operations with parameters, keeping your codebase clean.
You can bootstrap it in one line and start calling the server:
(require '[martian.core :as martian]
'[martian.clj-http :as martian-http])
(def m (martian-http/bootstrap-openapi "https://pedestal-api.oliy.co.uk/swagger.json"))
(martian/response-for m :create-pet {:name "Doggy McDogFace" :type "Dog" :age 3})
;; => {:status 201 :body {:id 123}}
(martian/response-for m :get-pet {:id 123})
;; => {:status 200 :body {:name "Doggy McDogFace" :type "Dog" :age 3}}
Implementations for many popular HTTP client libraries are supplied as modules (see below), but any other HTTP library can be used due to the extensibility of Martian's interceptor chain. It also allows custom behaviour to be injected in a uniform and powerful way.
The martian-test
module allows you to assert that your code constructs valid requests to remote servers without ever
actually calling them, using the OpenAPI/Swagger spec to validate the parameters. It can also generate responses in the
same way, ensuring that your response handling code is also correct. Examples are below.
martian-test
martian-vcr
Add the required dependency to the core Martian module:
Core Module | API Docs |
---|---|
Add one more dependency to the module for the target HTTP client library:
| HTTP client | Martian Module | JVM | BB | JS | API Docs |
| ----------- | -------------- | --- | -- | -- | -------- |
| hato || ✔ | | |
|
| clj-http |
| ✔ | | |
|
| clj-http-lite |
| ✔ | ✔ | |
|
| http-kit |
| ✔ | ✔ | |
|
| bb/http-client |
| ✔ | ✔ | |
|
| cljs-http |
| | | ✔ |
|
| cljs-http-promise |
| | | ✔ |
|
Optionally add dependencies on modules for testing and interop:
Martian Module | Docs | API Docs |
---|---|---|
README | ||
README | ||
README |
The martian-re-frame
integrates Martian event handlers into re-frame, simplifying
connecting your UI to data sources.
transit
, edn
, json
and more —
and handles both request encoding (serialisation) and response coercion (deserialisation)operationId
in the OpenAPI/Swagger definitionFor more details and rationale you can watch:
Given an OpenAPI/Swagger API definition like that provided by pedestal-api:
(require '[martian.core :as martian]
'[martian.clj-http :as martian-http])
;; Bootstrap the Martian instance
;; - in this case, by simply providing the URL serving the OpenAPI/Swagger spec
(let [m (martian-http/bootstrap-openapi "https://pedestal-api.oliy.co.uk/swagger.json")]
;; Explore all available endpoints
(martian/explore m)
;; => [[:get-pet "Loads a pet by id"]
;; [:create-pet "Creates a pet"]]
;; Explore a specific endpoint
(martian/explore m :get-pet)
;; => {:summary "Loads a pet by id"
;; :parameters {:id s/Int}}
;; Build the URL for a request
(martian/url-for m :get-pet {:id 123})
;; => https://pedestal-api.oliy.co.uk/pets/123
;; Build the request map for a request
(martian/request-for m :get-pet {:id 123})
;; => {:method :get
;; :url "https://pedestal-api.oliy.co.uk/pets/123"
;; :headers {"Accept" "application/transit+msgpack"}
;; :as :byte-array}
;; Perform the request (to create a pet and read back the `pet-id` from the response)
(let [pet-id (-> (martian/response-for m :create-pet {:name "Doggy McDogFace" :type "Dog" :age 3})
(get-in [:body :id]))]
;; Perform the request (to load the pet using its `:id` as a request parameter)
(martian/response-for m :get-pet {:id pet-id}))
;; => {:status 200
;; :body {:name "Doggy McDogFace"
;; :type "Dog"
;; :age 3}}
;; Perform the request
;; - `:martian.core/body` can optionally be used in lieu of explicitly naming the body schema
(-> (martian/response-for m :create-pet {::martian/body {:name "Doggy McDogFace" :type "Dog" :age 3}})
(get-in [:body :id]))
;; => 2
;; Perform the request
;; - the name (the `:pet` alias) of the body object can also be used to nest the body params
(-> (martian/response-for m :create-pet {:pet {:name "Doggy McDogFace" :type "Dog" :age 3}})
(get-in [:body :id])))
;; => 3
Note that when calling the bootstrap-openapi
functions to bootstrap your Martian instance you can also provide a path
to a local file/resource with the OpenAPI/Swagger spec, e.g. (martian-http/bootstrap-openapi "public/openapi.json")
.
For ClojureScript the file can only be read at compile time, so a slightly different form is required using the
martian.file/load-local-resource
macro:(ns ;; your namespace (:require [martian.core :as martian] [martian.cljs-http :as martian-http]) (:require-macros [martian.file :refer [load-local-resource]])) (martian/bootstrap-openapi "https://sandbox.example.com" (load-local-resource "openapi-test.json") martian-http/default-opts)
Although bootstrapping against a remote OpenAPI/Swagger spec using bootstrap-openapi
is simplest and allows you to use
the golden source to define the API, you may likely find yourself needing to integrate with an API beyond your control
which does not use OpenAPI/Swagger spec.
Martian offers a separate bootstrap
function which you can provide with handlers defined as data. Here's an example:
(require '[martian.core :as martian]
'[schema.core :as s])
(martian/bootstrap "https://api.org"
[{:route-name :load-pet
:path-parts ["/pets/" :id]
:method :get
:path-schema {:id s/Int}}
{:route-name :create-pet
:produces ["application/xml"]
:consumes ["application/xml"]
:path-parts ["/pets/"]
:method :post
:body-schema {:pet {:id s/Int
:name s/Str}}}])
If an API has a parameter called FooBar
it's difficult to stop that leaking into your own code — the Clojure idiom is
to use kebab-cased keywords such as :foo-bar
. Martian maps parameters to their kebab-cased equivalents so that your
code looks neater but preserves the mapping so that the API is passed the correct parameter names:
(require '[martian.core :as martian]
'[schema.core :as s])
(let [m (martian/bootstrap "https://api.org"
[{:route-name :create-pet
:path-parts ["/pets/"]
:method :post
:body-schema {:pet {:PetId s/Int
:FirstName s/Str
:LastName s/Str}}}])]
(martian/request-for m :create-pet {:pet-id 1 :first-name "Doggy" :last-name "McDogFace"}))
;; => {:method :post
;; :url "https://api.org/pets/"
;; :body {:PetId 1
;; :FirstName "Doggy"
;; :LastName "McDogFace"}}
Body parameters may be supplied in three ways: with an alias, destructured or as an explicit value.
(require '[martian.core :as martian])
;; the following three forms are equivalent
(request-for m :create-pet {:pet {:pet-id 1 :first-name "Doggy" :last-name "McDogFace"}}) ;; the :pet alias
(request-for m :create-pet {:pet-id 1 :first-name "Doggy" :last-name "McDogFace"}) ;; destructured
(request-for m :create-pet {::martian/body {:pet-id 1 :first-name "Doggy" :last-name "McDogFace"}}) ;; explicit value
Martian can read default
directives from OpenAPI/Swagger spec, or you can supply them with schema-tools.core/default
if bootstrapping from data.
They can be seen using explore
and merged with your params if you set the optional use-defaults?
option.
(require '[martian.core :as martian]
'[schema.core :as s]
'[schema-tools.core :as st])
(let [m (martian/bootstrap "https://api.org"
[{:route-name :create-pet
:path-parts ["/pets/"]
:method :post
:body-schema {:pet {:id s/Int
:name (st/default s/Str "Bryson")}}}]
{:use-defaults? true})]
(martian/explore m :create-pet)
;; => {:summary nil, :parameters {:pet {:id Int, :name (default Str "Bryson")}}, :returns {}}
(martian/request-for m :create-pet {:pet {:id 123}}))
;; => {:method :post, :url "https://api.org/pets/", :body {:id 123, :name "Bryson"}}
These media types are available out of the box and are used by Martian, e.g. for the "Content-Type" negotiation, when parsing the OpenAPI/Swagger definition, etc., in the following order:
application/transit
application/transit+msgpack
— supported for JVM HTTP clients only!application/transit+json
— supported for all target HTTP clientsapplication/edn
application/json
application/x-www-form-urlencoded
multipart/form-data
clj-http-lite
This is what you get when a Martian instance is bootstrapped with default options, which come with default-encoders
.
If necessary, they can also be configured more finely by passing options.
Martian provides a response validation interceptor which validates the response body against the response schemas. It is not included in the default interceptor stack, but you can include it yourself:
(require '[martian.core :as martian]
'[martian.clj-http :as martian-http]
'[martian.interceptors :as i])
(martian-http/bootstrap-openapi
"https://example-api.com"
{:interceptors (i/inject martian-http/default-interceptors
(i/validate-response-body {:strict? true})
:before ::martian/coerce-response)})
The strict?
argument defines whether any response with an undefined schema is allowed. For example, if a response
schema is defined for a 200
status code only, but the server returns a response with status code 500
, strict mode
will throw an error, while non-strict mode will allow it. The strict mode defaults to false
.
martian-test
Testing code that calls external systems can be tricky — you either build often elaborate stubs which start to become
as complex as the system you are calling, or else you ignore it all together with (constantly true)
.
Martian will assert that you provide the right parameters to the call, and the martian-test
will return a response
generated from the response schema of the remote application. This gives you more confidence that your integration is
correct without maintenance of a stub.
The following example shows how exceptions will be thrown by bad code and how responses can be generated using the
martian.test/respond-with-generated
function:
(require '[martian.core :as martian]
'[martian.httpkit :as martian-http]
'[martian.test :as martian-test])
(let [m (-> (martian-http/bootstrap-openapi "https://pedestal-api.oliy.co.uk/swagger.json")
(martian-test/respond-with-generated {:get-pet :random}))]
(martian/response-for m :get-pet {})
;; => ExceptionInfo Value cannot be coerced to match schema: {:id missing-required-key}
(martian/response-for m :get-pet {:id "bad-id"})
;; => ExceptionInfo Value cannot be coerced to match schema: {:id (not (integer? bad-id))}
(martian/response-for m :get-pet {:id 123}))
;; => {:status 200, :body {:id -3, :name "EcLR"}}
The martian-test
has generative interceptors that always give successful responses, always errors, or a random choice:
martian.test/generate-success-response
, martian.test/generate-error-response
, and martian.test/generate-response
.
By making your application code accept a Martian instance you can inject a test instance within your tests, making previously untestable code testable again.
All other non-generative testing approaches and techniques, such a mocks, stubs, and spies, are also supported.
The following example shows how mock responses can be created using the martian.test/respond-with
function:
(require '[martian.core :as martian]
'[martian.httpkit :as martian-http]
'[martian.test :as martian-test])
(let [m (-> (martian-http/bootstrap-openapi "https://pedestal-api.oliy.co.uk/swagger.json")
(martian-test/respond-with {:get-pet {:name "Fedor Mikhailovich" :type "Cat" :age 3}}))]
(martian/response-for m :get-pet {:id 123}))
;; => {:status 200, :body {:name "Fedor Mikhailovich" :type "Cat" :age 3}}
(let [m (-> (martian-http/bootstrap-openapi "https://pedestal-api.oliy.co.uk/swagger.json")
(martian-test/respond-with {:get-pet (fn [_request]
(let [rand-age (inc (rand-int 50))
ret-cat? (even? rand-age)]
{:name (if ret-cat? "Fedor Mikhailovich" "Doggy McDogFace")
:type (if ret-cat? "Cat" "Dog")
:age rand-age}))}))]
(martian/response-for m :get-pet {:id 123})
;; => {:status 200, :body {:name "Fedor Mikhailovich" :type "Cat" :age 12}}
(martian/response-for m :get-pet {:id 123}))
;; => {:status 200, :body {:name "Doggy McDogFace" :type "Dog" :age 7}}
More documentation is available at martian-test.
martian-vcr
The martian-vcr
module enables Martian instances to record responses from real HTTP requests and play them back later,
allowing you to build realistic test data quickly and easily.
(require '[martian.vcr :as vcr]
'[martian.core :as martian]
'[martian.clj-http :as martian-http]
'[martian.interceptors :refer [inject]])
(def m (martian-http/bootstrap "https://foo.com/api"
{:interceptors (inject martian-http/default-interceptors
(vcr/record opts)
:after ::martian-http/perform-request)}))
(martian/response-for m :load-pet {:id 123})
;; the response is now recorded and stored at "test-resources/vcr/load-pet/-655390368/0.edn"
More documentation is available at martian-vcr.
Martian supports a wide range of customisations — through interceptor chain, configurable encoders and media types, HTTP client options, and parameter schema coercion matcher.
You may wish to provide additional behaviour to requests. This can be done by providing Martian with interceptors which behave in the same way as pedestal interceptors.
You can add interceptors to the stack that get executed on every request when bootstrapping Martian. For example, if you wish to add an authentication header and a timer to all requests:
(require '[martian.core :as martian]
'[martian.clj-http :as martian-http])
(def add-authentication-header
{:name ::add-authentication-header
:enter (fn [ctx]
(assoc-in ctx [:request :headers "Authorization"] "Token: 12456abc"))})
(def request-timer
{:name ::request-timer
:enter (fn [ctx]
(assoc ctx ::start-time (System/currentTimeMillis)))
:leave (fn [ctx]
(->> ctx ::start-time
(- (System/currentTimeMillis))
(format "Request to %s took %sms" (get-in ctx [:handler :route-name]))
(println))
ctx)})
(let [m (martian-http/bootstrap-openapi
"https://pedestal-api.oliy.co.uk/swagger.json"
;; see the note on using `martian.interceptors/inject` fn below
{:interceptors (concat [add-authentication-header request-timer]
martian-http/default-interceptors)})]
(martian/response-for m :all-pets {:id 123}))
;; Request to :all-pets took 38ms
;; => {:status 200 :body {:pets []}}
There is also the martian.interceptors/inject
function that you can leverage to be more specific and descriptive when
adding a custom interceptor or replacing/removing an existing (default) one.
Sometimes individual routes require custom behaviour. This can be achieved by writing a
global interceptor which inspects the route-name and decides what to do, but a more specific
option exists using bootstrap
and providing :interceptors
as follows:
(require '[martian.core :as martian]
'[schema.core :as s])
(martian/bootstrap "https://api.org"
[{:route-name :load-pet
:path-parts ["/pets/" :id]
:method :get
:path-schema {:id s/Int}
:interceptors [{:name ::override-load-pet-method
:enter #(assoc-in % [:request :method] :xget)}]}])
Alternatively you can use the helpers like update-handler
to update a Martian created from bootstrap-openapi
:
(-> (martian/bootstrap-openapi "https://api.org" openapi-definition)
(martian/update-handler :load-pet assoc :interceptors [{:name ::override-load-pet-method
:enter #(assoc-in % [:request :method] :xget)}]))
Interceptors provided at a per-route level are inserted into the interceptor chain at execution time by the interceptor
called :martian.interceptors/enqueue-route-specific-interceptors
. Assuming the martian.interceptors
ns is required
with :as i
alias and, optionally, core ns for any target HTTP client module is required with :as martian-http
alias,
this results in the following interceptor chain:
::i/keywordize-params
::i/set-method
::i/set-url
::i/set-query-params
::i/set-body-params
::i/set-form-params
::i/set-header-params
::i/enqueue-route-specific-interceptors
— injects the following at runtime:
::override-load-pet-method
::i/encode-request
, if any::i/coerce-response
, if any::martian-http/perform-request
, if anyThis means your route interceptors have available to them the unencoded (non-serialised) request on :enter
stage and
the coerced (deserialised) response on :leave
stage. And, as with any other interceptor, you may move or provide your
own version of the ::i/enqueue-route-specific-interceptors
to change this behaviour.
There is also a way to augment/override the default coercion matcher that is used by a Martian instance for parameters coercion:
(require '[martian.core :as martian]
'[martian.httpkit :as martian-http])
;; adding an extra coercion instead/after the default one
(martian-http/bootstrap-openapi
"https://pedestal-api.oliy.co.uk/swagger.json"
{:coercion-matcher (fn [schema]
(or (martian/default-coercion-matcher schema)
(my-extra-coercion-matcher schema)))})
;; switching to some coercion matcher from 'schema-tools'
(require '[schema.core :as s]
'[schema-tools.coerce :as stc])
(martian/bootstrap
"https://api.org"
[{:route-name :create-pet
:path-parts ["/pets/"]
:method :post
:body-schema {:pet {:PetId s/Int
:FirstName s/Str
:LastName s/Str}}}]
{:coercion-matcher stc/json-coercion-matcher})
By default, the martian.encoders/default-encoders
is configured with {:json {:decode {:key-fn keyword}}}
options,
but you can provide custom options for the built-in media type encoders. The shape of the options map for this function
looks like this:
{:transit {:encode <transit-encode-opts>
:decode <transit-decode-opts>}
:json {:encode <json-encode-opts>
:decode <json-decode-opts>}
:edn {:encode <edn-encode-opts>
:decode <edn-decode-opts>}}
Check out the martian.encoders
ns for all supported Transit, JSON, and EDN encoding/decoding options.
Also, for your convenience, this namespace provides constructor functions for the encoders of all built-in media types:
transit-encoder
, json-encoder
, edn-encoder
, and form-encoder
. You can use them directly to create a customized
encoder instance for a specific media type. For example, you can pass an :as
param with a different raw type, e.g.:
;; Supported raw types: `:string` (default), `:stream`, `:byte-array`.
;; The last 2 are JVM/BB specific and won't work with JS HTTP clients.
(transit-encoder :json {:encode ..., :decode ...} :as :byte-array)
(edn-encoder {:encode ..., :decode ...} :as :stream)
(json-encoder {:encode ..., :decode ...} :as :stream)
Sometimes you might find it easier to patch a built-in Martian encoder in place, like this:
(def my-encoders
(update (encoders/default-encoders)
"application/transit+json" assoc :as :stream))
;; now the transit decoder will expect an InputStream from the HTTP client
Pass my-encoders
to the function that bootstraps a Martian instance, as shown below.
Martian allows you to add support for custom media types in addition to the default ones. They can be added independently for request and response encoders. Here's how it can be achieved in practice:
(require '[clojure.string :refer :all]
'[martian.core :as martian]
'[martian.encoders :as encoders]
'[martian.httpkit :as martian-http]
'[martian.interceptors :as i])
(def magical-encoder
{;; a unary fn of request `:body`, Str -> Str
:encode upper-case
;; a unary fn of response `:body`, Str -> Str
:decode lower-case
;; tells HTTP client what raw type to provide
;; one of `:string`, `:stream`, `:byte-array`
:as :string})
(let [request-encoders (assoc martian-http/default-request-encoders
"application/magical" magical-encoder)
response-encoders (assoc martian-http/default-response-encoders
"application/magical" magical-encoder)]
;; provide via `:request-encoders`/`:response-encoders` opts
(martian-http/bootstrap-openapi
"https://example-api.com"
{:request-encoders request-encoders
:response-encoders response-encoders})
;; or by rebuilding a complete interceptor chain from scratch
(martian-http/bootstrap-openapi
"https://example-api.com"
{:interceptors (conj martian/default-interceptors
(i/encode-request request-encoders)
(i/coerce-response response-encoders martian-http/response-coerce-opts)
martian-http/perform-request)})
;; or by leveraging the `martian.interceptors/inject` fn
(def my-encode-request (i/encode-request request-encoders))
(def my-coerce-response (i/coerce-response response-encoders martian-http/response-coerce-opts))
(martian-http/bootstrap-openapi
"https://example-api.com"
{:interceptors (-> martian-http/default-interceptors
(i/inject my-encode-request :replace ::martian/encode-request)
(i/inject my-coerce-response :replace ::martian/coerce-response))}))
Similar to what was described for request/response encoders in the Custom media types section, there may be other Martian bootstrap options that customize HTTP client-specific behavior.
Async-compatible HTTP clients, such as hato
and babashka/http-client
, support the async?
option (false by default)
which switches from using the ::martian-http/perform-request
interceptor to ::martian-http/perform-request-async
.
HTTP clients with rich "Content-Type"-based response auto-coercion capabilities, such as clj-http
and hato
, support
the use-client-output-coercion?
(false by default) which allows to skip Martian response decoding for some media types
that the client is known to be able to auto-coerce itself.
For a complete list of available options, check out the supported-custom-opts
var in the HTTP client's module core ns.
When Martian is bootstrapped it closes over the route definitions and any options you provide, returning an immutable instance. This can hamper REPL development when you wish to rapidly iterate on your Martian definition, so all Martian API calls also accept a function or a var that returns the instance instead:
(martian/url-for (fn [] (martian/bootstrap ... )) :load-pet {:id 123}) ;; => "https://api.com/pets/123"
Martian can be used from Java code as follows:
import martian.Martian;
import java.util.Map;
import java.util.HashMap;
Map<String, Object> swaggerSpec = { ... };
Martian martian = new Martian("https://pedestal-api.oliy.co.uk", swaggerSpec);
martian.urlFor("get-pet", new HashMap<String, Object> {{ put("id", 123); }});
// => https://pedestal-api.oliy.co.uk/pets/123
:operationId
in the OpenAPI/Swagger spec to name routes when using bootstrap-openapi
anyOf
, allOf
and oneOf
Use cider-jack-in-clj
or cider-jack-in-clj&cljs
to start Clojure (and CLJS where appropriate) REPLs for development.
You may need to lein install
first if you're working with/in a module that depends on another module.
Please feel free to raise issues on GitHub or send pull requests.
Martian uses tripod for routing, inspired by pedestal.
Can you improve this documentation? These fine people already did:
Oliver Hine, Mark Sto, Rahul De, Luciano Laratelli, The Alchemist, rgkirch, Camilo Polymeris & Mark IngramEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close