Skip to content

Commit

Permalink
Entity validation (#8)
Browse files Browse the repository at this point in the history
Validation of entities on create
  • Loading branch information
nickbauman committed Mar 9, 2020
1 parent 385d215 commit 47d01b7
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 76 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ You will need the [Java SDK for AppEngine and its dependencies](https://cloud.go
Leiningen Clojars dependency:

```
[gaeclj-ds "0.1.2"]
[gaeclj-ds "0.1.3"]
```


Expand Down Expand Up @@ -87,6 +87,50 @@ Leiningen Clojars dependency:
(delete! root-entity)
```

## Validation on create

Optionally, you can declare rules that are applied to each property before the entity is created.

```clojure
(defentity CostStrategy
[uuid
create-date
cost-uuid
strategy-description
ordered-member-uuids
ordered-percentages]
[:uuid gaeclj.valid/valid-uuid?
:create-date gaeclj.valid/long?
:cost-uuid gaeclj.valid/valid-uuid?
:strategy-description gaeclj.valid/string-or-nil?
:ordered-member-uuids gaeclj.valid/repeated-uuid?
:ordered-amounts gaeclj.valid/repeated-longs?])
```

When created a new `CostStrategy` the rules are applied.

```clojure
(create-CostStrategy "invalid uuid string"
(.getMillis (t/date-time 1999 12 31))
(str (uuid/v1))
"even distribution"
[(str (uuid/v1)) (str (uuid/v1))]
[1/2 1/2])
```

Results in an `RuntimeException` thrown.

```text
java.lang.RuntimeException: (create-CostStrategy ...) failed validation for props :uuid, :ordered-amounts
at gaeclj.test.valid$create_CostStrategy.invokeStatic (valid.clj:10)
gaeclj.test.valid$create_CostStrategy.invoke (valid.clj:10)
gaeclj.test.valid$fn__1961.invokeStatic (valid.clj:26)
gaeclj.test.valid/fn (valid.clj:24)
clojure.test$test_var$fn__9737.invoke (test.clj:717)
clojure.test$test_var.invokeStatic (test.clj:717)
clojure.test$test_var.invoke (test.clj:708)
```
## Runing the automated tests

Through leiningen
Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(def appengine-version "1.9.78")

(defproject gaeclj-ds "0.1.2"
(defproject gaeclj-ds "0.1.3"
:description "A DSL to support querying Google App Engine's Datastore"
:license {:name "Eclipse Public License - v 1.0"
:url "http://www.eclipse.org/legal/epl-v10.html"
Expand Down
167 changes: 93 additions & 74 deletions src/gaeclj/ds.clj
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
(ns gaeclj.ds
(:require [clj-time.core :as t]
[clj-time.coerce :as c]
[clojure.reflect :as r]
[clojure.tools.logging :as log]
[gaeclj.util :as u])
(:import [com.google.appengine.api.datastore
DatastoreServiceFactory
DatastoreService
Entity
EntityNotFoundException
FetchOptions$Builder
KeyFactory
Key
Query
Query$SortDirection
Query$CompositeFilter
Query$CompositeFilterOperator
Query$FilterPredicate
Query$FilterOperator
TransactionOptions$Builder]))
(:require [clj-time.coerce :as c]
[clojure.string :refer [join]]
[clojure.tools.logging :as log]
[gaeclj.util :as u])
(:import [com.google.appengine.api.datastore
DatastoreServiceFactory
DatastoreService
Entity
EntityNotFoundException
FetchOptions$Builder
KeyFactory
Key
Query
Query$SortDirection
Query$CompositeFilter
Query$CompositeFilterOperator
Query$FilterPredicate
Query$FilterOperator
TransactionOptions$Builder]))

(defprotocol NdbEntity
(save! [this] [this parent-key] "Saves the entity. Saves the entity with its parent key.")
Expand Down Expand Up @@ -68,7 +67,7 @@
java.util.List
(<-prop [d] d))

(defn make-key
(defn make-key
([kind value]
(KeyFactory/createKey (name kind) value))
([kind parent value]
Expand All @@ -85,26 +84,26 @@
([entity-type entity]
(save-entity entity-type entity nil))
([entity-type entity parent-key]
(log/debugf "Saving %s: %s" entity-type (pr-str entity))
(let [datastore (DatastoreServiceFactory/getDatastoreService)
gae-parent-key (check-key parent-key entity-type)
gae-ent (if (:key entity)
(if gae-parent-key
(Entity. (name entity-type) (:key entity) gae-parent-key)
(Entity. (name entity-type) (:key entity)))
(if gae-parent-key
(Entity. (name entity-type) gae-parent-key)
(Entity. (name entity-type))))]
(doseq [field (keys entity)]
(.setProperty gae-ent (name field) (->prop (field entity))))
(try
(.put datastore gae-ent)
(catch Exception e
(log/error e (str "Unable to save " (pr-str entity)))
(throw e)))
(if (:key entity)
entity
(assoc entity :key (.. gae-ent getKey getId))))))
(log/debugf "Saving %s: %s" entity-type (pr-str entity))
(let [datastore (DatastoreServiceFactory/getDatastoreService)
gae-parent-key (check-key parent-key entity-type)
gae-ent (if (:key entity)
(if gae-parent-key
(Entity. (name entity-type) (:key entity) gae-parent-key)
(Entity. (name entity-type) (:key entity)))
(if gae-parent-key
(Entity. (name entity-type) gae-parent-key)
(Entity. (name entity-type))))]
(doseq [field (keys entity)]
(.setProperty gae-ent (name field) (->prop (field entity))))
(try
(.put datastore gae-ent)
(catch Exception e
(log/error e (str "Unable to save " (pr-str entity)))
(throw e)))
(if (:key entity)
entity
(assoc entity :key (.. gae-ent getKey getId))))))

(defn- gae-entity->map [gae-entity]
(let [gae-key (.getKey gae-entity)
Expand All @@ -131,7 +130,7 @@
(let [datastore (DatastoreServiceFactory/getDatastoreService)]
(.delete datastore (map #(make-key entity-kind %) (conj more-keys entity-key)))))

; Start DS Query support ;;;
; Start DS Query support ;;;

(defn !=
"Available to complete the operator-map logic. Reverse logic of the = function"
Expand All @@ -140,16 +139,16 @@
([x y & more]
(not (apply = x y more))))

(def operator-map {< Query$FilterOperator/LESS_THAN
> Query$FilterOperator/GREATER_THAN
= Query$FilterOperator/EQUAL
(def operator-map {< Query$FilterOperator/LESS_THAN
> Query$FilterOperator/GREATER_THAN
= Query$FilterOperator/EQUAL
>= Query$FilterOperator/GREATER_THAN_OR_EQUAL
<= Query$FilterOperator/LESS_THAN_OR_EQUAL
!= Query$FilterOperator/NOT_EQUAL})

(def sort-order-map {:desc Query$SortDirection/DESCENDING
:asc Query$SortDirection/ASCENDING
nil Query$SortDirection/ASCENDING})
:asc Query$SortDirection/ASCENDING
nil Query$SortDirection/ASCENDING})

(defn filter-map
[keyw jfilter-predicates]
Expand All @@ -160,7 +159,7 @@
(defn get-option
[options option?]
(let [indexed-pairs (map-indexed vector options)]
(if-let [[[index _]] (seq (filter #(= option? (second %)) indexed-pairs))]
(if-let [[[index _]] (seq (filter #(= option? (second %)) indexed-pairs))]
(get (vec options) (inc index)))))

(defn add-sorts
Expand All @@ -186,11 +185,11 @@

(defn make-property-filter
[pred-coll]
(let [[property operator-fn query-value] pred-coll
filter-operator (operator-map operator-fn)]
(if filter-operator
(Query$FilterPredicate. (name property) filter-operator query-value)
(throw (RuntimeException. (str "operator " operator-fn " not found in operator-map " (keys operator-map)))))))
(let [[property operator-fn query-value] pred-coll
filter-operator (operator-map operator-fn)]
(if filter-operator
(Query$FilterPredicate. (name property) filter-operator query-value)
(throw (RuntimeException. (str "operator " operator-fn " not found in operator-map " (keys operator-map)))))))

(declare compose-query-filter)

Expand All @@ -199,8 +198,8 @@
(loop [jfilter-preds []
preds preds-coll]
(if (seq preds)
(let [filter-fn (if (u/in (ffirst preds) [:or :and])
compose-query-filter
(let [filter-fn (if (u/in (ffirst preds) [:or :and])
compose-query-filter
make-property-filter)]
(recur (conj jfilter-preds (filter-fn (first preds)))
(rest preds)))
Expand All @@ -214,6 +213,7 @@
(filter-map condition jfilter-predicates))))

(defn qbuild
"Build a query"
[predicates options ent-kind filters]
(->> (if (nil? filters) (make-property-filter predicates) filters)
(.setFilter (Query. (name ent-kind)))
Expand All @@ -224,7 +224,7 @@
(defn make-query
[predicates options ent-kind]
(if (seq predicates)
(qbuild predicates options ent-kind (compose-query-filter predicates))
(qbuild predicates options ent-kind (compose-query-filter predicates))
(->> (Query. (name ent-kind))
(set-ancestor-key options ent-kind)
(set-keys-only options)
Expand All @@ -234,13 +234,13 @@
([pq-iterable]
(lazify-qiterable pq-iterable (.iterator pq-iterable)))
([pq-iterable i]
(lazy-seq
(when (.hasNext i)
(cons (gae-entity->map (.next i)) (lazify-qiterable pq-iterable i))))))
(lazy-seq
(when (.hasNext i)
(cons (gae-entity->map (.next i)) (lazify-qiterable pq-iterable i))))))

(defn query-entity
[predicates options ent-sym]
(->> ent-sym
(->> ent-sym
(make-query predicates options)
(.prepare (DatastoreServiceFactory/getDatastoreService))
.asIterable
Expand All @@ -250,31 +250,35 @@
(defmacro ds-operation-in-transaction
[tx & body]
`(try
(let [body-result# (do ~@body)]
(.commit ~tx)
body-result#)
(catch Throwable err#
(do (.rollback ~tx)
(throw err#)))))

(defmacro with-xg-transaction
(let [body-result# (do ~@body)]
(.commit ~tx)
body-result#)
(catch Throwable err#
(do (.rollback ~tx)
(throw err#)))))

(defmacro with-xg-transaction
[& body]
`(let [tx# (.beginTransaction (DatastoreServiceFactory/getDatastoreService) (TransactionOptions$Builder/withXG true))]
(ds-operation-in-transaction tx# ~@body)))

(defmacro with-transaction
(defmacro with-transaction
[& body]
`(let [tx# (.beginTransaction (DatastoreServiceFactory/getDatastoreService))]
(ds-operation-in-transaction tx# ~@body)))

; End DS Query support ;;;
; End DS Query support ;;;
(defmacro get-validation-meta
[sym]
`(:validation (meta '~sym)))

(defmacro defentity
[entity-name entity-fields]
[entity-name entity-fields & validation]
(let [name entity-name
sym (symbol name)
empty-ent (symbol (str 'empty- name))
creator (symbol (str '-> name))]
`(do
creator (symbol (str '-> name))]
`(do
(defrecord ~name ~entity-fields
NdbEntity
(save! [this#] (save-entity '~sym this#))
Expand All @@ -286,6 +290,21 @@
~(conj (map (constantly nil) entity-fields) creator))

(defn ~(symbol (str 'create- name)) ~entity-fields
(if-let [val-rules# (seq '~validation)] ; optional validation
(let [validation-fns# (map second (partition 2 (first val-rules#)))
values# ~entity-fields
validators-to-values# (partition 2 (interleave validation-fns# values#))
keys# (map first (partition 2 (first val-rules#)))
results# (map
(fn [[f# v#]]
(require (quote ((namespace f#))))
(load (namespace f#))
((eval f#) (eval v#)))
validators-to-values#)
props-to-results# (partition 2 (interleave keys# results#))
invalid-props# (map first (filter (fn [[_# result#]] (false? result#)) props-to-results#))]
(if (seq invalid-props#)
(throw (RuntimeException. (str "(create-" (.getSimpleName ~name) " ...) failed validation for props " (join ", " invalid-props#)))))))
~(conj (seq entity-fields) creator))

(defn ~(symbol (str 'get- name)) [key#]
Expand All @@ -297,6 +316,6 @@
(if (get-option (first options#) :keys-only)
(map :key results#)
(map #(merge ~empty-ent %) results#))))

(defn ~(symbol (str 'delete- name)) [key#]
(delete-entity '~sym key#)))))
9 changes: 9 additions & 0 deletions src/gaeclj/util.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
(ns gaeclj.util)

(defmacro try-with-default [default & forms]
"Tries to do 'forms'. If forms throws an exception, does default."
`(try
(do ~@forms)
(catch Exception ~'ex
(do
(log/warn (str "Failed with " (type ~'ex) ": " (.getMessage ~'ex) ". Defaulting to " ~default))
~default))))

(defn in [scalar sequence]
"Returns true if scalar value is found in sequence, otherwise returns nil"
(some #(= scalar %) sequence))
30 changes: 30 additions & 0 deletions src/gaeclj/valid.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(ns gaeclj.valid
(:require [gaeclj.util :refer [try-with-default]]
[clojure.tools.logging :as log]))

(def uuid-regex #"[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}")

(defn valid-uuid?
[x]
(try-with-default false
(not (nil? (re-matches uuid-regex x)))))

(defn long?
[x]
(try-with-default false (instance? Long x)))

(defn string-or-nil?
[x]
(or (string? x) (nil? x)))

(defn repeated-uuid?
[x]
(and (seq x) (every? #(not (nil? %)) (map valid-uuid? x))))

(defn repeated-longs?
[x]
(and (seq x) (every? true? (map long? x))))

(defn repeated-floats?
[x]
(and (seq x) (every? true? (map #(or (float? %1) (ratio? %1)) x))))

0 comments on commit 47d01b7

Please sign in to comment.