speculation 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 21bae113d98b1aae417ce30534a9488f34ddbaa0
4
- data.tar.gz: aa53139eed9b655d4ce4a1ff9984881e94f4e954
3
+ metadata.gz: 7aa989a5796d79c562dc7ade44fe3b9b146b208e
4
+ data.tar.gz: 4e3249fae769e64f274f040ad59ef012ed4e50f5
5
5
  SHA512:
6
- metadata.gz: 23a0f3f05968623d4cc91d34f7929fc2e76c42679930e9abb35e6f7e9fadfc7648d48a18c4b80452ec6871832ab1c16d4880f735dc1c6033649560fa12e1eaa7
7
- data.tar.gz: 525d9d6479c8accd5b9347b7f8f97568ca708213c82db5538a731e072ba8b31e31df20e79ee5247b787611c8d16db8b22858a101419c5dbdff180447f6329e9f
6
+ metadata.gz: bf7cb77e9f081fa6605a7676fd733c1ea5a2d3d8da9634bf529bdfd523e37cf54795e96dabdc84e039c920efbd2e1a620f7fcd3d7abbff16c5d8eed90ca0a28d
7
+ data.tar.gz: b997fa5b070e09ac6c146bb0587278351e03da7bb27ef12d650765484c66045e7f05576bbdea13ccfaf8f4b72e44d53e1ab54e52149eafff7e3b10f48ee78d54
data/README.md CHANGED
@@ -1,17 +1,24 @@
1
1
  # Speculation [![Build Status](https://travis-ci.org/english/speculation.svg?branch=master)](https://travis-ci.org/english/speculation)
2
2
 
3
- A Ruby port of Clojure's `clojure.spec`. See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec). All advantages/disadvantages for clojure.spec should apply to Speculation too. This library is largely a copy-and-paste from clojure.spec so all credit goes to the clojure.spec authors.
3
+ A Ruby port of Clojure's `clojure.spec`. This library is largely a copy-and-paste from clojure.spec so all credit goes to Rich Hickey and contributors for their original work.
4
+
5
+ See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to write predicate specs (nothing to do with RSpec!) which enable:
6
+
7
+ * declarative data validation and destructuring
8
+ * error reporting
9
+ * runtime method instrumentation, argument checking and stubbing
10
+ * generative testing
4
11
 
5
12
  ## Project Goals
6
13
 
7
- The goal of this project is to match clojure.spec as closely as possible, from design to features to API. This decision comes with the trade-off that the library may not necessarily be idiomatic Ruby, however there's nothing stopping other libraries from being built on top of Speculation to bring a more Ruby-like feel. This library won't introduce features that do not exist in clojure.spec.
14
+ The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be any, significant departures from clojure.spec.
8
15
 
9
16
  ## Examples
10
17
 
11
- - [sinatra-web-app](examples/sinatra-web-app): A small sinatra web application demonstrating model validation and API error message generation.
12
- - [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec)
13
- - [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
14
- - [json_parser.rb](examples/json_parser.rb): JSON parser using Speculation.
18
+ * [sinatra-web-app](examples/sinatra-web-app): A small Sinatra web application demonstrating parameter validation and API error message generation.
19
+ * [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
20
+ * [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
21
+ * [json_parser.rb](examples/json_parser.rb): JSON parser using Speculation.
15
22
 
16
23
  ## Usage
17
24
 
@@ -21,7 +28,7 @@ Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/s
21
28
 
22
29
  ### Built in predicates
23
30
 
24
- clojure.spec leans on its macro system and rich standard library of predicate functions when writing specs. Ruby has neither of those, so we must be creative with what we define as a 'predicate' in Speculation. Each of the following are valid Speculation predicates:
31
+ clojure.spec utilises its rich standard library of predicate functions and data structures when writing specs. Ruby has neither of those, so we must be creative with what we define as a 'predicate' in Speculation. Each of the following are valid Speculation predicates:
25
32
 
26
33
  ```rb
27
34
  S.valid?(->(x) { x > 0 }, 2)
@@ -80,30 +87,24 @@ S.fdef(method(:hello), :args => S.cat(:name => String),
80
87
 
81
88
  #### Generators and quick check
82
89
 
83
- Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
90
+ Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While Clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
84
91
 
85
92
  Rantly gives Speculation the ability to shrink a failing test case down to its smallest failing case, however in Speculation we limit this to Integers and Strings. This is an area where Speculation may currently be significantly weaker than clojure.spec.
86
93
 
87
- ## Development
88
-
89
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
90
-
91
- ## Contributing
92
-
93
- Bug reports and pull requests are welcome on GitHub at https://github.com/english/speculation.
94
-
95
- ## TODO
94
+ ## Project status
96
95
 
97
- - tidy up tests
96
+ Speculation will mirror any changes made to clojure.spec. clojure.spec is still in alpha so breaking changes should be expected.
98
97
 
99
- ### clojure.spec features
98
+ While most of features of clojure.spec are implemented in Speculation, a few remain:
100
99
 
101
100
  - [`unform`](https://clojuredocs.org/clojure.spec/unform)
102
101
  - [`form`](https://clojuredocs.org/clojure.spec/form)
103
102
  - [`abbrev`](https://clojuredocs.org/clojure.spec/abbrev)
104
103
  - [`describe`](https://clojuredocs.org/clojure.spec/describe)
105
104
 
106
- ### Improvements
105
+ ## Improvements
106
+
107
+ Some things I hope to focus on in the near future:
107
108
 
108
109
  - Explore alternative generator library
109
110
  - Build up a library of generators around Rantly in the meantime?
@@ -111,6 +112,14 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
111
112
  - perhaps integrating with [Pry's documentation browsing](https://github.com/pry/pry/wiki/Documentation-browsing)?
112
113
  - Profile and optimise
113
114
 
115
+ ## Development
116
+
117
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
118
+
119
+ ## Contributing
120
+
121
+ Bug reports and pull requests are welcome on GitHub at https://github.com/english/speculation.
122
+
114
123
  ## License
115
124
 
116
125
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/bin/console CHANGED
@@ -19,7 +19,7 @@ def reload!
19
19
 
20
20
  load "./lib/speculation/namespaced_symbols.rb"
21
21
  load "./lib/speculation/pmap.rb"
22
- load "./lib/speculation/identifier.rb"
22
+ load "./lib/speculation/method_identifier.rb"
23
23
  load "./lib/speculation/utils.rb"
24
24
  load "./lib/speculation/error.rb"
25
25
 
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
- # See http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec
3
2
 
4
- require "bundler/inline"
5
- require "set"
3
+ # Speculation version of
4
+ # http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec by David
5
+ # Chelimsky.
6
6
 
7
- gemfile true do
8
- gem "speculation",
9
- :git => "https://github.com/english/speculation.git",
10
- :require => ["speculation", "speculation/test", "speculation/gen"]
11
- end
7
+ require "bundler/setup"
8
+ require "speculation"
9
+ require "speculation/gen"
10
+ require "speculation/test"
12
11
 
13
12
  S = Speculation
14
13
  Gen = S::Gen
@@ -103,10 +102,10 @@ end
103
102
 
104
103
  S.exercise_fn method(:score)
105
104
 
106
- # [[[[:c, :w, :r, :c, :c, :b], [:c, :y, :y, :r, :c, :b]], {:"main/exact_matches"=>3, :"main/loose_matches"=>0}],
107
- # [[[:r, :g, :w, :c, :y], [:y, :w, :r, :w, :r]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
108
- # [[[:b, :b, :r, :b, :r], [:b, :g, :c, :c, :g]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
109
- # [[[:g, :r, :b, :y], [:g, :c, :r, :w]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
105
+ # [[[[:y, :b, :b, :b, :b, :c], [:y, :y, :g, :r, :y, :b]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
106
+ # [[[:b, :c, :w, :w, :w, :g], [:r, :w, :c, :c, :b, :w]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
107
+ # [[[:c, :c, :g, :w, :r], [:g, :g, :g, :c, :w]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
108
+ # [[[:b, :y, :g, :y, :b, :y], [:g, :w, :b, :y, :r, :c]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
110
109
 
111
110
  STest.check method(:score)
112
111
 
@@ -139,10 +138,10 @@ S.fdef method(:exact_matches),
139
138
 
140
139
  S.exercise_fn method(:exact_matches)
141
140
 
142
- # [[[[:g, :b, :b, :b, :c], [:w, :y, :c, :y, :b]], 0],
143
- # [[[:c, :w, :r, :c], [:y, :r, :y, :g]], 0],
144
- # [[[:g, :c, :g, :g, :y], [:g, :g, :c, :c, :r]], 1],
145
- # [[[:g, :r, :y, :g, :g], [:g, :y, :y, :r, :w]], 2],
141
+ # [[[[:y, :y, :c, :g, :b], [:c, :w, :g, :b, :y]], nil, 0],
142
+ # [[[:y, :c, :w, :y, :b], [:y, :w, :c, :c, :r]], nil, 1],
143
+ # [[[:r, :g, :r, :y, :y], [:c, :r, :c, :c, :w]], nil, 0],
144
+ # [[[:r, :b, :r, :c], [:y, :r, :g, :b]], nil, 0],
146
145
 
147
146
  STest.check method(:exact_matches)
148
147
  # [{:spec=>Speculation::FSpec(main.exact_matches), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
@@ -150,10 +149,11 @@ STest.check method(:exact_matches)
150
149
  STest.instrument method(:exact_matches)
151
150
  S.exercise_fn method(:score)
152
151
 
153
- # [[[[:y, :r, :y, :r], [:g, :w, :b, :g]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
154
- # [[[:c, :g, :g, :y, :b, :c], [:w, :y, :g, :b, :y, :c]], {:"main/exact_matches"=>2, :"main/loose_matches"=>0}],
155
- # [[[:r, :b, :r, :g, :w, :r], [:c, :w, :y, :g, :g, :y]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
156
- # [[[:r, :y, :c, :y, :y, :b], [:g, :r, :b, :c, :r, :y]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
152
+ # [[[[:b, :g, :w, :b, :r], [:b, :g, :y, :g, :g]], nil, {:"Object/exact_matches"=>2, :"Object/loose_matches"=>0}],
153
+ # [[[:c, :w, :g, :b, :y], [:g, :r, :g, :y, :c]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
154
+ # [[[:c, :b, :c, :y, :r], [:r, :c, :y, :r, :b]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
155
+ # [[[:y, :c, :y, :g, :c, :c], [:w, :r, :b, :w, :b, :w]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
156
+ # [[[:b, :b, :w, :c, :g, :c], [:r, :b, :y, :r, :c, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
157
157
 
158
158
  def self.score(secret, guess)
159
159
  { ns(:exact_matches) => exact_matches(secret, guess.take(3)),
@@ -180,11 +180,12 @@ S.fdef method(:match_count),
180
180
  :ret => ns(S, :natural_integer),
181
181
  :fn => ->(fn) { fn[:ret].between?(0, fn[:args][:secret].count) }
182
182
 
183
- S.exercise_fn method(:exact_matches), :n => 10, :fspec => S.get_spec(method(:match_count))
183
+ S.exercise_fn method(:exact_matches), 10, S.get_spec(method(:match_count))
184
184
 
185
- # [[[[:r, :b, :g, :w, :b], [:b, :c, :c, :r, :w]], 0],
186
- # [[[:c, :r, :c, :g, :g, :y], [:y, :c, :b, :y, :y, :r]], 0],
187
- # [[[:c, :g, :r, :y, :y], [:w, :y, :y, :c, :w]], 0],
185
+ # [[[[:w, :c, :w, :b, :b], [:r, :r, :c, :c, :r]], nil, 0],
186
+ # [[[:y, :b, :g, :y, :y], [:w, :b, :g, :y, :w]], nil, 3],
187
+ # [[[:y, :r, :r, :r, :b], [:b, :w, :b, :c, :r]], nil, 0],
188
+ # [[[:w, :g, :r, :b, :b, :b], [:y, :c, :g, :b, :c, :b]], nil, 2],
188
189
 
189
190
  STest.check_method method(:exact_matches), S.get_spec(method(:match_count))
190
191
 
@@ -194,9 +195,10 @@ STest.instrument method(:exact_matches), :spec => { method(:exact_matches) => S.
194
195
 
195
196
  S.exercise_fn method(:score)
196
197
 
197
- # [[[[:y, :r, :y, :c, :y], [:b, :w, :c, :g, :b]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
198
- # [[[:r, :b, :w, :c, :w, :y], [:w, :r, :w, :g, :b, :r]], {:"main/exact_matches"=>1, :"main/loose_matches"=>0}],
199
- # [[[:c, :w, :r, :w, :g, :c], [:g, :g, :c, :c, :c, :b]], {:"main/exact_matches"=>0, :"main/loose_matches"=>0}],
198
+ # [[[[:y, :y, :r, :b, :c, :c], [:b, :b, :b, :w, :w, :r]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
199
+ # [[[:r, :b, :r, :b, :b], [:r, :y, :c, :y, :y]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
200
+ # [[[:c, :y, :y, :w], [:y, :c, :y, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
201
+ # [[[:w, :g, :w, :g, :b, :g], [:r, :y, :r, :y, :c, :b]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
200
202
 
201
203
  STest.check method(:score)
202
204
 
@@ -212,11 +214,12 @@ def self.all_matches(secret, guess)
212
214
  reduce(0, &:+)
213
215
  end
214
216
 
215
- S.exercise_fn method(:all_matches), :n => 10, :fspec => S.get_spec(method(:match_count))
217
+ S.exercise_fn method(:all_matches), 10, S.get_spec(method(:match_count))
216
218
 
217
- # [[[[:c, :b, :w, :w], [:b, :c, :g, :y]], 2],
218
- # [[[:y, :r, :g, :r, :b], [:r, :g, :w, :b, :r]], 4],
219
- # [[[:r, :g, :r, :g, :g], [:r, :g, :g, :b, :c]], 3],
219
+ # [[[[:b, :r, :c, :r, :y, :w], [:r, :r, :c, :w, :y, :w]], nil, 5],
220
+ # [[[:c, :y, :g, :c, :r, :c], [:b, :w, :b, :b, :w, :b]], nil, 0],
221
+ # [[[:w, :g, :y, :r, :y], [:g, :y, :w, :r, :w]], nil, 4],
222
+ # [[[:g, :c, :c, :g], [:w, :g, :y, :w]], nil, 1],
220
223
 
221
224
  def self.score(secret, guess)
222
225
  exact = exact_matches(secret, guess)
@@ -231,10 +234,10 @@ STest.instrument [method(:exact_matches), method(:all_matches)],
231
234
  method(:all_matches) => S.get_spec(method(:exact_matches)) }
232
235
 
233
236
  S.exercise_fn method(:score)
234
- # [[[[:w, :r, :w, :c, :c, :c], [:r, :y, :r, :w, :c, :b]], {:"main/exact_matches"=>1, :"main/loose_matches"=>2}],
235
- # [[[:r, :c, :w, :y, :c], [:g, :c, :y, :y, :y]], {:"main/exact_matches"=>2, :"main/loose_matches"=>0}],
236
- # [[[:y, :y, :b, :g, :b], [:g, :w, :b, :c, :g]], {:"main/exact_matches"=>1, :"main/loose_matches"=>1}],
237
- # [[[:c, :b, :r, :y, :g], [:g, :w, :r, :y, :y]], {:"main/exact_matches"=>2, :"main/loose_matches"=>1}],
237
+ # [[[[:w, :g, :y, :y], [:w, :c, :c, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
238
+ # [[[:y, :w, :g, :b, :b], [:y, :w, :g, :y, :b]], nil, {:"Object/exact_matches"=>4, :"Object/loose_matches"=>0}],
239
+ # [[[:b, :b, :c, :b], [:b, :y, :w, :y]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
240
+ # [[[:y, :w, :c, :y, :c], [:y, :g, :w, :w, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>1}],
238
241
 
239
242
  STest.summarize_results STest.check method(:score)
240
243
 
@@ -14,19 +14,15 @@ STest = S::Test
14
14
  module JSONParser
15
15
  extend S::NamespacedSymbols
16
16
 
17
- def self.re_literal(string, conformed)
17
+ def self.re_literal(string)
18
18
  kvs = string.each_char.each_with_index.reduce({}) { |h, (v, i)| h.merge(i => Set[v]) }
19
19
  S.cat(kvs)
20
20
  end
21
21
 
22
22
  S.def ns(:maybe_spaces), S.zero_or_more(Set[" "])
23
23
 
24
- S.def ns(:null), re_literal("null", nil)
25
- S.def ns(:true), re_literal("true", true)
26
- S.def ns(:false), re_literal("false", false)
27
-
28
- S.def ns(:boolean), S.alt(:true => ns(:true),
29
- :false => ns(:false))
24
+ S.def ns(:boolean), S.alt(:true => re_literal("true"),
25
+ :false => re_literal("false"))
30
26
 
31
27
  S.def ns(:number), S.alt(:integer => ns(:integer),
32
28
  :float => ns(:float))
@@ -134,7 +130,7 @@ module JSONParser
134
130
  :tail => ns(:object_contents)))
135
131
 
136
132
  S.def ns(:json), S.cat(:pre => ns(:maybe_spaces),
137
- :val => S.alt(:null => ns(:null),
133
+ :val => S.alt(:null => re_literal("null"),
138
134
  :boolean => ns(:boolean),
139
135
  :number => ns(:number),
140
136
  :string => ns(:string),
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
- # See https://clojure.org/guides/spec (as of 2017-18-02)
2
+
3
+ # Speculation version of the clojure.spec guide by Alex Miller: https://clojure.org/guides/spec
3
4
  # Output generated by https://github.com/JoshCheek/seeing_is_believing
4
5
 
6
+ require "bundler/setup"
5
7
  require "set"
6
8
  require "date"
7
9
  require "speculation"
@@ -249,7 +251,7 @@ S.explain :"unq/person", :first_name => "Elon"
249
251
  # >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
250
252
  # >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
251
253
 
252
- # Unqualified keys can also be used to validate record attributes # TODO for objects/structs
254
+ # Unqualified keys can also be used to validate record attributes - don't support
253
255
  # Keyword args keys* - don't support
254
256
 
255
257
  # Sometimes it will be convenient to declare entity maps in parts, either
@@ -905,16 +907,16 @@ S.exercise S.or(:k => Symbol, :s => String, :n => Numeric), :n => 5
905
907
  # invokes the spec’ed function and returns the args and the return value.
906
908
 
907
909
  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]]
910
+ # => [[[-700291252660959460, 1315380256022004247], nil, -169059138562218507],
911
+ # [[-574949810996775321, -378316617969347527], nil, -540044395410145946],
912
+ # [[-760338081857905380, 484821106293575090], nil, 34570709564512062],
913
+ # [[-992673786379804322, 1487953925990349054], nil, -422423278764959748],
914
+ # [[-247390476258570074, 772122472766305147], nil, 397399655705976787],
915
+ # [[-1690412389556301777, -759316596276798578], nil, -1369751846456512938],
916
+ # [[1784449723230598375, 2047557531834071249], nil, 1819590014631499324],
917
+ # [[1116688093572002660, 1455424852058678548], nil, 1197557588479387983],
918
+ # [[-355113538297959975, 665164320042412170], nil, 195897461527121732],
919
+ # [[-631825347800361288, 2171238312189431384], nil, 885825441795070534]]
918
920
 
919
921
  ## Using S.and Generators
920
922
 
@@ -1022,7 +1024,6 @@ Gen.sample S.gen(ns(:syms)), 5
1022
1024
  # :"my.domain/occupation",
1023
1025
  # :"my.domain/id"]
1024
1026
 
1025
- # TODO: make gens no-arg functions???
1026
1027
  # Note that with_gen (and other places that take a custom generator) take a
1027
1028
  # one-arg function that returns the generator, allowing it to be lazily
1028
1029
  # realized.
@@ -1284,5 +1285,3 @@ STest.summarize_results STest.check(method(:run_query))
1284
1285
  # In this guide we have covered most of the features for designing and using
1285
1286
  # specs and generators. We expect to add some more advanced generator
1286
1287
  # techniques and help on testing in a future update.
1287
-
1288
- # Original author of clojure.spec guide: Alex Miller
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec.gen:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec/gen.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  require "set"
4
8
  require "rantly"
5
9
  require "rantly/property"
@@ -13,14 +17,16 @@ require "time"
13
17
 
14
18
  module Speculation
15
19
  module Gen
16
- # Adds `pred` as a Rantly `guard` to generator `gen`.
17
- # @param pred
20
+ # Adds pred block as a Rantly `guard` to generator `gen`.
18
21
  # @param gen [Proc]
22
+ # @yield generated value
19
23
  # @return [Proc]
20
24
  # @see https://github.com/abargnesi/rantly Rantly
21
- def self.such_that(pred, gen)
25
+ def self.such_that(gen)
26
+ raise ArgumentError, "block required" unless block_given?
27
+
22
28
  ->(rantly) do
23
- gen.call(rantly).tap { |val| rantly.guard(pred.call(val)) }
29
+ gen.call(rantly).tap { |val| rantly.guard(yield(val)) }
24
30
  end
25
31
  end
26
32
 
@@ -63,6 +69,13 @@ module Speculation
63
69
  end
64
70
  end
65
71
 
72
+ # @private
73
+ def self.tuple(*generators)
74
+ ->(r) do
75
+ generators.map { |g| g.call(r) }
76
+ end
77
+ end
78
+
66
79
  # @private
67
80
  GEN_BUILTINS = {
68
81
  Integer => ->(r) { r.integer },
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Speculation
4
4
  # @private
5
- class Identifier
5
+ class MethodIdentifier
6
6
  attr_reader :namespace, :name
7
7
 
8
8
  def initialize(namespace, name, is_instance_method)
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class AndSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(preds)
13
+ def initialize(preds, gen = nil)
10
14
  @preds = preds
15
+ @gen = gen
11
16
  @specs = Concurrent::Delay.new do
12
17
  preds.map { |pred| S.send(:specize, pred) }
13
18
  end
@@ -27,6 +32,10 @@ module Speculation
27
32
  S.explain_pred_list(@preds, path, via, inn, value)
28
33
  end
29
34
 
35
+ def with_gen(gen)
36
+ self.class.new(@preds, gen)
37
+ end
38
+
30
39
  def gen(overrides, path, rmap)
31
40
  if @gen
32
41
  @gen
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class EverySpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(predicate, options)
13
+ def initialize(predicate, options, gen = nil)
10
14
  @predicate = predicate
11
15
  @options = options
16
+ @gen = gen
12
17
 
13
18
  collection_predicates = [options.fetch(:kind, Enumerable)]
14
19
 
@@ -107,6 +112,10 @@ module Speculation
107
112
  probs.compact
108
113
  end
109
114
 
115
+ def with_gen(gen)
116
+ self.class.new(@predicate, @options, gen)
117
+ end
118
+
110
119
  def gen(overrides, path, rmap)
111
120
  return @gen if @gen
112
121
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class FSpec < Spec
@@ -8,11 +12,12 @@ module Speculation
8
12
 
9
13
  attr_reader :args, :ret, :fn, :block
10
14
 
11
- def initialize(args: nil, ret: nil, fn: nil, block: nil)
12
- @args = args
13
- @ret = ret
14
- @fn = fn
15
+ def initialize(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
16
+ @args = args
17
+ @ret = ret
18
+ @fn = fn
15
19
  @block = block
20
+ @gen = gen
16
21
  end
17
22
 
18
23
  def conform(f)
@@ -56,6 +61,10 @@ module Speculation
56
61
  end
57
62
  end
58
63
 
64
+ def with_gen(gen)
65
+ self.class.new(:args => @args, :ret => @ret, :fn => @fn, :block => @block, :gen => gen)
66
+ end
67
+
59
68
  def gen(overrides, _path, _rmap)
60
69
  return @gen if @gen
61
70
 
@@ -77,18 +86,13 @@ module Speculation
77
86
  # @private
78
87
  # returns f if valid, else smallest
79
88
  def self.validate_fn(f, specs, iterations)
80
- args_gen = S.gen(specs[:args])
81
-
82
- block_gen = if specs[:block]
83
- S.gen(specs[:block])
84
- else
85
- Utils.constantly(nil)
86
- end
87
-
88
- combined = ->(r) { [args_gen.call(r), block_gen.call(r)] }
89
+ args_gen = S.gen(specs[:args])
90
+ block_gen = specs[:block] ? S.gen(specs[:block]) : Utils.constantly(nil)
91
+ arg_block_gen = Gen.tuple(args_gen, block_gen)
89
92
 
90
93
  generator_guard = ->(genned_val) { S.valid?(specs[:args], genned_val) }
91
- ret = S::Test.send(:rantly_quick_check, combined, iterations, generator_guard) { |(args, block)|
94
+
95
+ ret = S::Test.send(:rantly_quick_check, arg_block_gen, iterations, generator_guard) { |(args, block)|
92
96
  call_valid?(f, specs, args, block)
93
97
  }
94
98
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class HashSpec < Spec
@@ -8,12 +12,13 @@ module Speculation
8
12
 
9
13
  attr_reader :id
10
14
 
11
- def initialize(req, opt, req_un, opt_un)
15
+ def initialize(req, opt, req_un, opt_un, gen = nil)
12
16
  @id = SecureRandom.uuid
13
17
  @req = req
14
18
  @opt = opt
15
19
  @req_un = req_un
16
20
  @opt_un = opt_un
21
+ @gen = gen
17
22
 
18
23
  req_keys = req.flat_map(&method(:extract_keys))
19
24
  req_un_specs = req_un.flat_map(&method(:extract_keys))
@@ -100,8 +105,8 @@ module Speculation
100
105
  problems.compact
101
106
  end
102
107
 
103
- def specize
104
- self
108
+ def with_gen(gen)
109
+ self.class.new(@req, @opt, @req_un, @opt_un, gen)
105
110
  end
106
111
 
107
112
  def gen(overrides, path, rmap)
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class MergeSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(preds)
13
+ def initialize(preds, gen = nil)
10
14
  @preds = preds
15
+ @gen = gen
11
16
  end
12
17
 
13
18
  def conform(x)
@@ -26,6 +31,10 @@ module Speculation
26
31
  compact
27
32
  end
28
33
 
34
+ def with_gen(gen)
35
+ self.class.new(@preds, gen)
36
+ end
37
+
29
38
  def gen(overrides, path, rmap)
30
39
  return @gen if @gen
31
40
 
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class NilableSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(pred)
13
+ def initialize(pred, gen = nil)
10
14
  @pred = pred
15
+ @gen = gen
11
16
  @delayed_spec = Concurrent::Delay.new { S.send(:specize, pred) }
12
17
  end
13
18
 
@@ -24,6 +29,10 @@ module Speculation
24
29
  )
25
30
  end
26
31
 
32
+ def with_gen(gen)
33
+ self.class.new(@pred, gen)
34
+ end
35
+
27
36
  def gen(overrides, path, rmap)
28
37
  return @gen if @gen
29
38
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
7
+ module Speculation
8
+ # @private
9
+ class NonconformingSpec < Spec
10
+ include NamespacedSymbols
11
+ S = Speculation
12
+
13
+ def initialize(spec, gen = nil)
14
+ @spec = spec
15
+ @gen = gen
16
+ @delayed_spec = Concurrent::Delay.new { S.send(:specize, spec) }
17
+ end
18
+
19
+ def conform(value)
20
+ ret = @delayed_spec.value!.conform(value)
21
+
22
+ S.invalid?(ret) ? S::INVALID : value
23
+ end
24
+
25
+ def explain(path, via, inn, value)
26
+ @delayed_spec.value!.explain(path, via, inn, value)
27
+ end
28
+
29
+ def with_gen(gen)
30
+ self.class.new(@spec, gen)
31
+ end
32
+
33
+ def gen(overrides, path, rmap)
34
+ @delayed_spec.value!.gen(overrides, path, rmap)
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class OrSpec < Spec
@@ -8,11 +12,12 @@ module Speculation
8
12
 
9
13
  attr_reader :id
10
14
 
11
- def initialize(named_specs)
15
+ def initialize(named_specs, gen = nil)
12
16
  @id = SecureRandom.uuid
13
17
  @named_specs = named_specs
14
18
  @keys = named_specs.keys
15
19
  @preds = preds = named_specs.values
20
+ @gen = gen
16
21
 
17
22
  @delayed_specs = Concurrent::Delay.new do
18
23
  preds.map { |spec| S.send(:specize, spec) }
@@ -40,8 +45,12 @@ module Speculation
40
45
  end
41
46
  end
42
47
 
48
+ def with_gen(gen)
49
+ self.class.new(@named_specs, gen)
50
+ end
51
+
43
52
  def gen(overrides, path, rmap)
44
- return gen if @gen
53
+ return @gen if @gen
45
54
 
46
55
  gs = @keys.zip(@preds).
47
56
  map { |(k, p)|
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class PredicateSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(predicate, should_conform)
13
+ def initialize(predicate, should_conform, gen = nil)
10
14
  @predicate = predicate
11
15
  @should_conform = should_conform
16
+ @gen = gen
12
17
  end
13
18
 
14
19
  def conform(value)
@@ -31,6 +36,10 @@ module Speculation
31
36
  end
32
37
  end
33
38
 
39
+ def with_gen(gen)
40
+ self.class.new(@predicate, @should_conform, gen)
41
+ end
42
+
34
43
  def gen(_, _, _)
35
44
  if @gen
36
45
  @gen
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class RegexSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(regex)
13
+ def initialize(regex, gen = nil)
10
14
  @regex = regex
15
+ @gen = gen
11
16
  end
12
17
 
13
18
  def conform(value)
@@ -26,6 +31,10 @@ module Speculation
26
31
  end
27
32
  end
28
33
 
34
+ def with_gen(gen)
35
+ self.class.new(@regex, gen)
36
+ end
37
+
29
38
  def gen(overrides, path, rmap)
30
39
  return @gen if @gen
31
40
 
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  module Speculation
4
8
  # @private
5
9
  class TupleSpec < Spec
6
10
  include NamespacedSymbols
7
11
  S = Speculation
8
12
 
9
- def initialize(preds)
13
+ def initialize(preds, gen = nil)
10
14
  @preds = preds
15
+ @gen = gen
11
16
 
12
17
  @delayed_specs = Concurrent::Delay.new do
13
18
  preds.map { |pred| S.send(:specize, pred) }
@@ -52,6 +57,10 @@ module Speculation
52
57
  end
53
58
  end
54
59
 
60
+ def with_gen(gen)
61
+ self.class.new(@preds, gen)
62
+ end
63
+
55
64
  def gen(overrides, path, rmap)
56
65
  return @gen if @gen
57
66
 
@@ -3,7 +3,7 @@
3
3
  module Speculation
4
4
  # @private
5
5
  class Spec
6
- attr_accessor :name, :gen
6
+ attr_accessor :name
7
7
  attr_reader :id
8
8
 
9
9
  def conform(_x)
@@ -34,3 +34,4 @@ require_relative "spec/every_spec"
34
34
  require_relative "spec/regex_spec"
35
35
  require_relative "spec/f_spec"
36
36
  require_relative "spec/nilable_spec"
37
+ require_relative "spec/nonconforming_spec"
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec.test:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec/test.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  require "concurrent"
4
8
  require "pp"
5
9
  require "speculation/pmap"
@@ -32,7 +36,7 @@ module Speculation
32
36
  # Given an opts hash as per instrument, returns the set of methods that can
33
37
  # be instrumented.
34
38
  # @param opts [Hash]
35
- # @return [Array<Identifier>]
39
+ # @return [Array<Method>]
36
40
  def self.instrumentable_methods(opts = {})
37
41
  if opts[:gen]
38
42
  unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
@@ -79,12 +83,12 @@ module Speculation
79
83
  # @return [Array<Method>] a collection of methods instrumented.
80
84
  def self.instrument(method_or_methods = instrumentable_methods, opts = {})
81
85
  if opts[:gen]
82
- gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
86
+ gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }
83
87
  opts = opts.merge(:gen => gens)
84
88
  end
85
89
 
86
90
  Array(method_or_methods).
87
- map { |method| S.Identifier(method) }.
91
+ map { |method| S.MethodIdentifier(method) }.
88
92
  uniq.
89
93
  map { |ident| instrument1(ident, opts) }.
90
94
  compact.
@@ -99,7 +103,7 @@ module Speculation
99
103
  method_or_methods ||= @instrumented_methods.value.keys
100
104
 
101
105
  Array(method_or_methods).
102
- map { |method| S.Identifier(method) }.
106
+ map { |method| S.MethodIdentifier(method) }.
103
107
  map { |ident| unstrument1(ident) }.
104
108
  compact.
105
109
  map(&method(:Method))
@@ -113,7 +117,7 @@ module Speculation
113
117
  # @see check see check for options and return
114
118
  def self.check_method(method, spec, opts = {})
115
119
  validate_check_opts(opts)
116
- check1(S.Identifier(method), spec, opts)
120
+ check1(S.MethodIdentifier(method), spec, opts)
117
121
  end
118
122
 
119
123
  # @param opts [Hash] an opts hash as per `check`
@@ -137,7 +141,7 @@ module Speculation
137
141
  # @option opts :num_tests [Integer] (1000) number of times to generatively test each method
138
142
  # @option opts :gen [Hash] map from spec names to generator overrides.
139
143
  # Generator overrides are passed to Speculation.gen when generating method args.
140
- # @return [Array<Identifier>] an array of check result hashes with the following keys:
144
+ # @return [Array<Hash>] an array of check result hashes with the following keys:
141
145
  # * :spec the spec tested
142
146
  # * :method optional method tested
143
147
  # * :failure optional test failure
@@ -155,10 +159,10 @@ module Speculation
155
159
  method_or_methods ||= checkable_methods
156
160
 
157
161
  checkable = Set(checkable_methods(opts))
158
- checkable.map!(&S.method(:Identifier))
162
+ checkable.map!(&S.method(:MethodIdentifier))
159
163
 
160
164
  methods = Set(method_or_methods)
161
- methods.map!(&S.method(:Identifier))
165
+ methods.map!(&S.method(:MethodIdentifier))
162
166
 
163
167
  pmap(methods.intersection(checkable)) { |ident|
164
168
  check1(ident, S.get_spec(ident), opts)
@@ -300,9 +304,9 @@ module Speculation
300
304
  end
301
305
 
302
306
  def instrument_choose_fn(f, spec, ident, opts)
303
- stubs = (opts[:stub] || []).map(&S.method(:Identifier))
307
+ stubs = (opts[:stub] || []).map(&S.method(:MethodIdentifier))
304
308
  over = opts[:gen] || {}
305
- replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
309
+ replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }
306
310
 
307
311
  if stubs.include?(ident)
308
312
  Gen.generate(S.gen(spec, over))
@@ -313,7 +317,7 @@ module Speculation
313
317
 
314
318
  def instrument_choose_spec(spec, ident, overrides)
315
319
  (overrides || {}).
316
- reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }.
320
+ reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }.
317
321
  fetch(ident, spec)
318
322
  end
319
323
 
@@ -402,7 +406,7 @@ module Speculation
402
406
  Utils.constantly(nil)
403
407
  end
404
408
 
405
- arg_block_gen = ->(r) { [args_gen.call(r), block_gen.call(r)] }
409
+ arg_block_gen = Gen.tuple(args_gen, block_gen)
406
410
 
407
411
  generator_guard = ->(genned_val) { S.valid?(spec.args, genned_val) }
408
412
  rantly_quick_check(arg_block_gen, num_tests, generator_guard) do |(args, block)|
@@ -460,7 +464,7 @@ module Speculation
460
464
  end
461
465
 
462
466
  def fn_spec_name?(spec_name)
463
- spec_name.is_a?(S::Identifier)
467
+ spec_name.is_a?(S::MethodIdentifier)
464
468
  end
465
469
 
466
470
  # Reimplementation of Rantly's `check` since it does not provide direct access to results
@@ -558,10 +562,10 @@ module Speculation
558
562
  end
559
563
  end
560
564
 
561
- # if x is an Identifier, return its method
565
+ # if x is an MethodIdentifier, return its method
562
566
  def Method(x)
563
567
  case x
564
- when Identifier then x.get_method
568
+ when MethodIdentifier then x.get_method
565
569
  when Method, UnboundMethod then x
566
570
  else raise ArgumentError, "unexpected method-like object #{x}"
567
571
  end
@@ -44,7 +44,7 @@ module Speculation
44
44
  end
45
45
 
46
46
  def self.ident?(x)
47
- x.is_a?(Symbol) || x.is_a?(Identifier)
47
+ x.is_a?(Symbol) || x.is_a?(MethodIdentifier)
48
48
  end
49
49
 
50
50
  def self.method?(x)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Speculation
4
- VERSION = "0.3.1".freeze
4
+ VERSION = "0.4.0".freeze
5
5
  end
data/lib/speculation.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is a Ruby translation of clojure.spec:
4
+ # https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
5
+ # All credit belongs with Rich Hickey and contributors for their original work.
6
+
3
7
  require "concurrent"
4
8
  require "set"
5
9
  require "securerandom"
6
10
 
7
11
  require "speculation/version"
8
12
  require "speculation/namespaced_symbols"
9
- require "speculation/identifier"
13
+ require "speculation/method_identifier"
10
14
  require "speculation/utils"
11
15
  require "speculation/spec"
12
16
  require "speculation/error"
@@ -152,7 +156,7 @@ module Speculation
152
156
  # @param value value to conform
153
157
  # @return [Symbol, Object] :Speculation/invalid if value does not match spec, else the (possibly destructured) value
154
158
  def self.conform(spec, value)
155
- spec = Identifier(spec)
159
+ spec = MethodIdentifier(spec)
156
160
  specize(spec).conform(value)
157
161
  end
158
162
 
@@ -164,7 +168,7 @@ module Speculation
164
168
  if regex?(spec)
165
169
  spec.merge(ns(:gfn) => gen)
166
170
  else
167
- specize(spec).tap { |s| s.gen = gen }
171
+ specize(spec).with_gen(gen)
168
172
  end
169
173
  end
170
174
 
@@ -189,7 +193,7 @@ module Speculation
189
193
  # where problem-hash has at least :path :pred and :val keys describing the
190
194
  # predicate and the value that failed at that path.
191
195
  def self.explain_data(spec, x)
192
- spec = Identifier(spec)
196
+ spec = MethodIdentifier(spec)
193
197
  name = spec_name(spec)
194
198
  _explain_data(spec, [], Array(name), [], x)
195
199
  end
@@ -250,7 +254,7 @@ module Speculation
250
254
  g = gfn || spec.gen(overrides, path, rmap)
251
255
 
252
256
  if g
253
- Gen.such_that(->(x) { valid?(spec, x) }, g)
257
+ Gen.such_that(g) { |x| valid?(spec, x) }
254
258
  else
255
259
  raise Speculation::Error.new("unable to construct gen at: #{path.inspect} for: #{spec.inspect}",
256
260
  ns(:failure) => :no_gen, ns(:path) => path)
@@ -272,15 +276,15 @@ module Speculation
272
276
  # @param overrides <Hash>
273
277
  # @return [Proc]
274
278
  def self.gen(spec, overrides = nil)
275
- spec = Identifier(spec)
279
+ spec = MethodIdentifier(spec)
276
280
  gensub(spec, overrides, [], RECURSION_LIMIT => recursion_limit)
277
281
  end
278
282
 
279
283
  # @private
280
- def self.Identifier(x)
284
+ def self.MethodIdentifier(x)
281
285
  case x
282
- when Method then Identifier.new(x.receiver, x.name, false)
283
- when UnboundMethod then Identifier.new(x.owner, x.name, true)
286
+ when Method then MethodIdentifier.new(x.receiver, x.name, false)
287
+ when UnboundMethod then MethodIdentifier.new(x.owner, x.name, true)
284
288
  else x
285
289
  end
286
290
  end
@@ -291,7 +295,7 @@ module Speculation
291
295
  # @param spec [Spec, Symbol, Proc, Hash] a spec, spec name, predicate or regex-op
292
296
  # @return [Symbol, Method]
293
297
  def self.def(key, spec)
294
- key = Identifier(key)
298
+ key = MethodIdentifier(key)
295
299
 
296
300
  unless Utils.ident?(key) && (!key.is_a?(Symbol) || NamespacedSymbols.namespace(key))
297
301
  raise ArgumentError, "key must be a namespaced Symbol, e.g. #{ns(:my_spec)}, or a Method"
@@ -300,14 +304,14 @@ module Speculation
300
304
  spec = if spec?(spec) || regex?(spec) || registry[spec]
301
305
  spec
302
306
  else
303
- spec_impl(spec, false)
307
+ spec_impl(spec, nil, false)
304
308
  end
305
309
 
306
310
  @registry_ref.swap do |reg|
307
311
  reg.merge(key => with_name(spec, key)).freeze
308
312
  end
309
313
 
310
- key.is_a?(Identifier) ? key.get_method : key
314
+ key.is_a?(MethodIdentifier) ? key.get_method : key
311
315
  end
312
316
 
313
317
  # @return [Hash] the registry hash
@@ -319,7 +323,7 @@ module Speculation
319
323
  # @param key [Symbol, Method]
320
324
  # @return [Spec, nil] spec registered for key, or nil
321
325
  def self.get_spec(key)
322
- registry[Identifier(key)]
326
+ registry[MethodIdentifier(key)]
323
327
  end
324
328
 
325
329
  # NOTE: it is not generally necessary to wrap predicates in spec when using
@@ -346,11 +350,7 @@ module Speculation
346
350
  # arg (Rantly instance) that generates a valid value.
347
351
  # @return [Spec]
348
352
  def self.spec(pred, gen: nil)
349
- if pred
350
- spec_impl(pred, false).tap do |spec|
351
- spec.gen = gen if gen
352
- end
353
- end
353
+ spec_impl(pred, gen, false) if pred
354
354
  end
355
355
 
356
356
  # Creates and returns a hash validating spec. :req and :opt are both arrays of
@@ -384,9 +384,7 @@ module Speculation
384
384
  # @param gen [Proc] generator function, which must be a proc of one arg
385
385
  # (Rantly instance) that generates a valid value
386
386
  def self.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil)
387
- HashSpec.new(req, opt, req_un, opt_un).tap do |spec|
388
- spec.gen = gen
389
- end
387
+ HashSpec.new(req, opt, req_un, opt_un, gen)
390
388
  end
391
389
 
392
390
  # @see keys
@@ -450,9 +448,7 @@ module Speculation
450
448
  def self.every(pred, opts = {})
451
449
  gen = opts.delete(:gen)
452
450
 
453
- EverySpec.new(pred, opts).tap do |spec|
454
- spec.gen = gen
455
- end
451
+ EverySpec.new(pred, opts, gen)
456
452
  end
457
453
 
458
454
  # Like 'every' but takes separate key and val preds and works on associative collections.
@@ -557,11 +553,11 @@ module Speculation
557
553
  { OP => AMP, :p1 => re, :predicates => preds }
558
554
  end
559
555
 
560
- # @yield [value] predicate function with the semantics of conform i.e. it should
556
+ # @param f predicate function with the semantics of conform i.e. it should
561
557
  # return either a (possibly converted) value or :"Speculation/invalid"
562
- # @return [Spec] a spec that uses block as a predicate/conformer.
563
- def self.conformer(&pred)
564
- spec_impl(pred, true)
558
+ # @return [Spec] a spec that uses pred as a predicate/conformer.
559
+ def self.conformer(f)
560
+ spec_impl(f, nil, true)
565
561
  end
566
562
 
567
563
  # Takes :args :ret and (optional) :block and :fn kwargs whose values are preds and returns a spec
@@ -581,9 +577,7 @@ module Speculation
581
577
  # @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a
582
578
  # full description of :args, :block, :ret and :fn
583
579
  def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
584
- FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block)).tap do |spec|
585
- spec.gen = gen
586
- end
580
+ FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block), :gen => gen)
587
581
  end
588
582
 
589
583
  # @param preds [Array] one or more preds
@@ -618,7 +612,7 @@ module Speculation
618
612
  # @note Note that :fn specs require the presence of :args and :ret specs to conform values, and so :fn
619
613
  # specs will be ignored if :args or :ret are missing.
620
614
  def self.fdef(method, spec)
621
- self.def(Identifier(method), fspec(spec))
615
+ self.def(MethodIdentifier(method), fspec(spec))
622
616
  method
623
617
  end
624
618
 
@@ -626,7 +620,7 @@ module Speculation
626
620
  # @param x
627
621
  # @return [Boolean] true when x is valid for spec.
628
622
  def self.valid?(spec, x)
629
- spec = Identifier(spec)
623
+ spec = MethodIdentifier(spec)
630
624
  spec = specize(spec)
631
625
 
632
626
  !invalid?(spec.conform(x))
@@ -638,6 +632,14 @@ module Speculation
638
632
  NilableSpec.new(pred)
639
633
  end
640
634
 
635
+ # @param spec
636
+ # @return [Spec] a spec that has the same properies as the given spec, except
637
+ # `conform` will return the original (not the conformed) value. Note, will
638
+ # specize regex ops.
639
+ def self.nonconforming(spec)
640
+ NonconformingSpec.new(spec)
641
+ end
642
+
641
643
  # Generates a number (default 10) of values compatible with spec and maps
642
644
  # conform over them, returning a sequence of [val conformed-val] tuples.
643
645
  # @param spec
@@ -657,12 +659,15 @@ module Speculation
657
659
  # @param method [Method]
658
660
  # @param n [Integer]
659
661
  # @param fspec [Spec]
660
- # @return [Array] an arrray of tuples of [args, ret].
661
- def self.exercise_fn(method, n: 10, fspec: nil)
662
+ # @return [Array] an array of triples of [args, block, ret].
663
+ def self.exercise_fn(method, n = 10, fspec = nil)
662
664
  fspec ||= get_spec(method)
663
665
  raise ArgumentError, "No fspec found for #{method}" unless fspec
664
666
 
665
- Gen.sample(gen(fspec.args), n).map { |args| [args, method.call(*args)] }
667
+ block_gen = fspec.block ? gen(fspec.block) : Utils.constantly(nil)
668
+ gen = Gen.tuple(gen(fspec.args), block_gen)
669
+
670
+ Gen.sample(gen, n).map { |(args, block)| [args, block, method.call(*args, &block)] }
666
671
  end
667
672
 
668
673
  ### impl ###
@@ -718,15 +723,16 @@ module Speculation
718
723
  end
719
724
 
720
725
  # @private
721
- def self.spec_impl(pred, should_conform)
726
+ def self.spec_impl(pred, gen, should_conform)
722
727
  if spec?(pred)
723
- pred
728
+ with_gen(pred, gen)
724
729
  elsif regex?(pred)
725
- RegexSpec.new(pred)
730
+ RegexSpec.new(pred, gen)
726
731
  elsif Utils.ident?(pred)
727
- the_spec(pred)
732
+ spec = the_spec(pred)
733
+ gen ? with_gen(spec, gen) : spec
728
734
  else
729
- PredicateSpec.new(pred, should_conform)
735
+ PredicateSpec.new(pred, should_conform, gen)
730
736
  end
731
737
  end
732
738
 
@@ -978,10 +984,10 @@ module Speculation
978
984
  spec
979
985
  else
980
986
  case spec
981
- when Symbol, Identifier
987
+ when Symbol, MethodIdentifier
982
988
  specize(reg_resolve!(spec))
983
989
  else
984
- spec_impl(spec, false)
990
+ spec_impl(spec, nil, false)
985
991
  end
986
992
  end
987
993
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: speculation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie English
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-27 00:00:00.000000000 Z
11
+ date: 2017-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -177,7 +177,7 @@ files:
177
177
  - lib/speculation.rb
178
178
  - lib/speculation/error.rb
179
179
  - lib/speculation/gen.rb
180
- - lib/speculation/identifier.rb
180
+ - lib/speculation/method_identifier.rb
181
181
  - lib/speculation/namespaced_symbols.rb
182
182
  - lib/speculation/pmap.rb
183
183
  - lib/speculation/spec.rb
@@ -187,6 +187,7 @@ files:
187
187
  - lib/speculation/spec/hash_spec.rb
188
188
  - lib/speculation/spec/merge_spec.rb
189
189
  - lib/speculation/spec/nilable_spec.rb
190
+ - lib/speculation/spec/nonconforming_spec.rb
190
191
  - lib/speculation/spec/or_spec.rb
191
192
  - lib/speculation/spec/predicate_spec.rb
192
193
  - lib/speculation/spec/regex_spec.rb