j2119 0.1.0
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 +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
|