speculation 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +154 -53
- data/examples/codebreaker.rb +8 -8
- data/examples/game_of_life.rb +120 -0
- data/examples/sinatra-web-app/Gemfile +1 -1
- data/examples/sinatra-web-app/Gemfile.lock +3 -5
- data/examples/sinatra-web-app/app.rb +5 -5
- data/examples/spec_guide.rb +33 -28
- data/lib/speculation.rb +175 -145
- data/lib/speculation/gen.rb +11 -0
- data/lib/speculation/namespaced_symbols.rb +4 -2
- data/lib/speculation/predicates.rb +48 -0
- data/lib/speculation/spec.rb +4 -0
- data/lib/speculation/spec/and_spec.rb +6 -2
- data/lib/speculation/spec/every_spec.rb +41 -18
- data/lib/speculation/spec/f_spec.rb +8 -4
- data/lib/speculation/spec/hash_spec.rb +49 -28
- data/lib/speculation/spec/merge_spec.rb +6 -2
- data/lib/speculation/spec/nilable_spec.rb +8 -4
- data/lib/speculation/spec/nonconforming_spec.rb +5 -1
- data/lib/speculation/spec/or_spec.rb +10 -3
- data/lib/speculation/spec/predicate_spec.rb +15 -4
- data/lib/speculation/spec/regex_spec.rb +8 -4
- data/lib/speculation/spec/tuple_spec.rb +14 -6
- data/lib/speculation/test.rb +24 -24
- data/lib/speculation/utils.rb +4 -46
- data/lib/speculation/version.rb +1 -1
- metadata +5 -3
data/lib/speculation/gen.rb
CHANGED
@@ -76,6 +76,13 @@ module Speculation
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
+
# @private
|
80
|
+
def self.fmap(gen)
|
81
|
+
->(rantly) do
|
82
|
+
yield gen.call(rantly)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
79
86
|
# @private
|
80
87
|
GEN_BUILTINS = {
|
81
88
|
Integer => ->(r) { r.integer },
|
@@ -101,6 +108,10 @@ module Speculation
|
|
101
108
|
gen = Gen.gen_for_pred(Array)
|
102
109
|
Set.new(gen.call(r))
|
103
110
|
end,
|
111
|
+
SortedSet => ->(r) do
|
112
|
+
gen = Gen.gen_for_pred(Array)
|
113
|
+
SortedSet.new(gen.call(r))
|
114
|
+
end,
|
104
115
|
Hash => ->(r) do
|
105
116
|
kgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time))
|
106
117
|
vgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time, Set[true, false]))
|
@@ -13,17 +13,19 @@ module Speculation
|
|
13
13
|
namespace = name_or_namespace
|
14
14
|
else
|
15
15
|
name = name_or_namespace
|
16
|
-
namespace = is_a?(Module) ? self
|
16
|
+
namespace = is_a?(Module) ? self : self.class
|
17
17
|
end
|
18
18
|
|
19
19
|
NamespacedSymbols.symbol(namespace, name)
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.symbol(ns, name)
|
23
|
+
ns = ns.name if ns.is_a?(Module)
|
24
|
+
|
23
25
|
:"#{ns}/#{name}"
|
24
26
|
end
|
25
27
|
|
26
|
-
def self.
|
28
|
+
def self.namespaced_name(sym)
|
27
29
|
sym.to_s.split("/").last
|
28
30
|
end
|
29
31
|
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Speculation
|
2
|
+
# Collection of predicate methods used within Speculation.
|
3
|
+
# These may appear as the value for the `:pred` key in the return value of
|
4
|
+
# `Speculation.explain_data`.
|
5
|
+
module Predicates
|
6
|
+
def self.hash?(x)
|
7
|
+
x.respond_to?(:store) && x.respond_to?(:key?) && x.respond_to?(:[])
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.array?(x)
|
11
|
+
x.respond_to?(:at) && x.respond_to?(:[])
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.collection?(xs)
|
15
|
+
xs.respond_to?(:each)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.distinct?(xs)
|
19
|
+
seen = Set[]
|
20
|
+
|
21
|
+
xs.each do |x|
|
22
|
+
if seen.include?(x)
|
23
|
+
return false
|
24
|
+
else
|
25
|
+
seen << x
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.count_eq?(count, coll)
|
33
|
+
coll.count == count
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.count_between?(min_count, max_count, coll)
|
37
|
+
coll.count.between?(min_count, max_count)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.key?(hash, key)
|
41
|
+
hash.key?(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.empty?(coll)
|
45
|
+
coll.empty?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/speculation/spec.rb
CHANGED
@@ -22,12 +22,16 @@ module Speculation
|
|
22
22
|
@specs.value!.each do |spec|
|
23
23
|
value = spec.conform(value)
|
24
24
|
|
25
|
-
return
|
25
|
+
return :"Speculation/invalid" if S.invalid?(value)
|
26
26
|
end
|
27
27
|
|
28
28
|
value
|
29
29
|
end
|
30
30
|
|
31
|
+
def unform(value)
|
32
|
+
@preds.reverse.reduce(value) { |val, pred| S.unform(pred, val) }
|
33
|
+
end
|
34
|
+
|
31
35
|
def explain(path, via, inn, value)
|
32
36
|
S.explain_pred_list(@preds, path, via, inn, value)
|
33
37
|
end
|
@@ -38,7 +42,7 @@ module Speculation
|
|
38
42
|
|
39
43
|
def gen(overrides, path, rmap)
|
40
44
|
if @gen
|
41
|
-
@gen
|
45
|
+
@gen.call
|
42
46
|
else
|
43
47
|
S.gensub(@preds.first, overrides, path, rmap)
|
44
48
|
end
|
@@ -28,21 +28,27 @@ module Speculation
|
|
28
28
|
end)
|
29
29
|
end
|
30
30
|
|
31
|
+
if options[:distinct]
|
32
|
+
collection_predicates.push(->(coll) do
|
33
|
+
coll.empty? || Predicates.distinct?(coll)
|
34
|
+
end)
|
35
|
+
end
|
36
|
+
|
31
37
|
@collection_predicate = ->(coll) { collection_predicates.all? { |f| f.respond_to?(:call) ? f.call(coll) : f === coll } }
|
32
38
|
@delayed_spec = Concurrent::Delay.new { S.send(:specize, predicate) }
|
33
|
-
@kfn = options.fetch(
|
39
|
+
@kfn = options.fetch(:kfn, ->(i, _v) { i })
|
34
40
|
@conform_keys, @conform_all, @kind, @gen_into, @gen_max, @distinct, @count, @min_count, @max_count =
|
35
|
-
options.values_at(:conform_keys,
|
41
|
+
options.values_at(:conform_keys, :conform_all, :kind, :into, :gen_max, :distinct, :count, :min_count, :max_count)
|
36
42
|
@gen_max ||= 20
|
37
43
|
@conform_into = @gen_into
|
38
44
|
|
39
45
|
# returns a tuple of [init add complete] fns
|
40
46
|
@cfns = ->(x) do
|
41
|
-
if
|
42
|
-
[
|
47
|
+
if Predicates.array?(x) && (!@conform_into || Predicates.array?(@conform_into))
|
48
|
+
[->(init) { init.dup },
|
43
49
|
->(ret, i, v, cv) { v.equal?(cv) ? ret : ret.tap { |r| r[i] = cv } },
|
44
50
|
Utils.method(:itself)]
|
45
|
-
elsif
|
51
|
+
elsif Predicates.hash?(x) && ((@kind && !@conform_into) || Predicates.hash?(@conform_into))
|
46
52
|
[@conform_keys ? Utils.method(:empty) : Utils.method(:itself),
|
47
53
|
->(ret, _i, v, cv) {
|
48
54
|
if v.equal?(cv) && !@conform_keys
|
@@ -61,7 +67,7 @@ module Speculation
|
|
61
67
|
end
|
62
68
|
|
63
69
|
def conform(value)
|
64
|
-
return
|
70
|
+
return :"Speculation/invalid" unless @collection_predicate.call(value)
|
65
71
|
|
66
72
|
spec = @delayed_spec.value!
|
67
73
|
|
@@ -74,7 +80,7 @@ module Speculation
|
|
74
80
|
conformed_value = spec.conform(val)
|
75
81
|
|
76
82
|
if S.invalid?(conformed_value)
|
77
|
-
return
|
83
|
+
return :"Speculation/invalid"
|
78
84
|
else
|
79
85
|
return_value = add.call(return_value, index, val, conformed_value)
|
80
86
|
end
|
@@ -87,13 +93,30 @@ module Speculation
|
|
87
93
|
|
88
94
|
value.each_with_index do |item, index|
|
89
95
|
return value if index == limit
|
90
|
-
return
|
96
|
+
return :"Speculation/invalid" unless S.valid?(spec, item)
|
91
97
|
end
|
92
98
|
|
93
99
|
value
|
94
100
|
end
|
95
101
|
end
|
96
102
|
|
103
|
+
def unform(value)
|
104
|
+
if @conform_all
|
105
|
+
spec = @delayed_spec.value!
|
106
|
+
init, add, complete = @cfns.call(value)
|
107
|
+
|
108
|
+
ret = value.
|
109
|
+
each_with_index.
|
110
|
+
reduce(init.call(value)) { |memo, (val, index)|
|
111
|
+
add.call(memo, index, val, spec.unform(val))
|
112
|
+
}
|
113
|
+
|
114
|
+
complete.call(ret)
|
115
|
+
else
|
116
|
+
value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
97
120
|
def explain(path, via, inn, value)
|
98
121
|
probs = collection_problems(value, @kind, @distinct, @count, @min_count, @max_count, path, via, inn)
|
99
122
|
return probs if probs
|
@@ -117,7 +140,7 @@ module Speculation
|
|
117
140
|
end
|
118
141
|
|
119
142
|
def gen(overrides, path, rmap)
|
120
|
-
return @gen if @gen
|
143
|
+
return @gen.call if @gen
|
121
144
|
|
122
145
|
pgen = S.gensub(@predicate, overrides, path, rmap)
|
123
146
|
|
@@ -132,13 +155,13 @@ module Speculation
|
|
132
155
|
|
133
156
|
val = if @distinct
|
134
157
|
if @count
|
135
|
-
rantly.array(@count, &pgen).tap { |arr| rantly.guard(
|
158
|
+
rantly.array(@count, &pgen).tap { |arr| rantly.guard(Predicates.distinct?(arr)) }
|
136
159
|
else
|
137
160
|
min = @min_count || 0
|
138
161
|
max = @max_count || [@gen_max, 2 * min].max
|
139
162
|
count = rantly.range(min, max)
|
140
163
|
|
141
|
-
rantly.array(count, &pgen).tap { |arr| rantly.guard(
|
164
|
+
rantly.array(count, &pgen).tap { |arr| rantly.guard(Predicates.distinct?(arr)) }
|
142
165
|
end
|
143
166
|
elsif @count
|
144
167
|
rantly.array(@count, &pgen)
|
@@ -160,26 +183,26 @@ module Speculation
|
|
160
183
|
private
|
161
184
|
|
162
185
|
def collection_problems(x, kfn, distinct, count, min_count, max_count, path, via, inn)
|
163
|
-
pred = kfn ||
|
186
|
+
pred = kfn || Predicates.method(:collection?)
|
164
187
|
|
165
188
|
unless S.pvalid?(pred, x)
|
166
189
|
return S.explain1(pred, path, via, inn, x)
|
167
190
|
end
|
168
191
|
|
169
|
-
if count && !
|
170
|
-
return [{ :path => path, :pred => [
|
192
|
+
if count && !Predicates.count_eq?(count, x)
|
193
|
+
return [{ :path => path, :pred => [Predicates.method(:count_eq?), [count, x]], :val => x, :via => via, :in => inn }]
|
171
194
|
end
|
172
195
|
|
173
196
|
if min_count || max_count
|
174
197
|
min_count ||= 0
|
175
198
|
max_count ||= Float::INFINITY
|
176
|
-
unless
|
177
|
-
return [{ :path => path, :pred => [
|
199
|
+
unless Predicates.count_between?(min_count, max_count, x)
|
200
|
+
return [{ :path => path, :pred => [Predicates.method(:count_between?), [min_count, max_count, x]], :val => x, :via => via, :in => inn }]
|
178
201
|
end
|
179
202
|
end
|
180
203
|
|
181
|
-
if distinct && !x.empty? && !
|
182
|
-
[{ :path => path, :pred => [
|
204
|
+
if distinct && !x.empty? && !Predicates.distinct?(x)
|
205
|
+
[{ :path => path, :pred => [Predicates.method(:distinct?), [x]], :val => x, :via => via, :in => inn }]
|
183
206
|
end
|
184
207
|
end
|
185
208
|
end
|
@@ -14,7 +14,7 @@ module Speculation
|
|
14
14
|
|
15
15
|
def initialize(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
|
16
16
|
@args = args
|
17
|
-
@ret = ret
|
17
|
+
@ret = ret || :"Speculation/any"
|
18
18
|
@fn = fn
|
19
19
|
@block = block
|
20
20
|
@gen = gen
|
@@ -23,17 +23,21 @@ module Speculation
|
|
23
23
|
def conform(f)
|
24
24
|
raise "Can't conform fspec without args spec: #{inspect}" unless @args
|
25
25
|
|
26
|
-
return
|
26
|
+
return :"Speculation/invalid" unless f.is_a?(Proc) || f.is_a?(Method)
|
27
27
|
|
28
28
|
specs = { :args => @args, :ret => @ret, :fn => @fn, :block => @block }
|
29
29
|
|
30
30
|
if f.equal?(FSpec.validate_fn(f, specs, S.fspec_iterations))
|
31
31
|
f
|
32
32
|
else
|
33
|
-
|
33
|
+
:"Speculation/invalid"
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
def unform(f)
|
38
|
+
f
|
39
|
+
end
|
40
|
+
|
37
41
|
def explain(path, via, inn, f)
|
38
42
|
unless f.respond_to?(:call)
|
39
43
|
return [{ :path => path, :pred => [f.method(:respond_to?), [:call]], :val => f, :via => via, :in => inn }]
|
@@ -66,7 +70,7 @@ module Speculation
|
|
66
70
|
end
|
67
71
|
|
68
72
|
def gen(overrides, _path, _rmap)
|
69
|
-
return @gen if @gen
|
73
|
+
return @gen.call if @gen
|
70
74
|
|
71
75
|
->(_rantly) do
|
72
76
|
->(*args, &block) do
|
@@ -31,7 +31,7 @@ module Speculation
|
|
31
31
|
req_specs = req_keys + req_un_specs
|
32
32
|
req_keys += req_un_specs.map(&method(:unqualify_key))
|
33
33
|
|
34
|
-
pred_exprs = [
|
34
|
+
pred_exprs = [Predicates.method(:hash?)]
|
35
35
|
pred_exprs.push(->(v) { parse_req(req, v, Utils.method(:itself)).empty? }) if req.any?
|
36
36
|
pred_exprs.push(->(v) { parse_req(req_un, v, method(:unqualify_key)).empty? }) if req_un.any?
|
37
37
|
|
@@ -44,43 +44,61 @@ module Speculation
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def conform(value)
|
47
|
-
return
|
47
|
+
return :"Speculation/invalid" unless @keys_pred.call(value)
|
48
48
|
|
49
49
|
reg = S.registry
|
50
|
-
ret = value
|
51
50
|
|
52
|
-
value.
|
53
|
-
spec_name =
|
51
|
+
value.reduce(value) do |ret, (key, v)|
|
52
|
+
spec_name = key_to_spec_name(key)
|
54
53
|
spec = reg[spec_name]
|
55
54
|
|
56
|
-
|
55
|
+
if spec
|
56
|
+
conformed_value = S.conform(spec, v)
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
ret = ret.merge(key => conformed_value)
|
58
|
+
if S.invalid?(conformed_value)
|
59
|
+
break :"Speculation/invalid"
|
60
|
+
elsif conformed_value.equal?(v)
|
61
|
+
ret
|
62
|
+
else
|
63
|
+
ret.merge(key => conformed_value)
|
65
64
|
end
|
65
|
+
else
|
66
|
+
ret
|
66
67
|
end
|
67
68
|
end
|
69
|
+
end
|
68
70
|
|
69
|
-
|
71
|
+
def unform(value)
|
72
|
+
reg = S.registry
|
73
|
+
|
74
|
+
value.reduce(value) do |ret, (key, conformed_value)|
|
75
|
+
if reg.key?(key_to_spec_name(key))
|
76
|
+
unformed_value = S.unform(key_to_spec_name(key), conformed_value)
|
77
|
+
|
78
|
+
if conformed_value.equal?(unformed_value)
|
79
|
+
ret
|
80
|
+
else
|
81
|
+
ret.merge(key => unformed_value)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
ret
|
85
|
+
end
|
86
|
+
end
|
70
87
|
end
|
71
88
|
|
72
89
|
def explain(path, via, inn, value)
|
73
|
-
unless
|
74
|
-
return [{ :path => path, :pred => [
|
90
|
+
unless Predicates.hash?(value)
|
91
|
+
return [{ :path => path, :pred => [Predicates.method(:hash?), [value]], :val => value, :via => via, :in => inn }]
|
75
92
|
end
|
76
93
|
|
94
|
+
reg = S.registry
|
77
95
|
problems = []
|
78
96
|
|
79
97
|
if @req.any?
|
80
98
|
failures = parse_req(@req, value, Utils.method(:itself))
|
81
99
|
|
82
100
|
failures.each do |failure_sexp|
|
83
|
-
pred = [
|
101
|
+
pred = [Predicates.method(:key?), [failure_sexp]]
|
84
102
|
problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
|
85
103
|
end
|
86
104
|
end
|
@@ -89,17 +107,16 @@ module Speculation
|
|
89
107
|
failures = parse_req(@req_un, value, method(:unqualify_key))
|
90
108
|
|
91
109
|
failures.each do |failure_sexp|
|
92
|
-
pred = [
|
110
|
+
pred = [Predicates.method(:key?), [failure_sexp]]
|
93
111
|
problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
|
94
112
|
end
|
95
113
|
end
|
96
114
|
|
97
115
|
problems += value.flat_map { |(k, v)|
|
98
|
-
next unless
|
116
|
+
next unless reg.key?(key_to_spec_name(k))
|
117
|
+
next if S.pvalid?(key_to_spec_name(k), v)
|
99
118
|
|
100
|
-
|
101
|
-
S.explain1(@key_to_spec_map.fetch(k), Utils.conj(path, k), via, Utils.conj(inn, k), v)
|
102
|
-
end
|
119
|
+
S.explain1(key_to_spec_name(k), Utils.conj(path, k), via, Utils.conj(inn, k), v)
|
103
120
|
}
|
104
121
|
|
105
122
|
problems.compact
|
@@ -110,7 +127,7 @@ module Speculation
|
|
110
127
|
end
|
111
128
|
|
112
129
|
def gen(overrides, path, rmap)
|
113
|
-
return @gen if @gen
|
130
|
+
return @gen.call if @gen
|
114
131
|
|
115
132
|
rmap = S.inck(rmap, @id)
|
116
133
|
|
@@ -149,7 +166,7 @@ module Speculation
|
|
149
166
|
end
|
150
167
|
|
151
168
|
def unqualify_key(x)
|
152
|
-
NamespacedSymbols.
|
169
|
+
NamespacedSymbols.namespaced_name(x).to_sym
|
153
170
|
end
|
154
171
|
|
155
172
|
def parse_req(ks, v, f)
|
@@ -158,20 +175,20 @@ module Speculation
|
|
158
175
|
ret = if key.is_a?(Array)
|
159
176
|
op, *kks = key
|
160
177
|
case op
|
161
|
-
when
|
178
|
+
when :"Speculation/or"
|
162
179
|
if kks.any? { |k| parse_req([k], v, f).empty? }
|
163
180
|
[]
|
164
181
|
else
|
165
182
|
transform_keys([key], f)
|
166
183
|
end
|
167
|
-
when
|
184
|
+
when :"Speculation/and"
|
168
185
|
if kks.all? { |k| parse_req([k], v, f).empty? }
|
169
186
|
[]
|
170
187
|
else
|
171
188
|
transform_keys([key], f)
|
172
189
|
end
|
173
190
|
else
|
174
|
-
raise "Expected or, and, got #{op}"
|
191
|
+
raise "Expected Speculation/or, Speculation/and, got #{op}"
|
175
192
|
end
|
176
193
|
elsif v.key?(f.call(key))
|
177
194
|
[]
|
@@ -190,10 +207,14 @@ module Speculation
|
|
190
207
|
keys.map { |key|
|
191
208
|
case key
|
192
209
|
when Array then transform_keys(key, f)
|
193
|
-
when
|
210
|
+
when :"Speculation/and", :"Speculation/or" then key
|
194
211
|
else f.call(key)
|
195
212
|
end
|
196
213
|
}
|
197
214
|
end
|
215
|
+
|
216
|
+
def key_to_spec_name(k)
|
217
|
+
@key_to_spec_map.fetch(k, k)
|
218
|
+
end
|
198
219
|
end
|
199
220
|
end
|