oliyh / martian

The HTTP abstraction library for Clojure/script, supporting OpenAPI, Swagger, Schema, re-frame and more
MIT License
525 stars 42 forks source link

Resolve $ref params and route-level params #113

Closed onetom closed 2 years ago

onetom commented 3 years ago

Support parameter refs

{:components
 {:parameters {"ParamRef" {:name "some-param", :in "query"}}},

 :paths
 {"/some-path"
  {:get {:operationId "someOpId",
         :parameters  [{:name "hardwired-param", :in "query"}
                       {:$ref "#/components/parameters/ParamRef"}]}}}}

Support route-level parameters (including parameter refs)

{:components
 {:parameters
  {"ParamRef" {:name "indirect-route-param", :in "query"}}},

 :paths
 {"/some-path"
  {:parameters [{:$ref "#/components/parameters/ParamRef"}
                {:name "route-param", :in "query"}],
   :get        {:operationId "someOpId",
                :parameters
                             [{:name "hardwired-param", :in "query"}
                              {:name "method-param", :in "query"}]}}}}
onetom commented 3 years ago

Unfortunately, this is failing with some strange validation errors at the moment:

(martian/request-for xero-api :get-report-profit-and-loss)
Execution error (ExceptionInfo) at schema-tools.coerce/coerce-or-error! (coerce.cljc:24).
Could not coerce value to schema: {:xero-tenant-id disallowed-key}
(martian/request-for xero-api :get-report-profit-and-loss
                       {:xero-tenant-id "<tenant id override>"})
Execution error (ExceptionInfo) at schema-tools.coerce/coerce-or-error! (coerce.cljc:24).
Could not coerce value to schema: {:xero-tenant-id disallowed-key}

I have the suspicion that the source of the error is caused by some of the changes between v0.1.16 andmaster` and not by my patches.

Either way, we have decided to resolve the $refs and inline the path-level params in the openapi format, before passing it into martian and that worked.

Here is the rough summary of our approach:

(ns xero.repl
  (:require
    [clojure.data.json :as json]
    [clojure.string :as str]
    [martian.core :as martian]
    [martian.openapi :as openapi]
    [martian.interceptors :as interceptors]
    [martian.clj-http :as martian-http]))

(defn map-vals
  "{:a 1
    :b 2}
   ->
   {:a (f 1)
    :b (f 2)}"
  [f m] (reduce-kv #(assoc %1 %2 (f %3)) {} m))

(defn json-decode
  "Blank strings are decoded as nil."
  [json-str & args]
  (if (str/blank? json-str)
    nil
    (apply json/read-str json-str :key-fn keyword args)))

;; Get Xero OpenAPI in JSON format:
;;   curl -s https://raw.githubusercontent.com/XeroAPI/Xero-OpenAPI/master/xero_accounting.yaml | yaml2json > xero_accounting.json

(defn select-paths [openapi paths]
  (update openapi :paths select-keys (map keyword paths)))

(def xero-openapi
  (-> (json-decode (slurp "xero_accounting.json"))
      (select-paths ["/Reports/ProfitAndLoss"])))

(defn de$ref
  "Can use with clojure.walk/postwalk to substitute JSON references with
   their values.

   Documentation about JSON references:
     https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html"
  [components x]
  (if-let [ref (and (map? x) (:$ref x))]
    (#'openapi/lookup-ref components ref)
    x))

(defn preprocess-params
  "Inject path-level parameters into the operation-level parameter lists and
   resolve parameter references."
  [openapi]
  (let [de$ref-params (partial map (partial de$ref (:components openapi)))]
    (update openapi :paths
            (partial map-vals
                     (fn [ops]
                       ;; ops is a map. Its keys are keywordized HTTP method
                       ;; names OR :parameters or :description
                       (let [+route-params (partial into (:parameters ops))]
                         (->> ops
                              (map-vals
                                (fn [op]
                                  (cond-> op
                                          (:operationId op)
                                          (update :parameters
                                                  (comp
                                                    de$ref-params
                                                    +route-params))))))))))))

(defn xero-api []
  (martian/bootstrap-openapi
    (openapi/base-url xero-openapi)
    (preprocess-params xero-openapi)))

(comment
  (martian/request-for xero-api :get-report-profit-and-loss
                       {:xero-tenant-id "<tenant id override>"})
  )

What's your opinion @oliyh ?

onetom commented 3 years ago

Here is a version of the preprocessing with Specter:

(defn preprocess-params-with-specter [openapi]
  (->> openapi
       (transform
         [(collect-one :components)
          :paths MAP-VALS
          (collect-one :parameters)
          MAP-VALS #(:operationId %) :parameters]

         (fn [components path-params method-params]
           (->> (into path-params method-params)
                (map (partial de$ref components)))))))
oliyh commented 3 years ago

Hello,

Thank you for all your efforts here. I haven't had time to look properly at your code yet but it should be possible for martian to understand this natively without you needing to preprocess anything.

I'll try to find time to look into this for you.

behrica commented 2 years ago

Having the same issue, and it would be cool to have it solved

oliyh commented 2 years ago

Hello,

I used some of your code to implement this in #140, thank you. It's now available in 0.1.21-SNAPSHOT, so closing this PR.