speculation 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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