Do Unless

Whilst experimenting with clj-webdriver and writing social-phobia I have created an interesting macro and would like to share it with you and get some feedback.

Lets write some simple script, log in to GitHub

1
2
3
4
5
6
(set-driver! {:browser :firefox})
(to "https://github.com/login")
(input-text "#login_field" (auth :login))
(input-text "#password" (auth :pass))
(click {:xpath "//input[@type='submit']"})
...

If we have wrong URL or don’t have an element on the page, we basically get that:

1
NullPointerException   clojure.lang.Reflector.invokeInstanceMethod (Reflector.java:26)

In a different way we can get the same result like this:

1
2
3
4
5
6
=> (def el (find-element {:css "#login_fiel"}))
#'user/el
=> el
#clj_webdriver.element.Element{:webelement nil}
=> (input-text el "asd")
NullPointerException   clojure.lang.Reflector.invokeInstanceMethod (Reflector.java:26)

Awesome. But I would like to receive more useful messages if GitHub changes URL or IDs of HTML elements. Let’s make our code “safer”:

1
2
3
4
5
6
7
8
9
10
11
12
13
(defn error
  "Returns \"\"#uername\" not found\" for e.g."
  [selector]
  {:error (str (first (vals selector)) " not found")})

(defn safe-find-element
  "Find the element, if the element is found, call the f,
  if not return a map with an error."
  [selector f]
  (let [el (find-element selector)]
    (if (:webelement el)
      (f el)
      (error selector))))

It means if we find an element we call function with that element, if not we return error which looks like {error: "#signin-email not found"}. Nice, but not so useful so far. Let’s wrap click and input-text functions in this wrapper.

1
2
3
4
5
6
7
8
(defn- safe-input-text [selector text]
  (safe-find-element selector
                     #(-> %
                        clear
                        (input-text text))))

(defn- safe-click [selector]
  (safe-find-element selector #(click %)))

Let’s see how it works:

1
2
=> (safe-input-text {:css "#login_fiel"} "asd")
{:error "#login_fiel not found"}

And try to rewrite our code:

1
2
3
4
5
(set-driver! {:browser :firefox})
(to "https://github.com/login")
(or (:error (safe-input-text {:css "#login_field"} (auth :login)))
    (:error (safe-input-text {:css "#password"} (auth :pass)))
    (:error (safe-click {:xpath "//input[@type='submit']"})))

Hm, it’s not so DRY.

1
2
3
4
=> (do-unless :error (safe-input-text {:css "#login_field"} (auth :login))
                     (safe-input-text {:css "#passwor"} (auth :pass))
                     (safe-click {:xpath "//input[@type='submit']"}))
{:error "#passwor not found"}

Much nicer. But how is it supposed to work? Tada!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(defmacro do-unless
  "
  Evaluates the expr if the condition which was called
  with result of previous expression returned false.

  Examples:
  =========

  (do-unless nil? (println 1) (println 2))
  1
  nil
  (do-unless nil? (do (println 1) 1) (println 2))
  1
  2
  nil"
  ([condition expr & exprs]
   `(let [r# ~expr]
      (if (~condition r#)
        r#
        (do-unless ~condition ~@exprs))))
  ([condition expr]
   expr))

Macroexpand:

1
2
3
4
5
6
7
8
9
10
(macroexpand '(do-unless :error (safe-input-text {:css "#login_field"} (auth :login))
                     (safe-input-text {:css "#passwor"} (auth :pass))
                     (safe-click {:xpath "//input[@type='submit']"})))

(let* [r__1016__auto__ (safe-input-text {:css "#login_field"} (auth :login))]
  (if (:error r__1016__auto__)
      r__1016__auto__
      (social-phobia.core/do-unless :error
                                    (safe-input-text {:css "#passwor"} (auth :pass))
                                    (safe-click {:xpath "//input[@type='submit']"}))))

Basically this macro recursively produces, a set of nested let and if expressions.

  1. Cache result of expression
  2. Check is’t map with :error message or not
  3. If yes, returns that map
  4. If not start this cycle for next expression

Nice, also it should be faster than catch exceptions and also we don’t produce any extra function calls like we do with monads. I think this macro is pretty much fun for this kind of code with a purely imperative nature.

Wdyt?

Peace!

Comments