j2119 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/LICENSE +201 -0
- data/NOTICE.txt +3 -0
- data/README.md +97 -0
- data/Rakefile +10 -0
- data/j2119.gemspec +15 -0
- data/lib/j2119/allowed_fields.rb +35 -0
- data/lib/j2119/assigner.rb +223 -0
- data/lib/j2119/conditional.rb +40 -0
- data/lib/j2119/constraints.rb +312 -0
- data/lib/j2119/deduce.rb +39 -0
- data/lib/j2119/json_path_checker.rb +58 -0
- data/lib/j2119/matcher.rb +233 -0
- data/lib/j2119/node_validator.rb +78 -0
- data/lib/j2119/oxford.rb +67 -0
- data/lib/j2119/parser.rb +101 -0
- data/lib/j2119/role_constraints.rb +32 -0
- data/lib/j2119/role_finder.rb +143 -0
- data/lib/j2119.rb +77 -0
- metadata +77 -0
@@ -0,0 +1,312 @@
|
|
1
|
+
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
|
4
|
+
# not use this file except in compliance with the License. A copy of the
|
5
|
+
# License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the LICENSE.txt file accompanying this file. This file is distributed
|
10
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
11
|
+
# express or implied. See the License for the specific language governing
|
12
|
+
# permissions and limitations under the License.
|
13
|
+
|
14
|
+
require 'date'
|
15
|
+
|
16
|
+
module J2119
|
17
|
+
|
18
|
+
# These all respond_to
|
19
|
+
# check(node, path, problem)
|
20
|
+
# - node is the JSON node being checked
|
21
|
+
# - path is the current path, for reporting practices
|
22
|
+
# - problems is a list of problem reports
|
23
|
+
# TODO: Add a "role" argument to enrich error reporting
|
24
|
+
class Constraint
|
25
|
+
def initialize
|
26
|
+
@conditions = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_condition(condition)
|
30
|
+
@conditions << condition
|
31
|
+
end
|
32
|
+
|
33
|
+
def applies(node, role)
|
34
|
+
return @conditions.empty? ||
|
35
|
+
@conditions.map{|c| c.constraint_applies(node, role)}.any?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Verify that there is only one of a selection of fields
|
40
|
+
#
|
41
|
+
class OnlyOneOfConstraint < Constraint
|
42
|
+
def initialize(fields)
|
43
|
+
super()
|
44
|
+
@fields = fields
|
45
|
+
end
|
46
|
+
|
47
|
+
def check(node, path, problems)
|
48
|
+
if (@fields & node.keys).size > 1
|
49
|
+
problems <<
|
50
|
+
"#{path} may have only one of #{@fields}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Verify that array field is not empty
|
56
|
+
#
|
57
|
+
class NonEmptyConstraint < Constraint
|
58
|
+
def initialize(name)
|
59
|
+
super()
|
60
|
+
@name = name
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
conds = (@conditions.empty?) ? '' : " #{@conditions.size} conditions"
|
65
|
+
"<Array field #{@name} should not be empty#{conds}>"
|
66
|
+
end
|
67
|
+
|
68
|
+
def check(node, path, problems)
|
69
|
+
if node[@name] && node[@name].is_a?(Array) && (node[@name].size == 0)
|
70
|
+
problems <<
|
71
|
+
"#{path}.#{@name} is empty, non-empty required"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Verify node has the named field, or one of the named fields
|
77
|
+
#
|
78
|
+
class HasFieldConstraint < Constraint
|
79
|
+
def initialize(name)
|
80
|
+
super()
|
81
|
+
if name.is_a?(String)
|
82
|
+
@names = [ name ]
|
83
|
+
else
|
84
|
+
@names = name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
conds = (@conditions.empty?) ? '' : " #{@conditions.size} conditions"
|
90
|
+
"<Field #{@names} should be present#{conds}>"
|
91
|
+
end
|
92
|
+
|
93
|
+
def check(node, path, problems)
|
94
|
+
if (node.keys & @names).empty?
|
95
|
+
if @names.size == 1
|
96
|
+
problems <<
|
97
|
+
"#{path} does not have required field \"#{@names[0]}\""
|
98
|
+
else
|
99
|
+
problems <<
|
100
|
+
"#{path} does not have required field from #{@names}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Verify node does not have the named field
|
107
|
+
#
|
108
|
+
class DoesNotHaveFieldConstraint < Constraint
|
109
|
+
def initialize(name)
|
110
|
+
super()
|
111
|
+
@name = name
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_s
|
115
|
+
conds = (@conditions.empty?) ? '' : " #{@conditions.size} conditions"
|
116
|
+
"<Field #{@name} should be absent#{conds}>"
|
117
|
+
end
|
118
|
+
|
119
|
+
def check(node, path, problems)
|
120
|
+
if node[@name]
|
121
|
+
problems <<
|
122
|
+
"#{path} has forbidden field \"#{@name}\""
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Verify type of a field in a node
|
128
|
+
#
|
129
|
+
class FieldTypeConstraint < Constraint
|
130
|
+
def initialize(name, type, is_array, is_nullable)
|
131
|
+
super()
|
132
|
+
@name = name
|
133
|
+
@type = type
|
134
|
+
@is_array = is_array
|
135
|
+
@is_nullable = is_nullable
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_s
|
139
|
+
conds = (@conditions.empty?) ? '' : " #{@conditions.size} conditions"
|
140
|
+
nullable = @is_nullable ? ' (nullable)' : ''
|
141
|
+
"<Field #{@name} should be of type #{@type}#{conds}>#{nullable}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def check(node, path, problems)
|
145
|
+
|
146
|
+
# type-checking is orthogonal to existence checking
|
147
|
+
return if !node.key?(@name)
|
148
|
+
|
149
|
+
value = node[@name]
|
150
|
+
path = "#{path}.#{@name}"
|
151
|
+
|
152
|
+
if value == nil
|
153
|
+
if !@is_nullable
|
154
|
+
problems << "#{path} should be non-null"
|
155
|
+
end
|
156
|
+
return
|
157
|
+
end
|
158
|
+
|
159
|
+
if @is_array
|
160
|
+
if value.is_a?(Array)
|
161
|
+
i = 0
|
162
|
+
value.each do |element|
|
163
|
+
value_check(element, "#{path}[#{i}]", problems)
|
164
|
+
i += 1
|
165
|
+
end
|
166
|
+
else
|
167
|
+
report(path, value, 'an Array', problems)
|
168
|
+
end
|
169
|
+
else
|
170
|
+
value_check(value, path, problems)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def value_check(value, path, problems)
|
175
|
+
case @type
|
176
|
+
when :object
|
177
|
+
report(path, value, 'an Object', problems) if !value.is_a?(Hash)
|
178
|
+
when :array
|
179
|
+
report(path, value, 'an Array', problems) if !value.is_a?(Array)
|
180
|
+
when :string
|
181
|
+
report(path, value, 'a String', problems) if !value.is_a?(String)
|
182
|
+
when :integer
|
183
|
+
report(path, value, 'an Integer', problems) if !value.is_a?(Integer)
|
184
|
+
when :float
|
185
|
+
report(path, value, 'a Float', problems) if !value.is_a?(Float)
|
186
|
+
when :boolean
|
187
|
+
if value != true && value != false
|
188
|
+
report(path, value, 'a Boolean', problems)
|
189
|
+
end
|
190
|
+
when :numeric
|
191
|
+
report(path, value, 'numeric', problems) if !value.is_a?(Numeric)
|
192
|
+
when :json_path
|
193
|
+
report(path, value, 'a JSONPath', problems) if !JSONPathChecker.is_path?(value)
|
194
|
+
when :reference_path
|
195
|
+
report(path, value, 'a Reference Path', problems) if !JSONPathChecker.is_reference_path?(value)
|
196
|
+
when :timestamp
|
197
|
+
begin
|
198
|
+
DateTime.rfc3339 value
|
199
|
+
rescue
|
200
|
+
report(path, value, 'an RFC3339 timestamp', problems)
|
201
|
+
end
|
202
|
+
when :URI
|
203
|
+
if !(value.is_a?(String) && (value =~ /^[a-z]+:/))
|
204
|
+
report(path, value, 'A URI', problems)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
def report(path, value, message, problems)
|
211
|
+
if value.is_a?(String)
|
212
|
+
value = '"' + value + '"'
|
213
|
+
end
|
214
|
+
problems << "#{path} is #{value} but should be #{message}"
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
# Verify constraints on values of a named field
|
220
|
+
#
|
221
|
+
class FieldValueConstraint < Constraint
|
222
|
+
|
223
|
+
def initialize(name, params)
|
224
|
+
super()
|
225
|
+
@name = name
|
226
|
+
@params = params
|
227
|
+
end
|
228
|
+
|
229
|
+
def to_s
|
230
|
+
conds = (@conditions.empty?) ? '' : " #{@conditions.size} conditions"
|
231
|
+
"<Field #{@name} has constraints #{@params}#{conds}>"
|
232
|
+
end
|
233
|
+
|
234
|
+
def check(node, path, problems)
|
235
|
+
|
236
|
+
# value-checking is orthogonal to existence checking
|
237
|
+
return if !node.key?(@name)
|
238
|
+
|
239
|
+
value = node[@name]
|
240
|
+
|
241
|
+
if @params[:enum]
|
242
|
+
if !(@params[:enum].include?(value))
|
243
|
+
problems <<
|
244
|
+
"#{path}.#{@name} is \"#{value}\", " +
|
245
|
+
"not one of the allowed values #{@params[:enum]}"
|
246
|
+
end
|
247
|
+
|
248
|
+
# if enum constraint are provided, others are ignored
|
249
|
+
return
|
250
|
+
end
|
251
|
+
|
252
|
+
if @params[:equal]
|
253
|
+
begin
|
254
|
+
if value != @params[:equal]
|
255
|
+
problems <<
|
256
|
+
"#{path}.#{@name} is #{value} " +
|
257
|
+
"but required value is #{@params[:equal]}"
|
258
|
+
end
|
259
|
+
rescue Exception
|
260
|
+
# should be caught by type constraint
|
261
|
+
end
|
262
|
+
end
|
263
|
+
if @params[:floor]
|
264
|
+
begin
|
265
|
+
if value <= @params[:floor]
|
266
|
+
problems <<
|
267
|
+
"#{path}.#{@name} is #{value} " +
|
268
|
+
"but allowed floor is #{@params[:floor]}"
|
269
|
+
end
|
270
|
+
rescue Exception
|
271
|
+
# should be caught by type constraint
|
272
|
+
end
|
273
|
+
end
|
274
|
+
if @params[:min]
|
275
|
+
begin
|
276
|
+
if value < @params[:min]
|
277
|
+
problems <<
|
278
|
+
"#{path}.#{@name} is #{value} " +
|
279
|
+
"but allowed minimum is #{@params[:min]}"
|
280
|
+
end
|
281
|
+
rescue Exception
|
282
|
+
# should be caught by type constraint
|
283
|
+
end
|
284
|
+
end
|
285
|
+
if @params[:ceiling]
|
286
|
+
begin
|
287
|
+
if value >= @params[:ceiling]
|
288
|
+
problems <<
|
289
|
+
"#{path}.#{@name} is #{value} " +
|
290
|
+
"but allowed ceiling is #{@params[:ceiling]}"
|
291
|
+
end
|
292
|
+
rescue Exception
|
293
|
+
# should be caught by type constraint
|
294
|
+
end
|
295
|
+
end
|
296
|
+
if @params[:max]
|
297
|
+
begin
|
298
|
+
if value > @params[:max]
|
299
|
+
problems <<
|
300
|
+
"#{path}.#{@name} is #{value} " +
|
301
|
+
"but allowed maximum is #{@params[:max]}"
|
302
|
+
end
|
303
|
+
rescue Exception
|
304
|
+
# should be caught by type constraint
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
311
|
+
|
312
|
+
|
data/lib/j2119/deduce.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
|
4
|
+
# not use this file except in compliance with the License. A copy of the
|
5
|
+
# License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the LICENSE.txt file accompanying this file. This file is distributed
|
10
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
11
|
+
# express or implied. See the License for the specific language governing
|
12
|
+
# permissions and limitations under the License.
|
13
|
+
|
14
|
+
module J2119
|
15
|
+
|
16
|
+
class Deduce
|
17
|
+
|
18
|
+
# we have to deduce the JSON value from they way they expressed it as
|
19
|
+
# text in the J2119 file.
|
20
|
+
#
|
21
|
+
def self.value(val)
|
22
|
+
case val
|
23
|
+
when /^"(.*)"$/
|
24
|
+
$1
|
25
|
+
when 'true'
|
26
|
+
true
|
27
|
+
when 'false'
|
28
|
+
false
|
29
|
+
when 'null'
|
30
|
+
nil
|
31
|
+
when /^\d+$/
|
32
|
+
val.to_i
|
33
|
+
else
|
34
|
+
val.to_f
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
|
5
|
+
# not use this file except in compliance with the License. A copy of the
|
6
|
+
# License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0/
|
9
|
+
#
|
10
|
+
# or in the LICENSE.txt file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
|
15
|
+
# Examines fields which are supposed to be JSONPath expressions or Reference
|
16
|
+
# Paths, which are JSONPaths that are singular, i.e. don't produce array
|
17
|
+
# results
|
18
|
+
#
|
19
|
+
module J2119
|
20
|
+
|
21
|
+
INITIAL_NAME_CLASSES = [ 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl' ]
|
22
|
+
NON_INITIAL_NAME_CLASSES = [ 'Mn', 'Mc', 'Nd', 'Pc' ]
|
23
|
+
FOLLOWING_NAME_CLASSES = INITIAL_NAME_CLASSES | NON_INITIAL_NAME_CLASSES
|
24
|
+
DOT_SEPARATOR = '\.\.?'
|
25
|
+
|
26
|
+
class JSONPathChecker
|
27
|
+
|
28
|
+
def self.classes_to_re classes
|
29
|
+
re_classes = classes.map {|x| "\\p{#{x}}" }
|
30
|
+
"[#{re_classes.join('')}]"
|
31
|
+
end
|
32
|
+
|
33
|
+
@@name_re = classes_to_re(INITIAL_NAME_CLASSES) +
|
34
|
+
classes_to_re(FOLLOWING_NAME_CLASSES) + '*'
|
35
|
+
dot_step = DOT_SEPARATOR + '((' + @@name_re + ')|(\*))'
|
36
|
+
rp_dot_step = DOT_SEPARATOR + @@name_re
|
37
|
+
bracket_step = '\[' + "'" + @@name_re + "'" + '\]'
|
38
|
+
rp_num_index = '\[\d+\]'
|
39
|
+
num_index = '\[\d+(, *\d+)?\]'
|
40
|
+
star_index = '\[\*\]'
|
41
|
+
colon_index = '\[(-?\d+)?:(-?\d+)?\]'
|
42
|
+
index = '((' + num_index + ')|(' + star_index + ')|(' + colon_index + '))'
|
43
|
+
step = '((' + dot_step + ')|(' + bracket_step + '))' + '(' + index + ')?'
|
44
|
+
rp_step = '((' + rp_dot_step + ')|(' + bracket_step + '))' + '(' + rp_num_index + ')?'
|
45
|
+
path = '^\$' + '(' + step + ')+$'
|
46
|
+
reference_path = '^\$' + '(' + rp_step + ')+$'
|
47
|
+
@@path_re = Regexp.new(path)
|
48
|
+
@@reference_path_re = Regexp.new(reference_path);
|
49
|
+
|
50
|
+
def self.is_path?(s)
|
51
|
+
s.is_a?(String) && @@path_re.match(s)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.is_reference_path?(s)
|
55
|
+
s.is_a?(String) && @@reference_path_re.match(s)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
|
4
|
+
# not use this file except in compliance with the License. A copy of the
|
5
|
+
# License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the LICENSE.txt file accompanying this file. This file is distributed
|
10
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
11
|
+
# express or implied. See the License for the specific language governing
|
12
|
+
# permissions and limitations under the License.
|
13
|
+
|
14
|
+
module J2119
|
15
|
+
|
16
|
+
# Does the heavy lifting of parsing j2119 files to extract all
|
17
|
+
# the assertions, making egregious use of regular expressions.
|
18
|
+
#
|
19
|
+
# This is the kind of thing I actively discourage when other
|
20
|
+
# programmers suggest it. If I were a real grown-up I'd
|
21
|
+
# implement a proper lexer and bullet-proof parser.
|
22
|
+
#
|
23
|
+
class Matcher
|
24
|
+
|
25
|
+
# crutch for RE debugging
|
26
|
+
attr_reader :constraint_match, :roledef_match, :only_one_match
|
27
|
+
|
28
|
+
# actual exports
|
29
|
+
attr_reader :role_matcher, :eachof_match
|
30
|
+
|
31
|
+
MUST = '(?<modal>MUST|MAY|MUST NOT)'
|
32
|
+
TYPES = [
|
33
|
+
'array',
|
34
|
+
'object',
|
35
|
+
'string',
|
36
|
+
'boolean',
|
37
|
+
'numeric',
|
38
|
+
'integer',
|
39
|
+
'float',
|
40
|
+
'timestamp',
|
41
|
+
'JSONPath',
|
42
|
+
'referencePath',
|
43
|
+
'URI'
|
44
|
+
]
|
45
|
+
|
46
|
+
RELATIONS = [
|
47
|
+
'', 'equal to', 'greater than', 'less than',
|
48
|
+
'greater than or equal to', 'less than or equal to'
|
49
|
+
].join('|')
|
50
|
+
RELATION = "((?<relation>#{RELATIONS})\\s+)"
|
51
|
+
|
52
|
+
S = '"[^"]*"' # string
|
53
|
+
V = '\S+' # non-string value: number, true, false, null
|
54
|
+
RELATIONAL = "#{RELATION}(?<target>#{S}|#{V})"
|
55
|
+
|
56
|
+
CHILD_ROLE = ';\s+((its\s+(?<child_type>value))|' +
|
57
|
+
'each\s+(?<child_type>field|element))' +
|
58
|
+
'\s+is\s+an?\s+' +
|
59
|
+
'"(?<child_role>[^"]+)"'
|
60
|
+
|
61
|
+
@@initialized = false
|
62
|
+
|
63
|
+
# constants that need help from oxford
|
64
|
+
def constants
|
65
|
+
if !@@initialized
|
66
|
+
@@initialized = true
|
67
|
+
|
68
|
+
@@strings = Oxford.re(S, :capture_name => 'strings')
|
69
|
+
enum = "one\s+of\s+#{@@strings}"
|
70
|
+
|
71
|
+
@@predicate = "(#{RELATIONAL}|#{enum})"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def reconstruct
|
76
|
+
make_type_regex
|
77
|
+
|
78
|
+
# conditional clause
|
79
|
+
excluded_roles = "not\\s+" +
|
80
|
+
Oxford.re(@role_matcher,
|
81
|
+
:capture_name => 'excluded',
|
82
|
+
:use_article => true) +
|
83
|
+
"\\s+"
|
84
|
+
conditional = "which\\s+is\\s+" +
|
85
|
+
excluded_roles
|
86
|
+
|
87
|
+
# regex for matching constraint lines
|
88
|
+
c_start = '^An?\s+' +
|
89
|
+
"(?<role>#{@role_matcher})" + '\s+' +
|
90
|
+
"(#{conditional})?" +
|
91
|
+
MUST + '\s+have\s+an?\s+'
|
92
|
+
field_list = "one\\s+of\\s+" +
|
93
|
+
Oxford.re('"[^"]+"', :capture_name => 'field_list')
|
94
|
+
c_match = c_start +
|
95
|
+
"((?<type>#{@type_regex})\\s+)?" +
|
96
|
+
"field\\s+named\\s+" +
|
97
|
+
"((\"(?<field_name>[^\"]+)\")|(#{field_list}))" +
|
98
|
+
'(\s+whose\s+value\s+MUST\s+be\s+' + @@predicate + ')?' +
|
99
|
+
'(' + CHILD_ROLE + ')?' +
|
100
|
+
'\.'
|
101
|
+
|
102
|
+
# regexp for matching lines of the form
|
103
|
+
# "An X MUST have only one of "Y", "Z", and "W".
|
104
|
+
# There's a pattern here, building a separate regex rather than
|
105
|
+
# adding more complexity to @constraint_matcher. Any further
|
106
|
+
# additions should be done this way, and
|
107
|
+
# TODO: Break @constraint_matcher into a bunch of smaller patterns
|
108
|
+
# like this.
|
109
|
+
oo_start = '^An?\s+' +
|
110
|
+
"(?<role>#{@role_matcher})" + '\s+' +
|
111
|
+
MUST + '\s+have\s+only\s+'
|
112
|
+
oo_field_list = "one\\s+of\\s+" +
|
113
|
+
Oxford.re('"[^"]+"',
|
114
|
+
:capture_name => 'field_list',
|
115
|
+
:connector => 'and')
|
116
|
+
oo_match = oo_start + oo_field_list
|
117
|
+
|
118
|
+
# regex for matching role-def lines
|
119
|
+
val_match = "whose\\s+\"(?<fieldtomatch>[^\"]+)\"" +
|
120
|
+
"\\s+field's\\s+value\\s+is\\s+" +
|
121
|
+
"(?<valtomatch>(\"[^\"]*\")|([^\"\\s]\\S+))\\s+"
|
122
|
+
with_a_match = "with\\s+an?\\s+\"(?<with_a_field>[^\"]+)\"\\s+field\\s"
|
123
|
+
|
124
|
+
rd_match = '^An?\s+' +
|
125
|
+
"(?<role>#{@role_matcher})" + '\s+' +
|
126
|
+
"((?<val_match_present>#{val_match})|(#{with_a_match}))?" +
|
127
|
+
"is\\s+an?\\s+" +
|
128
|
+
"\"(?<newrole>[^\"]*)\"\\.\\s*$"
|
129
|
+
@roledef_match = Regexp.new(rd_match)
|
130
|
+
|
131
|
+
@constraint_start = Regexp.new(c_start)
|
132
|
+
@constraint_match = Regexp.new(c_match)
|
133
|
+
|
134
|
+
@only_one_start = Regexp.new(oo_start)
|
135
|
+
@only_one_match = Regexp.new(oo_match)
|
136
|
+
|
137
|
+
eo_match = "^Each\\s+of\\s" +
|
138
|
+
Oxford.re(@role_matcher,
|
139
|
+
:capture_name => 'each_of',
|
140
|
+
:use_article => true,
|
141
|
+
:connector => 'and') +
|
142
|
+
"\\s+(?<trailer>.*)$"
|
143
|
+
|
144
|
+
@eachof_match = Regexp.new(eo_match)
|
145
|
+
end
|
146
|
+
|
147
|
+
def initialize(root)
|
148
|
+
constants
|
149
|
+
@roles = []
|
150
|
+
add_role root
|
151
|
+
reconstruct
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_role(role)
|
155
|
+
@roles << role
|
156
|
+
@role_matcher = @roles.join('|')
|
157
|
+
reconstruct
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.tokenize_strings(s)
|
161
|
+
# should be a way to do this with capture groups but I'm not smart enough
|
162
|
+
strings = []
|
163
|
+
r = Regexp.new '^[^"]*"([^"]*)"'
|
164
|
+
while s =~ r
|
165
|
+
strings << $1
|
166
|
+
s = $'
|
167
|
+
end
|
168
|
+
strings
|
169
|
+
end
|
170
|
+
|
171
|
+
def tokenize_values(vals)
|
172
|
+
vals.gsub(',', ' ').gsub('or', ' ').split(/\s+/)
|
173
|
+
end
|
174
|
+
|
175
|
+
def make_type_regex
|
176
|
+
|
177
|
+
# add modified numeric types
|
178
|
+
types = TYPES.clone
|
179
|
+
number_types = [ 'float', 'integer', 'numeric' ]
|
180
|
+
number_modifiers = [ 'positive', 'negative', 'nonnegative' ]
|
181
|
+
number_types.each do |number_type|
|
182
|
+
number_modifiers.each do |number_modifier|
|
183
|
+
types << "#{number_modifier}-#{number_type}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# add array types
|
188
|
+
array_types = types.map { |t| "#{t}-array" }
|
189
|
+
types |= array_types
|
190
|
+
nonempty_array_types = array_types.map { |t| "nonempty-#{t}" }
|
191
|
+
types |= nonempty_array_types
|
192
|
+
nullable_types = types.map { |t| "nullable-#{t}" }
|
193
|
+
types |= nullable_types
|
194
|
+
@type_regex = types.join('|')
|
195
|
+
end
|
196
|
+
|
197
|
+
def is_role_def_line(line)
|
198
|
+
line =~ %r{is\s+an?\s+"[^"]*"\.\s*$}
|
199
|
+
end
|
200
|
+
|
201
|
+
def build_role_def(line)
|
202
|
+
build(@roledef_match, line)
|
203
|
+
end
|
204
|
+
|
205
|
+
def build(re, line)
|
206
|
+
data = {}
|
207
|
+
match = re.match(line)
|
208
|
+
match.names.each do |name|
|
209
|
+
data[name] = match[name]
|
210
|
+
end
|
211
|
+
data
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_only_one(line)
|
215
|
+
build(@only_one_match, line)
|
216
|
+
end
|
217
|
+
|
218
|
+
def is_constraint_line(line)
|
219
|
+
line =~ @constraint_start
|
220
|
+
end
|
221
|
+
|
222
|
+
def is_only_one_match_line(line)
|
223
|
+
line =~ @only_one_start
|
224
|
+
end
|
225
|
+
|
226
|
+
def build_constraint(line)
|
227
|
+
build(@constraint_match, line)
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
end
|