Fork me on GitHub
#clojure-spec
<
2023-06-07
>
Kelvin17:06:07

Is there a good way to dynamically create new s/or specs at runtime? As s/or is a macro, it feels almost impossible to do that, hence I’d have to go down to or-spec-impl which is very much discouraged:

(defn json-schema-or-spec
  "Create a `s/or` spec from a coll-valued JSON Schema `type`"
  [schema-types]
  (let [pairs (map (fn [schema-type]
                     (case schema-type
                       "null"    [:null ::null]
                       "boolean" [:boolean ::boolean]
                       "integer" [:integer ::integer]
                       "number"  [:number (type->spec "number")]
                       "string"  [:string (type->spec "string")]
                       "array"   [:array (type->spec "array")]
                       "object"  [:object (type->spec "object")]))
                   schema-types)
        keys  (mapv first pairs)
        preds (mapv second pairs)]
    (s/or-spec-impl keys preds preds nil))
(or alternatively end up creating a case statement with 128 different cases)

Alex Miller (Clojure team)17:06:25

you can write that case with a macro around s/or :)

Alex Miller (Clojure team)17:06:11

but maybe a different kind of spec is better, like s/multi-spec?

Kelvin17:06:28

Writing a macro around s/or was my first attempt:

(defn- schema-type->pair
  [type->spec schema-type]
  (case schema-type
    "null"    `[:null ::null]
    "boolean" `[:boolean ::boolean]
    "integer" `[:integer ::integer]
    "number"  `[:number (~type->spec "number")]
    "string"  `[:string (~type->spec "string")]
    "array"   `[:array (~type->spec "array")]
    "object"  `[:object (~type->spec "object")]))

(defmacro coll-schema-spec
  [type->spec schema-types]
  (let [schema-type->pair (partial schema-type->pair type->spec)]
    `(s/or ~@(mapcat schema-type->pair schema-types))))

Kelvin17:06:06

Unfortunately it did not go very well: I ended up encountering a lot of

Don't know how to create ISeq from: clojure.lang.Symbol
errors because I was passing in schema-types as a variable

Alex Miller (Clojure team)18:06:39

well I think you're on the right track there, but you don't want ~@ - this is one of those cases that might be easier as literal construction with list cons etc

Alex Miller (Clojure team)18:06:38

~ turns off quoting and turns eval back on, but you don't want to eval

Alex Miller (Clojure team)18:06:08

also, schema-type->pair is just a map

Kelvin18:06:21

Still getting the same error; I think the fundamental issue is that schema-types is a coll whose values are only known as runtime, so trying to fit it inside any compile-time macro like s/or will only result in failure.

Kelvin18:06:56

(And it’s these times when I wished we used malli in our projects)

Alex Miller (Clojure team)18:06:24

can you just register these schemas at runtime then?

Alex Miller (Clojure team)18:06:51

think about it as dynamically making static specs, not statically making dynamic specs

Kelvin18:06:42

So you actually get that already with the ::null, ::boolean, and ::integer specs

Kelvin18:06:26

The issue comes with those other specs whose implementations depend on the JSON Schema user input (the type->spec function elides a lot of complexity)

Kelvin18:06:45

For example with array and object you’re basically creating s/coll-of and s/keys specs from user input - not really conducive for registering static specs

Alex Miller (Clojure team)18:06:17

not conducive at compile time, but totally fine at runtime

Ben Sless18:06:55

You can dispatch on the count of schema-types to unrolled calls to s/or

Kelvin18:06:24

I tried doing that, but that didn’t work either

Kelvin18:06:50

Since s/or requires keyword literals as keys, not variables representing those keywords

Ben Sless18:06:29

Well you can use eval

Kelvin18:06:31

I used eval to pass in my schema-types variable to the macro, but that gave a “can’t eval locals” error

Kelvin18:06:15

Anyways, going back to @U064X3EF3’s point, I’m not sure what he meant by “register these schemas at runtime” since when I think of registering specs, I automatically think of s/def

Kelvin18:06:43

I guess you can also generate the spec keywords at runtime, but given the potentially infinite schema values that does not seem to be a good idea

Alex Miller (Clojure team)18:06:06

ultimately, the spec registry is a map in your Clojure runtime. you can put things in it anytime, not just when you load a namespace that calls s/def

Alex Miller (Clojure team)18:06:44

(all of this is more readily available in the spec 2 api, both making specs and registering them without s/def)

Kelvin18:06:47

And spec 2 is supposed to have more support for dynamic, data-driven specs like the varardic s/or I’m trying to make, right?

Kelvin19:06:20

Anyways I think I’m going to go with or-spec-impl as my near-term solution

Kelvin19:06:00

It works like a charm just like any other spec, and there’s not really any downside other than a strongly worded docstring

Alex Miller (Clojure team)20:06:42

spec 2 has a data form you can use to make a spec, and a non-macro way to register a spec