clojure-spec

Kelvin 2023-06-07T17:48:07.732169Z

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) 2023-06-07T17:54:25.326559Z

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

Alex Miller (Clojure team) 2023-06-07T17:55:11.777179Z

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

Kelvin 2023-06-07T17:56:28.993589Z

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))))

Kelvin 2023-06-07T17:57:06.423739Z

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) 2023-06-07T18:00:39.677549Z

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) 2023-06-07T18:01:38.572359Z

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

Alex Miller (Clojure team) 2023-06-07T18:02:08.205399Z

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

Kelvin 2023-06-07T18:13:21.356689Z

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.

Kelvin 2023-06-07T18:13:56.858369Z

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

Alex Miller (Clojure team) 2023-06-07T18:19:24.230069Z

can you just register these schemas at runtime then?

Alex Miller (Clojure team) 2023-06-07T18:19:51.699669Z

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

Kelvin 2023-06-07T18:20:42.681019Z

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

Kelvin 2023-06-07T18:21:26.945769Z

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)

Kelvin 2023-06-07T18:22:45.828079Z

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) 2023-06-07T18:29:17.824749Z

not conducive at compile time, but totally fine at runtime

Ben Sless 2023-06-07T18:37:55.026879Z

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

Kelvin 2023-06-07T18:38:24.482009Z

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

Kelvin 2023-06-07T18:38:50.274369Z

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

Ben Sless 2023-06-07T18:39:10.449459Z

Oh right

Ben Sless 2023-06-07T18:39:29.919309Z

Well you can use eval

Kelvin 2023-06-07T18:40:31.348369Z

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

Kelvin 2023-06-07T18:45:15.997579Z

Anyways, going back to @alexmiller’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

Kelvin 2023-06-07T18:45:43.358959Z

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) 2023-06-07T18:50:06.031859Z

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) 2023-06-07T18:50:44.806529Z

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

Kelvin 2023-06-07T18:51:47.506939Z

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?

Kelvin 2023-06-07T19:02:20.394759Z

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

Kelvin 2023-06-07T19:03:00.584029Z

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) 2023-06-07T20:44:42.663929Z

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