Write spectacular data definitions! Our goal is to make the border between Clojure and Datomic a more convenient and safe place to live. Browse the API or continue scrolling.
Define your Datomic schemas using spec-tacular's spec DSL and receive the following in return:
-
Representation of Datomic entities as maps that verify (upon creation and association) that entity attributes have the correct fields, and in turn, the correct types
-
Core Typed aliases for each spec
-
Specialized query language with a map-like syntax that allows queries to be expressed with domain-specific spec keywords instead of Datomic attribute-keywords. Entities returned from queries are lazily constructed and can be used in typed code without extra casts.
-
Simple transaction interface with Datomic, using
create!
as a constructor, andassoc!
as an update function.
WARNING: spec-tacular is not maintained.
[spec-tacular "0.6.2-SNAPSHOT"] ; unstable
[spec-tacular "0.6.1"]
<dependency>
<groupId>spec-tacular</groupId>
<artifactId>spec-tacular</artifactId>
<version>0.6.1</version>
</dependency>
(require '[spark.spec-tacular :as sp :refer [defspec defunion defenum]])
;; Sets up a House entity containing a mandantory color and optionally
;; a Mailbox. It may also link in any number of Occupants.
(defspec House
(:link [occupants :is-many :Occupant])
[mailbox :is-a :Mailbox]
[color :is-a :Color :required])
(defenum Color ;; Houses can only be green or orange..
green, orange) ;; makes for interesting neighborhoods
(defspec Mailbox ;; Hope you don't want to get your mail
[has-mail? :is-a :boolean]) ;; cause mailboxes only know if they have mail
;; Specs can have docstrings
(defspec Chimney
"Chimneys are super complicated and require documentation"
(:link [house :is-a House]))
(doc Chimney) ;; Such words
;; Houses can be occupied by either People or Pets.
(defunion Occupant :Person :Pet)
;; Each Person has a name that serves as an identifying field
;; (implemented as Datomic's notion of identity), and an age.
(defspec Person
[name :is-a :string :identity :unique]
[age :is-a :long])
(defunion Pet :Dog :Cat :Porcupine)
(defspec Dog
[fleas? :is-a :boolean])
;; Cats can contain links (passed by reference to the database) to all
;; the occupants of the house that they hate. For their nefarious
;; plots, no doubt.
(defspec Cat
[hates :is-many :Occupant :link])
(defspec Porcupine) ;; No fields, porcupines are boring
(require '[spark.spec-tacular.schema :as schema])
;; Returns a schema with entries for each spec defined in my-ns
(schema/from-namespace *ns*)
;; => ({:db/id ....,
;; :db/ident :house/occupants,
;; :db/valueType :db.type/ref,
;; :db/cardinality :db.cardinality/many,
;; ....}
;; ....)
;; Creates a database with the earlier schema installed.
;; Returns a connection to that database.
(schema/to-database! (schema/from-namespace *ns*))
;; => #<LocalConnection datomic.peer.LocalConnection@....>
(require '[spark.spec-tacular.datomic :as sd])
;; Use the House schema to create a database and connection
(def conn-ctx {:conn (schema/to-database! (schema/from-namespace *ns*))})
;; Create a green house:
(def h (sd/create! conn-ctx (house {:color :Color/green})))
;; Some quick semantics:
(:color h) ;; => :Color/green
(= h (house {:color :Color/green})) ;; => false
(sp/refless= h (house {:color :Color/green})) ;; => true
(assoc h :random-kw 42) ;; => error
(set [h h]) ;; => #{h}
(set [h (house {:color :Color/green})]) ;; => #{h (house {:color :Color/green})}
;; Let some people move in:
(def joe (sd/create! conn-ctx (person {:name "Joe" :age 32})))
(def bernard (sd/create! conn-ctx (person {:name "Bernard" :age 25})))
(def new-h (sd/assoc! conn-ctx h :occupants [joe bernard]))
;; => assoc! returns a new House with the new field
h ;; => is still the simple green house
(sd/refresh conn-ctx h) ;; => new-h
;; In most cases, you can forego the `refresh` and just use the return
;; value of `assoc!`
;; Bernard and Joe get a cat, who hates both of them,
(def zuzu (sd/create! conn-ctx (cat {:hates (:occupants new-h)})))
(sd/assoc! conn-ctx h :occupants (conj (:occupants new-h) zuzu))
;; They build a mailbox, and try to put it up in another House:
(let [mb (mailbox {:has-mail? false})
h1 (sd/assoc! conn-ctx h :mailbox mb)
h2 (sd/create! conn-ctx (house {:color :Color/orange :mailbox mb}))]
;; But since Mailboxes are passed by value,
;; the Mailbox get duplicated
(= (:mailbox h1) (:mailbox h2)) ;; => false
....)
(require '[spark.spec-tacular.datomic :as sd])
;; First let's distinguish the mailboxes -- let's say Joe and Bernard
;; get some mail
(def mb1 (sd/assoc! conn-ctx (:mailbox h1) :has-mail? true))
;; Get the database
(def db (sd/db conn-ctx))
;; Use % to look for the only find variable
(sd/q :find [:Mailbox ...] :in db :where [% {:has-mail? false}])
;; => #{(:mailbox h2)}, the mailbox from house h2
(sd/q :find [:Mailbox ...] :in db :where [% {:has-mail? true}])
;; => #{mb1}, that's Joe and Bernard's mailbox
;; Find the Houses without mail
(sd/q :find [:House ...] :in db :where
[% {:mailbox {:has-mail false}}])
;; => #{h2}
;; Find the House and it's human occupants when the mailbox has mail
;; Use %1 and %2 to to look for multiple find variables
(sd/q :find :House :Person :in db :where
[%1 {:occupants %2 :mailbox {:has-mail true}}])
;; => #{[h1 joe] [h2 bernard]}
This last example means we're looking for any :occupants
that are
:Person
s. Even though we represent Datomic's cardinality "many" as
a collection in Clojure, we still use a relation to search for members
of that collection on the database. Those familiar with Datomic may
understand that this part of the query (roughly) expands to
[.... [?house :house/occupants ?person] ....]
When we get the result of the query back in Clojure, we take that result and return it as a set. Onwards!
;; If you want to get the spec name of entities on the database, you
;; can use the special :spec-tacular/spec keyword. Here we restrict
;; the occupants to the :Pet spec and then return all kinds of Pet's
;; that live in houses:
(sd/q :find [:string ...] :in db :where
[:House {:occupants [:Person {:name %}]}])
;; => #{"Joe" "Bernard"}
Although maps work as you would expect in a query, the vector form
[<spec> <map>]
is protected syntax meaning the map
should be
restricted to things of type <spec>
.
- Replace all
spark.sparkspec
namespaces withspark.spec-tacular
- Check all calls to
=
to see ifrefless=
is more appropriate - Check all
set
s if you mix local instances and instances on the database; these are nolonger=
nor do they hash to the same number even if they are otherwise equivalent. - Rename
defenum
todefunion
:is-many
fields are now represented asclojure.lang.PersistentHashSet
sspark.spec-tacular.restify
was removed, it may come back eventually but in the meantime, if you need web serialization we accept pull requests- some queries that dynamically pull out
:spec-tacular/spec
s are no longer supported, use pull orsd/query
instead ofsd/q
- Create
defattr
that can be used as a field type to allow shared Datomic namespaces between fields of different specs
Copyright © 2014-2015 Spark Community Investment
Distributed under the Apache License Version 2.0