speculation 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +10 -1
- data/Gemfile +1 -0
- data/README.md +4 -1
- data/Rakefile +2 -1
- data/examples/json_parser.rb +328 -0
- data/lib/speculation/error.rb +1 -0
- data/lib/speculation/gen.rb +1 -0
- data/lib/speculation/pmap.rb +1 -0
- data/lib/speculation/spec/every_spec.rb +1 -0
- data/lib/speculation/spec/hash_spec.rb +1 -0
- data/lib/speculation/spec/merge_spec.rb +1 -0
- data/lib/speculation/spec/nilable_spec.rb +1 -0
- data/lib/speculation/spec/or_spec.rb +1 -0
- data/lib/speculation/spec/predicate_spec.rb +1 -0
- data/lib/speculation/spec/regex_spec.rb +1 -0
- data/lib/speculation/spec/tuple_spec.rb +1 -0
- data/lib/speculation/test.rb +1 -0
- data/lib/speculation/utils.rb +1 -0
- data/lib/speculation/version.rb +2 -1
- data/lib/speculation.rb +120 -116
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21bae113d98b1aae417ce30534a9488f34ddbaa0
|
4
|
+
data.tar.gz: aa53139eed9b655d4ce4a1ff9984881e94f4e954
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23a0f3f05968623d4cc91d34f7929fc2e76c42679930e9abb35e6f7e9fadfc7648d48a18c4b80452ec6871832ab1c16d4880f735dc1c6033649560fa12e1eaa7
|
7
|
+
data.tar.gz: 525d9d6479c8accd5b9347b7f8f97568ca708213c82db5538a731e072ba8b31e31df20e79ee5247b787611c8d16db8b22858a101419c5dbdff180447f6329e9f
|
data/.rubocop.yml
CHANGED
@@ -4,7 +4,7 @@ AllCops:
|
|
4
4
|
- '*.*'
|
5
5
|
- 'vendor/**/*'
|
6
6
|
- 'examples/**/*'
|
7
|
-
TargetRubyVersion: 2.
|
7
|
+
TargetRubyVersion: 2.0
|
8
8
|
|
9
9
|
Metrics/ClassLength:
|
10
10
|
Enabled: false
|
@@ -98,3 +98,12 @@ Performance/RedundantBlockCall:
|
|
98
98
|
|
99
99
|
Style/MethodName:
|
100
100
|
Enabled: false
|
101
|
+
|
102
|
+
Style/SymbolArray:
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
Style/IndentHeredoc:
|
106
|
+
Enabled: false
|
107
|
+
|
108
|
+
Style/EmptyLiteral:
|
109
|
+
Enabled: false
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -11,10 +11,13 @@ The goal of this project is to match clojure.spec as closely as possible, from d
|
|
11
11
|
- [sinatra-web-app](examples/sinatra-web-app): A small sinatra web application demonstrating model validation and API error message generation.
|
12
12
|
- [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec)
|
13
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.
|
14
15
|
|
15
16
|
## Usage
|
16
17
|
|
17
|
-
The API is more-or-less the same as `clojure.spec`. If you're already familiar clojure.spec with then you should feel at home with Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
|
18
|
+
Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/speculation). The API is more-or-less the same as `clojure.spec`. If you're already familiar clojure.spec with then you should feel at home with Speculation. Most guides, talks and discussion around clojure.spec should apply equally well to Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
|
19
|
+
|
20
|
+
## Differences with clojure.spec
|
18
21
|
|
19
22
|
### Built in predicates
|
20
23
|
|
data/Rakefile
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "bundler/gem_tasks"
|
3
4
|
require "rubocop"
|
4
5
|
require "yard"
|
5
6
|
|
6
7
|
task :rubocop do
|
7
|
-
status = RuboCop::CLI.new.run([])
|
8
|
+
status = RuboCop::CLI.new.run(["--display-cop-names"])
|
8
9
|
raise "failed with status #{status}" unless status.zero?
|
9
10
|
end
|
10
11
|
|
@@ -0,0 +1,328 @@
|
|
1
|
+
# Exercise from Ruby Quiz: http://rubyquiz.com/quiz155.html
|
2
|
+
# Borrowed heavily from TreeTop parser used in http://learnruby.com/examples/ruby-quiz-155.shtml
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "minitest/autorun"
|
6
|
+
require "speculation"
|
7
|
+
require "speculation/gen"
|
8
|
+
require "speculation/test"
|
9
|
+
|
10
|
+
S = Speculation
|
11
|
+
Gen = S::Gen
|
12
|
+
STest = S::Test
|
13
|
+
|
14
|
+
module JSONParser
|
15
|
+
extend S::NamespacedSymbols
|
16
|
+
|
17
|
+
def self.re_literal(string, conformed)
|
18
|
+
kvs = string.each_char.each_with_index.reduce({}) { |h, (v, i)| h.merge(i => Set[v]) }
|
19
|
+
S.cat(kvs)
|
20
|
+
end
|
21
|
+
|
22
|
+
S.def ns(:maybe_spaces), S.zero_or_more(Set[" "])
|
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))
|
30
|
+
|
31
|
+
S.def ns(:number), S.alt(:integer => ns(:integer),
|
32
|
+
:float => ns(:float))
|
33
|
+
|
34
|
+
S.def ns(:digit), /\A[[:digit:]]\z/
|
35
|
+
S.def ns(:digits), S.one_or_more(ns(:digit))
|
36
|
+
S.def ns(:exp), Set["e", "E"]
|
37
|
+
S.def ns(:maybe_sign), S.zero_or_one(Set["-", "+"])
|
38
|
+
S.def ns(:exponent), S.cat(:pre => ns(:exp),
|
39
|
+
:sign => ns(:maybe_sign),
|
40
|
+
:digits => ns(:digits))
|
41
|
+
S.def ns(:maybe_exponent), S.zero_or_one(ns(:exponent))
|
42
|
+
|
43
|
+
S.def ns(:integer), S.cat(:neg => S.zero_or_one(Set["-"]),
|
44
|
+
:digits => ns(:digits),
|
45
|
+
:exponent => ns(:maybe_exponent))
|
46
|
+
|
47
|
+
S.def ns(:maybe_neg), S.zero_or_one(Set["-"])
|
48
|
+
S.def ns(:float), S.cat(:neg => ns(:maybe_neg),
|
49
|
+
:digits => ns(:digits),
|
50
|
+
:dot => Set["."],
|
51
|
+
:more_digits => ns(:digits),
|
52
|
+
:exponent => ns(:maybe_exponent))
|
53
|
+
|
54
|
+
S.def ns(:quote), Set['"']
|
55
|
+
S.def ns(:maybe_characters), S.zero_or_more(ns(:character))
|
56
|
+
S.def ns(:string), S.cat(:open => ns(:quote),
|
57
|
+
:contents => ns(:maybe_characters),
|
58
|
+
:close => ns(:quote))
|
59
|
+
|
60
|
+
|
61
|
+
S.def ns(:character), S.alt(:escaped => ns(:escaped_character),
|
62
|
+
:special => ns(:special_character),
|
63
|
+
:unicode => ns(:unicode_character),
|
64
|
+
:regular => ns(:regular_character))
|
65
|
+
|
66
|
+
S.def ns(:unicode_character), S.cat(:escape => Set['\\'],
|
67
|
+
:u => Set['u'],
|
68
|
+
:digits => S.constrained(S.one_or_more(ns(:hex_digit)), ->(digits) { digits.count == 4 }))
|
69
|
+
|
70
|
+
S.def ns(:hex_digit), /[0-9a-fA-F]/
|
71
|
+
|
72
|
+
S.def ns(:escaped_character), S.cat(:escape => Set['\\'],
|
73
|
+
:val => Set['\\', '"'])
|
74
|
+
|
75
|
+
special_character_map = {
|
76
|
+
"b" => "\b",
|
77
|
+
"f" => "\f",
|
78
|
+
"n" => "\n",
|
79
|
+
"r" => "\r",
|
80
|
+
"t" => "\t",
|
81
|
+
}
|
82
|
+
|
83
|
+
S.def ns(:special_character), S.cat(:escape => Set['\\'],
|
84
|
+
:val => Set['b', 'f', 'n', 'r', 't'])
|
85
|
+
|
86
|
+
S.def ns(:regular_character), ->(s) { !['\\', '"'].include?(s) }
|
87
|
+
|
88
|
+
S.def ns(:open_square_bracket), Set["["]
|
89
|
+
S.def ns(:close_square_bracket), Set["]"]
|
90
|
+
S.def ns(:empty_array), S.cat(:open => ns(:open_square_bracket),
|
91
|
+
:spaces => ns(:maybe_spaces),
|
92
|
+
:close => ns(:close_square_bracket))
|
93
|
+
|
94
|
+
S.def ns(:non_empty_array), S.cat(:open => ns(:open_square_bracket),
|
95
|
+
:space => ns(:maybe_spaces),
|
96
|
+
:value_list => ns(:value_list),
|
97
|
+
:more_space => ns(:maybe_spaces),
|
98
|
+
:close => ns(:close_square_bracket))
|
99
|
+
|
100
|
+
S.def ns(:array), S.alt(:non_empty => ns(:non_empty_array),
|
101
|
+
:empty => ns(:empty_array))
|
102
|
+
|
103
|
+
S.def ns(:value_list), S.alt(:val => ns(:json),
|
104
|
+
:rest => S.cat(:val => ns(:json),
|
105
|
+
:comma => Set[","],
|
106
|
+
:space => ns(:maybe_spaces),
|
107
|
+
:tail => ns(:value_list)))
|
108
|
+
|
109
|
+
S.def ns(:open_brace), Set["{"]
|
110
|
+
S.def ns(:close_brace), Set["}"]
|
111
|
+
S.def ns(:empty_object), S.cat(:open => ns(:open_brace),
|
112
|
+
:spaces => ns(:maybe_spaces),
|
113
|
+
:close => ns(:close_brace))
|
114
|
+
|
115
|
+
S.def ns(:non_empty_object), S.cat(:open => ns(:open_brace),
|
116
|
+
:spaces => ns(:maybe_spaces),
|
117
|
+
:contents => ns(:object_contents),
|
118
|
+
:more_spaces => ns(:maybe_spaces),
|
119
|
+
:close => ns(:close_brace))
|
120
|
+
|
121
|
+
S.def ns(:object), S.alt(:empty_object => ns(:empty_object),
|
122
|
+
:non_empty_object => ns(:non_empty_object))
|
123
|
+
|
124
|
+
S.def ns(:kv), S.cat(:before => ns(:maybe_spaces),
|
125
|
+
:key => ns(:string),
|
126
|
+
:colon => Set[":"],
|
127
|
+
:after => ns(:maybe_spaces),
|
128
|
+
:val => ns(:json))
|
129
|
+
|
130
|
+
S.def ns(:object_contents), S.alt(:kv => ns(:kv),
|
131
|
+
:rest => S.cat(:kv => ns(:kv),
|
132
|
+
:comma => Set[","],
|
133
|
+
:space => ns(:maybe_spaces),
|
134
|
+
:tail => ns(:object_contents)))
|
135
|
+
|
136
|
+
S.def ns(:json), S.cat(:pre => ns(:maybe_spaces),
|
137
|
+
:val => S.alt(:null => ns(:null),
|
138
|
+
:boolean => ns(:boolean),
|
139
|
+
:number => ns(:number),
|
140
|
+
:string => ns(:string),
|
141
|
+
:array => ns(:array),
|
142
|
+
:object => ns(:object)),
|
143
|
+
:post => ns(:maybe_spaces))
|
144
|
+
|
145
|
+
def self.parse(s)
|
146
|
+
chars = s.split("")
|
147
|
+
json_data = S.conform(ns(:json), chars)
|
148
|
+
|
149
|
+
if S.invalid?(json_data)
|
150
|
+
fail S.explain_str(ns(:json), chars)
|
151
|
+
else
|
152
|
+
transform_json(json_data)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.transform_json(data)
|
157
|
+
tag, val = data[:val]
|
158
|
+
|
159
|
+
case tag
|
160
|
+
when :boolean then transform_boolean(val)
|
161
|
+
when :null then nil
|
162
|
+
when :number then transform_number(val)
|
163
|
+
when :string then transform_string(val)
|
164
|
+
when :array then transform_array(val)
|
165
|
+
when :object then transform_object(val)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.transform_boolean(tagged_val)
|
170
|
+
tag, val = tagged_val
|
171
|
+
|
172
|
+
case tag
|
173
|
+
when :true then true
|
174
|
+
when :false then false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.transform_number(tagged_val)
|
179
|
+
tag, val = tagged_val
|
180
|
+
|
181
|
+
case tag
|
182
|
+
when :integer
|
183
|
+
Integer(val.values_at(:neg, :digits, :exponent).join)
|
184
|
+
when :float
|
185
|
+
exp = Hash(val[:exponent]).values_at(:pre, :sign, :digits).join
|
186
|
+
val = val.merge(:exp => exp)
|
187
|
+
Float(val.values_at(:neg, :digits, :dot, :more_digits, :exp).join)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
SPECIAL_CHARACTER_MAP = {
|
192
|
+
"b" => "\b",
|
193
|
+
"f" => "\f",
|
194
|
+
"n" => "\n",
|
195
|
+
"r" => "\r",
|
196
|
+
"t" => "\t",
|
197
|
+
}
|
198
|
+
|
199
|
+
def self.transform_string(val)
|
200
|
+
Array(val[:contents]).map { |(tag, char)|
|
201
|
+
case tag
|
202
|
+
when :regular then char
|
203
|
+
when :special then SPECIAL_CHARACTER_MAP.fetch(char[:val])
|
204
|
+
when :escaped then char[:val]
|
205
|
+
when :unicode then [char[:digits].join.hex].pack("U")
|
206
|
+
end
|
207
|
+
}.join
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.transform_array(tagged_val)
|
211
|
+
tag, val = tagged_val
|
212
|
+
|
213
|
+
case tag
|
214
|
+
when :empty then []
|
215
|
+
when :non_empty then transform_value_list(val[:value_list])
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.transform_value_list(tagged_val)
|
220
|
+
tag, val = tagged_val
|
221
|
+
|
222
|
+
case tag
|
223
|
+
when :rest
|
224
|
+
head = [transform_json(val[:val])]
|
225
|
+
tail = transform_value_list(val[:tail])
|
226
|
+
head + tail
|
227
|
+
when :val
|
228
|
+
[transform_json(val)]
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.transform_object(tagged_val)
|
233
|
+
tag, val = tagged_val
|
234
|
+
|
235
|
+
case tag
|
236
|
+
when :empty_object then {}
|
237
|
+
when :non_empty_object then transform_object_contents(val[:contents])
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def self.transform_object_contents(tagged_val)
|
242
|
+
tag, val = tagged_val
|
243
|
+
|
244
|
+
case tag
|
245
|
+
when :kv
|
246
|
+
{ transform_string(val[:key]) => transform_json(val[:val]) }
|
247
|
+
when :rest
|
248
|
+
transform_object_contents([:kv, val[:kv]]).
|
249
|
+
merge(transform_object_contents(val[:tail]))
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class TestJSONParser < Minitest::Test
|
255
|
+
def test_keyword_parsing
|
256
|
+
assert_parses true, "true"
|
257
|
+
assert_parses false, "false"
|
258
|
+
assert_parses nil, "null"
|
259
|
+
end
|
260
|
+
|
261
|
+
def test_number_parsing
|
262
|
+
assert_parses 42, "42"
|
263
|
+
assert_parses -13, "-13"
|
264
|
+
assert_parses 3.1415, "3.1415"
|
265
|
+
assert_parses -0.01, "-0.01"
|
266
|
+
|
267
|
+
assert_parses 0.2e1, "0.2e1"
|
268
|
+
assert_parses 0.2e+1, "0.2e+1"
|
269
|
+
assert_parses 0.2e-1, "0.2e-1"
|
270
|
+
assert_parses 0.2E1, "0.2e1"
|
271
|
+
end
|
272
|
+
|
273
|
+
def test_string_parsing
|
274
|
+
assert_parses String.new, '""'
|
275
|
+
assert_parses "JSON", '"JSON"'
|
276
|
+
|
277
|
+
assert_parses 'nested "quotes"', '"nested \"quotes\""'
|
278
|
+
assert_parses "\n", '"\\n"'
|
279
|
+
|
280
|
+
assert_parses "µ", '"\\u00b5"'
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_array_parsing
|
284
|
+
assert_parses [], '[]'
|
285
|
+
|
286
|
+
assert_parses ["foo", "bar", "baz"], '["foo", "bar", "baz"]'
|
287
|
+
assert_parses ["JSON", 3.1415, true], '["JSON", 3.1415, true]'
|
288
|
+
assert_parses [1, [2, [3]]], '[1, [2, [3]]]'
|
289
|
+
end
|
290
|
+
|
291
|
+
def test_object_parsing
|
292
|
+
assert_parses Hash[], '{}'
|
293
|
+
assert_parses Hash["foo" => "bar"], '{"foo": "bar"}'
|
294
|
+
assert_parses Hash["foo" => "bar", "baz" => "qux"], '{"foo": "bar", "baz": "qux"}'
|
295
|
+
assert_parses Hash["JSON" => 3.1415, "data" => true], '{"JSON": 3.1415, "data": true}'
|
296
|
+
|
297
|
+
assert_parses Hash["Array" => [1, 2, 3], "Object" => {"nested" => "objects"}],
|
298
|
+
'{"Array": [1, 2, 3], "Object": {"nested": "objects"}}'
|
299
|
+
end
|
300
|
+
|
301
|
+
def test_parse_errors
|
302
|
+
assert_invalid "{"
|
303
|
+
assert_invalid %q{{"key": true false}}
|
304
|
+
|
305
|
+
assert_invalid "["
|
306
|
+
assert_invalid "[1,,2]"
|
307
|
+
assert_invalid '"'
|
308
|
+
assert_invalid '"\\i"'
|
309
|
+
|
310
|
+
assert_invalid "$1,000"
|
311
|
+
assert_invalid "1_000"
|
312
|
+
assert_invalid "1K"
|
313
|
+
|
314
|
+
assert_invalid "unknown"
|
315
|
+
end
|
316
|
+
|
317
|
+
def assert_invalid(json_string)
|
318
|
+
assert_raises(RuntimeError) { JSONParser.parse(json_string) }
|
319
|
+
end
|
320
|
+
|
321
|
+
def assert_parses(expected_val, json_string)
|
322
|
+
if expected_val.nil?
|
323
|
+
assert_nil(JSONParser.parse(json_string))
|
324
|
+
else
|
325
|
+
assert_equal(expected_val, JSONParser.parse(json_string))
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/lib/speculation/error.rb
CHANGED
data/lib/speculation/gen.rb
CHANGED
data/lib/speculation/pmap.rb
CHANGED
data/lib/speculation/test.rb
CHANGED
data/lib/speculation/utils.rb
CHANGED
data/lib/speculation/version.rb
CHANGED
data/lib/speculation.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "concurrent"
|
3
4
|
require "set"
|
4
5
|
require "securerandom"
|
@@ -44,6 +45,27 @@ module Speculation
|
|
44
45
|
|
45
46
|
INVALID = ns(:invalid)
|
46
47
|
|
48
|
+
# @private
|
49
|
+
OP = ns(:op)
|
50
|
+
# @private
|
51
|
+
ALT = ns(:alt)
|
52
|
+
# @private
|
53
|
+
AMP = ns(:amp)
|
54
|
+
# @private
|
55
|
+
PCAT = ns(:pcat)
|
56
|
+
# @private
|
57
|
+
REP = ns(:rep)
|
58
|
+
# @private
|
59
|
+
ACCEPT = ns(:accept)
|
60
|
+
# @private
|
61
|
+
NIL = ns(:nil)
|
62
|
+
# @private
|
63
|
+
RECURSION_LIMIT = ns(:recursion_limit)
|
64
|
+
# @private
|
65
|
+
GEN = ns(:gen)
|
66
|
+
# @private
|
67
|
+
NAME = ns(:name)
|
68
|
+
|
47
69
|
# Can be enabled or disabled at runtime:
|
48
70
|
# - enabled/disabled by setting `check_asserts`.
|
49
71
|
# - enabled by setting environment variable SPECULATION_CHECK_ASSERTS to the
|
@@ -71,10 +93,11 @@ module Speculation
|
|
71
93
|
# @return [Spec] that validates floats
|
72
94
|
def self.float_in(min: nil, max: nil, infinite: true, nan: true)
|
73
95
|
preds = [Float]
|
74
|
-
|
75
|
-
preds
|
76
|
-
preds
|
77
|
-
preds
|
96
|
+
|
97
|
+
preds.push(->(x) { !x.nan? }) unless nan
|
98
|
+
preds.push(->(x) { !x.infinite? }) unless infinite
|
99
|
+
preds.push(->(x) { x <= max }) if max
|
100
|
+
preds.push(->(x) { x >= min }) if min
|
78
101
|
|
79
102
|
min ||= Float::MIN
|
80
103
|
max ||= Float::MAX
|
@@ -116,7 +139,7 @@ module Speculation
|
|
116
139
|
# @param x [Hash, Object]
|
117
140
|
# @return [Hash, false] x if x is a (Speculation) regex op, else logical false
|
118
141
|
def self.regex?(x)
|
119
|
-
Utils.hash?(x) && x[
|
142
|
+
Utils.hash?(x) && x[OP] && x
|
120
143
|
end
|
121
144
|
|
122
145
|
# @param value return value of a `conform` call
|
@@ -250,7 +273,7 @@ module Speculation
|
|
250
273
|
# @return [Proc]
|
251
274
|
def self.gen(spec, overrides = nil)
|
252
275
|
spec = Identifier(spec)
|
253
|
-
gensub(spec, overrides, [],
|
276
|
+
gensub(spec, overrides, [], RECURSION_LIMIT => recursion_limit)
|
254
277
|
end
|
255
278
|
|
256
279
|
# @private
|
@@ -500,7 +523,7 @@ module Speculation
|
|
500
523
|
# @return [Hash] regex op that matches zero or one value matching pred. Produces a
|
501
524
|
# single value (not a collection) if matched.
|
502
525
|
def self.zero_or_one(pred)
|
503
|
-
_alt([pred, accept(
|
526
|
+
_alt([pred, accept(NIL)], nil)
|
504
527
|
end
|
505
528
|
|
506
529
|
# @param kv_specs [Hash] key+pred pairs
|
@@ -531,7 +554,7 @@ module Speculation
|
|
531
554
|
# resulting value to the conjunction of the predicates, and any conforming
|
532
555
|
# they might perform.
|
533
556
|
def self.constrained(re, *preds)
|
534
|
-
{
|
557
|
+
{ OP => AMP, :p1 => re, :predicates => preds }
|
535
558
|
end
|
536
559
|
|
537
560
|
# @yield [value] predicate function with the semantics of conform i.e. it should
|
@@ -555,7 +578,8 @@ module Speculation
|
|
555
578
|
# @param gen [Proc] generator proc, which must be a proc of one arg (Rantly
|
556
579
|
# instance) that generates a valid value.
|
557
580
|
# @return [Spec]
|
558
|
-
# @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a
|
581
|
+
# @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a
|
582
|
+
# full description of :args, :block, :ret and :fn
|
559
583
|
def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
|
560
584
|
FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block)).tap do |spec|
|
561
585
|
spec.gen = gen
|
@@ -645,7 +669,7 @@ module Speculation
|
|
645
669
|
|
646
670
|
# @private
|
647
671
|
def self.recur_limit?(rmap, id, path, k)
|
648
|
-
rmap[id] > rmap[
|
672
|
+
rmap[id] > rmap[RECURSION_LIMIT] &&
|
649
673
|
path.include?(k)
|
650
674
|
end
|
651
675
|
|
@@ -731,7 +755,7 @@ module Speculation
|
|
731
755
|
p = reg_resolve!(p)
|
732
756
|
|
733
757
|
id, op, ps, ks, p1, p2, ret, id, gen = p.values_at(
|
734
|
-
:id,
|
758
|
+
:id, OP, :predicates, :keys, :p1, :p2, :return_value, :id, GEN
|
735
759
|
) if regex?(p)
|
736
760
|
|
737
761
|
id = p.id if spec?(p)
|
@@ -767,8 +791,8 @@ module Speculation
|
|
767
791
|
|
768
792
|
if p
|
769
793
|
case op
|
770
|
-
when
|
771
|
-
if ret ==
|
794
|
+
when ACCEPT
|
795
|
+
if ret == NIL
|
772
796
|
->(_rantly) { [] }
|
773
797
|
else
|
774
798
|
->(_rantly) { [ret] }
|
@@ -777,9 +801,9 @@ module Speculation
|
|
777
801
|
g = gensub(p, overrides, path, rmap)
|
778
802
|
|
779
803
|
->(rantly) { [g.call(rantly)] }
|
780
|
-
when
|
804
|
+
when AMP
|
781
805
|
re_gen(p1, overrides, path, rmap)
|
782
|
-
when
|
806
|
+
when PCAT
|
783
807
|
gens = ggens.call(ps, ks)
|
784
808
|
|
785
809
|
if gens.all?
|
@@ -787,11 +811,11 @@ module Speculation
|
|
787
811
|
gens.flat_map { |gg| gg.call(rantly) }
|
788
812
|
end
|
789
813
|
end
|
790
|
-
when
|
814
|
+
when ALT
|
791
815
|
gens = ggens.call(ps, ks).compact
|
792
816
|
|
793
817
|
->(rantly) { rantly.branch(*gens) } unless gens.empty?
|
794
|
-
when
|
818
|
+
when REP
|
795
819
|
if recur_limit?(rmap, id, [id], id)
|
796
820
|
->(_rantly) { [] }
|
797
821
|
else
|
@@ -809,26 +833,17 @@ module Speculation
|
|
809
833
|
|
810
834
|
# @private
|
811
835
|
def self.re_conform(regex, data)
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
836
|
+
data.each do |x|
|
837
|
+
regex = deriv(regex, x)
|
838
|
+
return INVALID unless regex
|
839
|
+
end
|
816
840
|
|
841
|
+
if accept_nil?(regex)
|
817
842
|
return_value = preturn(regex)
|
818
843
|
|
819
|
-
|
820
|
-
nil
|
821
|
-
else
|
822
|
-
return_value
|
823
|
-
end
|
844
|
+
return_value == NIL ? nil : return_value
|
824
845
|
else
|
825
|
-
|
826
|
-
|
827
|
-
if dp
|
828
|
-
re_conform(dp, xs)
|
829
|
-
else
|
830
|
-
INVALID
|
831
|
-
end
|
846
|
+
INVALID
|
832
847
|
end
|
833
848
|
end
|
834
849
|
|
@@ -845,7 +860,7 @@ module Speculation
|
|
845
860
|
end
|
846
861
|
|
847
862
|
if accept?(p)
|
848
|
-
if p[
|
863
|
+
if p[OP] == PCAT
|
849
864
|
return op_explain(p, path, via, Utils.conj(inn, index), input[index..-1])
|
850
865
|
else
|
851
866
|
return [{ :path => path,
|
@@ -908,7 +923,7 @@ module Speculation
|
|
908
923
|
if Utils.ident?(spec)
|
909
924
|
spec
|
910
925
|
elsif regex?(spec)
|
911
|
-
spec.merge(
|
926
|
+
spec.merge(NAME => name)
|
912
927
|
else
|
913
928
|
spec.tap { |s| s.name = name }
|
914
929
|
end
|
@@ -918,7 +933,7 @@ module Speculation
|
|
918
933
|
if Utils.ident?(spec)
|
919
934
|
spec
|
920
935
|
elsif regex?(spec)
|
921
|
-
spec[
|
936
|
+
spec[NAME]
|
922
937
|
elsif spec.respond_to?(:name)
|
923
938
|
spec.name
|
924
939
|
end
|
@@ -950,17 +965,12 @@ module Speculation
|
|
950
965
|
end
|
951
966
|
|
952
967
|
def and_preds(x, preds)
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
if invalid?(x)
|
958
|
-
INVALID
|
959
|
-
elsif preds.empty?
|
960
|
-
x
|
961
|
-
else
|
962
|
-
and_preds(x, preds)
|
968
|
+
preds.each do |pred|
|
969
|
+
x = dt(pred, x)
|
970
|
+
return INVALID if invalid?(x)
|
963
971
|
end
|
972
|
+
|
973
|
+
x
|
964
974
|
end
|
965
975
|
|
966
976
|
def specize(spec)
|
@@ -979,12 +989,12 @@ module Speculation
|
|
979
989
|
### regex ###
|
980
990
|
|
981
991
|
def accept(x)
|
982
|
-
{
|
992
|
+
{ OP => ACCEPT, :return_value => x }
|
983
993
|
end
|
984
994
|
|
985
995
|
def accept?(hash)
|
986
996
|
if hash.is_a?(Hash)
|
987
|
-
hash[
|
997
|
+
hash[OP] == ACCEPT
|
988
998
|
end
|
989
999
|
end
|
990
1000
|
|
@@ -997,7 +1007,7 @@ module Speculation
|
|
997
1007
|
return unless regex[:predicates].all?
|
998
1008
|
|
999
1009
|
unless accept?(predicate)
|
1000
|
-
return {
|
1010
|
+
return { OP => PCAT,
|
1001
1011
|
:predicates => regex[:predicates],
|
1002
1012
|
:keys => keys,
|
1003
1013
|
:return_value => regex[:return_value] }
|
@@ -1006,7 +1016,7 @@ module Speculation
|
|
1006
1016
|
val = keys ? { key => predicate[:return_value] } : predicate[:return_value]
|
1007
1017
|
return_value = Utils.conj(regex[:return_value], val)
|
1008
1018
|
|
1009
|
-
if rest_predicates
|
1019
|
+
if rest_predicates.any?
|
1010
1020
|
pcat(:predicates => rest_predicates,
|
1011
1021
|
:keys => rest_keys,
|
1012
1022
|
:return_value => return_value)
|
@@ -1018,7 +1028,7 @@ module Speculation
|
|
1018
1028
|
def rep(p1, p2, return_value, splice)
|
1019
1029
|
return unless p1
|
1020
1030
|
|
1021
|
-
regex = {
|
1031
|
+
regex = { OP => REP, :p2 => p2, :splice => splice, :id => SecureRandom.uuid }
|
1022
1032
|
|
1023
1033
|
if accept?(p1)
|
1024
1034
|
regex.merge(:p1 => p2, :return_value => Utils.conj(return_value, p1[:return_value]))
|
@@ -1027,23 +1037,23 @@ module Speculation
|
|
1027
1037
|
end
|
1028
1038
|
end
|
1029
1039
|
|
1030
|
-
def filter_alt(ps, ks
|
1040
|
+
def filter_alt(ps, ks)
|
1031
1041
|
if ks
|
1032
|
-
pks = ps.zip(ks).select { |
|
1042
|
+
pks = ps.zip(ks).select { |(p, _k)| yield(p) }
|
1033
1043
|
[pks.map(&:first), pks.map(&:last)]
|
1034
1044
|
else
|
1035
|
-
[ps.select(
|
1045
|
+
[ps.select { |p| yield(p) }, ks]
|
1036
1046
|
end
|
1037
1047
|
end
|
1038
1048
|
|
1039
1049
|
def _alt(predicates, keys)
|
1040
|
-
predicates, keys = filter_alt(predicates, keys
|
1041
|
-
return
|
1050
|
+
predicates, keys = filter_alt(predicates, keys) { |p| p }
|
1051
|
+
return if predicates.empty?
|
1042
1052
|
|
1043
1053
|
predicate, *rest_predicates = predicates
|
1044
1054
|
key, *_rest_keys = keys
|
1045
1055
|
|
1046
|
-
return_value = {
|
1056
|
+
return_value = { OP => ALT, :predicates => predicates, :keys => keys }
|
1047
1057
|
return return_value unless rest_predicates.empty?
|
1048
1058
|
|
1049
1059
|
return predicate unless key
|
@@ -1061,24 +1071,24 @@ module Speculation
|
|
1061
1071
|
end
|
1062
1072
|
|
1063
1073
|
def no_ret?(p1, pret)
|
1064
|
-
return true if pret ==
|
1074
|
+
return true if pret == NIL
|
1065
1075
|
|
1066
1076
|
regex = reg_resolve!(p1)
|
1067
|
-
op = regex[
|
1077
|
+
op = regex[OP]
|
1068
1078
|
|
1069
|
-
[
|
1079
|
+
[REP, PCAT].include?(op) && pret.empty? || nil
|
1070
1080
|
end
|
1071
1081
|
|
1072
1082
|
def accept_nil?(regex)
|
1073
1083
|
regex = reg_resolve!(regex)
|
1074
1084
|
return unless regex?(regex)
|
1075
1085
|
|
1076
|
-
case regex[
|
1077
|
-
when
|
1078
|
-
when
|
1079
|
-
when
|
1080
|
-
when
|
1081
|
-
when
|
1086
|
+
case regex[OP]
|
1087
|
+
when ACCEPT then true
|
1088
|
+
when PCAT then regex[:predicates].all? { |p| accept_nil?(p) }
|
1089
|
+
when ALT then regex[:predicates].any? { |p| accept_nil?(p) }
|
1090
|
+
when REP then (regex[:p1] == regex[:p2]) || accept_nil?(regex[:p1])
|
1091
|
+
when AMP
|
1082
1092
|
p1 = regex[:p1]
|
1083
1093
|
|
1084
1094
|
return false unless accept_nil?(p1)
|
@@ -1086,7 +1096,7 @@ module Speculation
|
|
1086
1096
|
no_ret?(p1, preturn(p1)) ||
|
1087
1097
|
!invalid?(and_preds(preturn(p1), regex[:predicates]))
|
1088
1098
|
else
|
1089
|
-
raise "Unexpected #{
|
1099
|
+
raise "Unexpected #{OP} #{regex[OP]}"
|
1090
1100
|
end
|
1091
1101
|
end
|
1092
1102
|
|
@@ -1095,36 +1105,32 @@ module Speculation
|
|
1095
1105
|
return unless regex?(regex)
|
1096
1106
|
|
1097
1107
|
p0, *_pr = regex[:predicates]
|
1098
|
-
k, *
|
1108
|
+
k, *_ks = regex[:keys]
|
1099
1109
|
|
1100
|
-
case regex[
|
1101
|
-
when
|
1102
|
-
when
|
1103
|
-
when
|
1104
|
-
when
|
1110
|
+
case regex[OP]
|
1111
|
+
when ACCEPT then regex[:return_value]
|
1112
|
+
when PCAT then add_ret(p0, regex[:return_value], k)
|
1113
|
+
when REP then add_ret(regex[:p1], regex[:return_value], k)
|
1114
|
+
when AMP
|
1105
1115
|
pret = preturn(regex[:p1])
|
1106
1116
|
|
1107
1117
|
if no_ret?(regex[:p1], pret)
|
1108
|
-
|
1118
|
+
NIL
|
1109
1119
|
else
|
1110
1120
|
and_preds(pret, regex[:predicates])
|
1111
1121
|
end
|
1112
|
-
when
|
1113
|
-
|
1122
|
+
when ALT
|
1123
|
+
pred, key = regex[:predicates].zip(Array(regex[:keys])).find { |(p, _k)| accept_nil?(p) }
|
1114
1124
|
|
1115
|
-
r = if
|
1116
|
-
|
1125
|
+
r = if pred.nil?
|
1126
|
+
NIL
|
1117
1127
|
else
|
1118
|
-
preturn(
|
1128
|
+
preturn(pred)
|
1119
1129
|
end
|
1120
1130
|
|
1121
|
-
|
1122
|
-
[ks.first, r]
|
1123
|
-
else
|
1124
|
-
r
|
1125
|
-
end
|
1131
|
+
key ? [key, r] : r
|
1126
1132
|
else
|
1127
|
-
raise "Unexpected #{
|
1133
|
+
raise "Unexpected #{OP} #{regex[OP]}"
|
1128
1134
|
end
|
1129
1135
|
end
|
1130
1136
|
|
@@ -1132,30 +1138,27 @@ module Speculation
|
|
1132
1138
|
regex = reg_resolve!(regex)
|
1133
1139
|
return r unless regex?(regex)
|
1134
1140
|
|
1135
|
-
|
1141
|
+
case regex[OP]
|
1142
|
+
when ACCEPT, ALT, AMP
|
1136
1143
|
return_value = preturn(regex)
|
1137
1144
|
|
1138
|
-
if return_value
|
1145
|
+
if return_value == NIL
|
1139
1146
|
r
|
1140
1147
|
else
|
1141
|
-
|
1142
|
-
|
1143
|
-
regex[:splice] ? Utils.into(r, val) : Utils.conj(r, val)
|
1148
|
+
Utils.conj(r, key ? { key => return_value } : return_value)
|
1144
1149
|
end
|
1145
|
-
|
1146
|
-
|
1147
|
-
case regex[ns(:op)]
|
1148
|
-
when ns(:accept), ns(:alt), ns(:amp)
|
1150
|
+
when PCAT, REP
|
1149
1151
|
return_value = preturn(regex)
|
1150
1152
|
|
1151
|
-
if return_value
|
1153
|
+
if return_value.empty?
|
1152
1154
|
r
|
1153
1155
|
else
|
1154
|
-
|
1156
|
+
val = key ? { key => return_value } : return_value
|
1157
|
+
|
1158
|
+
regex[:splice] ? Utils.into(r, val) : Utils.conj(r, val)
|
1155
1159
|
end
|
1156
|
-
when ns(:pcat), ns(:rep) then prop.call
|
1157
1160
|
else
|
1158
|
-
raise "Unexpected #{
|
1161
|
+
raise "Unexpected #{OP} #{regex[OP]}"
|
1159
1162
|
end
|
1160
1163
|
end
|
1161
1164
|
|
@@ -1178,9 +1181,9 @@ module Speculation
|
|
1178
1181
|
pred, *rest_preds = predicates
|
1179
1182
|
key, *rest_keys = keys
|
1180
1183
|
|
1181
|
-
case regex[
|
1182
|
-
when
|
1183
|
-
when
|
1184
|
+
case regex[OP]
|
1185
|
+
when ACCEPT then nil
|
1186
|
+
when PCAT
|
1184
1187
|
regex1 = pcat(:predicates => [deriv(pred, value), *rest_preds], :keys => keys, :return_value => return_value)
|
1185
1188
|
regex2 = nil
|
1186
1189
|
|
@@ -1192,9 +1195,9 @@ module Speculation
|
|
1192
1195
|
end
|
1193
1196
|
|
1194
1197
|
alt2(regex1, regex2)
|
1195
|
-
when
|
1198
|
+
when ALT
|
1196
1199
|
_alt(predicates.map { |p| deriv(p, value) }, keys)
|
1197
|
-
when
|
1200
|
+
when REP
|
1198
1201
|
regex1 = rep(deriv(p1, value), p2, return_value, splice)
|
1199
1202
|
regex2 = nil
|
1200
1203
|
|
@@ -1203,18 +1206,18 @@ module Speculation
|
|
1203
1206
|
end
|
1204
1207
|
|
1205
1208
|
alt2(regex1, regex2)
|
1206
|
-
when
|
1209
|
+
when AMP
|
1207
1210
|
p1 = deriv(p1, value)
|
1208
1211
|
return unless p1
|
1209
1212
|
|
1210
|
-
if p1[
|
1213
|
+
if p1[OP] == ACCEPT
|
1211
1214
|
ret = and_preds(preturn(p1), predicates)
|
1212
1215
|
accept(ret) unless invalid?(ret)
|
1213
1216
|
else
|
1214
1217
|
constrained(p1, *predicates)
|
1215
1218
|
end
|
1216
1219
|
else
|
1217
|
-
raise "Unexpected #{
|
1220
|
+
raise "Unexpected #{OP} #{regex[OP]}"
|
1218
1221
|
end
|
1219
1222
|
end
|
1220
1223
|
|
@@ -1242,9 +1245,9 @@ module Speculation
|
|
1242
1245
|
end
|
1243
1246
|
end
|
1244
1247
|
|
1245
|
-
case p[
|
1246
|
-
when
|
1247
|
-
when
|
1248
|
+
case p[OP]
|
1249
|
+
when ACCEPT then nil
|
1250
|
+
when AMP
|
1248
1251
|
if input.empty?
|
1249
1252
|
if accept_nil?(p[:p1])
|
1250
1253
|
explain_pred_list(p[:predicates], path, via, inn, preturn(p[:p1]))
|
@@ -1260,13 +1263,14 @@ module Speculation
|
|
1260
1263
|
op_explain(p[:p1], path, via, inn, input)
|
1261
1264
|
end
|
1262
1265
|
end
|
1263
|
-
when
|
1264
|
-
pks = p[:predicates].zip(p[:keys]
|
1266
|
+
when PCAT
|
1267
|
+
pks = p[:predicates].zip(Array(p[:keys]))
|
1265
1268
|
pred, k = if pks.count == 1
|
1266
1269
|
pks.first
|
1267
1270
|
else
|
1268
|
-
pks.
|
1271
|
+
pks.find { |(predicate, _)| !accept_nil?(predicate) }
|
1269
1272
|
end
|
1273
|
+
|
1270
1274
|
path = Utils.conj(path, k) if k
|
1271
1275
|
|
1272
1276
|
if input.empty? && !pred
|
@@ -1274,18 +1278,18 @@ module Speculation
|
|
1274
1278
|
else
|
1275
1279
|
op_explain(pred, path, via, inn, input)
|
1276
1280
|
end
|
1277
|
-
when
|
1281
|
+
when ALT
|
1278
1282
|
return insufficient(p, path, via, inn) if input.empty?
|
1279
1283
|
|
1280
|
-
probs = p[:predicates].zip(p[:keys]).flat_map { |(predicate, key)|
|
1284
|
+
probs = p[:predicates].zip(Array(p[:keys])).flat_map { |(predicate, key)|
|
1281
1285
|
op_explain(predicate, key ? Utils.conj(path, key) : path, via, inn, input)
|
1282
1286
|
}
|
1283
1287
|
|
1284
1288
|
probs.compact
|
1285
|
-
when
|
1289
|
+
when REP
|
1286
1290
|
op_explain(p[:p1], path, via, inn, input)
|
1287
1291
|
else
|
1288
|
-
raise "Unexpected #{
|
1292
|
+
raise "Unexpected #{OP} #{p[OP]}"
|
1289
1293
|
end
|
1290
1294
|
end
|
1291
1295
|
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.
|
4
|
+
version: 0.3.1
|
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-
|
11
|
+
date: 2017-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -168,6 +168,7 @@ files:
|
|
168
168
|
- bin/console
|
169
169
|
- bin/setup
|
170
170
|
- examples/codebreaker.rb
|
171
|
+
- examples/json_parser.rb
|
171
172
|
- examples/sinatra-web-app/Gemfile
|
172
173
|
- examples/sinatra-web-app/Gemfile.lock
|
173
174
|
- examples/sinatra-web-app/app.rb
|