6
votes

I'm specifically trying to generate the boilerplate for crud functions to work with the Google App Engine datastore using appengine-magic in Clojure. I'm having difficulty working out how to generate values from a model that I've reproduced below.

(def *model* {:users [{:name "Adam"
                       :email "[email protected]"
                       :registered-on "07-05-2011"}
                      {:name "Greg"
                       :email "[email protected]"
                       :registered-on "11-05-2011"}]
              :post [{:title "A"
                      :authour "Adam"}
                     {:title "B"
                      :author "Greg"}]})

I'm fairly new to appengine-magic, but it provides a defentity which allows you to define entities that you can put into the datastore and save! which allows you to save predefined entities into the datastore.

These take the form of:

(ds/defentity Post [title author])
(ds/save! (Post. title author))

Now just to start with I've defined:

(defn list-entities [model]
  "Takes a representation of the model and lists the entities in preparation for generating defentities"
  (interleave (vec (map first (partition 1 (map (comp symbol capitalize #(str % ".") name) (keys model)))))
    (map vec (map keys (map first (vals model))))))

Calling it with:

(list-entities *model*)

Outputs:

(Users. [:name :email :registered-on] Post. [:title :author])

Now I am having difficulty defining gen-entities which will take the output above and repeatedly call ds/defentities defining as many entities as my model requires.

(defmacro gen-entities [entity fields]
  `(ds/defentity 'entity 'fields))

Additionally I am in no way certain that this is a reasonable way to go about solving this problem. I'm still very new to macros and probably making several mistakes. Any help/clarity would be appreciated.

NOTE:

That model I've realised is badly designed, the one below is a lot better:

(def *model* {:users [:name :email :registered-on]
              :post [:title :author]})

However it is more complex in terms of writing a macro so I will leave it as is.

1
Why wouldn't you want to make a function that transforms the input into calls to ds/defentity directly? The only reason you need macros here is because of the output your function generates. - Gert
Well I might be missing something here then, I've tried that and it doesn't appear to work. So I thought that this was a case in which I would have to use macros. I'm aware that we shouldn't use them unless we need to, so I thought I'd ask here about it. - toofarsideways

1 Answers

1
votes

I think a macro is required, because defentity seems to define a type.

(defmacro gen-entities
  [model]
  `(do
     ~@(for [[entity-kw values] model]
         (let [entity-sym (-> entity-kw name capitalize symbol)
               fields     (map (comp symbol name) (keys (first values)))]
           `(ds/defentity ~entity-sym [~@fields])))))

You don't have to fiddle the keys and values from each other, just to put them together again with interleave. Mapping over a map will give you the key and the corresponding value in one go.

user=> (macroexpand-1 `(gen-entities ~model))
(do
  (ds/defentity Users [name registered-on email])
  (ds/defentity Post [title authour]))

Note: this won't work with the model stored in a Var. You'll have to specify the model in the call to gen-entities.

user=> (macroexpand-1 '(gen-entities model))
(
#<IllegalArgumentException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol>