metosin / spec-tools

Clojure(Script) tools for clojure.spec
Eclipse Public License 2.0
593 stars 94 forks source link

Support transform of multi-spec to JSON Schema #281

Closed werenall closed 5 months ago

werenall commented 10 months ago
(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:

  1. the json schema is incapable of picking up the quirks of complex dispatch-fns of multimethods. So without it the resulting json schema would be incomplete.
  2. 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})
ikitommi commented 5 months ago

LGTM, thanks!