speculation 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1288 @@
1
+ # frozen_string_literal: true
2
+ # See https://clojure.org/guides/spec (as of 2017-18-02)
3
+ # Output generated by https://github.com/JoshCheek/seeing_is_believing
4
+
5
+ require "set"
6
+ require "date"
7
+ require "speculation"
8
+
9
+ S = Speculation
10
+ extend S::NamespacedSymbols
11
+
12
+ ## Predicates
13
+
14
+ # Each spec describes a set of allowed values. There are several ways to build
15
+ # specs and all of them can be composed to build more sophisticated specs.
16
+
17
+ # A Ruby proc that takes a single argument and returns a truthy value is a
18
+ # valid predicate spec. We can check whether a particular data value conforms
19
+ # to a spec using conform:
20
+
21
+ S.conform :even?.to_proc, 1000 # => 1000
22
+
23
+ # The conform function takes something that can be a spec and a data value.
24
+ # Here we are passing a predicate which is implicitly converted into a spec.
25
+ # The return value is "conformed". Here, the conformed value is the same as the
26
+ # original value - we’ll see later where that starts to deviate. If the value
27
+ # does not conform to the spec, the special value :"Speculation/invalid" is
28
+ # returned.
29
+
30
+ # If you don’t want to use the conformed value or check for
31
+ # :"Speculation/invalid", the helper valid? can be used instead to return a
32
+ # boolean.
33
+
34
+ S.valid? :even?.to_proc, 10 # => true
35
+
36
+ # Note that again valid? implicitly converts the predicate function into a
37
+ # spec. The spec library allows you to leverage all of the functions you
38
+ # already have - there is no special dictionary of predicates. Some more
39
+ # examples:
40
+
41
+ S.valid? :nil?.to_proc, nil # => true
42
+ S.valid? ->(x) { x.is_a?(String) }, "abc" # => true
43
+ S.valid? ->(x) { x > 5 }, 10 # => true
44
+ S.valid? ->(x) { x > 5 }, 0 # => false
45
+
46
+ # Regexps, Classes and Modules can be used as predicates.
47
+
48
+ S.valid? /^\d+$/, "123" # => true
49
+ S.valid? String, "abc" # => true
50
+ S.valid? Enumerable, [1, 2, 3] # => true
51
+ S.valid? Date, Date.new # => true
52
+
53
+ # Sets can also be used as predicates that match one or more literal values:
54
+
55
+ S.valid? Set[:club, :diamond, :heart, :spade], :club # => true
56
+ S.valid? Set[:club, :diamond, :heart, :spade], 42 # => false
57
+ S.valid? Set[42], 42 # => true
58
+
59
+ ## Registry
60
+
61
+ # Until now, we’ve been using specs directly. However, spec provides a central
62
+ # registry for globally declaring reusable specs. The registry associates a
63
+ # namespaced symbol with a specification. The use of namespaces ensures that
64
+ # we can define reusable non-conflicting specs across libraries or
65
+ # applications.
66
+
67
+ # Specs are registered using def. It’s up to you to register the specification
68
+ # in a namespace that makes sense (typically a namespace you control).
69
+
70
+ S.def ns(:date), Date # => :"Object/date"
71
+ S.def ns(:suit), Set[:club, :diamond, :heart, :spade] # => :"Object/suit"
72
+
73
+ # A registered spec identifier can be used in place of a spec definition in the
74
+ # operations we’ve seen so far - conform and valid?.
75
+
76
+ S.valid? ns(:date), Date.new # => true
77
+ S.conform ns(:suit), :club # => :club
78
+
79
+ # You will see later that registered specs can (and should) be used anywhere we
80
+ # compose specs.
81
+
82
+ ## Composing predicates
83
+
84
+ # The simplest way to compose specs is with and and or. Let’s create a spec
85
+ # that combines several predicates into a composite spec with S.and:
86
+
87
+ S.def ns(:big_even), S.and(Integer, :even?.to_proc, ->(x) { x > 1000 })
88
+ S.valid? ns(:big_even), :foo # => false
89
+ S.valid? ns(:big_even), 10 # => false
90
+ S.valid? ns(:big_even), 100000 # => true
91
+
92
+ # We can also use S.or to specify two alternatives:
93
+
94
+ S.def ns(:name_or_id), S.or(:name => String, :id => Integer)
95
+ S.valid? ns(:name_or_id), "abc" # => true
96
+ S.valid? ns(:name_or_id), 100 # => true
97
+ S.valid? ns(:name_or_id), :foo # => false
98
+
99
+ # This or spec is the first case we’ve seen that involves a choice during
100
+ # validity checking. Each choice is annotated with a tag (here, between :name
101
+ # and :id) and those tags give the branches names that can be used to
102
+ # understand or enrich the data returned from conform and other spec functions.
103
+
104
+ # When an or is conformed, it returns an array with the tag name and conformed
105
+ # value:
106
+
107
+ S.conform ns(:name_or_id), "abc" # => [:name, "abc"]
108
+ S.conform ns(:name_or_id), 100 # => [:id, 100]
109
+
110
+ # Many predicates that check an instance’s type do not allow nil as a valid
111
+ # value (String, ->(x) { x.even? }, /foo/, etc). To include nil as a valid
112
+ # value, use the provided function nilable to make a spec:
113
+
114
+ S.valid? String, nil # => false
115
+ S.valid? S.nilable(String), nil # => true
116
+
117
+ ## Explain
118
+
119
+ # explain is another high-level operation in spec that can be used to report
120
+ # (to STDOUT) why a value does not conform to a spec. Let’s see what explain
121
+ # says about some non-conforming examples we’ve seen so far.
122
+
123
+ S.explain ns(:suit), 42
124
+ # >> val: 42 fails spec: :"Object/suit" predicate: [#<Set: {:club, :diamond, :heart, :spade}>, [42]]
125
+
126
+ S.explain ns(:big_even), 5
127
+ # >> val: 5 fails spec: :"Object/big_even" predicate: [#<Proc:0x007fb69b1908f8(&:even?)>, [5]]
128
+
129
+ S.explain ns(:name_or_id), :foo
130
+ # >> val: :foo fails spec: :"Object/name_or_id" at: [:name] predicate: [String, [:foo]]
131
+ # >> val: :foo fails spec: :"Object/name_or_id" at: [:id] predicate: [Integer, [:foo]]
132
+
133
+ # Let’s examine the output of the final example more closely. First note that
134
+ # there are two errors being reported - spec will evaluate all possible
135
+ # alternatives and report errors on every path. The parts of each error are:
136
+
137
+ # - val - the value in the user’s input that does not match
138
+ # - spec - the spec that was being evaluated
139
+ # - at - a path (an array of symbols) indicating the location within the spec
140
+ # where the error occurred - the tags in the path correspond to any tagged part
141
+ # in a spec (the alternatives in an or or alt, the parts of a cat, the keys in
142
+ # a map, etc)
143
+ # - predicate - the actual predicate that was not satsified by val
144
+ # - in - the key path through a nested data val to the failing value. In this
145
+ # example, the top-level value is the one that is failing so this is
146
+ # essentially an empty path and is omitted.
147
+ # - For the first reported error we can see that the value :foo did not satisfy
148
+ # the predicate String at the path :name in the spec ns(:name-or-id). The second
149
+ # reported error is similar but fails on the :id path instead. The actual value
150
+ # is a Symbol so neither is a match.
151
+
152
+ # In addition to explain, you can use explain_str to receive the error messages
153
+ # as a string or explain_data to receive the errors as data.
154
+
155
+ S.explain_data ns(:name_or_id), :foo
156
+ # => {:"Speculation/problems"=>
157
+ # [{:path=>[:name],
158
+ # :val=>:foo,
159
+ # :via=>[:"Object/name_or_id"],
160
+ # :in=>[],
161
+ # :pred=>[String, [:foo]]},
162
+ # {:path=>[:id],
163
+ # :val=>:foo,
164
+ # :via=>[:"Object/name_or_id"],
165
+ # :in=>[],
166
+ # :pred=>[Integer, [:foo]]}]}
167
+
168
+ ## Entity hashes
169
+
170
+ # Ruby programs rely heavily on passing around hashes of data. (That may not be
171
+ # true...) A common approach in other libraries is to describe each entity
172
+ # type, combining both the keys it contains and the structure of their values.
173
+ # Rather than define attribute (key+value) specifications in the scope of the
174
+ # entity (the hash), specs assign meaning to individual attributes, then
175
+ # collect them into shahes using set semantics (on the keys). This approach
176
+ # allows us to start assigning (and sharing) semantics at the attribute level
177
+ # across our libraries and applications.
178
+
179
+ # This statement isn't quite true for Ruby's Ring equivalent, Rack:
180
+ # ~~For example, most Ring middleware functions modify the request or response
181
+ # map with unqualified keys. However, each middleware could instead use
182
+ # namespaced keys with registered semantics for those keys. The keys could then
183
+ # be checked for conformance, creating a system with greater opportunities for
184
+ # collaboration and consistency.~~
185
+
186
+ # Entity maps in spec are defined with keys:
187
+
188
+ email_regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/
189
+
190
+ S.def ns(:email_type), S.and(String, email_regex)
191
+
192
+ S.def ns(:acctid), Integer
193
+ S.def ns(:first_name), String
194
+ S.def ns(:last_name), String
195
+ S.def ns(:email), ns(:email_type)
196
+
197
+ S.def ns(:person), S.keys(:req => [ns(:first_name), ns(:last_name), ns(:email)], :opt => [ns(:phone)])
198
+
199
+ # This registers a ns(:person) spec with the required keys ns(:first-name),
200
+ # ns(:last_name), and ns(:email), with optional key ns(:phone). The hash spec
201
+ # never specifies the value spec for the attributes, only what attributes are
202
+ # required or optional.
203
+
204
+ # When conformance is checked on a hash, it does two things - checking that the
205
+ # required attributes are included, and checking that every registered key has
206
+ # a conforming value. We’ll see later where optional attributes can be useful.
207
+ # Also note that ALL attributes are checked via keys, not just those listed in
208
+ # the :req and :opt keys. Thus a bare S.keys is valid and will check all
209
+ # attributes of a map without checking which keys are required or optional.
210
+
211
+ S.valid? ns(:person), ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "elon@example.com" # => true
212
+
213
+ # Fails required key check
214
+ S.explain ns(:person), ns(:first_name) => "Elon"
215
+ # >> val: {:"Object/first_name"=>"Elon"} fails spec: :"Object/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
216
+ # >> val: {:"Object/first_name"=>"Elon"} fails spec: :"Object/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
217
+
218
+ # Fails attribute conformance
219
+ S.explain ns(:person), ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "n/a"
220
+ # >> In: [:"Object/email"] val: "n/a" fails spec: :"Object/email_type" at: [:"Object/email"] predicate: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/, ["n/a"]]
221
+
222
+ # Let’s take a moment to examine the explain error output on that final example:
223
+
224
+ # - in - the path within the data to the failing value (here, a key in the person instance)
225
+ # - val - the failing value, here "n/a"
226
+ # - spec - the spec that failed, here :my.domain/email
227
+ # - at - the path in the spec where the failing value is located
228
+ # - predicate - the predicate that failed, here (re-matches email-regex %)
229
+
230
+ # Much existing Ruby code does not use hashes with namespaced keys and so keys
231
+ # can also specify :req_un and :opt_un for required and optional unqualified
232
+ # keys. These variants specify namespaced keys used to find their
233
+ # specification, but the map only checks for the unqualified version of the
234
+ # keys.
235
+
236
+ # Let’s consider a person map that uses unqualified keys but checks conformance
237
+ # against the namespaced specs we registered earlier:
238
+
239
+ S.def :"unq/person", S.keys(:req_un => [ns(:first_name), ns(:last_name), ns(:email)],
240
+ :opt_un => [ns(:phone)])
241
+
242
+ S.conform :"unq/person", :first_name => "Elon", :last_name => "Musk", :email => "elon@example.com"
243
+ # => {:first_name=>"Elon", :last_name=>"Musk", :email=>"elon@example.com"}
244
+
245
+ S.explain :"unq/person", :first_name => "Elon", :last_name => "Musk", :email => "n/a"
246
+ # >> In: [:email] val: "n/a" fails spec: :"Object/email_type" at: [:email] predicate: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/, ["n/a"]]
247
+
248
+ S.explain :"unq/person", :first_name => "Elon"
249
+ # >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
250
+ # >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
251
+
252
+ # Unqualified keys can also be used to validate record attributes # TODO for objects/structs
253
+ # Keyword args keys* - don't support
254
+
255
+ # Sometimes it will be convenient to declare entity maps in parts, either
256
+ # because there are different sources for requirements on an entity map or
257
+ # because there is a common set of keys and variant-specific parts. The S.merge
258
+ # spec can be used to combine multiple S.keys specs into a single spec that
259
+ # combines their requirements. For example consider two keys specs that define
260
+ # common animal attributes and some dog-specific ones. The dog entity itself
261
+ # can be described as a merge of those two attribute sets:
262
+
263
+ S.def :"animal/kind", String
264
+ S.def :"animal/says", String
265
+ S.def :"animal/common", S.keys(:req => [:"animal/kind", :"animal/says"])
266
+ S.def :"dog/tail?", ns(S, :boolean)
267
+ S.def :"dog/breed", String
268
+ S.def :"animal/dog", S.merge(:"animal/common", S.keys(:req => [:"dog/tail?", :"dog/breed"]))
269
+
270
+ S.valid? :"animal/dog", :"animal/kind" => "dog", :"animal/says" => "woof", :"dog/tail?" => true, :"dog/breed" => "retriever" # => true
271
+
272
+ ## Multi-spec - don't support
273
+
274
+ ## Collections
275
+
276
+ # A few helpers are provided for other special collection cases - coll_of,
277
+ # tuple, and hash_of.
278
+
279
+ # For the special case of a homogenous collection of arbitrary size, you can
280
+ # use coll_of to specify a collection of elements satisfying a predicate.
281
+
282
+ S.conform S.coll_of(Symbol), [:a, :b, :c] # => [:a, :b, :c]
283
+ S.conform S.coll_of(Numeric), Set[5, 10, 2] # => #<Set: {5, 10, 2}>
284
+
285
+ # Additionally, coll-of can be passed a number of keyword arg options:
286
+
287
+ # :kind - a predicate or spec that the incoming collection must satisfy, such as `Array`
288
+ # :count - specifies exact expected count
289
+ # :min_count, :max_count - checks that collection has `count.between?(min_count, max_count)`
290
+ # :distinct - checks that all elements are distinct
291
+ # :into - one of [], {}, or Set[] for output conformed value. If :into is not specified, the input collection type will be used.
292
+
293
+ # Following is an example utilizing some of these options to spec an array
294
+ # containing three distinct numbers conformed as a set and some of the errors
295
+ # for different kinds of invalid values:
296
+
297
+ S.def ns(:vnum3), S.coll_of(Numeric, :kind => Array, :count => 3, :distinct => true, :into => Set[])
298
+ S.conform ns(:vnum3), [1, 2, 3] # => #<Set: {1, 2, 3}>
299
+ S.explain ns(:vnum3), Set[1, 2, 3] # not an array
300
+ # >> val: #<Set: {1, 2, 3}> fails spec: :"Object/vnum3" predicate: [Array, [#<Set: {1, 2, 3}>]]
301
+ S.explain ns(:vnum3), [1, 1, 1] # not distinct
302
+ # >> val: [1, 1, 1] fails spec: :"Object/vnum3" predicate: [#<Method: Speculation::Utils.distinct?>, [[1, 1, 1]]]
303
+ S.explain ns(:vnum3), [1, 2, :a] # not a number
304
+ # >> In: [2] val: :a fails spec: :"Object/vnum3" predicate: [Numeric, [:a]]
305
+
306
+ # NOTE: Both coll-of and map-of will conform all of their elements, which may
307
+ # make them unsuitable for large collections. In that case, consider every or
308
+ # for maps every-kv.
309
+
310
+ # While coll-of is good for homogenous collections of any size, another case is
311
+ # a fixed-size positional collection with fields of known type at different
312
+ # positions. For that we have tuple.
313
+
314
+ S.def ns(:point), S.tuple(Float, Float, Float)
315
+ S.conform ns(:point), [1.5, 2.5, -0.5] # => [1.5, 2.5, -0.5]
316
+
317
+ # Note that in this case of a "point" structure with x/y/z values we actually
318
+ # had a choice of three possible specs:
319
+
320
+ # - Regular expression - S.cat :x => Float, :y => Float, :z => Float
321
+ # - Allows for matching nested structure (not needed here)
322
+ # - Conforms to hash with named keys based on the cat tags
323
+ # - Collection - S.coll_of Float
324
+ # - Designed for arbitrary size homogenous collections
325
+ # - Conforms to an array of the values
326
+ # - Tuple - S.tuple Float, Float, Float
327
+ # - Designed for fixed size with known positional "fields"
328
+ # - Conforms to an array of the values
329
+
330
+ # In this example, coll_of will match other (invalid) values as well (like
331
+ # [1.0] or [1.0 2.0 3.0 4.0]), so it is not a suitable choice - we want fixed
332
+ # fields. The choice between a regular expression and tuple here is to some
333
+ # degree a matter of taste, possibly informed by whether you expect either the
334
+ # tagged return values or error output to be better with one or the other.
335
+
336
+ # In addition to the support for information hashes via keys, spec also
337
+ # provides hash_of for maps with homogenous key and value predicates.
338
+
339
+ S.def ns(:scores), S.hash_of(String, Integer)
340
+ S.conform ns(:scores), "Sally" => 1000, "Joe" => 300 # => {"Sally"=>1000, "Joe"=>300}
341
+
342
+ # By default hash_of will validate but not conform keys because conformed keys
343
+ # might create key duplicates that would cause entries in the map to be
344
+ # overridden. If conformed keys are desired, pass the option
345
+ # `:conform_keys => # true`.
346
+
347
+ # You can also use the various count-related options on hash_of that you have
348
+ # with coll_of.
349
+
350
+ ## Sequences
351
+
352
+ # Sometimes sequential data is used to encode additional structure. spec
353
+ # provides the standard regular expression operators to describe the structure
354
+ # of a sequential data value:
355
+
356
+ # - cat - concatenation of predicates/patterns
357
+ # - alt - choice among alternative predicates/patterns
358
+ # - zero_or_more - 0 or more of a predicate/pattern
359
+ # - one_or_more - 1 or more of a predicate/pattern
360
+ # - zero_or_one - 0 or 1 of a predicate/pattern
361
+
362
+ # Like or, both cat and alt tag their "parts" - these tags are then used in the
363
+ # conformed value to identify what was matched, to report errors, and more.
364
+
365
+ # Consider an ingredient represented by an array containing a quantity (number)
366
+ # and a unit (symbol). The spec for this data uses cat to specify the right
367
+ # components in the right order. Like predicates, regex operators are
368
+ # implicitly converted to specs when passed to functions like conform, valid?,
369
+ # etc.
370
+
371
+ S.def ns(:ingredient), S.cat(:quantity => Numeric, :unit => Symbol)
372
+ S.conform ns(:ingredient), [2, :teaspoon] # => {:quantity=>2, :unit=>:teaspoon}
373
+
374
+ # The data is conformed as a hash with the tags as keys. We can use explain to
375
+ # examine non-conforming data.
376
+
377
+ # pass string for unit instead of keyword
378
+ S.explain ns(:ingredient), [11, "peaches"]
379
+ # >> In: [1] val: "peaches" fails spec: :"Object/ingredient" at: [:unit] predicate: [Symbol, ["peaches"]]
380
+
381
+ # leave out the unit
382
+ S.explain ns(:ingredient), [2]
383
+ # >> val: [] fails spec: :"Object/ingredient" at: [:unit] predicate: [Symbol, []], "Insufficient input"
384
+
385
+ # Let’s now see the various occurence operators zero_or_more, one_or_more, and zero_or_one:
386
+
387
+ S.def ns(:seq_of_symbols), S.zero_or_more(Symbol)
388
+ S.conform ns(:seq_of_symbols), [:a, :b, :c] # => [:a, :b, :c]
389
+ S.explain ns(:seq_of_symbols), [10, 20]
390
+ # >> In: [0] val: 10 fails spec: :"Object/seq_of_symbols" predicate: [Symbol, [10]]
391
+
392
+ S.def ns(:odds_then_maybe_even), S.cat(:odds => S.one_or_more(:odd?.to_proc),
393
+ :even => S.zero_or_one(:even?.to_proc))
394
+ S.conform ns(:odds_then_maybe_even), [1, 3, 5, 100] # => {:odds=>[1, 3, 5], :even=>100}
395
+
396
+ S.conform ns(:odds_then_maybe_even), [1] # => {:odds=>[1]}
397
+ S.explain ns(:odds_then_maybe_even), [100]
398
+ # >> In: [0] val: 100 fails spec: :"Object/odds_then_maybe_even" at: [:odds] predicate: [#<Proc:0x007fb69a186ea8(&:odd?)>, [100]]
399
+
400
+ # opts are alternating symbols and booleans
401
+ S.def ns(:opts), S.zero_or_more(S.cat(:opt => Symbol, :val => ns(S, :boolean)))
402
+ S.conform ns(:opts), [:silent?, false, :verbose, true]
403
+ # => [{:opt=>:silent?, :val=>false}, {:opt=>:verbose, :val=>true}]
404
+
405
+ # Finally, we can use alt to specify alternatives within the sequential data.
406
+ # Like cat, alt requires you to tag each alternative but the conformed data is
407
+ # a vector of tag and value.
408
+
409
+ S.def ns(:config), S.zero_or_more(S.cat(:prop => String,
410
+ :val => S.alt(:s => String, :b => ns(S, :boolean))))
411
+ S.conform ns(:config), ["-server", "foo", "-verbose", true, "-user", "joe"]
412
+ # => [{:prop=>"-server", :val=>[:s, "foo"]},
413
+ # {:prop=>"-verbose", :val=>[:b, true]},
414
+ # {:prop=>"-user", :val=>[:s, "joe"]}]
415
+
416
+ # TODO: If you need a description of a specification, use describe to retrieve one.
417
+
418
+ # Spec also defines one additional regex operator, `constrained`, which takes a
419
+ # regex operator and constrains it with one or more additional predicates. This
420
+ # can be used to create regular expressions with additional constraints that
421
+ # would otherwise require custom predicates. For example, consider wanting to
422
+ # match only sequences with an even number of strings:
423
+
424
+ S.def ns(:even_strings), S.constrained(S.zero_or_more(String), ->(coll) { coll.count.even? })
425
+
426
+ S.valid? ns(:even_strings), ["a"] # => false
427
+ S.valid? ns(:even_strings), ["a", "b"] # => true
428
+ S.valid? ns(:even_strings), ["a", "b", "c"] # => false
429
+ S.valid? ns(:even_strings), ["a", "b", "c", "d"] # => true
430
+
431
+ # When regex ops are combined, they describe a single sequence. If you need to
432
+ # spec a nested sequential collection, you must use an explicit call to spec to
433
+ # start a new nested regex context. For example to describe a sequence like
434
+ # [:names, ["a", "b"], :nums, [1 2 3]], you need nested regular expressions to
435
+ # describe the inner sequential data:
436
+
437
+ S.def ns(:nested), S.cat(:names_sym => Set[:names],
438
+ :names => S.spec(S.zero_or_more(String)),
439
+ :nums_sym => Set[:nums],
440
+ :nums => S.spec(S.zero_or_more(Numeric)))
441
+
442
+ S.conform ns(:nested), [:names, ["a", "b"], :nums, [1, 2, 3]]
443
+ # => {:names_sym=>:names,
444
+ # :names=>["a", "b"],
445
+ # :nums_sym=>:nums,
446
+ # :nums=>[1, 2, 3]}
447
+
448
+ # If the specs were removed this spec would instead match a sequence like
449
+ # [:names, "a", "b", :nums, 1, 2, 3].
450
+
451
+ S.def ns(:unnested), S.cat(:names_sym => Set[:names],
452
+ :names => S.zero_or_more(String),
453
+ :nums_sym => Set[:nums],
454
+ :nums => S.zero_or_more(Numeric))
455
+
456
+ S.conform ns(:unnested), [:names, "a", "b", :nums, 1, 2, 3]
457
+ # => {:names_sym=>:names,
458
+ # :names=>["a", "b"],
459
+ # :nums_sym=>:nums,
460
+ # :nums=>[1, 2, 3]}
461
+
462
+ ## Using spec for validation
463
+
464
+ # Now is a good time to step back and think about how spec can be used for
465
+ # runtime data validation.
466
+
467
+ # One way to use spec is to explicitly call valid? to verify input data passed
468
+ # to a function. ~~You can, for example, use the existing pre- and post-condition
469
+ # support built into defn:~~
470
+
471
+ def self.person_name(person)
472
+ raise "invalid" unless S.valid? ns(:person), person
473
+ name = "#{person[ns(:first_name)]} #{person[ns(:last_name)]}"
474
+ raise "invalid" unless S.valid? String, name
475
+ name
476
+ end
477
+
478
+ person_name 43 rescue $! # => #<RuntimeError: invalid>
479
+ person_name ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "elon@example.com"
480
+ # => "Elon Musk"
481
+
482
+ # When the function is invoked with something that isn’t valid ns(:person) data,
483
+ # the pre-condition fails. Similarly, if there was a bug in our code and the
484
+ # output was not a string, the post-condition would fail.
485
+
486
+ # Another option is to use S.assert within your code to assert that a value
487
+ # satisfies a spec. On success the value is returned and on failure an
488
+ # assertion error is thrown. By default assertion checking is off - this can be
489
+ # changed by setting S.check_asserts or having the environment variable
490
+ # "SPECULATION_CHECK_ASSERTS=true".
491
+
492
+ def self.person_name(person)
493
+ p = S.assert ns(:person), person
494
+ "#{p[ns(:first_name)]} #{p[ns(:last_name)]}"
495
+ end
496
+
497
+ S.check_asserts = true
498
+ person_name 100 rescue $!
499
+ # => #<Speculation::Error: Spec assertion failed
500
+ # val: 100 fails predicate: [#<Method: Speculation::Utils.hash?>, [100]]
501
+ # Speculation/failure :assertion_failed
502
+ # {:"Speculation/problems"=>
503
+ # [{:path=>[],
504
+ # :pred=>[#<Method: Speculation::Utils.hash?>, [100]],
505
+ # :val=>100,
506
+ # :via=>[],
507
+ # :in=>[]}],
508
+ # :"Speculation/failure"=>:assertion_failed}
509
+ # >
510
+
511
+ # A deeper level of integration is to call conform and use the return value to
512
+ # destructure the input. This will be particularly useful for complex inputs
513
+ # with alternate options.
514
+
515
+ # Here we conform using the config specification defined above:
516
+
517
+ def self.set_config(prop, val)
518
+ # dummy fn
519
+ puts "set #{prop} #{val}"
520
+ end
521
+
522
+ def self.configure(input)
523
+ parsed = S.conform(ns(:config), input)
524
+ if parsed == ns(S, :invalid)
525
+ raise "Invalid input\n#{S.explain_str(ns(:config), input)}"
526
+ else
527
+ parsed.each do |config|
528
+ prop, val = config.values_at(:prop, :val)
529
+ _type, val = val
530
+
531
+ set_config(prop[1..-1], val)
532
+ end
533
+ end
534
+ end
535
+
536
+ configure ["-server", "foo", "-verbose", true, "-user", "joe"]
537
+ # set server foo
538
+ # set verbose true
539
+ # set user joe
540
+
541
+ # Here configure calls conform to destructure the config input. The result is
542
+ # either the special :"Speculation/invalid" value or a destructured form of the
543
+ # result:
544
+
545
+ [{ :prop => "-server", :val => [:s, "foo"] },
546
+ { :prop => "-verbose", :val => [:b, true] },
547
+ { :prop => "-user", :val => [:s, "joe"] }]
548
+
549
+ # In the success case, the parsed input is transformed into the desired shape
550
+ # for further processing. In the error case, we call explain_str to generate
551
+ # an error message. The explain string contains information about what
552
+ # expression failed to conform, the path to that expression in the
553
+ # specification, and the predicate it was attempting to match.
554
+
555
+ ## Spec’ing methods
556
+
557
+ # The pre- and post-condition example in the previous section hinted at an
558
+ # interesting question - how do we define the input and output specifications
559
+ # for a method.
560
+
561
+ # Spec has explicit support for this using fdef, which defines specifications
562
+ # for a function - the arguments and/or the return value spec, and optionally a
563
+ # function that can specify a relationship between args and return.
564
+
565
+ # Let’s consider a ranged-rand function that produces a random number in a
566
+ # range:
567
+
568
+ def self.ranged_rand(from, to)
569
+ rand(from...to)
570
+ end
571
+
572
+ # We can then provide a specification for that function:
573
+
574
+ S.fdef(method(:ranged_rand),
575
+ :args => S.and(S.cat(:start => Integer, :end => Integer), ->(args) { args[:start] < args[:end] }),
576
+ :ret => Integer,
577
+ :fn => S.and(->(fn) { fn[:ret] >= fn[:args][:start] },
578
+ ->(fn) { fn[:ret] < fn[:args][:end] }))
579
+
580
+ # This function spec demonstrates a number of features. First the :args is a
581
+ # compound spec that describes the function arguments. This spec is invoked
582
+ # with the args in an array, as if they were invoked like `method.call(*args)`
583
+ # Because the args are sequential and the args are positional fields, they are
584
+ # almost always described using a regex op, like cat, alt, or zero_or_more.
585
+
586
+ # The second :args predicate takes as input the conformed result of the first
587
+ # predicate and verifies that start < end. The :ret spec indicates the return
588
+ # is also an integer. Finally, the :fn spec checks that the return value is >=
589
+ # start and < end.
590
+
591
+ # We’ll see later how we can use a function spec for development and testing.
592
+
593
+ ## Higher order functions
594
+
595
+ # Higher order functions are common in ~Clojure~ Ruby and spec provides fspec
596
+ # to support spec’ing them.
597
+
598
+ # For example, consider the adder function:
599
+
600
+ def self.adder(x)
601
+ ->(y) { x + y }
602
+ end
603
+
604
+ # adder returns a proc that adds x. We can declare a function spec for adder
605
+ # using fspec for the return value:
606
+
607
+ S.fdef method(:adder),
608
+ :args => S.cat(:x => Numeric),
609
+ :ret => S.fspec(:args => S.cat(:y => Numeric), :ret => Numeric),
610
+ :fn => ->(fn) { fn[:args][:x] == fn[:ret].call(0) }
611
+
612
+ # The :ret spec uses fspec to declare that the returning function takes and
613
+ # returns a number. Even more interesting, the :fn spec can state a general
614
+ # property that relates the :args (where we know x) and the result we get from
615
+ # invoking the function returned from adder, namely that adding 0 to it should
616
+ # return x.
617
+
618
+ ## Macros - noop
619
+
620
+ ## A game of cards
621
+
622
+ # Here’s a bigger set of specs to model a game of cards:
623
+
624
+ suit = Set[:club, :diamond, :heart, :spade]
625
+ rank = Set[:jack, :queen, :king, :ace].merge(2..10)
626
+ deck = rank.to_a.product(suit.to_a)
627
+
628
+ S.def ns(:card), S.tuple(rank, suit)
629
+ S.def ns(:hand), S.zero_or_more(ns(:card))
630
+
631
+ S.def ns(:name), String
632
+ S.def ns(:score), Integer
633
+ S.def ns(:player), S.keys(:req => [ns(:name), ns(:score), ns(:hand)])
634
+
635
+ S.def ns(:players), S.zero_or_more(ns(:player))
636
+ S.def ns(:deck), S.zero_or_more(ns(:card))
637
+ S.def ns(:game), S.keys(:req => [ns(:players), ns(:deck)])
638
+
639
+ # We can validate a piece of this data against the schema:
640
+
641
+ kenny = { ns(:name) => "Kenny Rogers",
642
+ ns(:score) => 100,
643
+ ns(:hand) => [] }
644
+ S.valid? ns(:player), kenny
645
+ # => true
646
+
647
+ # Or look at the errors we’ll get from some bad data:
648
+
649
+ S.explain ns(:game),
650
+ ns(:deck) => deck,
651
+ ns(:players) => [{ ns(:name) => "Kenny Rogers",
652
+ ns(:score) => 100,
653
+ ns(:hand) => [[2, :banana]] }]
654
+ # >> In: [:"Object/players", 0, :"Object/hand", 0, 1] val: :banana fails spec: :"Object/card" at: [:"Object/players", :"Object/hand", 1] predicate: [#<Set: {:club, :diamond, :heart, :spade}>, [:banana]]
655
+
656
+ # The error indicates the key path in the data structure down to the invalid
657
+ # value, the non-matching value, the spec part it’s trying to match, the path
658
+ # in that spec, and the predicate that failed.
659
+
660
+ # If we have a function `deal` that doles out some cards to the players we can
661
+ # spec that function to verify the arg and return value are both suitable
662
+ # data values. We can also specify a :fn spec to verify that the count of
663
+ # cards in the game before the deal equals the count of cards after the deal.
664
+
665
+ def self.total_cards(game)
666
+ game, players = game.values_at(ns(:game), ns(:players))
667
+ players.map { |player| player[ns(:hand)].count }.reduce(deck.count, &:+)
668
+ end
669
+
670
+ def self.deal(game)
671
+ # ...
672
+ end
673
+
674
+ S.fdef method(:deal),
675
+ :args => S.cat(:game => ns(:game)),
676
+ :ret => ns(:game),
677
+ :fn => ->(fn) { total_cards(fn[:args][:game]) == total_cards(fn[:ret]) }
678
+
679
+ ## Generators
680
+
681
+ # A key design constraint of spec is that all specs are also designed to act as
682
+ # generators of sample data that conforms to the spec (a critical requirement
683
+ # for property-based testing).
684
+
685
+ ## ~~Project Setup~~
686
+
687
+ # Nothing to do, Rantly is included by default. May look at removing the hard
688
+ # dependency in the future.
689
+
690
+ # In your code you also need to require the speculation/gen lib.
691
+
692
+ require "speculation/gen"
693
+ Gen = S::Gen
694
+
695
+ ## Sampling Generators
696
+
697
+ # The gen function can be used to obtain the generator for any spec.
698
+
699
+ # Once you have obtained a generator with gen, there are several ways to use
700
+ # it. You can generate a single sample value with generate or a series of
701
+ # samples with sample. Let’s see some basic examples:
702
+
703
+ Gen.generate S.gen(Integer) # => 372495152381320358
704
+ Gen.generate S.gen(NilClass) # => nil
705
+ Gen.sample S.gen(String), 5
706
+ # => ["RhzOLQjmSjhWavH", "y", "", "O", "peoPwXHRBBAPjDxzEZQh"]
707
+ Gen.sample S.gen(Set[:club, :diamond, :heart, :spade]), 5
708
+ # => [:heart, :spade, :club, :spade, :spade]
709
+
710
+ Gen.sample S.gen(S.cat(:k => Symbol, :ns => S.one_or_more(Numeric))), 4
711
+ # => [[:csWKkimBORwN,
712
+ # -298753312314306397,
713
+ # -2303961522202434118,
714
+ # 1679934373136969303,
715
+ # -262631322747429978,
716
+ # 1.7157706401801108e+308,
717
+ # 1758361237993287532,
718
+ # 712842522394861335,
719
+ # -883871273503318653,
720
+ # 1283229873044628318,
721
+ # 1.5298057192258154e+308,
722
+ # 1.7789073686150528e+308,
723
+ # -2281793086040303873,
724
+ # 120746116914138063,
725
+ # -404134654833569820,
726
+ # -54740933266507251,
727
+ # 5.01892001701602e+307],
728
+ # [:RetYrsJr,
729
+ # 1.3391749738917395e+308,
730
+ # 1.0920197216545966e+307,
731
+ # 1.384947546752308e+307,
732
+ # 1.3364975035426882e+308,
733
+ # 327082393035103718,
734
+ # 1.0209866964240673e+308,
735
+ # 512415813150328683],
736
+ # [:UdDv,
737
+ # 3.0578102207508006e+307,
738
+ # 1.1626478137534508e+308,
739
+ # 1.7939796459941183e+308,
740
+ # 1494374259430455477,
741
+ # 1.342849042383955e+308,
742
+ # -281429214092326237,
743
+ # -552507314062007344,
744
+ # 4.1453903880025765e+307,
745
+ # -973157747452936365,
746
+ # 1.1388886925899274e+308,
747
+ # 2056792483501668313,
748
+ # 999682663796411736,
749
+ # 7.395274944717998e+306,
750
+ # -1514851160913660499,
751
+ # -2167762478595098510,
752
+ # 824382210168550458,
753
+ # 1614922845514653160],
754
+ # [:s,
755
+ # -234772724560973590,
756
+ # 1.0042104238108253e+308,
757
+ # 1.3942217537031457e+307,
758
+ # -1553774642616973743,
759
+ # -360282579504585923]]
760
+
761
+ # What about generating a random player in our card game?
762
+
763
+ Gen.generate S.gen(ns(:player))
764
+ # => {:"Object/name"=>"qrmY",
765
+ # :"Object/score"=>-188402685781919929,
766
+ # :"Object/hand"=>
767
+ # [[10, :heart],
768
+ # [:king, :heart],
769
+ # [2, :spade],
770
+ # [7, :heart],
771
+ # [9, :club],
772
+ # [7, :club],
773
+ # [10, :diamond],
774
+ # [:jack, :spade],
775
+ # [2, :diamond],
776
+ # [3, :diamond],
777
+ # [:king, :spade],
778
+ # [5, :spade],
779
+ # [10, :heart],
780
+ # [:king, :heart],
781
+ # [:jack, :spade],
782
+ # [:king, :spade],
783
+ # [:queen, :club],
784
+ # [6, :diamond],
785
+ # [5, :club],
786
+ # [6, :club]]}
787
+
788
+ # What about generating a whole game?
789
+
790
+ Gen.generate S.gen(ns(:game))
791
+ # it works! but the output is really long, so not including it here
792
+
793
+ # So we can now start with a spec, extract a generator, and generate some data.
794
+ # All generated data will conform to the spec we used as a generator. For specs
795
+ # that have a conformed value different than the original value (anything using
796
+ # S.or, S.cat, S.alt, etc) it can be useful to see a set of generated samples
797
+ # plus the result of conforming that sample data.
798
+
799
+ ## Exercise
800
+
801
+ # For this we have `exercise`, which returns pairs of generated and conformed
802
+ # values for a spec. exercise by default produces 10 samples (like sample) but
803
+ # you can pass both functions a number indicating the number of samples to
804
+ # produce.
805
+
806
+ S.exercise S.cat(:k => Symbol, :ns => S.one_or_more(Numeric)), :n => 5
807
+ # => [[[:AXgNzoRmshVeKju,
808
+ # -817925373115395462,
809
+ # 1.5359311568381347e+308,
810
+ # 1.1061449248034022e+308,
811
+ # -235267876425474208,
812
+ # 3.955857252356689e+307,
813
+ # -889011872905836841,
814
+ # 9.082764829559406e+307,
815
+ # 3.8449893386631863e+307,
816
+ # 1399473921337276004,
817
+ # 1.1035252898212735e+308],
818
+ # {:k=>:AXgNzoRmshVeKju,
819
+ # :ns=>
820
+ # [-817925373115395462,
821
+ # 1.5359311568381347e+308,
822
+ # 1.1061449248034022e+308,
823
+ # -235267876425474208,
824
+ # 3.955857252356689e+307,
825
+ # -889011872905836841,
826
+ # 9.082764829559406e+307,
827
+ # 3.8449893386631863e+307,
828
+ # 1399473921337276004,
829
+ # 1.1035252898212735e+308]}],
830
+ # [[:Nsndjayf,
831
+ # 1.9984725870793707e+307,
832
+ # 1.5323527859487139e+308,
833
+ # 1.0526758425396865e+308,
834
+ # 2187215078751341740,
835
+ # 2000267805737910757,
836
+ # 672724827310048814,
837
+ # 7.353660057508847e+307,
838
+ # -499603991431322628,
839
+ # 823374880053618568,
840
+ # 988019501395130231,
841
+ # -85062962445868544,
842
+ # 1208854825028261939,
843
+ # -239585966232519771],
844
+ # {:k=>:Nsndjayf,
845
+ # :ns=>
846
+ # [1.9984725870793707e+307,
847
+ # 1.5323527859487139e+308,
848
+ # 1.0526758425396865e+308,
849
+ # 2187215078751341740,
850
+ # 2000267805737910757,
851
+ # 672724827310048814,
852
+ # 7.353660057508847e+307,
853
+ # -499603991431322628,
854
+ # 823374880053618568,
855
+ # 988019501395130231,
856
+ # -85062962445868544,
857
+ # 1208854825028261939,
858
+ # -239585966232519771]}],
859
+ # [[:kKknKqGtQjl, 1.781549997030396e+305, -2255917728752340059],
860
+ # {:k=>:kKknKqGtQjl,
861
+ # :ns=>[1.781549997030396e+305, -2255917728752340059]}],
862
+ # [[:OknzgVGj,
863
+ # -2263138309988902357,
864
+ # 6.780757328421502e+307,
865
+ # 1159675302983770930,
866
+ # 8.619504625294373e+307,
867
+ # -102111175606505256,
868
+ # 3.1369602174703924e+307,
869
+ # 714218663950371918,
870
+ # 1072428045010760820,
871
+ # 1.7120457957881442e+308,
872
+ # 1.7220639025345156e+308,
873
+ # 7.318059339504824e+307,
874
+ # -627281432214439965,
875
+ # 1285330282675190977,
876
+ # 5.624663033422957e+307],
877
+ # {:k=>:OknzgVGj,
878
+ # :ns=>
879
+ # [-2263138309988902357,
880
+ # 6.780757328421502e+307,
881
+ # 1159675302983770930,
882
+ # 8.619504625294373e+307,
883
+ # -102111175606505256,
884
+ # 3.1369602174703924e+307,
885
+ # 714218663950371918,
886
+ # 1072428045010760820,
887
+ # 1.7120457957881442e+308,
888
+ # 1.7220639025345156e+308,
889
+ # 7.318059339504824e+307,
890
+ # -627281432214439965,
891
+ # 1285330282675190977,
892
+ # 5.624663033422957e+307]}],
893
+ # [[:mifpKjpS, 3.8475669790437504e+307, 1.5541847940699583e+307],
894
+ # {:k=>:mifpKjpS,
895
+ # :ns=>[3.8475669790437504e+307, 1.5541847940699583e+307]}]]
896
+
897
+ S.exercise S.or(:k => Symbol, :s => String, :n => Numeric), :n => 5
898
+ # => [[-1310754584514288, [:n, -1310754584514288]],
899
+ # [872148706486332083, [:n, 872148706486332083]],
900
+ # [:rHCoqRLZYhzSgOu, [:k, :rHCoqRLZYhzSgOu]],
901
+ # [-395552003092497804, [:n, -395552003092497804]],
902
+ # [:WoaPnjB, [:k, :WoaPnjB]]]
903
+
904
+ # For spec’ed functions we also have exercise_fn, which generates sample args,
905
+ # invokes the spec’ed function and returns the args and the return value.
906
+
907
+ S.exercise_fn(method(:ranged_rand))
908
+ # => [[[-2128611012334186431, -1417738444057945122], -1635106169064592441],
909
+ # [[1514518280943101595, 1786254628919354373], 1739796291756227578],
910
+ # [[-46749061680797208, 822766248044755470], -7474228458851983],
911
+ # [[-649513218842008808, 1875894039691321060], -390581384114488816],
912
+ # [[858361555883341214, 1741658980258358628], 1374077212657449917],
913
+ # [[-1258388171360603963, -985723099401376708], -1123010455669592843],
914
+ # [[-1035489322616947034, 1688366643195138662], 441214083022620176],
915
+ # [[-2229284211372056198, -893085296484913242], -1161469637076511831],
916
+ # [[819684425123939548, 1044514159372510410], 971678102106589235],
917
+ # [[366502776249932529, 1318835861470496704], 377553467194155955]]
918
+
919
+ ## Using S.and Generators
920
+
921
+ # All of the generators we’ve seen worked fine but there are a number of cases
922
+ # where they will need some additional help. One common case is when the
923
+ # predicate implicitly presumes values of a particular type but the spec does
924
+ # not specify them:
925
+
926
+ Gen.generate S.gen(:even?.to_proc) rescue $! # => #<Speculation::Error: unable to construct gen at: [] for: Speculation::Spec(#<Proc:0x007fb69b1908f8(&:even?)>) {:"Speculation/failure"=>:no_gen, :"Speculation/path"=>[]}\n>
927
+
928
+ # In this case spec was not able to find a generator for the even? predicate.
929
+ # Most of the primitive generators in spec are mapped to the common type
930
+ # predicates (classes, modules, built-in specs).
931
+
932
+ # However, spec is designed to support this case via `and` - the first
933
+ # predicate will determine the generator and subsequent branches will act as
934
+ # filters by applying the predicate to the produced values.
935
+
936
+ # If we modify our predicate to use an `and` and a predicate with a mapped
937
+ # generator, the even? can be used as a filter for generated values instead:
938
+
939
+ Gen.generate S.gen(S.and(Integer, :even?.to_proc))
940
+ # => 1875527059787064980
941
+
942
+ # We can use many predicates to further refine the generated values. For
943
+ # example, say we only wanted to generate numbers that were positive multiples
944
+ # of 3:
945
+
946
+ def self.divisible_by(n)
947
+ ->(x) { (x % n).zero? }
948
+ end
949
+
950
+ Gen.sample S.gen(S.and(Integer, :positive?.to_proc, divisible_by(3)))
951
+ # => [1003257946641857673,
952
+ # 1302633092686504620,
953
+ # 1067379217208623728,
954
+ # 882135641374726149,
955
+ # 1933864978000820676,
956
+ # 235089151558168077,
957
+ # 470438340672134322,
958
+ # 2268668240213030931,
959
+ # 1061519505888350829,
960
+ # 1868667505095337938]
961
+
962
+ # However, it is possible to go too far with refinement and make something that
963
+ # fails to produce any values. The Rantly `guard` that implements the
964
+ # refinement will throw an error if the refinement predicate cannot be resolved
965
+ # within a relatively small number of attempts. For example, consider trying to
966
+ # generate strings that happen to contain the world "hello":
967
+
968
+ # hello, are you the one I'm looking for?
969
+ Gen.sample S.gen(S.and(String, ->(s) { s.include?("hello") })) rescue $!
970
+ # => #<Rantly::TooManyTries: Exceed gen limit 1000: 1001 failed guards)>
971
+
972
+ # Given enough time (maybe a lot of time), the generator probably would come up
973
+ # with a string like this, but the underlying `guard` will make only 100
974
+ # attempts to generate a value that passes the filter. This is a case where you
975
+ # will need to step in and provide a custom generator.
976
+
977
+ ## Custom Generators
978
+
979
+ # Building your own generator gives you the freedom to be either narrower
980
+ # and/or be more explicit about what values you want to generate. Alternately,
981
+ # custom generators can be used in cases where conformant values can be
982
+ # generated more efficiently than using a base predicate plus filtering. Spec
983
+ # does not trust custom generators and any values they produce will also be
984
+ # checked by their associated spec to guarantee they pass conformance.
985
+
986
+ # There are three ways to build up custom generators - in decreasing order of
987
+ # preference:
988
+
989
+ # - Let spec create a generator based on a predicate/spec
990
+ # - Create your own generator using Rantly directly
991
+
992
+ # First consider a spec with a predicate to specify symbols from a particular
993
+ # namespace:
994
+
995
+ S.def ns(:syms), S.and(Symbol, ->(s) { S::NamespacedSymbols.namespace(s) == "my.domain" })
996
+ S.valid? ns(:syms), :"my.domain/name" # => true
997
+ Gen.sample S.gen(ns(:syms)) rescue $! # => #<Rantly::TooManyTries: Exceed gen limit 1000: 1001 failed guards)>
998
+
999
+ # The simplest way to start generating values for this spec is to have spec
1000
+ # create a generator from a fixed set of options. A set is a valid predicate
1001
+ # spec so we can create one and ask for it’s generator:
1002
+
1003
+ sym_gen = S.gen(Set[:"my.domain/name", :"my.domain/occupation", :"my.domain/id"])
1004
+ Gen.sample sym_gen, 5
1005
+ # => [:"my.domain/id",
1006
+ # :"my.domain/name",
1007
+ # :"my.domain/occupation",
1008
+ # :"my.domain/name",
1009
+ # :"my.domain/id"]
1010
+
1011
+ # To redefine our spec using this custom generator, use with_gen which takes a
1012
+ # spec and a replacement generator as a block:
1013
+
1014
+ gen = S.gen(Set[:"my.domain/name", :"my.domain/occupation", :"my.domain/id"])
1015
+ S.def(ns(:syms), S.with_gen(S.and(Symbol, ->(s) { S::NamespacedSymbols.namespace(s) == "my.domain" }), gen))
1016
+
1017
+ S.valid? ns(:syms), :"my.domain/name"
1018
+ Gen.sample S.gen(ns(:syms)), 5
1019
+ # => [:"my.domain/name",
1020
+ # :"my.domain/id",
1021
+ # :"my.domain/occupation",
1022
+ # :"my.domain/occupation",
1023
+ # :"my.domain/id"]
1024
+
1025
+ # TODO: make gens no-arg functions???
1026
+ # Note that with_gen (and other places that take a custom generator) take a
1027
+ # one-arg function that returns the generator, allowing it to be lazily
1028
+ # realized.
1029
+
1030
+ # One downside to this approach is we are missing what property testing is
1031
+ # really good at: automatically generating data across a wide search space to
1032
+ # find unexpected problems.
1033
+
1034
+ # Rantly has a small library of generators that can be utilized.
1035
+
1036
+ # In this case we want our keyword to have open names but fixed namespaces.
1037
+ # There are many ways to accomplish this but one of the simplest is to use fmap
1038
+ # to build up a keyword based on generated strings:
1039
+
1040
+ sym_gen_2 = ->(rantly) do
1041
+ size = rantly.range(1, 10)
1042
+ string = rantly.sized(size) { rantly.string(:alpha) }
1043
+ :"my.domain/#{string}"
1044
+ end
1045
+ Gen.sample sym_gen_2, 5 # => [:"my.domain/hLZnEpj", :"my.domain/kvy", :"my.domain/VqWbqD", :"my.domain/imq", :"my.domain/eHeZleWzj"]
1046
+
1047
+ # Returning to our "hello" example, we now have the tools to make that
1048
+ # generator:
1049
+
1050
+ S.def ns(:hello), S.with_gen(->(s) { s.include?("hello") }, ->(rantly) {
1051
+ s1 = rantly.sized(rantly.range(0, 10)) { rantly.string(:alpha) }
1052
+ s2 = rantly.sized(rantly.range(0, 10)) { rantly.string(:alpha) }
1053
+ "#{s1}hello#{s2}"
1054
+ })
1055
+
1056
+ Gen.sample S.gen(ns(:hello))
1057
+ # => ["XRLhtLshelloaY",
1058
+ # "tXxZQHhZOhelloJ",
1059
+ # "ExhellozzlPYz",
1060
+ # "MaiierIhelloel",
1061
+ # "WKZBJprQkhelloGdGToCbI",
1062
+ # "RDFCZhello",
1063
+ # "PXPsYwJLhellosYoYngd",
1064
+ # "SuhelloJ",
1065
+ # "wWhelloodQFFvdW",
1066
+ # "pNhello"]
1067
+
1068
+ # Here we generate a tuple of a random prefix and random suffix strings, then
1069
+ # insert "hello" bewteen them.
1070
+
1071
+ ## Range Specs and Generators
1072
+
1073
+ # There are several cases where it’s useful to spec (and generate) values in a
1074
+ # range and spec provides helpers for these cases.
1075
+
1076
+ # For example, in the case of a range of integer values (for example, a bowling
1077
+ # roll), use int_in to spec a range:
1078
+
1079
+ S.def ns(:roll), S.int_in(0..10)
1080
+ Gen.sample S.gen(ns(:roll))
1081
+ # => [10, 0, 8, 2, 6, 10, 1, 10, 10, 0]
1082
+
1083
+ # spec also includes date_in for a range of dates:
1084
+
1085
+ S.def ns(:the_aughts), S.date_in(Date.new(2000, 1, 1)..Date.new(2010))
1086
+ Gen.sample S.gen(ns(:the_aughts)), 5
1087
+ # => [#<Date: 2003-07-06 ((2452827j,0s,0n),+0s,2299161j)>,
1088
+ # #<Date: 2009-04-13 ((2454935j,0s,0n),+0s,2299161j)>,
1089
+ # #<Date: 2009-08-03 ((2455047j,0s,0n),+0s,2299161j)>,
1090
+ # #<Date: 2006-07-07 ((2453924j,0s,0n),+0s,2299161j)>,
1091
+ # #<Date: 2001-06-12 ((2452073j,0s,0n),+0s,2299161j)>]
1092
+
1093
+ # spec also includes time_in for a range of times:
1094
+
1095
+ S.def ns(:the_aughts), S.time_in(Time.new(2000)..Time.new(2010))
1096
+ Gen.sample S.gen(ns(:the_aughts)), 5
1097
+ # => [2000-03-26 07:01:27 -0800,
1098
+ # 2009-03-28 12:00:18 -0700,
1099
+ # 2003-09-20 15:33:42 -0700,
1100
+ # 2007-06-26 20:07:13 -0700,
1101
+ # 2003-11-25 16:59:38 -0800]
1102
+
1103
+ # Finally, float_in has support for double ranges and special options for
1104
+ # checking special float values like NaN (not a number), Infinity, and
1105
+ # -Infinity.
1106
+
1107
+ S.def ns(:floats), S.float_in(:min => -100.0, :max => 100.0, :nan => false, :infinite => false)
1108
+ S.valid? ns(:floats), 2.9 # => true
1109
+ S.valid? ns(:floats), Float::INFINITY # => false
1110
+ Gen.sample S.gen(ns(:floats)), 5 # => [65.53711851243327, 67.31921045318401, -71.92560111608772, 81.66336359400515, -30.4921955594738]
1111
+
1112
+ ## Instrumentation and Testing
1113
+
1114
+ # spec provides a set of development and testing functionality in the
1115
+ # Speculation::Test namespace, which we can include with:
1116
+
1117
+ require "speculation/test"
1118
+ STest = Speculation::Test
1119
+
1120
+ ## Instrumentation
1121
+
1122
+ # Instrumentation validates that the :args spec is being invoked on
1123
+ # instrumented functions and thus provides validation for external uses of a
1124
+ # function. Let’s turn on instrumentation for our previously spec’ed
1125
+ # ranged-rand function:
1126
+
1127
+ STest.instrument method(:ranged_rand)
1128
+
1129
+ # If the function is invoked with args that do not conform with the :args spec
1130
+ # you will see an error like this:
1131
+
1132
+ ranged_rand 8, 5 rescue $!
1133
+ # => #<Speculation::Error: Call to 'main.ranged_rand' did not conform to spec:
1134
+ # val: {:start=>8, :end=>5} fails at: [:args] predicate: [#<Proc:0x007fb69a8a0fb8@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:548 (lambda)>, [{:start=>8, :end=>5}]]
1135
+ # Speculation/args [8, 5]
1136
+ # Speculation/failure :instrument
1137
+ # Speculation::Test/caller "/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:901:in `<main>'"
1138
+ # {:"Speculation/problems"=>
1139
+ # [{:path=>[:args],
1140
+ # :val=>{:start=>8, :end=>5},
1141
+ # :via=>[],
1142
+ # :in=>[],
1143
+ # :pred=>
1144
+ # [#<Proc:0x007fb69a8a0fb8@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:548 (lambda)>,
1145
+ # [{:start=>8, :end=>5}]]}],
1146
+ # :"Speculation/args"=>[8, 5],
1147
+ # :"Speculation/failure"=>:instrument,
1148
+ # :"Speculation::Test/caller"=>
1149
+ # "/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:901:in `<main>'"}
1150
+ # >
1151
+
1152
+ # The error fails in the second args predicate that checks `start < end`. Note
1153
+ # that the :ret and :fn specs are not checked with instrumentation as
1154
+ # validating the implementation should occur at testing time.
1155
+
1156
+ # Instrumentation can be turned off using the complementary function
1157
+ # unstrument. Instrumentation is likely to be useful at both development time
1158
+ # and during testing to discover errors in calling code. It is not recommended
1159
+ # to use instrumentation in production due to the overhead involved with
1160
+ # checking args specs.
1161
+
1162
+ ## Testing
1163
+
1164
+ # We mentioned earlier that ~~clojure.spec.test~~ Speculation provides tools
1165
+ # for automatically testing functions. When functions have specs, we can use
1166
+ # check, to automatically generate tests that check the function using the
1167
+ # specs.
1168
+
1169
+ # check will generate arguments based on the :args spec for a function, invoke
1170
+ # the function, and check that the :ret and :fn specs were satisfied.
1171
+
1172
+ STest.check method(:ranged_rand)
1173
+ # => [{:spec=>Speculation::FSpec(main.ranged_rand),
1174
+ # :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true},
1175
+ # :method=>#<Method: main.ranged_rand>}]
1176
+
1177
+ # check also takes a number of options ~~that can be passed to test.check to
1178
+ # influence the test run~~, as well as the option to override generators for
1179
+ # parts of the spec, by either name or path.
1180
+
1181
+ # Imagine instead that we made an error in the ranged-rand code and swapped
1182
+ # start and end:
1183
+
1184
+ def self.ranged_rand(from, to) ## broken!
1185
+ (from + rand(to)).to_i
1186
+ end
1187
+
1188
+ # This broken function will still create random integers, just not in the
1189
+ # expected range. Our :fn spec will detect the problem when checking the var:
1190
+
1191
+ STest.abbrev_result STest.check(method(:ranged_rand)).first
1192
+ # >> {:spec=>"Speculation::FSpec(main.ranged_rand)",
1193
+ # >> :method=>#<Method: main.ranged_rand>,
1194
+ # >> :failure=>
1195
+ # >> {:"Speculation/problems"=>
1196
+ # >> [{:path=>[:fn],
1197
+ # >> :val=>{:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0},
1198
+ # >> :via=>[],
1199
+ # >> :in=>[],
1200
+ # >> :pred=>
1201
+ # >> [#<Proc:0x007fb69a8a0c98@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:551 (lambda)>,
1202
+ # >> [{:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0}]]}],
1203
+ # >> :"Speculation::Test/args"=>[-1, 0],
1204
+ # >> :"Speculation::Test/val"=>
1205
+ # >> {:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0},
1206
+ # >> :"Speculation/failure"=>:check_failed}}
1207
+
1208
+ # check has reported an error in the :fn spec. We can see the arguments passed
1209
+ # were -1 and 0 and the return value was -0, which is out of the expected
1210
+ # range (it should be less that the `end` argument).
1211
+
1212
+ # To test all of the spec’ed functions in a module/class (or multiple
1213
+ # module/classes), use enumerate-methods to generate the set of symbols
1214
+ # naming vars in the namespace:
1215
+
1216
+ STest.summarize_results STest.check(STest.enumerate_methods(self))
1217
+ # => {:total=>3, :check_failed=>2, :check_passed=>1}
1218
+
1219
+ # And you can check all of the spec’ed functions by calling STest.check without any arguments.
1220
+
1221
+ ## Combining check and instrument
1222
+
1223
+ # While both `instrument` (for enabling :args checking) and `check` (for generating
1224
+ # tests of a function) are useful tools, they can be combined to provide even
1225
+ # deeper levels of test coverage.
1226
+
1227
+ # `instrument` takes a number of options for changing the behavior of
1228
+ # instrumented functions, including support for swapping in alternate
1229
+ # (narrower) specs, stubbing functions (by using the :ret spec to generate
1230
+ # results), or replacing functions with an alternate implementation.
1231
+
1232
+ # Consider the case where we have a low-level function that invokes a remote
1233
+ # service and a higher-level function that calls it.
1234
+
1235
+ # code under test
1236
+
1237
+ def self.invoke_service(service, request)
1238
+ # invokes remote service
1239
+ end
1240
+
1241
+ def self.run_query(service, query)
1242
+ response = invoke_service(service, ns(:query) => query)
1243
+ result, error = response.values_at(ns(:result), ns(:error))
1244
+ result || error
1245
+ end
1246
+
1247
+ # We can spec these functions using the following specs:
1248
+
1249
+ S.def ns(:query), String
1250
+ S.def ns(:request), S.keys(:req => [ns(:query)])
1251
+ S.def ns(:result), S.coll_of(String, :gen_max => 3)
1252
+ S.def ns(:error), Integer
1253
+ S.def ns(:response), S.or(:ok => S.keys(:req => [ns(:result)]),
1254
+ :err => S.keys(:req => [ns(:error)]))
1255
+
1256
+ S.fdef method(:invoke_service),
1257
+ :args => S.cat(:service => ns(S, :any), :request => ns(:request)),
1258
+ :ret => ns(:response)
1259
+
1260
+ S.fdef method(:run_query),
1261
+ :args => S.cat(:service => ns(S, :any), :query => String),
1262
+ :ret => S.or(:ok => ns(:result), :err => ns(:error))
1263
+
1264
+ # And then we want to test the behavior of run_query while stubbing out
1265
+ # invoke_service with instrument so that the remote service is not invoked:
1266
+
1267
+ STest.instrument method(:invoke_service), :stub => [method(:invoke_service)]
1268
+
1269
+ invoke_service nil, ns(:query) => "test"
1270
+ # => {:"Object/result"=>["LtqDYvzOfVzCHWN", "ZASNKhtkwBAXyTF"]}
1271
+ invoke_service nil, ns(:query) => "test"
1272
+ # => {:"Object/result"=>["yvccRd", "xREXEgc"]}
1273
+ STest.summarize_results STest.check(method(:run_query))
1274
+ # => {:total=>1, :check_passed=>1}
1275
+
1276
+ # The first call here instruments and stubs invoke_service. The second and
1277
+ # third calls demonstrate that calls to invoke_service now return generated
1278
+ # results (rather than hitting a service). Finally, we can use check on the
1279
+ # higher level function to test that it behaves properly based on the generated
1280
+ # stub results returned from invoke_service.
1281
+
1282
+ ## Wrapping Up
1283
+
1284
+ # In this guide we have covered most of the features for designing and using
1285
+ # specs and generators. We expect to add some more advanced generator
1286
+ # techniques and help on testing in a future update.
1287
+
1288
+ # Original author of clojure.spec guide: Alex Miller