clojure-spec

ray 2025-01-13T09:14:15.545349Z

I thought of this the other day as a way to get a very specific version of a spec to be applied in context:

(s/fdef handler
  :args (s/cat :request ::ring-request)
  :ret ::ring-response
  :fn (fn input-output-check [{:keys [args ret]}]
        ;; Ensure we apply the correct spec
        (if (->> args :request :uri server-request?)
          (s/assert ::file-body (:body ret))
          (s/assert ::html-body (:body ret)))))
It's a way to have a ::body that might be a set of specs or any? to be more specifically checked in context. I know that spec2 has something to say about specificity but this seems useful with the current version. Maybe it was obvious but I hadn't seen it before.

2025-01-13T17:15:05.796069Z

You probably already know some or all of this. But... I think it's probably better to use s/or in your spec definition for ring-response. I think you're essentially doing an or but in a way that can't be automatically used to generate data like s/or can. Also the fdef :fn is only used during testing during calls to clojure.spec.test.alpha/check so you would only be useful during testing. In fdef, the args and ret are conformed, so you will see the responses "tagged" with the branch of the or they take I don't think this is actually a case where the context stuff from spec2 (e.g. select) is relevant. You would still want to use a s/or there. Full example of code while I was messing with this:

(ns ringspecexample
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.gen.alpha :as gen]
            [clojure.spec.test.alpha :as st]
            [ :as io]))

(s/check-asserts true)

(s/def ::file-body #(instance? java.io.File %))
(s/def ::html-body string?)
(s/def ::body      (s/or :file ::file-body :html ::html-body))
(s/def ::uri       string?)
(s/def ::ring-request  (s/keys :req-un [::uri]))
(s/def ::ring-response (s/keys :req-un [::body]))

(defn file-request? [req]
  (re-find #"^file:"(:uri req)))

(defn handler [req]
  ; currently broken for file requests, which should return a File.
  {:body "body"})

(s/exercise ::ring-response)
; excersise gives generated data + conformed version for each
#_([{:body ""} {:body [:html ""]}]
 [{:body "U"} {:body [:html "U"]}]
 [{:body "9"} {:body [:html "9"]}]
 [{:body ""} {:body [:html ""]}]
 [{:body #object[java.io.File 0x23a918c7 "t7"]}
  {:body [:file #object[java.io.File 0x23a918c7 "t7"]]}]
 [{:body #object[java.io.File 0x7a45d714 ""]}
  {:body [:file #object[java.io.File 0x7a45d714 ""]]}]
 [{:body #object[java.io.File 0x4483d35 "wsThZ"]}
  {:body [:file #object[java.io.File 0x4483d35 "wsThZ"]]}]
 [{:body #object[java.io.File 0x204abeff "k4wjYx"]}
  {:body [:file #object[java.io.File 0x204abeff "k4wjYx"]]}]
 [{:body #object[java.io.File 0x4b4ee511 "i92D"]}
  {:body [:file #object[java.io.File 0x4b4ee511 "i92D"]]}]
 [{:body "phlH9JVXA"} {:body [:html "phlH9JVXA"]}])


(s/fdef handler
  :args (s/cat :request ::ring-request)
  :ret  ::ring-response
  :fn   (fn [{:keys [args ret]}]
          ; both args and ret are the conformed versions here
          (prn "args:" args "ret:" ret)
          ;"args:" {:request {:uri "file:"}} "ret:" {:body [:html "body"]}
          (if (file-request? (:request args))
            (-> ret :body first (= :file))
            (-> ret :body first (= :html)))))

(st/instrument `handler)
;;instrument just causes :args to be checkd
(handler {:uri ""})
(handler 123) ;;
#_{:clojure.spec.alpha/problems
   [{:path [:request],
     :pred clojure.core/map?,
     :val  123,
     :via  [:ringspecexample/ring-request :ringspecexample/ring-request],
     :in   [0]}]
   ...}

; st/check checks ret/fn
(st/check `handler
  {:gen {::uri
         #(gen/frequency
            [[1 (gen/fmap (partial str "file:") (s/gen string?))]
             [1 (s/gen string?)]])
         ::file-body
         #(gen/fmap io/file (s/gen string?))
         }})

;;will show failures like:
; {:shrunk
;    {:total-nodes-visited 6, :depth 2, :pass? false, :result #error {
;  :cause "Specification-based check failed"
;  :data {:clojure.spec.alpha/problems [{:path [:fn], :pred (clojure.core/fn [{:keys [args ret]}] (clojure.core/prn "args:" args "ret:" ret) (if (ringspecexample/file-request? (:request args)) (clojure.core/-> ret :body clojure.core/first (clojure.core/= :file)) (clojure.core/-> ret :body clojure.core/first (clojure.core/= :html)))), :val {:args {:request {:uri "file:"}}, :ret {:body [:html "body"]}}, :via [], :in []}], :clojure.spec.alpha/spec #object[clojure.spec.alpha$spec_impl$reify__2046 0x3fd05b3e "clojure.spec.alpha$spec_impl$reify__2046@3fd05b3e"], :clojure.spec.alpha/value {:args {:request {:uri "file:"}}, :ret {:body [:html "body"]}}, :clojure.spec.test.alpha/args ({:uri "file:"}), :clojure.spec.test.alpha/val {:args {:request {:uri "file:"}}, :ret {:body [:html "body"]}}, :clojure.spec.alpha/failure :check-failed}
;  :via ... }}

ray 2025-01-13T17:41:16.263159Z

this is all great stuff and let's leave aside the spec2 side of things - I shouldn't have mentioned it 🤦🏻

👍 1
ray 2025-01-13T17:45:36.005959Z

I know the convention is to use instrumentation during tests but there is no law against using it at runtime ... unless the spec:male-police-officer:🏼🚨🚓👮🏼‍♀️ is real 😅

ray 2025-01-13T17:46:52.848179Z

the issue is coordinating the request and the response cos they can be different and are varied on the input

2025-01-13T17:47:32.278149Z

haha yeah exactly, even using instrument at runtime will only check :args, never :ret or :fn

ray 2025-01-13T17:48:23.065909Z

well, it turns out that https://github.com/jeaye/orchestra does exactly that 😍

ray 2025-01-13T17:53:41.382689Z

I should have mentioned that I was using orchestra to achieve that goal.

ray 2025-01-13T17:54:42.605579Z

it seems to all work really well and is a great way to avoid a bunch of tests that are essentially just assertions anyway

ray 2025-01-13T17:56:31.054829Z

what I would ideally like is for a pairing of :request and :response such that the correct :body could be checked but I don't think it's on the table so this is the best I could do so far

ray 2025-01-13T18:22:02.788489Z

I might be able to make a generator like that tho 🤔

2025-01-13T18:30:18.745469Z

Haven't fully thought this through for this particular case, but you could always build up the data structure containing the request and the response then spec that, then validate that combined thing.

(s/def ::file-req+resp (s/keys :req-un [:file/request :file/response]))
;;alternatively:
(s/def ::file-req+resp (s/tuple :file/request :file/response))

(s/def ::req+resp (s/or :file ::file-req+resp
                        :html ::html-req+resp
                        ;...
                        ))
I think the spec2 stuff might start to help here since fully implementing the above does get a little awkward then having to do a req-un keys spec for each pair type containing e.g. file.response/body. Where in spec2 you could do something like
(s/or
  :file (s/schema {:request  (s/schema {:uri file-uri-string?})
                   :response (s/schema {:body ::file})})
  :html (s/schema {:request  (s/schema {:uri string?})
                   :response (s/schema {:body string?})}))

ray 2025-01-13T18:57:09.196519Z

We can but dream. Thanks for the suggestion and I’ll take a look when I’m back on an actual computer

ray 2025-01-13T20:17:06.193399Z

[ in your spec 2 example you have both :uri and :body defined in two ways ... that's what I meant, though clumsily expressed ]

ray 2025-01-13T20:19:40.439209Z

cos ::response will always have ::body and its spec is context dependent, hence the predicate dispatch in my example, where body is defined elsewhere as any? cos we want to ensure that the key is present at least.

ray 2025-01-13T20:23:09.656119Z

[ it's a proposal so don't let the lack of quality in the actual implementation get in your way 🙈 ]

ray 2025-01-13T20:29:57.773119Z

Coming back to the main problem ... I cannot have two context specific specs for the key ::body which I think was the point of Rich's Maybe Not talk

2025-01-13T20:40:39.911919Z

But I think that part isn't hard in spec1 (until you start to nest stuff). If you wanted to make the spec at the response-map level instead of the body level, instead of using s/or on ::body you could do

(s/def ::html-response
  (s/keys :req-un [:response.html/body]))
(s/def ::file-response
  (s/keys :req-un [:response.file/body]))

(s/def ::response
  (s/or :html-response ::html-response
        :file-response ::file-response))
both types still expect :body unqualified key but a different spec is used for each. This way could make more sense if there are other keys in the map depending on the "response type"

2025-01-13T20:46:38.423709Z

https://github.com/schmee/java-http-clj/blob/master/src/java_http_clj/specs.clj this might be a useful example for reference

ray 2025-01-13T20:47:12.176479Z

oh - ok, that's nice ... so you use the namespace as a way to do the variation.

ray 2025-01-13T20:50:31.508799Z

it still doesn't fix the fact that file / html are only correct based on the request. Cos that's where the bug lurks: a valid spec but an incorrect response to the request. So I think I'm still gonna need the :fn check to catch that.

ray 2025-01-13T20:51:44.532059Z

but it does get around the :body problem, for unqualified keys at least so gratitude-thank-you

2025-01-13T20:53:30.167369Z

no problem, yeah you're right that's the time to use :fn in fdef

ray 2025-01-13T20:54:24.875299Z

thanks for your time and support.

1
ray 2025-01-13T21:34:11.996859Z

fwiw the use of s/or makes processing the :args / :ret in :fn a lot more complex, so I stuck with generic specs 🤷🏻‍♂️

👍 1