Giter Site home page Giter Site logo

oliyh / martian Goto Github PK

View Code? Open in Web Editor NEW
514.0 18.0 40.0 22.33 MB

The HTTP abstraction library for Clojure/script, supporting OpenAPI, Swagger, Schema, re-frame and more

License: MIT License

Clojure 99.43% Java 0.45% HTML 0.07% Emacs Lisp 0.06%
martian swagger-api http http-client swagger clojure clojurescript interceptor open-api schema

martian's Introduction

Martian

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.

Latest versions & API docs

The core library (required):

Clojars Project cljdoc badge

Support for various HTTP client libraries:

HTTP client JVM Javascript Babashka Docs
Clojars Project cljdoc badge
Clojars Project cljdoc badge
Clojars Project cljdoc badge
Clojars Project cljdoc badge
Clojars Project cljdoc badge
Clojars Project cljdoc badge
Clojars Project cljdoc badge

Testing and other interop libraries:

Clojars Project README

Clojars Project cljdoc badge README

Clojars Project cljdoc badge README

Features

  • Bootstrap an instance from just a OpenAPI/Swagger url, a local definition file or provide your own API mapping
  • Modular with support for many HTTP client libraries (see table above)
  • Build urls and request maps from code or generate and perform the request, returning the response
  • Validate requests and responses to ensure they are correct before the data leaves/enters your system
  • Explore an API from your REPL
  • Extensible via interceptor pattern - inject your own interceptors anywhere in the chain
  • Negotiates the most efficient content-type and handles serialisation and deserialisation including transit, edn and json
  • Easy to add support for any other content-type
  • Support for integration testing without requiring external HTTP stubs
  • Routes are named as idiomatic kebab-case keywords of the operationId of the endpoint in the OpenAPI/Swagger definition
  • Parameters are aliased to kebab-case keywords so that your code remains idiomatic, neat and clean
  • Parameter defaults can be optionally applied
  • Simple, data driven behaviour with low coupling using libraries and patterns you already know
  • Pure client code, no server code or modifications required
  • Write generative, realistic tests using martian-test to generate response data
  • Record and play back HTTP calls using martian-vcr

For more details and rationale you can watch the talk given to London Clojurians or there is also an older talk given at ClojureX Bytes.

Clojure / ClojureScript

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)

No Swagger, no problem

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 with 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 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.

Recording and playback with martian-vcr

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.

Idiomatic parameters

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

Custom behaviour

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.

Global behaviour

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 []}}

Per route behaviour

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
    • etc
  • 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.

Custom content-types

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

Response validation

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.

Defaults

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

Development mode

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"

Java

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

Caveats

  • You need :operationId in the OpenAPI/Swagger spec to name routes when using bootstrap-openapi
    • pedestal-api automatically generates these from the route name

Development

Circle CI

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.

Issues and features

Please feel free to raise issues on Github or send pull requests.

Acknowledgements

Martian uses tripod for routing, inspired by pedestal.

martian's People

Contributors

andreacrotti avatar andrewmcveigh avatar benedekfazekas avatar bombaywalla avatar borkdude avatar czan avatar davidjameshumphreys avatar deas avatar dijonkitchen avatar jcdlbs avatar latacora-paul avatar lispyclouds avatar lucianolaratelli avatar markdingram avatar martinklepsch avatar norton avatar oliyh avatar omartell avatar polymeris avatar rafaeleal avatar rgkirch avatar rickmoynihan avatar rkirchofer avatar robertluo avatar samuelwagen avatar the-alchemist avatar wkok avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

martian's Issues

Document the behavior or per-route interceptor chain

I couldn't find an example of the per-route behavior and the current documentation is unclear how the per-route interceptor chain composes with the global interceptor chain. Does it replace the global chain? Is it prepended/appended to the global chain?

Body params should be taken from supplied values and should not be named

Currently martian attempts to give the body params a name and expects values in a submap under that name, e.g. (request-for m {:pet {:name "charlie" :age 2}}) where :pet is derived from the schema.

It should not do this; it is not robust and does not fit with the aim of abstracting HTTP structure from the function call. Instead, it should allow this: (request-for m {:name "charlie" :age 2}).

Generate responses using response schema

Swagger defines the responses you might get. In test mode we can not only validate the request but also return a reasonable response based on the response schema by using generators.

Produces / consumes should be part of Martian record

Currently it is left in the swagger definition and used by the http implementations, but as part of the data API it needs to be a first class citizen.

Produces / consumes should also be specifiable at the top level in the data API as it is in Swagger to avoid repetition.

Query and body param names cannot be kebab cased

These are supplied to the remote API and so must retain their casing.

Right now they are kebab cased which is wrong.

Ideally the user could supply the kebab cased keys and they would auto translate to the right thing. Could add an aliases interceptor to achieve this.

martian-test help please

Hi @oliyh,

I don’t remember if I found martian or pedestal-api first, but I’m trying to use them together as part of ephemeris-api here on a branch. I don’t know if I’m badly calling martian/bootstrap-swagger or martian/response-for. I’ve read most of what’s out there: posts, readme, examples, etc. I’ll break it up into three points I could use help with. The first one being what’s really tripping me.

  1. The martian #readme martian-test example uses the pedestal-api example, but I don’t think it works against that, because I tried it and swagger-definition is not recognized. I’ve worked out a ci deployments workflow and got test.astrolin.org ready for martian-test I just can’t figure out how to use it.

  2. I kind of copied testing a core namespace from spa-skeleton, but I’m thinking in the case of api without frontend I should perhaps be testing the service namespace... What do you think? I ask because these tests really run against a server rather than any namespace and I wonder if it matters at all? But then for a service maybe the handlers become core - would you move the handlers of a bigger web service / api into a core namespace?

  3. Is midje of any use with martian-test?

Question about use of :pet in example

In the example below a :pet structure is setup but then :pet is not used. Is this just a typo?

(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"}))

JSON array response error

(:response-schemas handler) fails with an error for this Swagger defn (as the array has no name):

"200": {
  "schema": {
    "type": "array",
    "items": {
      "$ref": "#/definitions/Device"
    }
  },
  "description": "all devices"
}
#error{:cause "Assert failed: (clojure.core/not (clojure.core/nil? s__1611__auto__))",
       :via [{:type java.lang.AssertionError,
              :message "Assert failed: (clojure.core/not (clojure.core/nil? s__1611__auto__))",
              :at [camel_snake_kebab.core$__GT_kebab_case_keyword invokeStatic "core.cljc" 21]}],
       :trace [[camel_snake_kebab.core$__GT_kebab_case_keyword invokeStatic "core.cljc" 21]
               [camel_snake_kebab.core$__GT_kebab_case_keyword doInvoke "core.cljc" 21]
               [clojure.lang.RestFn invoke "RestFn.java" 410]
               [martian.schema$make_schema invokeStatic "schema.cljc" 67]
               [martian.schema$make_schema invoke "schema.cljc" 47]

Workaround is to wrap the array in an object:

"200": {
  "schema": {
    "type": "object",
    "properties": {
      "devices": {
        "type": "array",
        "items": {
          "$ref": "#/definitions/Device"
        }
  }
}

Support OpenAPI v3

Hey! Nice stuff!
Unfortunately, my project uses OpenAPI v3, and YAML, it would be dope if martian supported that.

Auto negotiate content type

We know what content types the server produces and accepts. We should be able to choose one of these and satisfy it without the user having to specify anything.

Exotic encodings could be implemented with interceptors, of course.

Make Martian a record

At the moment it's an inscrutable closure that reifies the protocol. Making it a record would enable cool features like manipulation and attaching of metadata to individual routes (which would enable interceptors per route) without having to subvert the Swagger spec to include these features.

It doesn't work (for a lack of a better description)

Hi. I've tried using this library on a json spec which is generated by a library called servant-swagger.

This is the spec:

{"swagger":"2.0","info":{"version":"","title":"EZroute api specification"},"host":"localhost:8080","paths":{"/position/update/{id}":{"post":{"consumes":["application/json"],"produces":["application/json"],"parameters":[{"maximum":9223372036854775807,"format":"int64","minimum":-9223372036854775808,"required":true,"in":"path","name":"id","type":"integer"},{"required":true,"schema":{"$ref":"#/definitions/PositionUpdate"},"in":"body","name":"body"}],"responses":{"404":{"description":"`id` not found"},"400":{"description":"Invalid `body`"},"204":{"description":""}}}},"/company":{"get":{"produces":["application/json"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/Company"},"type":"array"},"description":""}}}},"/driver":{"get":{"produces":["application/json"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/Driver"},"type":"array"},"description":""}}}}},"definitions":{"PositionUpdate":{"required":["position","timestamp"],"properties":{"position":{"$ref":"#/definitions/Coordinate"},"timestamp":{"$ref":"#/definitions/UTCTime"}},"type":"object"},"Coordinate":{"required":["lat","lng"],"properties":{"lat":{"format":"double","type":"number"},"lng":{"format":"double","type":"number"}},"type":"object"},"UTCTime":{"example":"2016-07-22T00:00:00Z","format":"yyyy-mm-ddThh:MM:ssZ","type":"string"},"Company":{"required":["companyName"],"properties":{"companyName":{"type":"string"}},"type":"object"},"Driver":{"required":["driverName","driverLocation","driverCompany"],"properties":{"driverName":{"type":"string"},"driverLocation":{"$ref":"#/definitions/Coordinate"},"driverActive":{"$ref":"#/definitions/Key"},"driverCompany":{"$ref":"#/definitions/Key"}},"type":"object"},"Key":{"required":["unDeviceKey"],"properties":{"unDeviceKey":{"$ref":"#/definitions/BackendKey"}},"type":"object"},"BackendKey":{"required":["unSqlBackendKey"],"properties":{"unSqlBackendKey":{"maximum":9223372036854775807,"format":"int64","minimum":-9223372036854775808,"type":"integer"}},"type":"object"}}}

There is no exception if I just use martin-http/bootstrap-swagger. I just get an empty vector. Also, I tried it on the pedestal API example, which also didn't work. I have tried unsuccessfully to debug this in the debugger, but I'm pretty new to CLJS and honestly didn't really uncover much. I also tried looking at the source and (mostly) replicating what the http module was doing(it seemed I could just pass it the JSON spec and an URL), and that didn't work either(A vector was indexed with something that wasn't an integer). The stack trace was pretty useless since it doesn't seem to include any of the functions in this library, so I have no idea what is going on.

One last thing: I confirmed that a network request for the swagger spec was being performed successfully. Maybe an exception is being swallowed somewhere?

Here is the code

  (go (let [api (<! (m-http/bootstrap-swagger "http://localhost:3449/spec.json")) ]
        (info api)
        (info (m/explore api))
        ))

Generate ring request maps as well as just routes

Suitable for passing directly in to clj-http or cljs-http, e.g:

(request-for :update-pet {:id 123})

;; => {:url "https://api.com/pet/123"
         :method :put}

Not sure if it's too much of a stretch to imagine it taking more parameters and setting them on the body as well - the information is there in the Swagger API to be able to do so but it's starting to feel a bit like SOAP!

Handling self-sign SSL certificate

Hi,
this is really great stuff! I really enjoy using martian. But I have issue with self-sign SSL certificate, this is the error I get form repl:

SunCertPathBuilderException unable to find valid certification path to requested target  sun.security.provider.certpath.SunCertPathBuilder.build (SunCertPathBuilder.java:141)

And this is how my code looks like:

(defn authenticate
   "Authenticate"
[x u p]
(let [m (martian-http/bootstrap-swagger x)]
(martian/response-for m :Authenticate {:user u :password p})
))

Is there any way i can set program to accept self sign certificates?

Thanks

Route-specific interceptors

Should be able to annotate interceptors with an operation id or something and apply it only for that route

Can't use idiomatic parameters in nested body keys

I'm using Martian to interact with the Plaid API and a common thing they do is allow a json object to be passed in the post body under the "options" key. For example, the Balance endpoint allows "account_ids" in the "options" object.

With the config:

{:route-name  :get-balance
 :produces    ["application/json"]
 :consumes    ["application/json"]
 :path-parts  ["/institutions/get_by_id"]
 :method      :post
 :body-schema {:get-balance {:access_token
                             schema/Str

                             (schema/optional-key :options)
                             {(schema/optional-key :account_ids) [schema/Str]}}}
 :interceptors [set-client-id-and-secret]}

and the parameters {:access-token "some-access-token", :options {:account-ids ["some-account-id"]}}, an exception with the message: "clojure.lang.ExceptionInfo: Value cannot be coerced to match schema: {:options {:account-ids disallowed-key}}" is thrown.

Reading through the code, I get the feeling that this use-case wasn't considered. For now I'll just use the snake_cased form for my nested keyword, but I really love the idiomatic parameters feature and would love to see it expanded to work with nested keys.

By the way, this library is genius; I'd always wished for something like this but never was able to put my finger on exactly how to do it. Thank you.

Map idiomatic kebab keys for params for query, headers, body, form

A remote API with a query param called CamelKey must be provided as :CamelKey when calling that endpoint in martian.

An interceptor (or each of the interceptors for query, headers, body and form) should map between the two so that client code can be idiomatic and provide :camel-key.

Support array types

The following should result in [s/Str]
[:in "body" :schema {:type "array" :items {:type "string"}}]

More flexible way of supplying interceptors

Could use angel-interceptor, or at make martian interceptors public so that the user can weave them as them wish (this is probably best).

Also consider using merge :into or merge :replace

Explore functionality

Sometimes you don't quite know what the routes are going to be named or what parameters they will need - the explore functionality should help you with this.

Error handling

Ideally we will call the :error function of interceptors.

[re-frame] Pass opts parameter to ::request to be consumed by interceptors

I am using JWT-based auth, so I need to set the Authorization header of the request based on a token stored in re-frame's database. This can be accomplished easily enough by creating a martian interceptor like this:

(def set-authorization-header
  {:name  ::set-token
   :enter (fn [ctx]
            (if-let [token (:token @re-frame.db/app-db)]
              (assoc-in ctx [:request :headers "Authorization"] (str "Bearer " token))
              ctx))})

However, directly accessing the re-frame db atom like that is not very nice, of course. So instead, I suggest extending the ::martian.re-frame/request event with a fifth argument, which would be a map of options added to the context, i.e.

(re-frame/dispatch [::martian.re-frame/request operation-id data on-success on-failure {:authorization-token "foo"}])

would result in ctx containing a key :opts mapping to {:authorization-token "foo"}

I am willing to submit a PR if you think this is a good idea.

Optionally destructure body params

After changing the behaviour first introduced in #5 and then reverted to acommodate body arrays as per #26, the upgrade path from 0.1.2 to 0.1.3 was painful for anyone who used body params.

There should be a cascade of where martian looks for body params, in order:

  1. ::martian.core/body
  2. The name of the body schema, e.g. :pet
  3. The destructured body schema keys for map schemas, e.g. :name and :age

This means that the following 3 forms are equivalent:

(request-for :create-pet {:id 123 :name "charlie"})                                                                                                                                                                                                                               
(request-for :create-pet {:pet {:id 123 :name "charlie"}})                                                                                                                                                                                                                        
(request-for :create-pet {::martian/body {:id 123 :name "charlie"}}) 

This gives the benefits of #5 while also allowing easy use of body arrays as per #26 and #30 and make it easy to migrate up from 0.1.2.

Leave interceptors should be run in go block

cljs-http perform request should lift the context into the go block handling the response, just like the httpkit one. This means interceptors after the request can be unaware of the async nature.

Allow interceptors to be passed in

e.g. (bootstrap-swagger "https://api.com" swagger-definition :interceptors [...])

This will allow the user to

  1. Add hardcoded parameter values e.g. authentication headers
  2. Implement the HTTP call via an interceptor which would perform the role of the handler, but keep martian agnostic
  3. Implement a replacement of the HTTP call with a generator which uses the schema of the response to generate a realistic value without ever having to call the remote service.
  4. Add custom serialisation before the HTTP call

These interceptors should probably go at the end, although something like hardcoded parameter values would be harder - e.g. the routes one would have to go before the uri was formed. Perhaps angel-interceptor could be utilised, or else (or as well) make the martian ones public so that the user can order them as they please.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.