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 the body and many other things that leak into your codebase.
Martian takes a description of these details (either from your OpenAPI/Swagger server, 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])
(let [m (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/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
library allows you to assert that your code constructs valid requests to remote servers without ever
actually calling them, using the OpenApi 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-re-frame
integrates martian event handlers into re-frame
, simplifying connecting your UI to data sources.
The core library (required):
Support for various HTTP client libraries: |HTTP client|JVM|Javascript|Babashka|Docs| | --------- | --- | ------ | ------ | --- | ||✔|||| ||✔|||| ||✔||✔|| ||✔||✔|| |||✔||| |||✔||| ||✔||✔||
Testing and other interop libraries:
transit
, edn
and json
operationId
of the endpoint in the OpenAPI/Swagger definitionFor more details and rationale you can watch the talk given to London Clojurians or there is also an older talk given at ClojureX Bytes.
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 by simply providing the url serving the openapi/swagger description
(let [m (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/swagger.json")]
;; explore the endpoints
(martian/explore m)
;; => [[:get-pet "Loads a pet by id"]
;; [:create-pet "Creates a pet"]]
;; explore the :get-pet 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.herokuapp.com/pets/123
;; build the request map for a request
(martian/request-for m :get-pet {:id 123})
;; => {:method :get
;; :url "https://pedestal-api.herokuapp.com/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]))]
;; load the pet using the id
(martian/response-for m :get-pet {:id pet-id}))
;; => {:status 200
;; :body {:name "Doggy McDogFace"
;; :type "Dog"
;; :age 3}}
;; :martian.core/body can optionally be used in lieu of explicitly naming the body schema
(let [pet-id (-> (martian/response-for m :create-pet {::martian/body {:name "Doggy McDogFace" :type "Dog" :age 3}})
(get-in [:body :id]))])
;; the name of the body object can also be used to nest the body parameters
(let [pet-id (-> (martian/response-for m :create-pet {:pet {:name "Doggy McDogFace" :type "Dog" :age 3}})
(get-in [:body :id]))]))
Note that when calling bootstrap-openapi
you can also provide a url to a local resource, 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:
(martian/bootstrap-openapi "https://sandbox.example.com" (load-local-resource "openapi-test.json") martian-http/default-opts)
Although bootstrapping against a remote OpenAPI or Swagger API 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 or Swagger.
Martian offers a separate bootstrap
function which you can provide with handlers defined as data.
Here's an example:
(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}}}])
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 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:
(require '[martian.core :as martian]
'[martian.httpkit :as martian-http]
'[martian.test :as martian-test])
(let [m (-> (martian-http/bootstrap-openapi "https://pedestal-api.herokuapp.com/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"}}
martian-test
has interceptors that always give successful responses, always errors, or a random choice.
By making your application code accept a Martian instance you can inject a test instance within your tests, making
previously untestable code testable again.
More documentation is available at martian-test.
martian-vcr allows you 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])
(def m (http/bootstrap "https://foo.com/api"
{:interceptors (inject http/default-interceptors
(vcr/record opts)
:after http/perform-request)}))
(m/response-for m :load-pet {:id 123})
;; the response is recorded and now stored at test-resources/vcr/load-pet/-655390368/0.edn
More documentation is available at martian-vcr.
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:
(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.
;; 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 body value
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.herokuapp.com/swagger.json"
{: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 []}}
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:
(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
. This results in the following chain:
set-method
set-url
set-query-params
set-body-params
set-form-params
set-header-params
enqueue-route-specific-interceptors
- injects the following at runtime:
route-interceptor-1
e.g. ::override-load-pet-method
route-interceptor-2
encode-body
default-coerce-response
perform-request
This means your route interceptors have available to them the unserialised request on enter and the deserialised response on leave.
You may move or provide your own version of enqueue-route-specific-interceptors
to change this behaviour.
Martian allows you to add support for content-types in addition to those supported out of the box - transit
, edn
and json
.
(require '[martian.core :as m])
(require '[martian.httpkit :as http])
(require '[martian.encoders :as encoders])
(require '[martian.interceptors :as i])
(require '[clojure.string :as str])
(def magic-encoder str/upper-case)
(def magic-decoder str/lower-caser)
(let [encoders (assoc (encoders/default-encoders)
"application/magical" {:encode magic-encoder
:decode magic-decoder
:as :magic})]
(http/bootstrap-openapi
"https://example-api.com"
{:interceptors (concat m/default-interceptors
[(i/encode-body encoders)
(i/coerce-response encoders)
http/perform-request])}))
Martian provides a response validation interceptor which validates the response against the response schemas. It is not included in the default interceptor stack, but you can include it yourself:
(http/bootstrap-openapi
"https://example-api.com"
{:interceptors (cons (i/validate-response {:strict? true})
http/default-interceptors)})
The strict?
argument defines whether any response with an undefined schema is allowed, e.g. if a response
schema is defined for a 200 status code only, but the server returns a 500, strict mode will throw an error but
non-strict mode will allow it. Strict mode defaults to false.
Martian can read default
directives from Swagger, or you can supply them if bootstrapping from data. They can be seen using explore
and merged with your params if you set the optional use-defaults?
option.
(require '[schema-tools.core :as st])
(require '[martian.interceptors :refer [merge-defaults]])
(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"}}
)
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"
import martian.Martian;
import java.util.Map;
import java.util.HashMap;
Map<String, Object> swaggerSpec = { ... };
Martian martian = new Martian("https://pedestal-api.herokuapp.com", swaggerSpec);
martian.urlFor("get-pet", new HashMap<String, Object> {{ put("id", 123); }});
// => https://pedestal-api.herokuapp.com/pets/123
:operationId
in the OpenAPI/Swagger spec to name routes when using bootstrap-openapi
Use cider-jack-in-clj
or cider-jack-in-clj&cljs
to start Clojure (and Clojurescript where appropriate) REPLs for development.
You may need to lein install
first if you're working in a module that depends on another.
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, 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