This repository has been archived by the owner. It is now read-only.
Make the border between Clojure and Datomic a more convenient and safe place to live.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.circleci
src/spark
test/spark
.gitignore
LICENSE
README.md
project.clj

README.md

spec-tacular

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.

Quick Start

[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>

Usage

Creating Specs

(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

Creating Databases

(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@....>

Changing Databases

(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
  ....)

Querying Databases

(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 :Persons. 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>.

Updating from v.0.4.x to v0.5.0

  • Replace all spark.sparkspec namespaces with spark.spec-tacular
  • Check all calls to = to see if refless= is more appropriate
  • Check all sets 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 to defunion

Updating from v.0.5.x to v0.6.0

  • :is-many fields are now represented as clojure.lang.PersistentHashSets
  • spark.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/specs are no longer supported, use pull or sd/query instead of sd/q

Short Term Roadmap

  • Create defattr that can be used as a field type to allow shared Datomic namespaces between fields of different specs

License

Copyright © 2014-2015 Spark Community Investment

Distributed under the Apache License Version 2.0