nubank / matcher-combinators

Library for creating matcher combinator to compare nested data structures
Other
467 stars 23 forks source link

Proposal: fmap-like combinator to transform input before matching #175

Closed rafaeldff closed 2 years ago

rafaeldff commented 2 years ago

When matching complex heterogeneous data I often feel the need to transform parts of the actual data structure before matching, usually to perform some kind of parsing or deserialization of data.

For example, we might be dealing with actual data like

{:payloads ["{:foo :bar}"]}

And we want to match the edn strings against clojure data structures. I'm proposing a matcher combinator function that transforms the part of the actual input it's trying to match before delegating to an underlying matcher:

(is
   (match? {:payloads [(transforming read-string {:foo :bar})]}
           {:payloads ["{:foo :bar}"]}))

An initial implementation would be along the lines of:

(defn transforming [transform expected]
  (reify
    core/Matcher
    (-matcher-for [this] (core/-matcher-for expected))
    (-matcher-for [this x] (core/-matcher-for expected x))
    (-match [_ actual]
      (let [transformed (try (transform actual) (catch Exception e e))]
        (if (instance? Exception transformed)
          {::result/type   :mismatch
           ::result/value  (model/->Mismatch (list 'transformed expected) actual)
           ::result/weight 1}
          (core/match expected transformed))))
    (-base-name [_] (core/-base-name expected))))

Example output: image

Some questions for discussion:

I've been using it for a few days and it seems to improve quite a bit the readability of tests that previously were littered with long get-in and -> chains.

philomates commented 2 years ago

hey :wave:

Is this broadly useful or too niche to be included?

I think there is some related discussion in https://github.com/nubank/matcher-combinators/issues/148 we just never followed up on it. Given several voices chiming in from different sources seems useful enough :)

Opinions on the name? An alternative would be to call it fmap, though I'm not sure if Matcher would be a proper functor.

I'm not a fan of fmap because I always forget what fmap means (but that is a very personal rationale, so definitely open to considering it more). The linked issue talks about via and transformed. transforming feels good. One suggestion I would make about the implementation snippet: changing (defn transforming [transform expected] ..) to something like (defn transforming [transform-actual-fn expected] might help users quickly know that the first arg is applied to the actual value. Especially when the transforming thing is found in the expected part of the match?.

As it is, the mismatch output elides the transforming matcher, should it include it?

My gut response is it should be included somewhere. I think at the moment the clojure.test reporter for matcher-combinators would show it in the expected: (match? ... (transforming read-string ...) ...), but not sure how every tool renders that. Maybe we can leave it like that for now and see how it goes?

rafaeldff commented 2 years ago

Cool, I hadn't seen that other issue. I actually kind of like the name via, it reads nicely: match something via some-process

BTW, @dchelimsky had raised a similar concern to yours @philomates on this comment https://github.com/nubank/matcher-combinators/issues/148#issuecomment-1088766291, about the syntax being non-intuitive or misleading in a way, so maybe there is work to be done there.

philomates commented 2 years ago

posted an implementation at https://github.com/nubank/matcher-combinators/pull/178 if folks could check it out and comment + approve/merge (for those with admin rights)

dchelimsky commented 2 years ago

Resolved in https://github.com/nubank/matcher-combinators/pull/178

Released in nubank/matcher-combinators-3.6.0