A Clojure(script) library to define entities and their relationships via clojure.spec, and to ask questions about them.
See specomatic-db database tooling for a concrete library building on this.
The core concept in specomatic is the schema, a nested map that contains all the information about the structure of your data. The functions in the specomatic.core
, specomatic.etype-def
and specomatic.field-def
namespaces are pure functions that take the schema or parts of it as a first argument and answer questions about it.
The functions in the specomatic.registry
namespace query the clojure.spec registry to generate the schema. They can work with entities and relationships specified using clojure.spec in different ways.
Any part of the schema can be overridden.
It would also be possible to bypass these functions - or even clojure.spec itself - completely and to create the schema yourself. Supporting this is however not a goal of specomatic.
An entity type represents a domain concept. It specifies a map by a set of keys (fields), among them an identity field.
You can define your entity types in different ways:
defent
macroThe defent
macro takes the same arguments as s/keys
and
:id
to specify the id field for the entity type (optional, default is :entity-type/id
):field-ns
for a namespace to use key specs from (optional, default is current namespace)The name of the entity type is added as a namespace to any unqualified keywords.
If no spec exists for a field, but there is one for a keyword in the current namespace (overridden by :field-ns
)
and with the same name as the field, it is used for the field.
In addition, if the spec for a field is a keyword, the field is derive
d from the spec to facilitate multimethod dispatch.
(require '[clojure.spec.alpha :as s])
(require '[specomatic.registry :as sr])
(s/def :potato/harvest-date #(instance? java.time.LocalDateTime %))
(s/def ::weight integer?)
(s/def ::id integer?)
(sr/defent ::potato
:req [:harvest-date]
:opt [:weight])
s/keys
(require '[clojure.spec.alpha :as s])
(s/def :potato/harvest-date #(instance? java.time.LocalDateTime %))
(s/def :potato/weight integer?)
(s/def :potato/id integer?)
(s/keys ::potato
:req [:potato/harvest-date]
:opt [:potato/id :potato/weight])
This will yield an equivalent schema to the one above defined using defent
.
To use the spec predicate function of your choice for defining entity types, extend the following multimethods for it:
specomatic.registry/etype-spec-fn?
specomatic.registry/etype-spec-form-fields
specomatic.registry/etype-spec-form-required-fields
Relationships represent connections between domain concepts. They are specified using relational fields: reference and reference collection fields.
Reference fields can contain either the id of the target entity or (part of) the target entity itself.
:has-many-through
will be derived on the opposite side.reference
macro(require '[clojure.spec.alpha :as s])
(require '[specomatic.spec :as sp])
(s/def :review/movie (sp/reference ::movie))
s/or
(require '[clojure.spec.alpha :as s])
(s/def :review/movie (s/or :id integer? :entity ::movie)
This is what the reference
macro expands to.
To use keys of your choice for the s/or
spec, extend the following multimethods to them:
specomatic.registry/reference-id-key?
specomatic.registry/reference-entity-key?
To use the spec predicate function of your choice for defining reference fields, extend the following multimethods for them:
specomatic.registry/reference-spec-form?
specomatic.registry/reference-spec-form-referenced-etype
references
macro(require '[clojure.spec.alpha :as s])
(require '[specomatic.spec :as sp])
(s/def :movie/directors (sp/references ::director))
s/coll-of
with a reference field spec(require '[clojure.spec.alpha :as s])
(s/def :movie/directors (s/coll-of (s/or :id integer? :entity ::director)))
This is what the references
macro expands to.
To use the spec predicate function of your choice for defining reference collection fields, extend the following multimethods for them:
specomatic.registry/reference-coll-spec-form?
specomatic.registry/reference-coll-spec-form-referenced-etype
For a reference field, the default inverse field is a reference collection field of reference type :has-many
.
For a reference collection field, the default inverse field is a reference collection field of reference type :has-many-through
.
These can be overridden as described in the next section.
Generate the raw schema (only containing what is explicitely defined by the specs) by passing the namespaces with your specs to the schema
function:
(require '[specomatic.registry :as sr])
(sr/schema ['all 'the 'name 'spaces])
Generate the full schema containing defaults and inverse relational fields by passing the namespaces with your specs to the full-schema
function:
(require '[specomatic.registry :as sr])
(sr/full-schema ['all 'the 'name 'spaces]))
To override defaults and inverse fields, pass your overrides to the full-schema function as a second argument:
(require '[specomatic.registry :as sr])
(sr/full-schema ['all 'the 'name 'spaces] overrides)
The schema is a map of entity types to entity type definitions:
{::actor ...
::director ...
::movie ...
::review ...
::user ...}
Entity type definitions have the following shape:
{;; set of fields (keywords) that are part of the display name of the entity type.
:display-name-fields #{:movie/title}}
;; field definitions, see below
:field-defs ...
:id-field :movie/id
:required-fields #{:movie/title :movie/release-year}}
Simple (non-relational) field type definitions have the following shape:
{:kind ::sf/simple
;; the spec itself if it is a keyword, a description (from `s/describe`) if it is not
:dispatch 'string? }
Relational fields have the following shape:
{;; the inverse field in the schema (only defined for the entity type that does not own the relationship)
:inverse-of :review/movie
;; the kind of the field, ::sf/reference or ::sf/reference-coll (extensible)
:kind ::sf/reference-coll
;; the reference type of the field, can be :has-one, :has-many, :has-many-through
:reference-type :has-many
;; the target entity type of the reference
:target :schema/review
;; the reference field on the opposite side of the relationship, if available.
:via :review/movie}
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close