[org.clojars.rkaippully/clj-annotations "1.1.0"]
Clojure is very versatile in manipulating complex data structures. This ease of use comes with a cost though. It is not easy to understand the structure of the data because of the dynamic nature of clojure. There have been various attempts to solve this problem, each with its pros and cons. A common pattern in all these solutions is to declare the structure or shape of the data and validate values against it.
But what if we could extend this solution for aspects other than data validation? For example, one could declaratively specify whether a partcular piece of data should be skipped from logging. This will help prevent leaking of sensitive data. Or one could specify whether a string value is supposed to be used in a case sensitive manner. This will be useful in search operations.
clj-annotations
provides mechanisms to define schemas which allow one to attach
arbitrary properties to attributes of the schema. These properties can then be searched,
retrieved and used in various contexts.
The first step in using clj-annotations
is to define a schema using the defschema
macro. For example, here is a schema defining a data structure describing a person.
user=> (require '[clj-annotations.core :as ann :refer [defschema]])
user=> (defn non-blank
[s _]
(if (clojure.string/blank? s)
{:errors ["Expected a non-blank value"]}
{}))
user=> (defschema person
:name "Person"
:description "Represents a person in the system"
:attributes
{:id
{:type :number
:required true
:label "Identifier"}
:email
{:type :string
:required true
:label "Email Address"
:validity non-blank}})
This defines a schema for person
with a name and description. It has two attributes -
:id
and :email
. These attributes have some properties associated with them - :type
,
:required
, and :label
. The :email
attribute has an additional property named
:validity
.
The properties can be any key-value pairs. They do not have any inherent semantics imposed
by defschema
. These properties can be used in different ways depending on the use
case. For example, a validation library based on this schema may use :type
, :required
,
and :validity
properties to check if a value conforms to the schema. A UI library may
use the :label
property to display these attributes in the UI.
There are two ways to access information from the schema. The first is to use
get-annotations
. This is a swiss army knife to extract, the entire schema, properties of
an attribute, value of a specific property with an optional "not found" override.
user=> (ann/get-annotations person)
{:id {:type :number, :required true, :label "Identifier"}, :email {:type :string, :required true, :label "Email Address", :validity #object[user$non_blank 0x457d169a "user$non_blank@457d169a"]}}
user=> (ann/get-annotations person :id)
{:type :number, :required true, :label "Identifier"}
user=> (ann/get-annotations person :id :label)
"Identifier"
user=> (ann/get-annotations person :id :nullable?)
nil
user=> (ann/get-annotations person :id :nullable? :not-found)
:not-found
Another use case is to find all attributes from a schema having a specific value for a
property. For example, how do we find all attributes with :type
property set to
:string
? Its possible to do that with scan-attributes
function.
user=> (ann/scan-attributes person :type #(= % :string))
#{[:email]}
In many practical scenarios, you might want to build schemas based on composable
parts. For example, one might define a person schema as above with id and name
attributes and then might want to use them in employee and customer schemas. This can
easily be done with the :include
argument to defschema
.
user=> (defn company-domain
[s _]
(if (clojure.string/ends-with? s "@example.com")
{}
{:errors ["Email must have example.com domain"]}))
user=> (defschema employee
:include person
:attributes
{:email
{:validity company-domain}
:employeeId
{:type :number
:required true}})
The employee schema has all the attributes and properties from person schema except that
the :validity
property of :email
attribute is overridden. It also has an additional
attribute :employeeId
.
The :include
argument could point to a single schema or a sequence of schemas. In the
latter case, the schema attributes will be merged from left to right. In case of a
conflict in any properties, the last one in the sequence will win.
clj-annotations
includes a validation framework based on the schema definitions. Let us
get started with an example:
(require '[clj-annotations.validation :as v])
user=> (v/validate-object employee {})
({:kind :missing-required-attribute, :message "Missing required attribute", :path "/email", :level :error} {:kind :missing-required-attribute, :message "Missing required attribute", :path "/employeeId", :level :error} {:kind :missing-required-attribute, :message "Missing required attribute", :path "/id", :level :error})
user=> (v/validate-object employee {:email "test@google.com" :employeeId true :id "foo"})
({:kind :validation-failure, :message "Email must have example.com domain", :path "/email", :level :error} {:kind :type-mismatch, :message "Expected a number but found boolean", :path "/employeeId", :level :error} {:kind :type-mismatch, :message "Expected a number but found string", :path "/id", :level :error})
user=> (v/validate-object employee {:email "test@example.com" :employeeId 10042 :id 42})
()
The validation uses the :type
, :required
, and :validity
properties of the attributes
to check whether an object conforms to the schema.
The validation logic is highly customizable. It accepts an options map with the below mentioned keys to tune the validation behavior.
:make-result
- a function that takes the schema, the attribute value, the attribute
path, and a validation result and returns a validation result as a vector. See
make-validation-result
for an example.:type-checks
- a map from type names to functions validating the conformance of a
value to those types. See type-checks
for an example.:fail-on-unsupported-attributes?
- if true an error will be reported if the obj
contains an attribute that is not defined in schema
Copyright © 2019 Raghu Kaippully rkaippully@gmail.com
Distributed under the Mozilla Public License version 2.0.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close