Fork me on GitHub
#reitit
<
2023-10-10
>
adham11:10:34

Hey all, I'm trying to use the required parameter as described here https://swagger.io/docs/specification/describing-parameters/ and seen here

parameters:
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            default: 0
          required: false
and my configuration is
(st/schema
    s/Str
    {:name "Date to"
	 :description "TODO"
	 :openapi/example "2023-06-30"
	 :required false})
trying this does not work too
(st/schema
    s/Str
    {:name "Date to"
	 :description "TODO"
	 :openapi/example "2023-06-30"
	 :openapi/required false})
How can I use that parameter if it's available?

adham07:10:32

Figured it out, it looks like you have to use some form of optional from the coercion library you are using, such as (s/opitonal-key) from Schema. @U055NJ5CC I think this is worth documenting, should I attempt to make a PR for it?

Stig Brautaset13:10:42

I'm trying to generate OpenAPI spec (using 0.7.0-alpha7) for this Clojure Spec:

(s/def ::not-blank-string
  (s/and string? (complement str/blank?)))

(s/def ::foo ::not-blank-string)
(s/def ::bar ::not-blank-string)
(s/def ::quux ::not-blank-string)

(s/def ::foo-or-bar (s/or :foo (s/keys :req-un [::foo])
                          :bar (s/keys :req-un [::bar])))

(s/def ::merged
  (s/merge ::foo-or-bar
           (s/keys :req-un [::quux])))
Basically I want to require either the :foo or the :bar key. This appears to work in a repl, but the behaviour in Swagger UI doesn't make sense to me. (Full example in 🧵 )

Stig Brautaset13:10:37

By "works in repl" I mean:

toy.foo> (gen/sample (s/gen ::merged))
({:foo "z", :quux "G"}
 {:foo "r9", :quux "2I"}
 {:bar "69", :quux "p4"}
 {:bar "03", :quux "AwM"}
 {:foo "J", :quux "a"}
 {:foo "w1vLd", :quux "371"}
 {:foo "6QGxc9", :quux "Uf5C3"}
 {:foo "80", :quux "OpD98"}
 {:bar "sqHR6", :quux "1"}
 {:bar "s92X3yv", :quux "8CykTIik"})
toy.foo> (s/explain ::merged {:foo "234" :quux "bob"})
Success!
nil
toy.foo> (s/explain ::merged {:bar "234" :quux "bob"})
Success!
nil
toy.foo> (s/explain ::merged {:baz "234" :quux "bob"})
{:baz "234", :quux "bob"} - failed: (contains? % :foo) at: [:foo] spec: :toy.foo/foo-or-bar
{:baz "234", :quux "bob"} - failed: (contains? % :bar) at: [:bar] spec: :toy.foo/foo-or-bar
nil

Stig Brautaset13:10:13

This is the full example. deps.edn contains:

{:deps {expound/expound {:mvn/version "0.9.0"}
        org.clojure/clojure {:mvn/version "1.11.1"}
        metosin/reitit {:mvn/version "0.7.0-alpha7"}
        metosin/ring-swagger-ui {:mvn/version "5.9.0"}
        ring/ring-jetty-adapter {:mvn/version "1.10.0"}}}
(probably not all necessary.)

Stig Brautaset13:10:01

In Swagger UI I get this error:

{
  "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/merge :toy.foo/foo-or-bar (clojure.spec.alpha/keys :req-un [:toy.foo/quux])), :type :map, :leaf? false})",
  "problems": [
    {
      "path": [
        "foo"
      ],
      "pred": "(clojure.core/fn [%] (clojure.core/contains? % :foo))",
      "val": {
        "quux": "string"
      },
      "via": [
        "toy.foo/merged",
        "toy.foo/foo-or-bar"
      ],
      "in": []
    },
    {
      "path": [
        "bar"
      ],
      "pred": "(clojure.core/fn [%] (clojure.core/contains? % :bar))",
      "val": {
        "quux": "string"
      },
      "via": [
        "toy.foo/merged",
        "toy.foo/foo-or-bar"
      ],
      "in": []
    }
  ],
  "type": "reitit.coercion/request-coercion",
  "coercion": "spec",
  "value": {
    "bar": "string",
    "quux": "string"
  },
  "in": [
    "request",
    "body-params"
  ]
}

Stig Brautaset13:10:32

So it looks like somehow both foo and bar has become required?

Stig Brautaset13:10:33

The OpenAPI spec does not suggest that both "foo" and "bar" are required, so I'm not sure what's going on.

Stig Brautaset13:10:23

This alternative spec acts like I expect:

(s/def ::quux-foo-bar
  (s/keys :req-un [::quux]
          :opt-un [::foo ::bar]))
... but it does not convey that one of ::foo or ::bar is required.

opqdonut08:10:22

looks to me like it's not finding neither :foo nor :bar. there are two "problems"

opqdonut08:10:23

could it be that the map keys aren't getting coerced to keywords properly? that's why it's not finding :bar in the map

opqdonut08:10:12

I haven't looked in detail into how reitit coercion works for spec, especially for s/merge

opqdonut08:10:35

does it work if you use ::foo-or-bar as the parameter spec instead of ::merged?

Stig Brautaset12:10:15

Nope, similar error:

{
  "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/or :foo (clojure.spec.alpha/keys :req-un [:toy.foo/foo]) :bar (clojure.spec.alpha/keys :req-un [:toy.foo/bar])), :type [:or [:map]], :leaf? true})",
  "problems": [
    {
      "path": [
        "foo"
      ],
      "pred": "(clojure.core/fn [%] (clojure.core/contains? % :foo))",
      "val": {},
      "via": [
        "toy.foo/foo-or-bar"
      ],
      "in": []
    },
    {
      "path": [
        "bar"
      ],
      "pred": "(clojure.core/fn [%] (clojure.core/contains? % :bar))",
      "val": {},
      "via": [
        "toy.foo/foo-or-bar"
      ],
      "in": []
    }
  ],
  "type": "reitit.coercion/request-coercion",
  "coercion": "spec",
  "value": {
    "foo": "string"
  },
  "in": [
    "request",
    "body-params"
  ]
}

Stig Brautaset12:10:54

I wonder if the problem is that it's trying to evaluate the spec under the path "foo" or "bar"?

Stig Brautaset14:10:09

Hmm, whatever it is it seems like it's local to the Spec coercer. The following Malli schema & route works:

(def NonEmptyString [:string {:min 1}])

(def Merged
  [:or
   [:map
    [:foo NonEmptyString]
    [:baz NonEmptyString]]
   [:map
    [:bar NonEmptyString]
    [:baz NonEmptyString]]])
...
     ["/baz"
      {:post {:summary "Baz - Malli"
              :coercion reitit.coercion.malli/coercion
              :parameters {:body Merged}
              :responses {200 {:body Merged}}
              :handler (fn [{{merged :body} :parameters}]
                         (prn merged)
                         {:status 200
                          :body merged})}}]
...

Stig Brautaset16:10:29

... for certain values of "works". It won't actually blow up if you post both :foo and :bar, it will match one of them and strip the other key. So it hides the issue. Not quite what I want 🙂

opqdonut05:10:23

that's the default behaviour to help you make backwards-compatible APIs

opqdonut05:10:29

try disabling :strip-extra-keys maybe

Stig Brautaset10:10:58

I don't want to disable the extra-key stripping. My current approach is making both optional, document that the keys are mutually exclusive, and throw a 422 error if passed both.

Hendrik14:10:00

Is it possible to intercept a route change in reitit-fronted router and stop the routing? For example if I have a form with unsaved data and want to prevent the browser from routing away

Michaël Salihi11:11:00

Hi @U023LKF2PQV ! Exactly interested by this topic. Did you find any solution?

Hendrik17:11:21

no unfortunately, not. I think, the only way is to write custom route-to functions and write an interception function for the <a> tag handler

Hendrik17:11:41

This is because route-to directly invokes push-state

Michaël Salihi20:11:32

Thanks @U023LKF2PQV for your confirmation. This is also the approach I thought of.

bzmariano20:10:54

Hello. I'm building a web server with Ring, Reitit, and Malli. I'm currently encountering issues with the incoming data coercion. I'm going to share the issue and the code in a thread to avoid bothering.

bzmariano20:10:24

The scenario is as follows: I have an endpoint where I send an HTTP PUT request with a subset of values from an entity. For example, I have a user entity with a name and last name, and in the HTTP call, I'm only sending the lastname with the new value to update. I have a schema defined with {:optional true} on both properties, which I use for coercion in body parameters of the endpoint route. However, when making the call to the endpoint, the server fails with a 500 error and only says "error connection closed." I understand that the issue may lie in the value coercion and the Malli configuration. The rest of the endpoints (create, read, delete) work just fine so the problem shouldn't be in the routes. For this case, the server configuration (I copied from a github metosin/reitit/examples) is as follows: ;; schema ---------------------------------------------------- (def update-user-schema (mc/schema [:map [:username {:optional true} [:string {:min 8 :max 32}]] [:password {:optional true} [:string {:min 8 :max 32}]]])) ;; handler --------------------------------------------------- (defn update-one! [{:keys [db parameters]} :as req] (try (let [id (:id (:path parameters)) body (:body parameters) original (get-one! req) ;; next.jdbc/get-by-id newone (merge original body) foo (sql/update! db :user {:username (:username newone) :password (:password newone) :updated_at (java.time.Instant/now)} {:id id} {:suffix "RETURNING *"})] {:status 200 :body foo}) (catch Exception e {:status 400 :body e}))) ;; app -------------------------------------------------------- (defn create-app [db] (ring/ring-handler (ring/router routes {:exception pretty/exception :data {:db db :coercion (malli/create {:error-keys #{#_:type :coercion :in :schema :value :errors :humanized #_:transformed} :compile mu/closed-schema :strip-extra-keys true :default-values true :options nil}) :muuntaja m/instance :middleware [swagger/swagger-feature muuntaja/format-negotiate-middleware muuntaja/format-response-middleware exception/exception-middleware muuntaja/format-request-middleware coercion/coerce-exceptions-middleware coercion/coerce-request-middleware coercion/coerce-response-middleware mw/db [cors/wrap-cors :access-control-allow-origin [#"."] :access-control-allow-origin [:get :post :put :delete]]]}}) default-routes-handler))Hello. I'm building a REST API with Ring, Reitit, and Malli. I'm currently encountering issues with the incoming data coercion. The scenario is as follows: I have an endpoint where I send an HTTP PUT request with a subset of values from an entity. For example, I have a user entity with a name and last name, and in the HTTP call, I'm only sending the lastname with the new value to update. I have a schema defined with {:optional true} on both properties, which I use for coercion in body parameters of the endpoint route. However, when making the call to the endpoint, the server fails with a 500 error and only says "error connection closed." I understand that the issue may lie in the value coercion and the Malli configuration. The rest of the endpoints (create, read, delete) work just fine so the problem shouldn't be in the routes. For this case, the server configuration (I copied from a github metosin/reitit/examples) is as follows: ;; schema ---------------------------------------------------- (def update-user-schema (mc/schema [:map [:username {:optional true} [:string {:min 8 :max 32}]] [:password {:optional true} [:string {:min 8 :max 32}]]])) ;; handler --------------------------------------------------- (defn update-one! [{:keys [db parameters]} :as req] (try (let [id (:id (:path parameters)) body (:body parameters) original (get-one! req) ;; next.jdbc/get-by-id newone (merge original body) foo (sql/update! db :user {:username (:username newone) :password (:password newone) :updated_at (java.time.Instant/now)} {:id id} {:suffix "RETURNING *"})] {:status 200 :body foo}) (catch Exception e {:status 400 :body e}))) ;; app -------------------------------------------------------- (defn create-app [db] (ring/ring-handler (ring/router routes {:exception pretty/exception :data {:db db :coercion (malli/create {:error-keys #{#_:type :coercion :in :schema :value :errors :humanized #_:transformed} :compile mu/closed-schema :strip-extra-keys true :default-values true :options nil}) :muuntaja m/instance :middleware [swagger/swagger-feature muuntaja/format-negotiate-middleware muuntaja/format-response-middleware exception/exception-middleware muuntaja/format-request-middleware coercion/coerce-exceptions-middleware coercion/coerce-request-middleware coercion/coerce-response-middleware mw/db [cors/wrap-cors :access-control-allow-origin [#"."] :access-control-allow-origin [:get :post :put :delete]]]}}) default-routes-handler))

Stig Brautaset22:10:51

It looks like coercion from JSON is not working properly for uri? using the Malli coercer. (But it works for the spec coercer.) Am I doing something wrong, or is this a bug? 🧵

Stig Brautaset22:10:32

This snippet shows the issue

["/uri-spec"
      {:post {:summary "Post a URI - successfully coerced"
              :coercion reitit.coercion.spec/coercion
              :parameters {:body {:uri uri?}}
              :responses {200 {:body {:uri uri?}}}
              :handler (fn [{{{uri :uri} :body} :parameters}]
                         (prn uri)
                         {:status 200 :body {:uri uri}})}}]

     ["/uri-malli"
      {:post {:summary "Post a URI - fails coercion"
              :coercion reitit.coercion.malli/coercion
              :parameters {:body {:uri uri?}}
              :responses {200 {:body {:uri uri?}}}
              :handler (fn [{{{uri :uri} :body} :parameters}]
                         (prn uri)
                         {:status 200 :body {:uri uri}})}}]]
The routes are the same, except for the coercion used. With the spec version the uri in the body is coerced into a java.net.URI as expected, but with the Malli coercer I get this error:
{
  "value": {
    "uri": ""
  },
  "type": "reitit.coercion/request-coercion",
  "coercion": "malli",
  "in": [
    "request",
    "body-params"
  ],
  "humanized": {
    "uri": [
      "should be a uri"
    ]
  }
}

Stig Brautaset22:10:19

Complete example

opqdonut08:10:11

looks like malli doesn't have any encoders/decoders for uri?, you might need to add them yourself

Stig Brautaset09:10:43

There's no entry for :uri in the json-decoder just above.