coax icon indicating copy to clipboard operation
coax copied to clipboard

s/or ambiguity with "un-ordered" or clauses

Open joelsvictor opened this issue 1 year ago • 2 comments

Hello,

I am having some trouble coercing a heterogenous collection using coax. If you have a look at the example below, the ip collection has three different types of values. I want to convert them to the vector defined in op. The transformation is simple, just convert every string to a keyword.

I have written two specs for illustration. Here op conforms to spec-op. spec-ip is same as spec-op with keyword? replaced by string?. It can be seen that ip conforms to spec-ip as expected. This illustrates that the spec is correct.

The problem arises when I try to coerce ip using spec-op. It is failing when trying to coerce the third entry in the vector i.e. ["f3" ["a1", "a2", "a3"]]. The conform accepts the input. This seems to be a bug in coax because conform is working as expected but coerce is not working.

  (def ip ["f1"
           ["f2" "a1"]
           ["f3" ["a1" "a2" "a3"]]])

  (def op [:f1
           [:f2 :a1]
           [:f3 [:a1 :a2 :a3]]])

  (s/def ::fn-with-var-arg-kw (s/tuple keyword? (s/coll-of keyword?)))
  (s/def ::spec-op
    (s/coll-of (s/or :fn keyword?
                     :fn-with-arg (s/coll-of keyword?)
                     :fn-with-var-arg ::fn-with-var-arg-kw)))

  (s/def ::fn-with-var-arg-str (s/tuple string? (s/coll-of string?)))
  (s/def ::spec-ip
    (s/coll-of (s/or :fn string?
                     :fn-with-arg (s/coll-of string?)
                     :fn-with-var-arg ::fn-with-var-arg-str)))

  (s/conform ::spec-ip ip)
  ;; => [[:fn "f1"]
  ;;     [:fn-with-arg ["f2" "a1"]]
  ;;     [:fn-with-var-arg ["f3" ["a1" "a2" "a3"]]]]

  (s/conform ::spec-op op)
  ;; => [[:fn :f1]
  ;;     [:fn-with-arg [:f2 :a1]]
  ;;     [:fn-with-var-arg [:f3 [:a1 :a2 :a3]]]]

  (c/coerce ::spec-op ip)
  ;; => [:f1 [:f2 :a1] [:f3 ["a1" "a2" "a3"]]]

joelsvictor avatar Oct 07 '24 11:10 joelsvictor

Hi,

I kind of missed the notification about this one, sorry about that.

It's an ambiguity issue. I will have a look next week and fix it, but basically:

the or with : :fn-with-arg (s/coll-of keyword?) :fn-with-var-arg (s/tuple keyword? (s/coll-of keyword?))

coax tries the or specs in order, the first one modifying any value in the data is considered to be the one to be picked up, so it never reaches fn-with-var-arg. So coax decides to pick up fn-with-arg, modifies what it can to match keyword? and leaves the rest alone (as it does for anything else).

mpenet avatar Oct 11 '24 19:10 mpenet

Until then you can workaround it by changing the order in the s/or and registering a custom coercer for fn-with-arg that will reject the value when it should:

(s/def ::fn-with-var-arg-kw (s/tuple keyword? (s/coll-of keyword?)))
(s/def ::fn-with-arg (s/coll-of keyword?))
(s/def ::spec-op
  (s/coll-of (s/or :fn keyword?
                   :fn-with-arg ::fn-with-arg
                   :fn-with-var-arg ::fn-with-var-arg-kw)))
[...]

(c/coerce ::spec-op ip
          {:idents {::fn-with-arg
                    (fn [x _opts]
                      ;; could also be a call to a spec
                      (if (and (coll? x)
                               (every? #(or (string? %)
                                            (keyword? %))
                                       x))
                        ;; it matches we know for sure it's the a fn-with-arg, no more ambiguity, coercing is safe.
                        (c/coerce ::fn-with-arg x)
                        ;; otherwise it's invalid, the next `or` clause will be tried
                        :exoscale.coax/invalid))}})

Not great but better than nothing until it gets fixed.

mpenet avatar Oct 11 '24 19:10 mpenet