-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c4eecc0
Showing
8 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/target | ||
/classes | ||
/checkouts | ||
pom.xml | ||
pom.xml.asc | ||
*.jar | ||
*.class | ||
/.lein-* | ||
/.nrepl-port | ||
.hgignore | ||
.hg/ | ||
.idea | ||
*.iml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
|
||
# Specalog | ||
Generate composable Datalog (Datomic) queries from your `clojure.spec` definitions. | ||
|
||
### Why? | ||
* Build on your shareable spec, not your Datomic API library | ||
* Pull from Datomic using custom views defined in your spec. No code updates required. | ||
* Validate data before it's transacted | ||
|
||
### How? | ||
You probably have a spec that defines the shape of an entity in Datomic. For example, a Person entity might look like this: | ||
```clj | ||
(s/def :person/uuid uuid?) | ||
(s/def :person/first-name string?) | ||
(s/def :person/last-name string?) | ||
(s/def :person/email string?) | ||
(s/def :person/password string?) | ||
(s/def :acme/person (s/keys :req [:person/uuid | ||
:person/email | ||
:person/password] | ||
:opt [:person/first-name | ||
:person/last-name])) | ||
``` | ||
`specalog.query/pull-thing` builds a query that finds all entities with values in the spec's `:req` key, while returnng those values and an additional ones found in the `:opt` keys. | ||
### Example Pull | ||
Using Specalog, generate a Datalog query that pulls entities which fit the shape of the `:acme/person` spec | ||
```clj | ||
(q/pull-thing :acme/person) | ||
``` | ||
And the result is a query with the following properties: | ||
1. Entities returned must have a value for all attributes in the spec's :req key | ||
2. Return all values from both the `:req`: and `:opt` keys. | ||
```clj | ||
; Result | ||
{:find [(pull ?acme-person [:person/uuid | ||
:person/email | ||
:person/password | ||
:person/first-name | ||
:person/last-name])] | ||
:in [$] | ||
:where [[?acme-person :person/uuid] | ||
[?acme-person :person/email] | ||
[?acme-person :person/password]]} | ||
``` | ||
### Example Constraints | ||
Specalog supports simple constraints by passing an optional constraint map to `q/pull-thing`. For example, find all people-like entities with a specific email address. | ||
```clj | ||
(q/pull-thing :acme/person {:person/email "[email protected]"}) | ||
``` | ||
```clj | ||
{:find [(pull ?acme-person [:person/uuid :person/email | ||
:person/password | ||
:person/first-name | ||
:person/last-name])] | ||
:in [$] | ||
:where [[?acme-person :person/email "[email protected]"] | ||
[?acme-person :person/uuid] | ||
[?acme-person :person/email] | ||
[?acme-person :person/password]]} | ||
``` | ||
### Validate Transactions | ||
Use Specalog to validate input to a transaction | ||
```clj | ||
(q/put-thing :acme/person {:person/first-name "Jean-Luc" | ||
:person/last-name "Picard" | ||
:person/email "[email protected]" | ||
:person/password "makeitso" | ||
:person/uuid #uuid"c3a48253-0c34-4613-aca8-79749c6238bd"}) | ||
``` | ||
```clj | ||
; Result | ||
{:tx-data [#:person{:first-name "Jean-Luc", | ||
:last-name "Picard", | ||
:email "[email protected]", | ||
:password "makeitso", | ||
:uuid #uuid"c3a48253-0c34-4613-aca8-79749c6238bd"}]} | ||
``` | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
:paths ["src"] | ||
:deps { | ||
org.clojure/clojure {:mvn/version "1.9.0"} | ||
org.clojure/spec.alpha {:mvn/version "0.1.143"} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
(defproject specalog "0.1.0" | ||
:description "Generate Datalog queries using Spec" | ||
:url "https://github.com/joshkh/specalog" | ||
:license {:name "Eclipse Public License" | ||
:url "http://www.eclipse.org/legal/epl-v10.html"} | ||
:dependencies [[org.clojure/clojure "1.9.0"]] | ||
:target-path "target/%s" | ||
:profiles {:uberjar {:aot :all}}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
(ns specalog.jig | ||
(:require [clojure.spec.alpha :as s])) | ||
|
||
(def concatv (comp vec concat)) | ||
(def conjv (comp vec conj)) | ||
|
||
(def kw->symbol (fn [k] (symbol (str "?" (clojure.string/join "-" ((juxt namespace name) k)))))) | ||
|
||
(defn form->spec | ||
"Take the form of a map spec and turn it back into a map spec. | ||
In other words, identity function for a map spec" | ||
[spec] | ||
(->> spec | ||
; Get the form of the spec | ||
s/form | ||
; Take the good stuff | ||
rest | ||
; Group the results into k/v pairs (:req, :opt) | ||
(partition 2) | ||
; Pop it back into a map | ||
(reduce (fn [total [k v]] (assoc total k v)) {}))) | ||
|
||
(defn make-in | ||
"Ensure that a $ value is at the head of an :in clause" | ||
([] (make-in {})) | ||
([q] | ||
(cond-> q | ||
(not= '$ (get-in q [:in 0])) (update :in (comp vec (partial cons '$)))))) | ||
|
||
(defn make-pull | ||
"Add all :req and :opt values from a spec'ed map to the :find clause of a query" | ||
([spec] | ||
(make-pull spec {})) | ||
([spec q] | ||
(update q :find conjv (list 'pull (kw->symbol spec) (->> spec form->spec vals (apply concat) vec))))) | ||
|
||
(defn filter-ands | ||
[constraints] | ||
(filter (fn [[k v]] (not (coll? v))) constraints)) | ||
|
||
(defn filter-ors | ||
[constraints] | ||
(filter (fn [[k v]] (coll? v)) constraints)) | ||
|
||
(defn make-where-a | ||
[constraints] | ||
(let [ands (filter-ands constraints) | ||
ors (filter-ors constraints)] | ||
(println "ands" ands) | ||
|
||
)) | ||
|
||
(defn make-where | ||
"Add all :req values from a spec'ed map to the :find vector of a query. | ||
This constrains the query to return only things that fully match a spec, aka match-all" | ||
([spec] | ||
(make-where spec {})) | ||
([spec constraints] | ||
(make-where spec constraints {})) | ||
([spec constraints q] | ||
(cond-> q | ||
; Apply user provided constraints first. Query optimisation? ¯\_(ツ)_/¯ | ||
constraints (update :where concatv (mapcat (fn [[k v]] | ||
[(vector (kw->symbol spec) k v)]) constraints)) | ||
; Append the :req values as datalog constraints | ||
; ex. [?some-spec :some-spec/key] | ||
q (update :where concatv (map (fn [w] (vector (kw->symbol spec) w)) (->> spec form->spec :req vec)))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
(ns specalog.query | ||
(:require [specalog.jig :as jig] | ||
[clojure.spec.alpha :as s]) | ||
(:import [java.util.UUID])) | ||
|
||
(defn ruuid [] (java.util.UUID/randomUUID)) | ||
|
||
; Turn on assertion checking in order to throw Exceptions | ||
(s/check-asserts true) | ||
|
||
(defn pull-thing | ||
"Returns a datalog query where by returned entities meet the minimum requirements | ||
of a spec while pulling all potential values from a spec." | ||
([spec] (pull-thing spec {})) | ||
([spec constraints] (pull-thing spec constraints {})) | ||
([spec constraints query] | ||
(->> query | ||
(jig/make-pull spec) | ||
(jig/make-in) | ||
(jig/make-where spec constraints)))) | ||
|
||
|
||
(defn put-thing | ||
"Returns a datalog query" | ||
([data] | ||
(put-thing nil data)) | ||
([spec data] | ||
(cond-> {} | ||
; If we conform to the spec then return a transaction query | ||
(s/assert spec data) (update :tx-data jig/conjv data) ))) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
(ns specalog.core-test | ||
(:require [clojure.test :refer :all] | ||
[clojure.spec.alpha :as s] | ||
[specalog.specs] | ||
[specalog.jig :as jig] | ||
[specalog.query :as q])) | ||
|
||
;; spec helper functions | ||
|
||
(deftest test-jig-hydrate-spec | ||
(testing "Convert a spec'ed map back into its spec definition" | ||
(is (= | ||
(jig/form->spec :acme/person) | ||
{:req [:person/uuid | ||
:person/email | ||
:person/password] | ||
:opt [:person/first-name | ||
:person/last-name]})))) | ||
|
||
;; make-in tests | ||
|
||
(deftest test-jig-make-in-blank | ||
(testing "Prepend $ to an non-existing :in clause" | ||
(is (= (get (jig/make-in) :in) | ||
['$])))) | ||
|
||
(deftest test-jig-make-in-add-missing-db-$ | ||
(testing "Prepend $ to an existing :in clause if it's not there" | ||
(is (= (get (jig/make-in {:in ['exists]}) :in) | ||
['$ 'exists])))) | ||
|
||
(deftest test-jig-make-in-$-already-exists | ||
(testing "Don't prepend $ to an existing :in clause if it's already there" | ||
(is (= (get (jig/make-in '{:in [$]}) :in) | ||
['$])))) | ||
|
||
;; pull a thing | ||
|
||
(deftest test-pull-thing | ||
(testing "Pull a thing that matches a spec, returning all :req and :opt keys" | ||
(is (= (q/pull-thing :acme/person) | ||
'{:find [(pull ?acme-person [:person/uuid :person/email | ||
:person/password | ||
:person/first-name | ||
:person/last-name])], | ||
:in [$], | ||
:where [[?acme-person :person/uuid] | ||
[?acme-person :person/email] | ||
[?acme-person :person/password]]})))) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
(ns specalog.specs | ||
(:require [clojure.spec.alpha :as s] | ||
[clojure.spec.gen.alpha :as gen])) | ||
|
||
(def full-string? (s/and string? (complement empty?))) | ||
|
||
; Spec the entity's attributes | ||
(s/def :person/uuid uuid?) | ||
(s/def :person/first-name full-string?) | ||
(s/def :person/last-name full-string?) | ||
(s/def :person/email full-string?) | ||
(s/def :person/password full-string?) | ||
|
||
; ... and the entity itself as a map | ||
(s/def :acme/person (s/keys :req [:person/uuid | ||
:person/email | ||
:person/password] | ||
:opt [:person/first-name | ||
:person/last-name])) | ||
|
||
|
||
; TODO - spec a generic Datalog query | ||
;(pull ?root [:something]) | ||
;(s/def :datalog/pull-clause (s/and list? (s/tuple symbol? symbol? vector?))) | ||
;(s/def :datalog/pull (s/coll-of :datalog/pull-clause)) |