I am trying to evaluate an infixed expression in a string.
Some sample data to evaluate my code against:
(def data {:Location "US-NY-Location1"
:Priority 3})
(def qual "(Location = \"US\")")
I would like the qual
string to be converted to something like this form and evaluated by clojure:
(= (:Location data) "US")
I wrote the following macro to achieve this:
(defmacro parse-qual [[data-key op val] data-map]
`(~op ((keyword (str (quote ~data-key))) ~data-map) ~val))
and a helper function:
(defn eval-qual [qual-str data]
(eval `(parse-qual ~(clojure.edn/read-string qual-str) ~data)))
(eval-qual qual data)
provides me with the expected result
This is the first macro I have written and I am still trying to wrap my head around all the quoting and unquoting.
I want to know if there is a more efficient way to achieve the above? (Or even without the need for a macro at all)
How can I expand the macro to deal with nested expressions. To handle an expression like ((Location = "US") or (Priority > 2))
. Any pointers would be appreciated. I am currently trying to play with tree-seq
to solve this.
How can I make this more robust and be more graceful in case of an invalid qual
string.
I also wrote a second iteration of the parse-qual
macro as follows:
(defmacro parse-qual-2 [qual-str data-map]
(let [[data-key op val] (clojure.edn/read-string qual-str)]
`(~op ((keyword (str (quote ~data-key))) ~data-map) ~val)))
and on macroexpand
throws the following:
playfield.core> (macroexpand `(parse-qual-2 qual data))
java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.String
And I am at a loss on how to debug this!
Some extra information:
macroexpand
of parse-qual on the REPL gives me the following:
playfield.core> (macroexpand
`(parse-qual ~(clojure.edn/read-string qual) data))
(= ((clojure.core/keyword (clojure.core/str (quote Location))) playfield.core/data) "US")
Thank you @Alan Thompson, I was able to write this as a functions as follows, this also allows for nested expressions to be evaluated.
(def qual "(Location = \"US\")")
(def qual2 "((Location = \"US\") or (Priority > 2))")
(def qual3 "(Priority > 2)")
(def qual4 "(((Location = \"US\") or (Priority > 2)) and (Active = true))")
(defn eval-qual-2 [qual-str data]
(let [[l op r] (clojure.edn/read-string qual-str)]
(cond
(and (seq? l)
(seq? r)) (eval (list op (list eval-qual-2 (str l) data) (list eval-qual-2 (str r) data)))
(seq? l) (eval (list op (list eval-qual-2 (str l) data) r))
(seq? r) (eval (list op (list (keyword l) data) (list eval-qual-2 (str r) data)))
:else (eval (list op (list (keyword l) data) r)))))
(eval-qual-2 qual data) ; => false
(eval-qual-2 qual2 data) ; => true
(eval-qual-2 qual3 data) ; => true
(eval-qual-2 qual3 data) ; => true
You don't need or want a macro for this. A plain function can process data like this.
Macros are only for transforming source code - you are effectively adding a compiler extension when you write a macro.
For transforming data, just use a plain function.
Here is an outline of how you could do it:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[clojure.tools.reader.edn :as edn] ))
(def data {:Location "US-NY-Location1"
:Priority 3})
(def qual "(Location = \"US\")")
(dotest
(let-spy [
ast (spyx (edn/read-string qual))
ident-str (first ast)
ident-kw (keyword ident-str)
op (second ast)
data-val (last ast)
expr (list op (list ident-kw data) data-val)
result (eval expr)
]
))
and the results:
----------------------------------
Clojure 1.9.0 Java 10.0.1
----------------------------------
(edn/read-string qual) => (Location = "US")
ast => (Location = "US")
ident-str => Location
ident-kw => :Location
op => =
data-val => "US"
expr => (= (:Location {:Location "US-NY-Location1", :Priority 3}) "US")
result => false
Notice that you still need to fix the "US" part of the location before it will give you a true
result.
Docs for let-spy
are here and here.
For nested expressions, you generally want to use postwalk.
And, don't forget the Clojure CheatSheet!