Complex Specs - english/speculation GitHub Wiki
While predicates form the foundation of Speculation, higher order specs allow describing complex data structures easily.
coll_of
Homogeneous collections can be spec'd with S.coll_of:
S.valid? S.coll_of(String), [1, 2, 3] # => false
S.valid? S.coll_of(Integer), [1, 2, 3] # => true
Additional constraints can also be provided:
spec = S.coll_of(Integer, :min_count => 2, :max_count => 4, :kind => Array, :distinct => true)
S.valid? spec, [1, 2, 3] # => true
S.valid? spec, [1, 2, 3, 4, 5] # => false
S.valid? spec, [1, 2, 3, 3] # => false
tuple
S.tuple is for collections with (potentially heterogeneous) positional 'fields'.
S.valid? S.tuple(Integer, Integer), [1, 2] # => true
S.valid? S.tuple(String, Integer, Float), ["foo", 1, 1.2] # => true
hash_of
S.hash_of describes Hashes with homogeneous key and value predicates:
S.valid?(S.hash_of(String, String), { "foo" => "bar" }) # => true
S.valid?(S.hash_of(String, String), { "foo" => 1 }) # => false
keys
When working with heterogeneous hashes where keys have special meaning, S.keys steps in.
S.keys can be given required and optional spec names. When conforming a Hash, presence of required
keys is checked first:
spec = S.keys(:req => [:"foo/bar"])
S.valid?(spec, { :"foo/bar" => "baz" }) # => true
S.valid?(spec, {}) # => false
Second, each key in the hash is conformed against a registered spec of the same name:
S.def :"foo/bar", Integer
spec = S.keys(:req => [:"foo/bar"])
S.valid?(spec, { :"foo/bar" => "baz" }) # => false
S.explain(spec, { :"foo/bar" => "baz" })
# => In: [:"foo/bar"] val: "baz" fails spec: :"foo/bar" at: [:"foo/bar"] predicate: [Integer, ["baz"]]
S.keys will conform a hash's key if a matching one is found in the spec registry even if it isn't
a required key for that spec:
S.def :"foo/bar", Integer
spec = S.keys
S.valid?(spec, { :"foo/bar" => "baz" }) # => false
S.explain(spec, { :"foo/bar" => "baz" })
# => In: [:"foo/bar"] val: "baz" fails spec: :"foo/bar" at: [:"foo/bar"] predicate: [Integer, ["baz"]]
This may seem odd at first. However, the reasoning behind this is to separate the specification of required keysets and values. See clojure.spec's rationale for more detail on this feature.
S.keys can conform both namespaced keys and non-namespaced keys in hashes.
To require a non-namespaced symbol, use the :req_un option.
spec = S.keys(:req_un => [:"foo/bar"])
S.valid?(spec, { :bar => "baz" })
See the API documentation for additional arguments.
every
S.every is just like S.coll_of, except that it doesn't validate every single element of
a collection. The number of elements that will be validated can be configured with
S.coll_check_limit, with the default being 101. This upper bound on the
number of items to be validated makes S.every suitable for validating very large collections.
spec = S.every(Integer)
value = [1, 2, 3]
S.valid? spec, value # => true
value = 1.upto(S.coll_check_limit).to_a
value[S.coll_check_limit] = "not-a-number"
S.valid? spec, value # => true
value[S.coll_check_limit - 1] = "not-a-number"
S.valid? spec, value # => false
every_kv
Just like S.every, except that S.every_kv will take separate key and value
predicates, therefore making it useful for Hashes. Since S.every_kv will only validate up to a
fixed number of items, it is suitable for validating large Hashes.
spec = S.every_kv(String, Integer)
value = { "foo" => 1 }
S.valid? spec, value # => true