
Hodur is a descriptive domain modeling approach and related collection of libraries for Clojure.
By using Hodur you can define your domain model as data, parse and validate it, and then either consume your model via an API making your apps respond to the defined model or use one of the many plugins to help you achieve mechanical, repetitive results faster and in a purely functional manner.
This repo is the Hodur core engine that parses your model definitions and exposes a meta-API around it. For a list of what you can do once your model is in Hodur check here.
For a deeper insight into the motivations behind Hodur, check the motivation doc.
Hodur has a highly modular architecture. Hodur Engine (this project) is always required as it provides the meta-database functions and APIs consumed by plugins.
Add hodur-engine as a dependency in your deps.edn file:
{:deps {hodur/engine {:mvn/version "0.1.7"}}}
Either require Hodur as part of your ns definition or directly:
(require '[hodur-engine.core :as hodur])
In order to initialize an atom representing the meta-database of
your model call function hodur/init-schema:
(def meta-db (hodur/init-schema
'[Person
[^String first-name
^String last-name]]))
In the above example, we are defining a Person entity with a
first-name and a last-name both tagged as the scalar type
String.
Alternatively, Hodur can be initialized by raw EDN paths or from your
classpath using a File (i.e. clojure.java.io/resource):
(def meta-db (-> "schemas/person.edn"
io/resource
hodur/init-path))
Hodur's usefulness can be seen when used in conjunction with several
plugins that take care of the mechanical aspects of your
application. For the sake of getting started, we are also adding
hodur-datomic-schema, a plugin that creates Datomic Schemas out of
your model to the deps.edn file:
{:deps {hodur/engine {:mvn/version "0.1.5"}
hodur/datomic-schema {:mvn/version "0.1.0"}}}
You should require it any way you see fit:
(require '[hodur-datomic-schema.core :as hodur-datomic])
Let's expand our Person model above by "tagging" the Person entity
for Datomic. You can read more about the concept of tagging for
plugins in the sessions below but, in short, this is the way we, model
designers, use to specify which entities we want to be exposed to
which plugins.
(def meta-db (hodur/init-schema
'[^{:datomic/tag-recursive true}
Person
[^String first-name
^String last-name]]))
The hodur-datomic-schema plugin exposes a function called schema
that generates your model as a Datomic schema payload:
(def datomic-schema (hodur-datomic/schema meta-db))
When you inspect datomic-schema, this is what you have:
[{:db/ident :person/first-name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :person/last-name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}]
Assuming the Datomic client API is bound to datomic, and your
connection to the Database cluster is bound to db-conn, you can
simply transact your schema like this:
(datomic/transact db-conn {:tx-data datomic-schema})
Several other plugins are available and you can also write your own. The following sections detail not only how to model your domain but also these options in further detail.
For visualization/documentation:
Schemas for persistent systems:
Schemas for inbound interfaces:
Schemas for validation/data-generation:
Experimental adapters:
In Hodur Entities are the highest level representation of a model. An entity has any number of fields that qualify such entity.
For instance, an employee entity may have an employee-number, a
name and a salary as three distinct fields. An entity can have
as many fields as you need.
Fields can have any number of parameters. Parameters qualify the
field. For instance, a hypothetical height field could have a
parameter specifying which unit to use when interpreting this
field (CENTIMETERS or FEET for instance).
Hodur can be initialized by either a series of EDN files (using
function init-path) or vectors (using function init-schema).
A domain model is a vector of tuples of symbols and sub-vectors. The symbols represent entity names and the sub-vectors represent fields.
An Employee entity with name and salary as fields could be
defined as:
[Employee
[name
salary]]
With this setup we are not specifying what name and salary are. It
might be a good idea to do something like this:
[Employee
[^String name
^Float salary]]
Types are defined using a meta payload to the symbol that represents the field or the parameter. You can read more about scalar types below.
Types can also be represented by the more explicit meta object:
[Employee
[^{:type String} name
^{:type Float} salary]]
Entities are also considered types therefore, if an Employee has a
supervisor who's also an Employee you might write:
[Employee
[^String name
^Float salary
^Employee supervisor]]
You could want a height field that can return the employee's height
in a particular unit:
[Employee
[^String name
^Float salary
^Employee supervisor
^Integer height [^Unit unit]]
^{:enum true}
Unit
[CENTIMETERS FEET]]
There's quite a bit going on here that you can explore in detail in
the sections below. But here's a summary. First we've added the field
height to the Employee entity. It returns an Integer and it also
expects a parameter called unit of the type Unit.
We've defined Unit separately as an enum (you can see more details
in the sections below). Unit can be either CENTIMETER or FEET.
Hodur has five primitive scalar types that can be composed with your
own entities to design your model. Four of them are quite
self-explanatory: String, Float, Integer and Boolean.
The last two are highly opinionated and are DateTime and ID.
Hodur's plugins must have reasonable defaults to represent each one of these scalar types. Plugins may also expose finer grained controls to manage type precision (for instance 32bit integers vs 64bit integers).
One employee may have a series of reportees. This kind of cardinality
is defined with the :cardinality meta marker:
[Employee
[^{:type String} name
^{:type Float} salary
^{:type Employee
:cardinality [0 n]} reportees]]
In this example we are telling Hodur that reportees can be anywhere
from 0 employees to n employees.
You can be as specific as you want. A cardinality of [4] means
exactly 4 entries; [3 5] means 3 to 5. If :cardinality is
unspecified, it's assumed as [1].
Fields and parameters are required by default. In other words, plugins
must implement mechanisms to avoid null problems if a field or
parameter is mandatory.
If you want to make a field optional, use the :optional meta marker
on the field:
[Employee
[^{:type String} first-name
^{:type String
:optional true} middle-name
^{:type String} last-name]]
If you want to make a parameter optional, use the :optional meta
marker on the parameter:
[QueryRoot
[employees [^{:type String
:optional true} search-term]]]
A common pattern is to make a parameter optional while also assigning
a default value to it with :default:
[QueryRoot
[employees-by-location [^{:type String
:optional true
:default "HQ"} location]]]
Entities can be marked as :interface which can be used by plugins
that explore such a concept. Entities that implement an interface use
the :implements marker to indicate which interface(s) they
implement:
[^{:interface true}
Pet
[^String name]
^{:implements Pet}
Dog
[^String bark]
^{:implements Pet}
Cat
[^String mewow]]
The :implements marker also accepts a vector with a series of
interfaces that the entity implements.
Enums are special kind of entities. They can assume one of the values defined as fields. Enum fields do not support parameters.
Enums are marked with :enum:
[Employee
[^String name
^Float salary
^Employee supervisor
^Integer height [^Unit unit]]
^{:enum true}
Unit
[CENTIMETERS FEET]]
Unions are very similar to interfaces, but they don't get to specify any common fields between the types. They are useful when a certain field or parameter can be any one of the specified entities within the union.
In the following example the search field of the QueryRoot entity
returns a collection of SearchItem which are unions of Employee
and Company:
[Employee
[^String name
^Float salary]
Company
[^String address]
^{:union true}
SearchItem
[Employee Company]
QueryRoot
[^{:type SearchItem
:cardinality [0 n]}
search [^String term]]]
Entities, fields, and parameters can all be documented by using marker
:doc.
[^{:doc "A representation of an Employee"}
Employee
[^{:type String
:doc "The employee's name"} name
^{:type Float
:doc "The employee's salary"} salary]]
Entities, fields, and parameters can additionally be marked for
deprecation by using the marker :deprecation. Deprecation is a
string that describes the reasons for the deprecation as well as
points to alternatives.
[^{:doc "A representation of an Employee"}
Employee
[^{:type String
:doc "The employee's name"}
name
^{:type Float
:doc "The employee's salary"}
salary
^{:type Float
:deprecation "This field will be fully removed by December. Please use `name` instead."}
first-name]]
In general, plugins should only process entities, fields, and
parameters that have been tagged for them. I.e. a datomic plugin
will have a particular tagging marker such as :datomic/tag that
needs to be added to each symbol you want the plugin to process.
The following example tags Employee and its fields first-name and
last-name for the datomic plugin.
[^{:datomic/tag true}
Employee
[^{:type String
:datomic/tag true} first-name
^{:type String
:datomic/tag} last-name]
Project
[^{:type String} name]]
Tagging can be very repetitive so Hodur provides features for tagging in a recursive fashion. The example above could be rewritten with:
[^{:datomic/tag-recursive true}
Employee
[^{:type String} first-name
^{:type String} last-name]
Project
[^{:type String} name]]
This kind of scenario is ideal for entities that have several fields and/or parameters.
The marker :<plugin>/tag-recursive can also have filters such as
:only and :except.
The following example will only tag the Employee entity and the
fields first-name and last-name:
[^{:datomic/tag-recursive {:only [Employee first-name last-name]}}
Employee
[^{:type String} first-name
^{:type String} middle-name
^{:type String} last-name]]
The following example would achieve the same result as above but by
tagging everything but middle-name:
[^{:datomic/tag-recursive {:except [middle-name]}}
Employee
[^{:type String} first-name
^{:type String} middle-name
^{:type String} last-name]]
Some times you just want to tag everything you are sending as part of
a group of entities. In these scenarios you need to first name the
very first symbol of your group default and then mark it. Hodur will
apply whatever you mark on default to all items in the group.
In the following example, Hodur will tag everything for the datomic
plugin:
[^{:datomic/tag true}
default
Employee
[^{:type String} first-name
^{:type String} last-name]
Project
[^{:type String} name]]
The special default symbol can also be used to carry other markers
down into the group's items but the general usage is for tagging.
Hodur does not care about naming conventions. However, it does delegate naming choices fully to plugins. The way Hodur achieves this is by internally converting whatever naming convention was used in the symbols into several options. This is done by leveraging [[https://github.com/qerub/camel-snake-kebab][camel-snake-kebab]].
Once your model gets parsed, Hodur will retain an in-memory meta-database that can be queried by either plugins or your implementation proper.
The API is exposed as a DataScript API atom and DataScript proper is a dependency of Hodur. Therefore, you can require DataScript and use its query directly.
The example below uses both pull and a Datalog query to return all
the items which are marked with a :datomic/tag.
(require '[datascript.core :as d])
(d/q '[:find [(pull ?e [*]) ...]
:where
[?e :datomic/tag true]]
@c)
Attributes are named with qualified keywords in four different categories:
:type/...: all entities (AKA types):field/...: all fields:param/...: all parameters<plugin>/...: plugin names should qualify keywords (see
:datomic/tag above)For entities, fields, and parameters the provided name in the model is
exposed as either :type/name, :field/name, and
:param/name. Additionally, Hodur generates indexes with:
/kebab-case-name/PascalCaseName/camelCaseName/snake_case_nameEntities have Boolean attributes for interfaces, enums and unions:
:type/interface, :type/enum, and :type/union respectively.
TBD: :field/type and :field/parent (:field/_parent) :field/cardinality
TBD: :param/type and :param/parent (:param/_parent) :param/cardinality
TBD: choose naming convention, use d/q, filter by /tag, do your thing
If you find a bug, submit a GitHub issue
This project is looking for team members who can help this project succeed! If you are interested in becoming a team member please open an issue.
Copyright © 2018 Tiago Luchini
Distributed under the MIT License (see LICENSE).
Can you improve this documentation? These fine people already did:
Tiago Luchini & luchiniatworkEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |