Browse files

Merge branch 'v0.4.6'

  • Loading branch information...
2 parents a92e572 + fd10a64 commit 0f753820aeaa851fd515e127064a9eb5c833657e @gcv committed Nov 7, 2011
View
11 HISTORY.md
@@ -1,3 +1,14 @@
+## 0.4.6 (2011-11-07)
+
+* Basic support for App Engine SDK 1.5.5.
+* Added support for `:before-save` and `:after-load` hooks to `defentity`.
+* Made an internal change which improves support for interactive REPL use from
+ various programming tools. Clojure modes in Eclipse, IntelliJ IDEA, and
+ NetBeans should now work. VimClojure, SLIME (swank-clojure), and plain REPL
+ (`lein repl`) use should unaffected by this change.
+* Support for Clojure 1.3.
+
+
## 0.4.5 (2011-09-24)
* Basic support for App Engine SDK 1.5.4.
View
31 README.md
@@ -24,10 +24,9 @@ Please read the project's HISTORY file to learn what changed in recent releases.
## Dependencies
-* Clojure 1.2.1
+* Clojure 1.2.1 or Clojure 1.3.0
* Leiningen 1.6.1
-* Google App Engine SDK 1.5.4
-* swank-clojure 1.3.1 (optional)
+* Google App Engine SDK 1.5.5
@@ -103,7 +102,7 @@ functionality.
`core.clj` file created by Leiningen. You need to do this so that
appengine-magic can create a default file which correctly invokes the
`def-appengine-app` macro.
-3. Edit `project.clj`: add `[appengine-magic "0.4.5"]` to your
+3. Edit `project.clj`: add `[appengine-magic "0.4.6-SNAPSHOT"]` to your
`:dev-dependencies` (not `:dependencies`).
4. `lein deps`. This fetches appengine-magic, and makes its Leiningen plugin
tasks available. If you already have the App Engine SDK installed locally,
@@ -513,16 +512,19 @@ and transactions.
(ANY "*" [] {:status 404 :body "not found" :headers {"Content-Type" "text/plain"}}))
```
-- `defentity` (optional keyword: `:kind`): defines an entity record type
- suitable for storing in the App Engine datastore. These entities work just
- like Clojure records. Internally, they implement an additional protocol,
- EntityProtocol, which provides the `save!` method. When defining an entity,
- you may specify `^:key` metadata on any one field of the record, and the
- datastore will use this as the primary key. Omitting the key will make the
- datastore assign an automatic primary key to the entity. Specifying the
- optional `:kind` keyword (a string value) causes App Engine to save the entity
- under the given "kind" name — like a datastore table. This allows kinds to
- remain disjoint from entity record types.
+- `defentity` (optional keywords: `:kind`, `:before-save`, `:after-load`):
+ defines an entity record type suitable for storing in the App Engine
+ datastore. These entities work just like Clojure records. Internally, they
+ implement an additional protocol, EntityProtocol, which provides the `save!`
+ method. When defining an entity, you may specify `^:key` metadata on any one
+ field of the record, and the datastore will use this as the primary key.
+ Omitting the key will make the datastore assign an automatic primary key to
+ the entity. Specifying the optional `:kind` keyword (a string value) causes
+ App Engine to save the entity under the given "kind" name — like a datastore
+ table. This allows kinds to remain disjoint from entity record types. The
+ `:before-save` and `:after-load` keywords allow specifying a hook for
+ transforming the entity on its way into the datastore before it is saved, or
+ on its way out of the datastore after it is read.
- `new*`: instantiates a datastore entity record. You may also use standard
Clojure conventions to instantiate entity records, but creating entities
destined for entity groups requires using `new*`. To put the new entity into a
@@ -942,6 +944,7 @@ console, you'll see the polling requests.
The following Google services are not yet tested in the REPL environment:
+- Asynchronous Memcache API requests (from App Engine SDK 1.6.0)
- Pull queues (from App Engine SDK 1.5.0)
- Deferred API (from App Engine SDK 1.4.3)
- Remote API (from App Engine SDK 1.4.3)
View
23 project.clj
@@ -1,10 +1,10 @@
-(defproject appengine-magic "0.4.5"
+(defproject appengine-magic "0.4.6"
:description "Google App Engine library for Clojure."
:min-lein-version "1.6.1"
:repositories {"releases" "http://appengine-magic-mvn.googlecode.com/svn/releases/"
"snapshots" "http://appengine-magic-mvn.googlecode.com/svn/snapshots/"}
- :dependencies [[org.clojure/clojure "1.2.1"]
- [ring/ring-core "0.3.11"]
+ :exclusions [org.clojure/clojure]
+ :dependencies [[ring/ring-core "0.3.11"]
[org.apache.commons/commons-exec "1.1"]
;; App Engine supporting essentials
[javax.servlet/servlet-api "2.5"]
@@ -18,11 +18,12 @@
[taglibs/standard "1.1.2"] ; repackaged-appengine-jakarta-standard-1.1.2.jar
[commons-el "1.0"]
;; main App Engine libraries
- [com.google.appengine/appengine-api-1.0-sdk "1.5.4"]
- [com.google.appengine/appengine-api-labs "1.5.4"]
- [com.google.appengine/appengine-api-stubs "1.5.4"]
- [com.google.appengine/appengine-local-runtime "1.5.4"]
- [com.google.appengine/appengine-local-runtime-shared "1.5.4"]
- [com.google.appengine/appengine-testing "1.5.4"]
- [com.google.appengine/appengine-tools-api "1.5.4"]]
- :dev-dependencies [[swank-clojure "1.3.1" :exclusions [org.clojure/clojure]]])
+ [com.google.appengine/appengine-api-1.0-sdk "1.5.5"]
+ [com.google.appengine/appengine-api-labs "1.5.5"]
+ [com.google.appengine/appengine-api-stubs "1.5.5"]
+ [com.google.appengine/appengine-local-runtime "1.5.5"]
+ [com.google.appengine/appengine-local-runtime-shared "1.5.5"]
+ [com.google.appengine/appengine-testing "1.5.5"]
+ [com.google.appengine/appengine-tools-api "1.5.5"]]
+ :dev-dependencies [[org.clojure/clojure "1.2.1"]
+ [swank-clojure "1.3.3"]])
View
23 src/appengine_magic/core.clj
@@ -2,15 +2,7 @@
(:import com.google.apphosting.api.ApiProxy))
-(defn in-appengine-interactive-mode? []
- (try
- (let [stack-trace (.getStackTrace (Thread/currentThread))]
- (some #(or (.contains (.toString %) "swank.core")
- (.contains (.toString %) "vimclojure")
- (.contains (.toString %) "clojure.main$repl"))
- stack-trace))
- (catch java.security.AccessControlException ace
- false)))
+(declare appengine-environment-type)
(defn open-resource-stream [resource-name]
@@ -24,9 +16,16 @@
(defn appengine-environment-type []
(let [env-property (System/getProperty "com.google.appengine.runtime.environment")]
(cond
- (nil? env-property) :interactive
(= env-property "Development") :dev-appserver
- (= env-property "Production") :production)))
+ (= env-property "Production") :production
+ (nil? env-property) (try
+ (let [stack-trace (.getStackTrace (Thread/currentThread))]
+ (if (some #(.contains (.toString %) "clojure.lang.Compiler.compile")
+ stack-trace)
+ :compiling
+ :interactive))
+ (catch java.security.AccessControlException ace
+ :production)))))
(defn appengine-app-id []
@@ -43,6 +42,6 @@
(throw (RuntimeException. "the server must be running" npe)))))
-(if (in-appengine-interactive-mode?)
+(if (= :interactive (appengine-environment-type))
(load "core_local")
(load "core_google"))
View
2 src/appengine_magic/core_local.clj
@@ -79,7 +79,7 @@
;;; development server controls
;;; ----------------------------------------------------------------------------
-(defonce *server* (atom nil))
+(defonce ^{:dynamic true} *server* (atom nil))
(defn start [appengine-app & {:keys [port join?] :or {port 8080, join? false}}]
View
6 src/appengine_magic/local_env_helpers.clj
@@ -7,10 +7,10 @@
com.google.appengine.api.taskqueue.dev.LocalTaskQueue))
-(defonce *current-app-id* (atom nil))
-(defonce *current-app-version* (atom nil))
+(defonce ^{:dynamic true} *current-app-id* (atom nil))
+(defonce ^{:dynamic true} *current-app-version* (atom nil))
-(defonce *current-server-port* (atom nil))
+(defonce ^{:dynamic true} *current-server-port* (atom nil))
(defn make-thread-environment-proxy [& {:keys [user-email user-admin?]}]
View
4 src/appengine_magic/services/blobstore.clj
@@ -8,7 +8,7 @@
org.apache.commons.io.IOUtils))
-(defonce *blobstore-service* (atom nil))
+(defonce ^{:dynamic true} *blobstore-service* (atom nil))
(defn get-blobstore-service []
@@ -86,6 +86,6 @@
:payload payload)))
-(if (core/in-appengine-interactive-mode?)
+(if (= :interactive (core/appengine-environment-type))
(load "blobstore_local")
(load "blobstore_google"))
View
2 src/appengine_magic/services/channel.clj
@@ -3,7 +3,7 @@
(:import [com.google.appengine.api.channel ChannelServiceFactory ChannelMessage]))
-(defonce *channel-service* (atom nil))
+(defonce ^{:dynamic true} *channel-service* (atom nil))
(defrecord ClientStatus [id status])
View
56 src/appengine_magic/services/datastore.clj
@@ -15,21 +15,29 @@
;;; ----------------------------------------------------------------------------
+;;; forward declarations
+;;; ----------------------------------------------------------------------------
+
+(declare run-after-load)
+
+
+
+;;; ----------------------------------------------------------------------------
;;; helper variables and constants
;;; ----------------------------------------------------------------------------
-(defonce *datastore-service* (atom nil))
+(defonce ^{:dynamic true} *datastore-service* (atom nil))
-(defonce *current-transaction* nil)
+(defonce ^{:dynamic true} *current-transaction* nil)
-(defonce *datastore-read-policy-map*
+(defonce ^{:dynamic true} *datastore-read-policy-map*
{:eventual ReadPolicy$Consistency/EVENTUAL
:strong ReadPolicy$Consistency/STRONG})
-(defonce *datastore-implicit-transaction-policy-map*
+(defonce ^{:dynamic true} *datastore-implicit-transaction-policy-map*
{:auto ImplicitTransactionManagementPolicy/AUTO
:none ImplicitTransactionManagementPolicy/NONE})
@@ -124,6 +132,8 @@
group parent.")
(get-entity-object [this]
"Returns a datastore Entity object instance for the record.")
+ (run-after-load [this]
+ "Invokes the after-load callback.")
(save! [this]
"Writes the given entity to the data store."))
@@ -198,8 +208,9 @@
"entity has no valid :key metadata, and has no fields marked :key")))))
-(defn get-entity-object-helper [entity-record kind]
- (let [key-object (get-key-object entity-record)
+(defn get-entity-object-helper [entity-record kind before-save]
+ (let [entity-record (before-save entity-record)
+ key-object (get-key-object entity-record)
clj-properties (get-clj-properties entity-record)
entity-meta (meta entity-record)
entity (cond key-object (Entity. key-object)
@@ -341,16 +352,17 @@
model-record (record entity-record-type)]
(map #(let [v (.getValue %)]
(with-meta
- (merge model-record
- (entity->properties (.getProperties v)
- (get-clj-properties model-record)))
+ (run-after-load
+ (merge model-record
+ (entity->properties (.getProperties v)
+ (get-clj-properties model-record))))
{:key (.getKey v)}))
entities))
;; handles singleton values
(let [key-object (make-key-from-value key-value-or-values parent)
entity (.get (get-datastore-service) key-object)
raw-properties (into {} (.getProperties entity))
- entity-record (record entity-record-type)]
+ entity-record (run-after-load (record entity-record-type))]
(with-meta
(merge entity-record (entity->properties raw-properties
(get-clj-properties entity-record)))
@@ -362,7 +374,12 @@
:or {kind (unqualified-name (.getName entity-record-type))}}]
(try
(retrieve-helper entity-record-type key-value-or-values :parent parent :kind kind)
- (catch EntityNotFoundException _ nil)))
+ ;; XXX: Clojure 1.2.x only:
+ (catch EntityNotFoundException _ nil)
+ ;; XXX: Clojure 1.3.x:
+ (catch Exception ex (if (isa? (class (.getCause ex)) EntityNotFoundException)
+ nil
+ (throw ex)))))
(defn exists? [entity-record-type key-value-or-values &
@@ -378,8 +395,10 @@
(defmacro defentity [name properties &
- {:keys [kind]
- :or {kind (unqualified-name name)}}]
+ {:keys [kind before-save after-load]
+ :or {kind (unqualified-name name)
+ before-save identity
+ after-load identity}}]
;; TODO: Clojure 1.3: Remove the ugly Clojure version check.
(let [clj13? (fn [] (and (= 1 (:major *clojure-version*))
(= 3 (:minor *clojure-version*))))
@@ -407,7 +426,9 @@
(get-key-object [this# parent#]
(get-key-object-helper this# ~key-property ~kind parent#))
(get-entity-object [this#]
- (get-entity-object-helper this# ~kind))
+ (get-entity-object-helper this# ~kind ~before-save))
+ (run-after-load [this#]
+ (~after-load this#))
(save! [this#]
(save!-helper this#)))))
@@ -494,9 +515,10 @@
;; unknown type; just use a basic EntityProtocol
(EntityBase.))]
(map #(with-meta
- (merge model-record
- (entity->properties (.getProperties %)
- (get-clj-properties model-record)))
+ (run-after-load
+ (merge model-record
+ (entity->properties (.getProperties %)
+ (get-clj-properties model-record))))
{:key (.getKey %)})
results)))))
View
6 src/appengine_magic/services/images.clj
@@ -11,16 +11,16 @@
;;; helpers
;;; ----------------------------------------------------------------------------
-(defonce *images-service* (atom nil))
+(defonce ^{:dynamic true} *images-service* (atom nil))
-(defonce *output-formats*
+(defonce ^{:dynamic true} *output-formats*
{:jpg ImagesService$OutputEncoding/JPEG
:jpeg ImagesService$OutputEncoding/JPEG
:png ImagesService$OutputEncoding/PNG})
-;; (defonce *composite-anchor*
+;; (defonce ^{:dynamic true} *composite-anchor*
;; {:bottom Composite$Anchor/BOTTOM_CENTER
;; :bottom-left Composite$Anchor/BOTTOM_LEFT
;; :bottom-right Composite$Anchor/BOTTOM_RIGHT
View
4 src/appengine_magic/services/mail.clj
@@ -5,7 +5,7 @@
MailService$Message MailService$Attachment]))
-(defonce *mail-service* (atom nil))
+(defonce ^{:dynamic true} *mail-service* (atom nil))
(defn get-mail-service []
@@ -77,7 +77,7 @@
(defrecord #^{:private true} MessagePart [filename content-type data])
-(defn- deconstruct-message [#^MimeMessage message]
+(defn- deconstruct-message [#^javax.mail.internet.MimeMessage message]
(let [all-subparts (fn [part]
(map #(.getBodyPart part %) (range (.getCount part))))
subparts (fn subparts [part]
View
6 src/appengine_magic/services/memcache.clj
@@ -8,11 +8,11 @@
appengine_magic.services.datastore.EntityProtocol))
-(defonce *memcache-service* (atom nil))
-(defonce *namespaced-memcache-services* (atom {}))
+(defonce ^{:dynamic true} *memcache-service* (atom nil))
+(defonce ^{:dynamic true} *namespaced-memcache-services* (atom {}))
-(defonce *policy-type-map*
+(defonce ^{:dynamic true} *policy-type-map*
{:always MemcacheService$SetPolicy/SET_ALWAYS
:add-if-not-present MemcacheService$SetPolicy/ADD_ONLY_IF_NOT_PRESENT
:replace-only MemcacheService$SetPolicy/REPLACE_ONLY_IF_PRESENT})
View
6 src/appengine_magic/services/task_queues.clj
@@ -5,11 +5,11 @@
TaskOptions$Builder TaskOptions$Method]))
-(defonce *default-queue* (atom nil))
-(defonce *named-queues* (atom {}))
+(defonce ^{:dynamic true} *default-queue* (atom nil))
+(defonce ^{:dynamic true} *named-queues* (atom {}))
-(defonce *task-http-methods*
+(defonce ^{:dynamic true} *task-http-methods*
{:post TaskOptions$Method/POST
:delete TaskOptions$Method/DELETE
:get TaskOptions$Method/GET
View
2 src/appengine_magic/services/url_fetch.clj
@@ -8,7 +8,7 @@
HTTPRequest
HTTPMethod]))
-(defonce *urlfetch-service* (atom nil))
+(defonce ^{:dynamic true} *urlfetch-service* (atom nil))
(defn get-urlfetch-service []
(do (when (nil? @*urlfetch-service*)
View
2 src/appengine_magic/services/user.clj
@@ -2,7 +2,7 @@
(:import [com.google.appengine.api.users User UserService UserServiceFactory]))
-(defonce *user-service* (atom nil))
+(defonce ^{:dynamic true} *user-service* (atom nil))
(defn get-user-service []
View
32 src/appengine_magic/testing.clj
@@ -9,22 +9,22 @@
[com.google.apphosting.api ApiProxy]))
-(def *memcache-size-units*
- {:bytes LocalMemcacheServiceTestConfig$SizeUnit/BYTES
- :kb LocalMemcacheServiceTestConfig$SizeUnit/KB
- :mb LocalMemcacheServiceTestConfig$SizeUnit/MB})
-
-
-(def *logging-levels*
- {:all java.util.logging.Level/ALL
- :severe java.util.logging.Level/SEVERE
- :warning java.util.logging.Level/WARNING
- :info java.util.logging.Level/INFO
- :config java.util.logging.Level/CONFIG
- :fine java.util.logging.Level/FINE
- :finer java.util.logging.Level/FINER
- :finest java.util.logging.Level/FINEST
- :off java.util.logging.Level/OFF})
+(def ^{:dynamic true} *memcache-size-units*
+ {:bytes LocalMemcacheServiceTestConfig$SizeUnit/BYTES
+ :kb LocalMemcacheServiceTestConfig$SizeUnit/KB
+ :mb LocalMemcacheServiceTestConfig$SizeUnit/MB})
+
+
+(def ^{:dynamic true} *logging-levels*
+ {:all java.util.logging.Level/ALL
+ :severe java.util.logging.Level/SEVERE
+ :warning java.util.logging.Level/WARNING
+ :info java.util.logging.Level/INFO
+ :config java.util.logging.Level/CONFIG
+ :fine java.util.logging.Level/FINE
+ :finer java.util.logging.Level/FINER
+ :finest java.util.logging.Level/FINEST
+ :off java.util.logging.Level/OFF})
(defn memcache [& {:keys [max-size size-units]}]
View
24 test/test/appengine_magic/services/datastore.clj
@@ -15,6 +15,14 @@
(ds/defentity Parent [name child-counter])
(ds/defentity Child [name parent])
+(ds/defentity O1 [x]
+ :before-save (fn [entity] (assoc entity :x (inc (:x entity)))))
+(ds/defentity O2 [x]
+ :after-load (fn [entity] (assoc entity :x (dec (:x entity)))))
+(ds/defentity O3 [x y]
+ :before-save #(assoc % :x (str "modified before save " x))
+ :after-load #(assoc % :y (str "modified after load " y)))
+
(deftest basics
(let [alice (Author. "Alice")
@@ -30,6 +38,8 @@
(is (= alice alice-queried))
(is (= bob bob-retrieved))
(is (= charlie charlie-retrieved)))
+ ;; retrieve non-stored entity
+ (is (nil? (ds/retrieve Author "Drew")))
;; sorted query
(let [[charlie-queried bob-queried alice-queried] (ds/query :kind Author
:sort [[:name :desc]])]
@@ -128,3 +138,17 @@
(dotimes [i max]
(make-author i))
(is (= max (ds/query :kind Author :count-only? true)))))
+
+
+(deftest lifecycle-callbacks
+ (let [o1 (ds/save! (O1. 1))]
+ (is (= 2 (:x (first (ds/query :kind O1))))))
+ (let [o2 (ds/save! (O2. 1))]
+ (is (= 0 (:x (first (ds/query :kind O2))))))
+ (let [o3 (O3. 10 20)]
+ (is (= 10 (:x o3)))
+ (is (= 20 (:y o3)))
+ (ds/save! o3)
+ (let [o3 (first (ds/query :kind O3))]
+ (is (= "modified before save 10" (:x o3)))
+ (is (= "modified after load 20" (:y o3))))))

0 comments on commit 0f75382

Please sign in to comment.