Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
joshkh committed Nov 6, 2018
0 parents commit c4eecc0
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .gitignore
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
80 changes: 80 additions & 0 deletions README.md
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"}]}
```



7 changes: 7 additions & 0 deletions deps.edn
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"}
}
}
8 changes: 8 additions & 0 deletions project.clj
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}})
67 changes: 67 additions & 0 deletions src/specalog/jig.clj
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))))))
31 changes: 31 additions & 0 deletions src/specalog/query.clj
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) )))

50 changes: 50 additions & 0 deletions test/specalog/core_test.clj
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]]}))))

25 changes: 25 additions & 0 deletions test/specalog/specs.clj
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))

0 comments on commit c4eecc0

Please sign in to comment.