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.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 ... }}
this is all great stuff and let's leave aside the spec2 side of things - I shouldn't have mentioned it 🤦🏻
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 😅
the issue is coordinating the request and the response cos they can be different and are varied on the input
haha yeah exactly, even using instrument at runtime will only check :args, never :ret or :fn
well, it turns out that https://github.com/jeaye/orchestra does exactly that 😍
I should have mentioned that I was using orchestra to achieve that goal.
it seems to all work really well and is a great way to avoid a bunch of tests that are essentially just assertions anyway
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
I might be able to make a generator like that tho 🤔
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?})}))We can but dream. Thanks for the suggestion and I’ll take a look when I’m back on an actual computer
[ in your spec 2 example you have both :uri and :body defined in two ways ... that's what I meant, though clumsily expressed ]
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.
the full code is here https://github.com/content-made-simple/cms0/blob/main/src/cms0/routing_proposal.clj
[ it's a proposal so don't let the lack of quality in the actual implementation get in your way 🙈 ]
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
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"https://github.com/schmee/java-http-clj/blob/master/src/java_http_clj/specs.clj this might be a useful example for reference
oh - ok, that's nice ... so you use the namespace as a way to do the variation.
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.
but it does get around the :body problem, for unqualified keys at least so gratitude-thank-you
no problem, yeah you're right that's the time to use :fn in fdef
thanks for your time and support.
fwiw the use of s/or makes processing the :args / :ret in :fn a lot more complex, so I stuck with generic specs 🤷🏻♂️