Christopher Causer
Christopher Causer

Reputation: 1494

Clojure: overriding one function in a library

This question is off the back of a previous question I asked here a few days ago. One of the comments was that I should dispense with the Ring middleware for extracting query parameters and write my own. One alternative that I thought I'd play with was harnessing the existing one to get what I want and I've been doing some digging into the Ring source code. It does almost exactly what I want. If I write out how I understand it works:

  1. A middleware has the function wrap-params which calls params-request
  2. params-request adds a params map to the request map, calls assoc-query-params
  3. assoc-query-params eventually calls ring.util.codec/form-decode on the incoming query string to turn it into a map
  4. form-decode uses assoc-conj to merge values into an existing map via reduce
  5. assoc-conj's docstring says

Associate a key with a value in a map. If the key already exists in the map, a vector of values is associated with the key.

This last function is the one that is problematic in my previous question (TL;DR: I want the map's values to be consistent in class of either a string or a vector). With my object orientated hat on I would have easily solved this by subclassing and overriding the method that I need the behaviour changed. However for Clojure I cannot see how to just replace the one function without having to alter everything up the stack. Is this possible and is it easy, or should I be doing this another way? If it comes to it I could copy the entire middleware library and the codec one, but it seems a bit heavyweight to me.

Upvotes: 2

Views: 917

Answers (2)

Alan Thompson
Alan Thompson

Reputation: 29956

While a custom middleware is probably the clearest way to go for this problem, don't forget that you can always override any function using with-redefs. For example:

(ns tst.demo.core
  (:use tupelo.core tupelo.test))

(dotest
  (with-redefs [clojure.core/range (constantly "Bogus!")]
    (is= "Bogus!" (range 9))))

While this is primarily used during unit tests, it is a wide-open escape hatch that can be used to override any function.

To Clojure, there is no difference between a Var in your source code versus one in a library (or even clojure.core itself, as the example shows).

Upvotes: 3

amalloy
amalloy

Reputation: 92012

I disagree with the advice to not use Ring's param middleware. It gives you perfect information about the incoming parameters, so you if you don't like the default behavior of string-or-list, you can change the parameters however you want.

There are numerous ways to do this, but one obvious approach would be to write your own middleware, and insert it in between Ring's param middleware and your handlers.

(defn wrap-take-last-param []
  (fn [handler]
    (fn [req]
      (handler 
        (update req :params 
                (fn [params]
                  (into {} 
                     (for [[k v] params]
                       [k (if (string? v) v, (last v)]))))))))

You could write something fancier, like adding some arguments to the function to let you specify which parameters you want to receive only the last specified, and which you would like to always receive as a list. In that case you might not want to wrap it around your entire handler, but around each of your routes separately to specify their expected parameters.

Upvotes: 3

Related Questions