On Mon, May 2, 2011 at 6:37 AM, David Jagoe <davidja...@gmail.com> wrote: > Hi Everyone, > > Background to my problem: > > I am developing a compojure application, and there is lots of > duplication in listing field names in my current data model: > > (i) in the defstruct > (ii) in the public constructor's argument list > (iii) in the hiccup form fields > (iv) in the compojure argument destructuring > (v) in the handler's argument list > > Ideally I would like to declare my data model in one place like this: > > (def person-entity > {:name {:type String :validator name-validator} > :id-number {:type String :validator id-number-validator} > :height {:type Float :default 0.0} > :weight {:type Float :default 0.0} > :bmi {:type Float :internal true}}) > > And generate everything from there so that it is trivial to add fields > etc. (:internal true just means that this is a calculated field or for > some other reason is not supplied by the user - it is therefore not > needed by the constructor, and will not show up on the edit-person > form). > > I have tried to generate the defrecord etc from such a definition and > have battled a bit, so in the interest of making some progress with my > experiment I have tried this instead: > > (defn name-validator [val] val) > (defn id-number-validator [val] val) > (defn nil-validator [val] val) > > (defrecord Person > [#^String name > #^String id-number > #^Float height > #^Float weight > #^Float bmi]) > > (def person-traits > {:name {:validator name-validator} > :id-number {:validator id-number-validator} > :height {:default 100.0} > :weight {:default 100.0} > :bmi {:internal true}}) > > (def person-constructor (make-constructor Person person-traits)) > (def person-editor (make-editor Person person-traits)) > (def bob (person-constructor {:name "Bob" :id-number "123"})) > > ;;; I don't really know how bmi is calculated!! Let's pretend it is > weight (kg) / height (cm) > (bob :bmi) > 1.0 > (bob :id-number) > "123" > > And the person-editor is a snippet of hiccup which has all of the > relevant fields (e.g. text fields by default). > > I plan to use flutter validators, and not to use compojure > destructuring (I will have to pull the fields out of the request in > the generated form handlers). > > I am looking for advice in the following areas: > > (i) Is it possible to generate the (defrecord Person ...) from the > person-entity hash-map that I have shown?
Shoundn't be too hard. Something like (defn to-rec-field [kword entity-map] (let [rf (symbol (name kword))] (if-let [t (:type (entity-map kword))] (with-meta rf {:tag t}) rf))) (defmacro defentity [name entity-map] (let [fields (fn [ks] (map #(to-rec-field % entity-map) ks)) optionals (filter #(:default (entity-map %)) (keys entity-map)) optional-subsets (map #(set (drop % (reverse optionals))) (range (count optionals)))] `(do (defrecord ~(symbol (str "R" name)) ~@(fields (keys entity-map))) (defn ~(symbol (str "construct-" name)) ~@(map (fn [ops] (list (vec (fields (remove ops (keys entity-map)))) (cons (symbol (str "R" name ".")) (map #(if (ops %) (:default (entity-map %)) (to-rec-field % entity-map)) (keys entity-map))))) optional-subsets)) (defn ~(symbol (str "edit-" name)) ~@(left as an exercise for the reader))))) This should (untested) produce a defrecord and arity-overloaded constructors for the whole argument list and with successively more of the fields with defaults omitted, starting at the right -- so, for your person, you'd get constructors for [name id-number height weight bmi], [name id-number height bmi], and [name id-number bmi]. You probably want it to omit bmi from the argument lists and compute it -- that will complicate things, something like: (defmacro defentity [name entity-map] (let [fields (fn [ks] (map #(to-rec-field % entity-map) ks)) default #(:default (entity-map %)) optionals (filter default (keys entity-map)) optional-subsets (map #(set (drop % (reverse optionals))) (range (count optionals))) computer #(:computer (entity-map %)) computed (filter computer (keys entity-map))] `(do (defrecord ~(symbol (str "R" name)) ~@(fields (keys entity-map))) (defn ~(symbol (str "construct-" name)) ~@(map (fn [ops] (list (vec (fields (remove (concat ops computed) (keys entity-map)))) `(let ~(vec (interleave (fields (keys entity-map)) (map #(if (ops %) (default %) (to-rec-field % entity-map)) (keys entity-map)))) ~(symbol (str "R" name ".")) (map #(if (computer %) (computer %) (to-rec-field % entity-map)) (keys entity-map))))) optional-subsets)) (defn ~(symbol (str "edit-" name)) ~@(left as an exercise for the reader))))) used with something like (defentity person {:name {:type String} :id-number {:type Integer} :height {:type Double :default 100.0} :weight {:type Double :default 100.0} :bmi {:type Double :computer (/ weight height)}}) which should create a record Rperson and (defn construct-person ([#^String name #^Integer id-number #^Double height #^Double weight] (let [#^String name name #^Integer id-number id-number #^Double height height #^Double weight weight]) (Rperson. name id-number height weight (/ weight height)))) ([#^String name #^Integer id-number #^Double height] (let [#^String name name #^Integer id-number id-number #^Double height height #^Double weight 100.0]) (Rperson. name id-number height weight (/ weight height)))) ([#^String name #^Integer id-number] (let [#^String name name #^Integer id-number id-number #^Double height 100.0 #^Double weight 100.0]) (Rperson. name id-number height weight (/ weight height))))) Note that both the :default and the :computer can be s-expressions that just get inserted verbatim as code. The two differences are: 1. A field with :computer will not ever be a constructor parameter. 2. The :computer s-expression can refer to any of the fields; the :default can only refer to earlier ones and non-optional ones. I hope this shows how one might go about constructing a macro to take a definition similar to your original person-entity map and turn it into a record, constructor function, and possibly other structures. For example it could put code in the constructor function to run a :validator, if present, on that field, where the :validator throws an exception; or to turn a :validator field into a test and throw exception clause where the :validator is a boolean expression; etc. -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en