spec-tools
spec-tools copied to clipboard
Support transform of multi-spec to JSON Schema
(defmulti event-type :action)
(s/def :event.payload.add/action #{:add})
(s/def :event.payload.add/payload int?)
(defmethod event-type :add
[_]
(s/keys :req-un [:event.payload.add/action :event.payload.add/payload]))
(s/def :event.payload.result/action #{:result})
(s/def :event.payload.result/payload nil?)
(defmethod event-type :result
[_]
(s/keys :req-un [:event.payload.result/action]
:opt-un [:event.payload.result/payload]))
(s/def ::event.payload
(s/multi-spec event-type :action))
=> nil
=> :event.payload.add/action
=> :event.payload.add/payload
=> #object[clojure.lang.MultiFn 0x2a66cd26 "clojure.lang.MultiFn@2a66cd26"]
=> :event.payload.result/payload
=> :event.payload.result/action
=> #object[clojure.lang.MultiFn 0x2a66cd26 "clojure.lang.MultiFn@2a66cd26"]
=> :damian-test/event.payload
(json-schema/transform ::event.payload)
=>
{:anyOf [{:type "object", :properties {"action" {:enum [:result]}, "payload" {:type "null"}}, :required ["action"]}
{:type "object",
:properties {"action" {:enum [:add]}, "payload" {:type "integer", :format "int64"}},
:required ["action" "payload"]}]}
IMO it's best as a convention to add specs like (s/def :event.payload.add/action #{:add})
. It might look surprising in the beginning. After all, it's already enforced by the multimethod
mechanisms. But making that extra step prevents two issues:
- the json schema is incapable of picking up the quirks of complex
dispatch-fn
s of multimethods. So without it the resulting json schema would be incomplete. - whenever you need to use spec generators to create samples, the methods again won't pickup the logic without that extra step.
(gen/sample (s/gen ::event.payload))
=>
({:payload nil, :action :result}
{:action :result}
{:action :add, :payload 0}
{:payload nil, :action :result}
{:action :add, :payload -1}
{:payload nil, :action :result}
{:action :add, :payload -9}
{:action :add, :payload -1}
{:action :result}
{:action :add, :payload -104})