I tried to use java.jdbc insert! function, which can receive multiple objects.
the clojure.java.jdbc/insert! should be called like this:
(clojure.java.jdbc/insert! db {:name "john" :password "123"} {:name "george" :password "234"})
I defined a function to do add multiple user records:
(defn add-users [user & more]
(-add-users db-spec user more))
(defmacro -add-users
([db user] `(j/with-db-connection [con-db# db]
(j/insert! con-db# ~user)))
([db user & more] (let [users (-mk-user-list user more)]
`(j/with-db-connection [con-db# db]
(j/insert! con-db# ~#(flatten users)))))
(defmacro -mk-usre-list
([user] `~user)
([user & more] `(list ~user (-mk-user-list ~#more))))
when I macroexpand the -add-users, looks the result is ok like this:
=> (macroexpand '(-add-users db-spec {:name "john" :password "1234"}))
(let* [db-spec__21320__auto__ db-spec] (clojure.core/with-open [con__21321__auto__ (clojure.java.jdbc/get-connection db-spec__21320__auto__)] (clojure.core/let [con-db__23557__auto__ (clojure.java.jdbc/add-connection db-spec__21320__auto__ con__21321__auto__)] (clojure.java.jdbc/insert! con-db__23557__auto__ {:name "john", :password "1234"}))))
But when I run :
=> (add-users {:name "john" :password "1234"})
IllegalArgumentException insert called with columns but no values clojure.java.jdbc/insert-sql (jdbc.clj:992)
What's wrong with this macro? or how should I wrap and pass any number of arguments to function like insert! which needs the arguments flattened?
[Update]
the problem is found:
insert! needs table while in code it's not there.
(j/insert! con-db# ~user) should be (j/insert! con-db# :users ~user)
(j/insert! con-db# ~#(flatten users)) should be (j/insert! con-db# :users ~#(flatten users))
But is there anyway simpler to do it?
You can solve this without a macro, using apply:
user=> (doc apply)
-------------------------
clojure.core/apply
([f args] [f x args] [f x y args] [f x y z args] [f a b c d & args])
Applies fn f to the argument list formed by prepending intervening arguments to args.
(defn my-insert! [x & more]
(apply jdbc/insert! db :table x more))
In recent versions of clojure.java.jdbc, you can use insert-multi! which accepts a sequence of hash maps for the rows:
(defn my-insert! [& more]
(jdbc/insert-multi! db-spec :table more))
or you could just do:
(jdbc/insert-multi! db-spec :table [user-1 user-2 user-3])
Related
My Clojure app needs some handlers to do business, those handlers will preform some common parameters check, so I use a macro to do this like below:
(defmacro defapihandler [handler-name params & body]
`(defn ~handler-name ~params
(let [keyed-params# (map keyword '~params)
checked-ret# (check-param (zipmap keyed-params# ~params))]
(if (:is-ok checked-ret#)
(do ~#body)
(-> (response {:code 10000
:msg (format " %s are missing !!!" (:missed-params checked-ret#))})
(status 400))))))
Then I can use above macro like this:
(defapihandler create-user [username password birthday]
;; todo
)
Everything is fine this way.
As you can see, the params of generated fn is constructed directly from args of the marco, exception raised when params of generated fn can't constructed directly.
Take a example:
The params of the macro defapihandler now became like this:
[{:key :username :checker [not-nil?]} {:key :password :checkers [is-secure?]} ...]
In the macro, I want to build the param of the generated fn dynamicly like this:
(defmacro defapihandler [handler-name params & body]
`(defn ~handler-name [passed-param#]
(let [param-keys# (vec (map (comp symbol name :key)
~params))
{:keys param-keys#} passed-param#]
;; some check
(do ~#body))))
(defapihandler create-user [{:key :username :checkers []}]
(println username))
The structure of passed-param looks like this: {:username "foo" :password "bar"}
Now I want to construct the variables used in body block in let block, Then following exception is thrown:
Caused by java.lang.IllegalArgumentException
Don't know how to create ISeq from: clojure.lang.Symbol
macroexpand create-user got this:
(defn create-user [passed-param__10243__auto__]
(let [param-keys__10244__auto__ (vec
(map
(comp symbol name :key)
[{:key :username,
:checkers []}]))
{:keys param-keys__10244__auto__} passed-param__10243__auto__]
(do (println username))))
I suspect this exception is related to dynamic var used in let destructuring form, if my suspect is right, then how to construct variables used in body block ?
You need to pull the clause that builds your params-key vector out of the generated code.
So:
(defmacro defapihandler [handler-name params & body]
(let [param-keys (map (comp symbol name :key) params)]
`(defn ~handler-name [passed-param#]
(let [{:keys [~#param-keys]} passed-param#]
;; some check
(do ~#body)))))
Or if you don't need passed-param#:
(defmacro defapihandler [handler-name params & body]
(let [param-keys (map (comp symbol name :key) params)]
`(defn ~handler-name [{:keys [~#param-keys]}]
;; some check
(do ~#body))))
Is there anyway to include clojure.spec'd functions in a generalized test suite? I know we can register specs and directly spec functions.
(ns foo
(:require [clojure.spec :as s]
[clojure.spec.test :as stest]))
(defn average [list-sum list-count]
(/ list-sum list-count))
(s/fdef average
:args (s/and (s/cat :list-sum float? :list-count integer?)
#(not (zero? (:list-count %))))
:ret number?)
And later, if I want to run generative tests against that spec'd function, I can use stest/check.
=> (stest/check `average)
({:spec #object[clojure.spec$fspec_impl$reify__14282 0x68e9f37c "clojure.spec$fspec_impl$reify__14282#68e9f37c"], :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1479587517232}, :sym edgar.core.analysis.lagging/average})
But i) is there anyway to include these test runs in my general test suite? I'm thinking of the kind of clojure.test integration that test.check has. The closest thing that I can see ii) is the stest/instrument (see here) function. But that seems to just let us turn on checking at the repl. Not quite what I want. Also, iii) are function specs registered?
(defspec foo-test
100
;; NOT this
#_(prop/for-all [v ...]
(= v ...))
;; but THIS
(stest/some-unknown-spec-fn foo))
Ok, solved this one. Turns out there's no solution out of the box. But some people on the clojure-spec slack channel have put together a defspec-test solution for clojure.spec.test and clojure.test.
So given the code in the question. You can A) define the defspec-test macro that takes your test name and a list of spec'd functions. You can then B) use it in your test suite.
Thanks Clojure community!! And hopefully such a utility function makes it into the core library.
A)
(ns foo.test
(:require [clojure.test :as t]
[clojure.string :as str]))
(defmacro defspec-test
([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
([name sym-or-syms opts]
(when t/*load-tests*
`(def ~(vary-meta name assoc
:test `(fn []
(let [check-results# (clojure.spec.test/check ~sym-or-syms ~opts)
checks-passed?# (every? nil? (map :failure check-results#))]
(if checks-passed?#
(t/do-report {:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym check-results#)))})
(doseq [failed-check# (filter :failure check-results#)
:let [r# (clojure.spec.test/abbrev-result failed-check#)
failure# (:failure r#)]]
(t/do-report
{:type :fail
:message (with-out-str (clojure.spec/explain-out failure#))
:expected (->> r# :spec rest (apply hash-map) :ret)
:actual (if (instance? Throwable failure#)
failure#
(:clojure.spec.test/val failure#))})))
checks-passed?#)))
(fn [] (t/test-var (var ~name)))))))
B)
(ns foo-test
(:require [foo.test :refer [defspec-test]]
[foo]))
(defspec-test test-average [foo/average])
The above example can fail in the case where :failure is false due to how stest/abbrev-result tests for failure. See CLJ-2246 for more details. You can work around this by defining your own version of abbrev-result. Also, the formatting of failure data has changed.
(require
'[clojure.string :as str]
'[clojure.test :as test]
'[clojure.spec.alpha :as s]
'[clojure.spec.test.alpha :as stest])
;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))
(defn failure? [{:keys [:failure]}] (not (or (true? failure) (nil? failure))))
;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
(let [failure (:failure x)]
(if (failure? x)
(-> (dissoc x ::stc/ret)
(update :spec s/describe)
(update :failure unwrap-failure))
(dissoc x :spec ::stc/ret))))
(defn throwable? [x]
(instance? Throwable x))
(defn failure-report [failure]
(let [expected (->> (abbrev-result failure) :spec rest (apply hash-map) :ret)]
(if (throwable? failure)
{:type :error
:message "Exception thrown in check"
:expected expected
:actual failure}
(let [data (ex-data (get-in failure
[::stc/ret
:result-data
:clojure.test.check.properties/error]))]
{:type :fail
:message (with-out-str (s/explain-out data))
:expected expected
:actual (::s/value data)}))))
(defn check?
[msg [_ body :as form]]
`(let [results# ~body
failures# (filter failure? results#)]
(if (empty? failures#)
[{:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym results#)))}]
(map failure-report failures#))))
(defmethod test/assert-expr 'check?
[msg form]
`(dorun (map test/do-report ~(check? msg form))))
Here's a slightly modified version of grzm's excellent answer that works with [org.clojure/test.check "0.10.0-alpha4"]. It uses the new :pass? key that comes from this PR: https://github.com/clojure/test.check/commit/09927b64a60c8bfbffe2e4a88d76ee4046eef1bc#diff-5eb045ad9cf20dd057f8344a877abd89R1184.
(:require [clojure.test :as t]
[clojure.string :as str]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as stest])
(alias 'stc 'clojure.spec.test.check)
;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))
;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
(if (-> x :stc/ret :pass?)
(dissoc x :spec ::stc/ret)
(-> (dissoc x ::stc/ret)
(update :spec s/describe)
(update :failure unwrap-failure))))
(defn throwable? [x]
(instance? Throwable x))
(defn failure-report [failure]
(let [abbrev (abbrev-result failure)
expected (->> abbrev :spec rest (apply hash-map) :ret)
reason (:failure abbrev)]
(if (throwable? reason)
{:type :error
:message "Exception thrown in check"
:expected expected
:actual reason}
(let [data (ex-data (get-in failure
[::stc/ret
:shrunk
:result-data
:clojure.test.check.properties/error]))]
{:type :fail
:message (with-out-str (s/explain-out data))
:expected expected
:actual (::s/value data)}))))
(defn check?
[msg [_ body :as form]]
`(let [results# ~body
failures# (remove (comp :pass? ::stc/ret) results#)]
(if (empty? failures#)
[{:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym results#)))}]
(map failure-report failures#))))
(defmethod t/assert-expr 'check?
[msg form]
`(dorun (map t/do-report ~(check? msg form))))
Usage:
(deftest whatever-test
(is (check? (stest/check `whatever
;; optional
{:clojure.spec.test.check/opts {:num-tests 10000}})))
I'm not quite understanding what's wrong about
(defrecord Person [name age])
(defn make-person [& opts]
(let [defaults {:age 18}]
(map->Person (merge defaults opts))))
(make-person {:name "Jim"})
=> ClassCastException clojure.lang.PersistentArrayMap cannot be cast to java.util.Map$Entry clojure.lang.APersistentMap.cons (APersistentMap.java:42)
If I do:
(map->Person (merge {:age 18} {:name "Jim"}))
I can also get the make-person function working with a non optional argument.
(defn make-person [opts]
(let [defaults {:age 18}]
(map->Person (merge defaults opts))))
The solution I've settled on for what I want to do works like:
(defn make-person
([opts] (map->Person (merge {:age 18} opts)))
([] (map->Person {:age 18})))
So I guess I'm asking, what does & really do when destructuring function arguments?
The & opts returns a sequence, which is problematic if you pass a map in.
You probably want to destructure the sequence something like:
(defn make-person [& [opts]]
(map->Person (merge {:age 18} opts))
Which lets you do (make-person) or (make-person {:opt1 "foo" :opt2 "bar"})
you could take advantage of the special syntax to allow keyword args:
(defn make-person [ & {:as opts} ]
(map->Person (merge {:age 18} opts))
Allowing you to do (make-person :opt1 "foo" :opt2 "bar") but in my experience that makes calling make-person difficult if you want to do the merging outside the call (which you will one day) (i.e if you want (make-person (merge some-opts some-other-opts)
I am using lobos to create a set of tables:
(create
(table :users
(integer :id 20)
(varchar :name 100)))
The schema of a table is kept in a seq:
([:integer :id 20] [:varchar :name 100])
How can I generate that expression with the seq? I find clojure.contrib/apply-macro may be used, but are there other ways?
You can use the following macro:
(defmacro table-create [name coll]
`(~'create
(~'table ~name ~#(map (fn [r]
(let [[type c v] r]
(list (symbol (subs (str type) 1)) c v)))
coll))))
Here is a sample run:
(macroexpand-1 '(table-create :users ([:integer :id 20] [:varchar :name 100])))
;=> (create
(table :users
(integer :id 20)
(varchar :name 100)))
Given the below function -
(defn ^:export hi [] (+ 2 3))
I would like to write a macro that does this -
(defex hi [] (+ 2 3))
The macro defex just adds the ^:export metadata in front of the function. How do I do that?
Edit - I checked the function on repl (meta hi) and it gives nil. So most probably I dont want to add metedata but define a function in the above manner.
Thanks,
Murtaza
You don't want the meta on the function itself, you want it on the var (or whatever clojurescript's equivalent of that is):
user> (defmacro defex [name & defn-args]
`(defn ~(vary-meta name assoc :export true) ~#defn-args))
#'user/defex
user> (defex hi [] "hi")
#'user/hi
user> (meta #'hi)
{:arglists ([]), :ns #<Namespace user>, :name hi, :export true, :line 1, :file "NO_SOURCE_FILE"}
you can use a basic template-macro that builds a function and uses def to save it in a var
user> (defmacro defex [name args & body] `(def ~name ^{:export true} (fn ~args ~#body)))
#'user/defex
user> (defex hi [] (+ 2 3))
#'user/hi
user> (meta hi)
{:export true}
user>