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