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, and assoc!
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>
.
spark.sparkspec
namespaces with spark.spec-tacular
=
to see if refless=
is more appropriateset
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.defenum
to defunion
:is-many
fields are now represented as
clojure.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:spec-tacular/spec
s are no
longer supported, use pull or sd/query
instead of sd/q
defattr
that can be used as a field type to allow shared
Datomic namespaces between fields of different specsCopyright © 2014-2015 Spark Community Investment
Distributed under the Apache License Version 2.0
Can you improve this documentation? These fine people already did:
Claire Alvis, James Shargo & Donald BallEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close