speculation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +87 -0
  4. data/.travis.yml +16 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +116 -0
  9. data/Rakefile +29 -0
  10. data/bin/bundler +17 -0
  11. data/bin/byebug +17 -0
  12. data/bin/coderay +17 -0
  13. data/bin/console +70 -0
  14. data/bin/cucumber-queue +17 -0
  15. data/bin/minitest-queue +17 -0
  16. data/bin/pry +17 -0
  17. data/bin/rake +17 -0
  18. data/bin/rspec-queue +17 -0
  19. data/bin/rubocop +17 -0
  20. data/bin/ruby-parse +17 -0
  21. data/bin/ruby-rewrite +17 -0
  22. data/bin/setup +8 -0
  23. data/bin/testunit-queue +17 -0
  24. data/bin/yard +17 -0
  25. data/bin/yardoc +17 -0
  26. data/bin/yri +17 -0
  27. data/lib/speculation/conj.rb +32 -0
  28. data/lib/speculation/error.rb +17 -0
  29. data/lib/speculation/gen.rb +106 -0
  30. data/lib/speculation/identifier.rb +47 -0
  31. data/lib/speculation/namespaced_symbols.rb +28 -0
  32. data/lib/speculation/pmap.rb +30 -0
  33. data/lib/speculation/spec_impl/and_spec.rb +39 -0
  34. data/lib/speculation/spec_impl/every_spec.rb +176 -0
  35. data/lib/speculation/spec_impl/f_spec.rb +121 -0
  36. data/lib/speculation/spec_impl/hash_spec.rb +215 -0
  37. data/lib/speculation/spec_impl/merge_spec.rb +40 -0
  38. data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
  39. data/lib/speculation/spec_impl/or_spec.rb +62 -0
  40. data/lib/speculation/spec_impl/regex_spec.rb +35 -0
  41. data/lib/speculation/spec_impl/spec.rb +47 -0
  42. data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
  43. data/lib/speculation/spec_impl.rb +36 -0
  44. data/lib/speculation/test.rb +553 -0
  45. data/lib/speculation/utils.rb +64 -0
  46. data/lib/speculation/utils_specs.rb +57 -0
  47. data/lib/speculation/version.rb +4 -0
  48. data/lib/speculation.rb +1308 -0
  49. data/speculation.gemspec +43 -0
  50. metadata +246 -0
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using Speculation::NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class HashSpec < SpecImpl
8
+ S = Speculation
9
+
10
+ attr_reader :id
11
+
12
+ def initialize(req, opt, req_un, opt_un)
13
+ @id = SecureRandom.uuid
14
+ @req = req
15
+ @opt = opt
16
+ @req_un = req_un
17
+ @opt_un = opt_un
18
+
19
+ req_keys = req.flat_map(&method(:extract_keys))
20
+ req_un_specs = req_un.flat_map(&method(:extract_keys))
21
+
22
+ unless (req_keys + req_un_specs + opt + opt_un).all? { |s| s.is_a?(Symbol) && s.namespace }
23
+ raise "all keys must be namespaced Symbols"
24
+ end
25
+
26
+ req_specs = req_keys + req_un_specs
27
+ req_keys += req_un_specs.map(&method(:unqualify_key))
28
+
29
+ pred_exprs = [Utils.method(:hash?)]
30
+ pred_exprs.push(->(v) { parse_req(req, v, :itself.to_proc) == true }) if req.any?
31
+ pred_exprs.push(->(v) { parse_req(req_un, v, method(:unqualify_key)) == true }) if req_un.any?
32
+
33
+ @req_keys = req_keys
34
+ @req_specs = req_specs
35
+ @opt_keys = opt + opt_un.map(&method(:unqualify_key))
36
+ @opt_specs = opt + opt_un
37
+ @keys_pred = ->(v) { pred_exprs.all? { |p| p.call(v) } }
38
+ @key_to_spec_map = Hash[req_keys.concat(@opt_keys).zip(req_specs.concat(@opt_specs))]
39
+ end
40
+
41
+ def conform(value)
42
+ return :invalid.ns unless @keys_pred.call(value)
43
+
44
+ reg = S.registry
45
+ ret = value
46
+
47
+ value.each do |key, v|
48
+ spec_name = @key_to_spec_map.fetch(key, key)
49
+ spec = reg[spec_name]
50
+
51
+ next unless spec
52
+
53
+ conformed_value = S.conform(spec, v)
54
+
55
+ if S.invalid?(conformed_value)
56
+ return :invalid.ns
57
+ else
58
+ unless conformed_value.equal?(v)
59
+ ret = ret.merge(key => conformed_value)
60
+ end
61
+ end
62
+ end
63
+
64
+ ret
65
+ end
66
+
67
+ def explain(path, via, inn, value)
68
+ unless Utils.hash?(value)
69
+ return [{ :path => path, :pred => :hash?, :val => value, :via => via, :in => inn }]
70
+ end
71
+
72
+ problems = []
73
+
74
+ if @req.any?
75
+ valid_or_failure = parse_req(@req, value, :itself.to_proc)
76
+
77
+ unless valid_or_failure == true
78
+ valid_or_failure.each do |failure_sexp|
79
+ pred = sexp_to_rb(failure_sexp)
80
+ problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
81
+ end
82
+ end
83
+ end
84
+
85
+ if @req_un.any?
86
+ valid_or_failure = parse_req(@req_un, value, method(:unqualify_key))
87
+
88
+ unless valid_or_failure == true
89
+ valid_or_failure.each do |failure_sexp|
90
+ pred = sexp_to_rb(failure_sexp)
91
+ problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
92
+ end
93
+ end
94
+ end
95
+
96
+ problems += value.flat_map { |(k, v)|
97
+ next unless S.registry.key?(@key_to_spec_map[k])
98
+
99
+ unless S.pvalid?(@key_to_spec_map.fetch(k), v)
100
+ S.explain1(@key_to_spec_map.fetch(k), path.conj(k), via, inn.conj(k), v)
101
+ end
102
+ }
103
+
104
+ problems.compact
105
+ end
106
+
107
+ def specize
108
+ self
109
+ end
110
+
111
+ def gen(overrides, path, rmap)
112
+ return @gen if @gen
113
+
114
+ rmap = S.inck(rmap, @id)
115
+
116
+ reqs = @req_keys.zip(@req_specs).
117
+ reduce({}) { |m, (k, s)|
118
+ m.merge(k => S.gensub(s, overrides, path.conj(k), rmap))
119
+ }
120
+
121
+ opts = @opt_keys.zip(@opt_specs).
122
+ reduce({}) { |m, (k, s)|
123
+ if S.recur_limit?(rmap, @id, path, k)
124
+ m
125
+ else
126
+ m.merge(k => Gen.delay { S.gensub(s, overrides, path.conj(k), rmap) })
127
+ end
128
+ }
129
+
130
+ ->(rantly) do
131
+ count = rantly.range(0, opts.count)
132
+ opts = opts.to_a.shuffle.take(count).to_h
133
+
134
+ reqs.merge(opts).each_with_object({}) { |(k, spec_gen), h|
135
+ h[k] = spec_gen.call(rantly)
136
+ }
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def sexp_to_rb(sexp, level = 0)
143
+ if sexp.is_a?(Array)
144
+ op, *keys = sexp
145
+ rb_string = String.new
146
+
147
+ rb_string << "(" unless level.zero?
148
+
149
+ keys.each_with_index do |key, i|
150
+ unless i.zero?
151
+ rb_string << " #{op.name} "
152
+ end
153
+
154
+ rb_string << sexp_to_rb(key, level + 1)
155
+ end
156
+
157
+ rb_string << ")" unless level.zero?
158
+
159
+ rb_string
160
+ else
161
+ ":#{sexp}"
162
+ end
163
+ end
164
+
165
+ def extract_keys(symbol_or_arr)
166
+ if symbol_or_arr.is_a?(Array)
167
+ symbol_or_arr[1..-1].flat_map(&method(:extract_keys))
168
+ else
169
+ symbol_or_arr
170
+ end
171
+ end
172
+
173
+ def unqualify_key(x)
174
+ x.name.to_sym
175
+ end
176
+
177
+ def parse_req(ks, v, f)
178
+ key, *ks = ks
179
+
180
+ ret = if key.is_a?(Array)
181
+ op, *kks = key
182
+ case op
183
+ when :or.ns
184
+ if kks.one? { |k| parse_req([k], v, f) == true }
185
+ true
186
+ else
187
+ [key]
188
+ end
189
+ when :and.ns
190
+ if kks.all? { |k| parse_req([k], v, f) == true }
191
+ true
192
+ else
193
+ [key]
194
+ end
195
+ else
196
+ raise "Expected or, and, got #{op}"
197
+ end
198
+ elsif v.key?(f.call(key))
199
+ true
200
+ else
201
+ [key]
202
+ end
203
+
204
+ if ks.any?
205
+ if ret == true
206
+ parse_req(ks, v, f)
207
+ else
208
+ ret + parse_req(ks, v, f)
209
+ end
210
+ else
211
+ ret
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using NamespacedSymbols.refine(self)
4
+
5
+ # @private
6
+ class MergeSpec < SpecImpl
7
+ S = Speculation
8
+
9
+ def initialize(preds)
10
+ @preds = preds
11
+ end
12
+
13
+ def conform(x)
14
+ ms = @preds.map { |pred| S.dt(pred, x) }
15
+
16
+ if ms.any?(&S.method(:invalid?))
17
+ :invalid.ns(S)
18
+ else
19
+ ms.reduce(&:merge)
20
+ end
21
+ end
22
+
23
+ def explain(path, via, inn, x)
24
+ @preds.
25
+ flat_map { |pred| S.explain1(pred, path, via, inn, x) }.
26
+ compact
27
+ end
28
+
29
+ def gen(overrides, path, rmap)
30
+ return @gen if @gen
31
+
32
+ gens = @preds.
33
+ map { |pred| S.gensub(pred, overrides, path, rmap) }
34
+
35
+ ->(r) do
36
+ gens.map { |gen| gen.call(r) }.reduce(&:merge)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class NilableSpec < SpecImpl
8
+ S = Speculation
9
+
10
+ def initialize(pred)
11
+ @pred = pred
12
+ @delayed_spec = Concurrent::Delay.new { S.send(:specize, pred) }
13
+ end
14
+
15
+ def conform(value)
16
+ value.nil? ? value : @delayed_spec.value.conform(value)
17
+ end
18
+
19
+ def explain(path, via, inn, value)
20
+ return if S.pvalid?(@delayed_spec.value, value) || value.nil?
21
+
22
+ S.
23
+ explain1(@pred, path.conj(:pred.ns), via, inn, value).
24
+ conj(:path => path.conj(:nil.ns), :pred => NilClass, :val => value, :via => via, :in => inn)
25
+ end
26
+
27
+ def gen(overrides, path, rmap)
28
+ return @gen if @gen
29
+
30
+ ->(rantly) do
31
+ rantly.freq([1, Gen.delay { Utils.constantly(nil) }],
32
+ [9, Gen.delay { S.gensub(@pred, overrides, path.conj(:pred.ns), rmap) }])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class OrSpec < SpecImpl
8
+ S = Speculation
9
+
10
+ attr_reader :id
11
+
12
+ def initialize(named_specs)
13
+ @id = SecureRandom.uuid
14
+ @named_specs = named_specs
15
+ @keys = named_specs.keys
16
+ @preds = preds = named_specs.values
17
+
18
+ @delayed_specs = Concurrent::Delay.new do
19
+ preds.map { |spec| S.send(:specize, spec) }
20
+ end
21
+ end
22
+
23
+ def conform(value)
24
+ @delayed_specs.value.each_with_index do |spec, index|
25
+ conformed = spec.conform(value)
26
+
27
+ unless S.invalid?(conformed)
28
+ return [@keys[index], conformed]
29
+ end
30
+ end
31
+
32
+ :invalid.ns
33
+ end
34
+
35
+ def explain(path, via, inn, value)
36
+ return if S.pvalid?(self, value)
37
+
38
+ @keys.zip(@preds).flat_map do |(key, pred)|
39
+ next if S.pvalid?(pred, value)
40
+ S.explain1(pred, path.conj(key), via, inn, value)
41
+ end
42
+ end
43
+
44
+ def gen(overrides, path, rmap)
45
+ return gen if @gen
46
+
47
+ gs = @keys.zip(@preds).
48
+ map { |(k, p)|
49
+ rmap = S.inck(rmap, @id)
50
+
51
+ unless S.recur_limit?(rmap, @id, path, k)
52
+ Gen.delay { S.gensub(p, overrides, path.conj(k), rmap) }
53
+ end
54
+ }.
55
+ compact
56
+
57
+ unless gs.empty?
58
+ ->(rantly) { rantly.branch(*gs) }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using NamespacedSymbols.refine(self)
4
+
5
+ # @private
6
+ class RegexSpec < SpecImpl
7
+ S = Speculation
8
+
9
+ def initialize(regex)
10
+ @regex = regex
11
+ end
12
+
13
+ def conform(value)
14
+ if value.nil? || Utils.collection?(value)
15
+ S.re_conform(@regex, value)
16
+ else
17
+ :invalid.ns
18
+ end
19
+ end
20
+
21
+ def explain(path, via, inn, value)
22
+ if value.nil? || Utils.collection?(value)
23
+ S.re_explain(path, via, inn, @regex, value || [])
24
+ else
25
+ [{ :path => path, :val => value, :via => via, :in => inn }]
26
+ end
27
+ end
28
+
29
+ def gen(overrides, path, rmap)
30
+ return @gen if @gen
31
+
32
+ S.re_gen(@regex, overrides, path, rmap)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using Speculation::NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class Spec < SpecImpl
8
+ S = Speculation
9
+
10
+ def initialize(predicate, should_conform)
11
+ @predicate = predicate
12
+ @should_conform = should_conform
13
+ end
14
+
15
+ def conform(value)
16
+ ret = case @predicate
17
+ when Set then @predicate.include?(value)
18
+ when Regexp, Module then @predicate === value
19
+ else @predicate.call(value)
20
+ end
21
+
22
+ if @should_conform
23
+ ret
24
+ else
25
+ ret ? value : :invalid.ns
26
+ end
27
+ end
28
+
29
+ def explain(path, via, inn, value)
30
+ if S.invalid?(S.dt(@predicate, value))
31
+ [{ :path => path, :val => value, :via => via, :in => inn, :pred => @predicate }]
32
+ end
33
+ end
34
+
35
+ def gen(_, _, _)
36
+ if @gen
37
+ @gen
38
+ else
39
+ Gen.gen_for_pred(@predicate)
40
+ end
41
+ end
42
+
43
+ def inspect
44
+ "#{self.class}(#{@name || @predicate.inspect})"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using Speculation::NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class TupleSpec < SpecImpl
8
+ S = Speculation
9
+
10
+ def initialize(preds)
11
+ @preds = preds
12
+
13
+ @delayed_specs = Concurrent::Delay.new do
14
+ preds.map { |pred| S.send(:specize, pred) }
15
+ end
16
+ end
17
+
18
+ def conform(collection)
19
+ specs = @delayed_specs.value
20
+
21
+ unless Utils.array?(collection) && collection.count == specs.count
22
+ return :invalid.ns
23
+ end
24
+
25
+ return_value = collection.class.new
26
+
27
+ collection.zip(specs).each do |(value, spec)|
28
+ conformed_value = spec.conform(value)
29
+
30
+ if S.invalid?(conformed_value)
31
+ return :invalid.ns
32
+ else
33
+ return_value += [conformed_value]
34
+ end
35
+ end
36
+
37
+ return_value
38
+ end
39
+
40
+ def explain(path, via, inn, value)
41
+ if !Utils.array?(value)
42
+ [{ :path => path, :val => value, :via => via, :in => inn, :pred => "array?" }]
43
+ elsif @preds.count != value.count
44
+ [{ :path => path, :val => value, :via => via, :in => inn, :pred => "count == predicates.count" }]
45
+ else
46
+ probs = @preds.zip(value).each_with_index.flat_map { |(pred, x), index|
47
+ unless S.pvalid?(pred, x)
48
+ S.explain1(pred, path.conj(index), via, inn.conj(index), x)
49
+ end
50
+ }
51
+
52
+ probs.compact
53
+ end
54
+ end
55
+
56
+ def gen(overrides, path, rmap)
57
+ return @gen if @gen
58
+
59
+ gens = @preds.each_with_index.
60
+ map { |p, i| S.gensub(p, overrides, path.conj(i), rmap) }
61
+
62
+ ->(rantly) do
63
+ gens.map { |g| g.call(rantly) }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
4
+ # @private
5
+ class SpecImpl
6
+ attr_accessor :name, :gen
7
+ attr_reader :id
8
+
9
+ def conform(_x)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def explain(_path, _via, _inn, _value)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def gen(_overrides, _path, _rmap)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def inspect
22
+ "#{self.class}(#{name})"
23
+ end
24
+ end
25
+ end
26
+
27
+ require_relative "spec_impl/hash_spec"
28
+ require_relative "spec_impl/spec"
29
+ require_relative "spec_impl/tuple_spec"
30
+ require_relative "spec_impl/or_spec"
31
+ require_relative "spec_impl/and_spec"
32
+ require_relative "spec_impl/merge_spec"
33
+ require_relative "spec_impl/every_spec"
34
+ require_relative "spec_impl/regex_spec"
35
+ require_relative "spec_impl/f_spec"
36
+ require_relative "spec_impl/nilable_spec"