Note that again valid? implicitly converts the predicate function into a spec. The spec library allows you to leverage all of the functions you already have - there is no special dictionary of predicates. Some more examples:
Spec names are always fully-qualified keywords. Generally, Clojure code should use keyword namespaces that are sufficiently unique such that they will not conflict with specs provided by other libraries. If you are writing a library for public use, spec namespaces should include the project name, url, or organization. Within a private organization, you may be able to use shorter names - the important thing is that they are sufficiently unique to avoid conflicts.
at - a path (a vector of keywords) indicating the location within the spec where the error occurred - the tags in the path correspond to any tagged part in a spec (the alternatives in an or or alt, the parts of a cat, the keys in a map, etc)
For the first reported error we can see that the value :foo did not satisfy the predicate string? at the path :name in the spec :domain/name-or-id. The second reported error is similar but fails on the :id path instead. The actual value is a keyword so neither is a match.
Clojure programs rely heavily on passing around maps of data. A common approach in other libraries is to describe each entity type, combining both the keys it contains and the structure of their values. Rather than define attribute (key+value) specifications in the scope of the entity (the map), specs assign meaning to individual attributes,then collect them into maps using set semantics (on the keys). This approach allows us to start assigning (and sharing)semantics at the attribute level across our libraries and applications.
For example, most Ring middleware functions modify the request or response map with unqualified keys. However, each middleware could instead use namespaced keys with registered semantics for those keys. The keys could then be checked for conformance, creating a system with greater opportunities for collaboration and consistency.
This registers a :acct/person spec with the required keys :acct/first-name, :acct/last-name, and :acct/email, with optional key :acct/phone. The map spec never specifies the value spec for the attributes, only what attributes are required or optional.
Much existing Clojure code does not use maps with namespaced keys and so keys can also specify :req-un and :opt-un for required and optional unqualified keys. These variants specify namespaced keys used to find their specification, but the map only checks for the unqualified version of the keys.
One common occurrence in Clojure is the use of "keyword args" where keyword keys and values are passed in a sequential data structure as options. Spec provides special support for this pattern with the regex op keys*. keys* has the same syntax and semantics as keys but can be embedded inside a sequential regex structure.
Sometimes it will be convenient to declare entity maps in parts, either because there are different sources for requirements on an entity map or because there is a common set of keys and variant-specific parts. The s/merge spec can be used to combine multiple s/keys specs into a single spec that combines their requirements. For example consider two keys specs that define common animal attributes and some dog-specific ones. The dog entity itself can be described as a merge of those two attribute sets:
One common occurrence in Clojure is to use maps as tagged entities and a special field that indicates the "type" of the map where type indicates a potentially open set of types, often with shared attributes across the types.
As previously discussed, the attributes for all types are well-specified using attributes stored in the registry by namespaced keyword. Attributes shared across entity types automatically gain shared semantics. However, we also want to be able to specify the required keys per entity type and for that spec provides multi-spec which leverages a multimethod to provide for the specification of an open set of entity types based on a type tag.
The multi-spec approach allows us to create an open system for spec validation, just like multimethods and protocols. New event types can be added later by just extending the event-type multimethod.
In this example, coll-of will match other (invalid) values as well (like [1.0] or [1.0 2.0 3.0 4.0]), so it is not a suitable choice - we want fixed fields. The choice between a regular expression and tuple here is to some degree a matter of taste, possibly informed by whether you expect either the tagged return values or error output to be better with one or the other.
By default map-of will validate but not conform keys because conformed keys might create key duplicates that would cause entries in the map to be overridden. If conformed keys are desired, pass the option :conform-keys true.
Sometimes sequential data is used to encode additional structure (typically new syntax, often used in macros). spec provides the standard regular expression operators to describe the structure of a sequential data value:
Consider an ingredient represented by a vector containing a quantity (number) and a unit (keyword). The spec for this data uses cat to specify the right components in the right order. Like predicates, regex operators are implicitly converted to specs when passed to functions like conform, valid?, etc.
Spec also defines one additional regex operator, &, which takes a regex operator and constrains it with one or more additional predicates. This can be used to create regular expressions with additional constraints that would otherwise require custom predicates. For example, consider wanting to match only sequences with an even number of strings:
When regex ops are combined, they describe a single sequence. If you need to spec a nested sequential collection,you must use an explicit call to specto start a new nested regex context. For example to describe a sequence like [:names ["a" "b"] :nums [1 2 3]],you need nested regular expressions to describe the inner sequential data:
Another option is to use s/assert within your code to assert that a value satisfies a spec. On success the value is returned and on failure an assertion error is thrown. By default assertion checking is off - this can be changed at the REPL with s/check-asserts or on startup by setting the system property clojure.spec.check-asserts=true.
In the success case, the parsed input is transformed into the desired shape for further processing. In the error case, we call explain-data to generate error message data. The explain data contains information about what expression failed to conform, the path to that expression in the specification, and the predicate it was attempting to match.
Spec has explicit support for this using fdef, which defines specifications for a function - the arguments and/or the return value spec, and optionally a function that can specify a relationship between args and return.
This function spec demonstrates a number of features. First the :args is a compound spec that describes the function arguments. This spec is invoked with the args in a list, as if they were passed to (apply fn (arg-list)). Because the args are sequential and the args are positional fields, they are almost always described using a regex op, like cat, alt, or *.
The second :args predicate takes as input the conformed result of the first predicate and verifies that start < end. The :ret spec indicates the return is also an integer. Finally, the :fn spec checks that the return value is >= start and < end.
The :ret spec uses fspec to declare that the returning function takes and returns a number. Even more interesting, the :fn spec can state a general property that relates the :args (where we know x) and the result we get from invoking the function returned from adder, namely that adding 0 to it should return x.
If we have a function deal that doles out some cards to the players we can spec that function to verify the arg and return value are both suitable data values. We can also specify a :fn spec to verify that the count of cards in the game before the deal equals the count of cards after the deal.
spec generators rely on the Clojure property testing library test.check. However, this dependency is dynamically loaded and you can use the parts of spec other than gen, exercise, and testing without declaring test.check as a runtime dependency. When you wish to use these parts of spec (typically during testing), you will need to declare a dev dependency on test.check.
So we can now start with a spec, extract a generator, and generate some data. All generated data will conform to the spec we used as a generator. For specs that have a conformed value different than the original value (anything using s/or, s/cat, s/alt, etc) it can be useful to see a set of generated samples plus the result of conforming that sample data.
For this we have exercise, which returns pairs of generated and conformed values for a spec. exercise by default produces 10 samples (like sample) but you can pass both functions a number indicating the number of samples to produce.
However, it is possible to go too far with refinement and make something that fails to produce any values. The test.check such-that that implements the refinement will throw an error if the refinement predicate cannot be resolved within a relatively small number of attempts. For example, consider trying to generate strings that happen to contain the word "hello":
Given enough time (maybe a lot of time), the generator probably would come up with a string like this, but the underlying such-that will make only 100 attempts to generate a value that passes the filter. This is a case where you will need to step in and provide a custom generator.
Building your own generator gives you the freedom to be either narrower and/or be more explicit about what values you want to generate. Alternately, custom generators can be used in cases where conformant values can be generated more efficiently than using a base predicate plus filtering. Spec does not trust custom generators and any values they produce will also be checked by their associated spec to guarantee they pass conformance.
c80f0f1006