safrano 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/multipart.rb +92 -91
- data/lib/odata/attribute.rb +15 -6
- data/lib/odata/collection.rb +109 -106
- data/lib/odata/collection_filter.rb +14 -485
- data/lib/odata/collection_order.rb +15 -22
- data/lib/odata/entity.rb +31 -41
- data/lib/odata/filter/error.rb +53 -0
- data/lib/odata/filter/parse.rb +171 -0
- data/lib/odata/filter/sequel.rb +208 -0
- data/lib/odata/filter/token.rb +59 -0
- data/lib/odata/filter/tree.rb +368 -0
- data/lib/odata/relations.rb +36 -0
- data/lib/odata/url_parameters.rb +58 -0
- data/lib/odata/walker.rb +55 -42
- data/lib/request.rb +1 -3
- data/lib/safrano.rb +17 -0
- data/lib/safrano_core.rb +30 -7
- data/lib/sequel/plugins/join_by_paths.rb +239 -0
- data/lib/sequel_join_by_paths.rb +5 -0
- data/lib/service.rb +84 -112
- metadata +11 -3
@@ -0,0 +1,208 @@
|
|
1
|
+
require_relative './tree.rb'
|
2
|
+
module OData
|
3
|
+
module Filter
|
4
|
+
# Base class for Leaves, Trees, RootTrees etc
|
5
|
+
class Node
|
6
|
+
end
|
7
|
+
|
8
|
+
# Leaves are Nodes with a parent but no children
|
9
|
+
class Leave < Node
|
10
|
+
end
|
11
|
+
|
12
|
+
# RootTrees have childrens but no parent
|
13
|
+
class RootTree
|
14
|
+
def apply_to_dataset(dtcx, jh)
|
15
|
+
filtexpr = @children.first.leuqes(jh)
|
16
|
+
dtcx = jh.dataset.where(filtexpr).select_all(jh.start_model.table_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sequel_expr(jh)
|
20
|
+
@children.first.leuqes(jh)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Tree's have Parent and children
|
25
|
+
class Tree < RootTree
|
26
|
+
end
|
27
|
+
|
28
|
+
# For functions... should have a single child---> the argument list
|
29
|
+
class FuncTree < Tree
|
30
|
+
def leuqes(jh)
|
31
|
+
case @value
|
32
|
+
when :startswith
|
33
|
+
Sequel.like(args[0].leuqes(jh),
|
34
|
+
args[1].leuqes_starts_like(jh))
|
35
|
+
when :endswith
|
36
|
+
Sequel.like(args[0].leuqes(jh),
|
37
|
+
args[1].leuqes_ends_like(jh))
|
38
|
+
when :substringof
|
39
|
+
# there are two possible signatures
|
40
|
+
if (args[0].is_a?(QString) && args[1].is_a?(Literal))
|
41
|
+
# substringof('Rhum', name) -->
|
42
|
+
# name contains substr 'Rhum'
|
43
|
+
Sequel.like(args[1].leuqes(jh),
|
44
|
+
args[0].leuqes_substringof_like(jh))
|
45
|
+
# according to the spec it should be like that?
|
46
|
+
# elsif args[1].is_a? QString
|
47
|
+
## substringof(name, '__Route du Rhum__') -->
|
48
|
+
## '__Route du Rhum__' contains name as a substring
|
49
|
+
# Sequel.like(args[1].leuqes_substringof_like(dtcx),
|
50
|
+
# args[0].leuqes(dtcx) )
|
51
|
+
# or like that?
|
52
|
+
# elsif args[1].is_a? QString
|
53
|
+
# # substringof(name, 'Route du Rhum') -->
|
54
|
+
# # name contains substr 'Rhum' ??
|
55
|
+
# Sequel.like(args[0].leuqes(dtcx) ,
|
56
|
+
# args[1].leuqes_substringof_like(dtcx) )
|
57
|
+
elsif (args[0].is_a?(Literal) && args[1].is_a?(Literal))
|
58
|
+
Sequel.like(args[1].leuqes(jh),
|
59
|
+
args[0].leuqes_substringof_like(jh))
|
60
|
+
else
|
61
|
+
# TODO... actually not supported?
|
62
|
+
raise OData::Filter::Parser::ErrorFunctionArgumentType
|
63
|
+
end
|
64
|
+
when :concat
|
65
|
+
Sequel.join([args[0].leuqes(jh),
|
66
|
+
args[1].leuqes(jh)])
|
67
|
+
when :length
|
68
|
+
Sequel.char_length(args.first.leuqes(jh))
|
69
|
+
when :trim
|
70
|
+
Sequel.trim(args.first.leuqes(jh))
|
71
|
+
when :toupper
|
72
|
+
Sequel.function(:upper, args.first.leuqes(jh))
|
73
|
+
when :tolower
|
74
|
+
Sequel.function(:lower, args.first.leuqes(jh))
|
75
|
+
else
|
76
|
+
raise OData::FilterParseError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Indentity Func to use as "parent" func of parenthesis expressions
|
82
|
+
# --> allow to handle generically parenthesis always as argument of
|
83
|
+
# some function
|
84
|
+
class IdentityFuncTree < FuncTree
|
85
|
+
def leuqes(jh)
|
86
|
+
args.first.leuqes(jh)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# unary op eg. NOT
|
91
|
+
class UnopTree < Tree
|
92
|
+
def leuqes(jh)
|
93
|
+
case @value
|
94
|
+
when :not
|
95
|
+
Sequel.~(@children.first.leuqes(jh))
|
96
|
+
else
|
97
|
+
raise OData::FilterParseError
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Bin ops
|
103
|
+
class BinopTree < Tree
|
104
|
+
def leuqes(jh)
|
105
|
+
leuqes_op = case @value
|
106
|
+
when :eq
|
107
|
+
:'='
|
108
|
+
when :ne
|
109
|
+
:'!='
|
110
|
+
when :le
|
111
|
+
:<=
|
112
|
+
when :ge
|
113
|
+
:'>='
|
114
|
+
when :lt
|
115
|
+
:<
|
116
|
+
when :gt
|
117
|
+
:>
|
118
|
+
when :or
|
119
|
+
:OR
|
120
|
+
when :and
|
121
|
+
:AND
|
122
|
+
else
|
123
|
+
raise OData::FilterParseError
|
124
|
+
end
|
125
|
+
|
126
|
+
Sequel::SQL::BooleanExpression.new(leuqes_op,
|
127
|
+
@children[0].leuqes(jh),
|
128
|
+
@children[1].leuqes(jh))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Arguments or lists
|
133
|
+
class ArgTree
|
134
|
+
end
|
135
|
+
|
136
|
+
# Numbers (floating point, ints, dec)
|
137
|
+
class FPNumber
|
138
|
+
def leuqes(_jh)
|
139
|
+
Sequel.lit(@value)
|
140
|
+
end
|
141
|
+
|
142
|
+
def leuqes_starts_like(_jh)
|
143
|
+
"#{@value.to_s}%"
|
144
|
+
end
|
145
|
+
|
146
|
+
def leuqes_ends_like(_jh)
|
147
|
+
"%#{@value.to_s}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def leuqes_substringof_like(_jh)
|
151
|
+
"%#{@value.to_s}%"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Literals are unquoted words
|
156
|
+
class Literal
|
157
|
+
def leuqes(jh)
|
158
|
+
if jh.start_model.db_schema.has_key?(@value.to_sym)
|
159
|
+
Sequel[jh.start_model.table_name][@value.to_sym]
|
160
|
+
else
|
161
|
+
raise OData::Filter::Parser::ErrorWrongColumnName
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# non stantard extensions to support things like
|
166
|
+
# substringof(Rhum, name) ????
|
167
|
+
def leuqes_starts_like(_jh)
|
168
|
+
"#{@value}%"
|
169
|
+
end
|
170
|
+
|
171
|
+
def leuqes_ends_like(_jh)
|
172
|
+
"%#{@value}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def leuqes_substringof_like(_jh)
|
176
|
+
"%#{@value}%"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Qualit (qualified lits) are words separated by /
|
181
|
+
class Qualit
|
182
|
+
def leuqes(jh)
|
183
|
+
jh.add(@path)
|
184
|
+
talias = jh.start_model.get_alias_sym(@path)
|
185
|
+
Sequel[talias][@attrib.to_sym]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Quoted Strings
|
190
|
+
class QString
|
191
|
+
def leuqes(_jh)
|
192
|
+
@value
|
193
|
+
end
|
194
|
+
|
195
|
+
def leuqes_starts_like(_jh)
|
196
|
+
"#{@value}%"
|
197
|
+
end
|
198
|
+
|
199
|
+
def leuqes_ends_like(_jh)
|
200
|
+
"%#{@value}"
|
201
|
+
end
|
202
|
+
|
203
|
+
def leuqes_substringof_like(_jh)
|
204
|
+
"%#{@value}%"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# top level OData namespace
|
2
|
+
module OData
|
3
|
+
module Filter
|
4
|
+
class Parser
|
5
|
+
# Input tokenizer
|
6
|
+
module Token
|
7
|
+
FUNCNAMES = %w[concat substringof endswith startswith length indexof
|
8
|
+
replace substring trim toupper tolower].freeze
|
9
|
+
FUNCRGX = FUNCNAMES.join('|').freeze
|
10
|
+
QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
11
|
+
BINOPSRGX = '[eE][qQ]|[LlgGNn][eETt]|and|AND|or|OR'.freeze
|
12
|
+
NOTRGX = 'not|NOT'.freeze
|
13
|
+
FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'.freeze
|
14
|
+
QUALITRGX = '\w+(?:\/\w+)+'.freeze
|
15
|
+
RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOPSRGX})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
|
16
|
+
def each_typed_token(inp)
|
17
|
+
typ = nil
|
18
|
+
|
19
|
+
inp.scan(RGX) do |groups|
|
20
|
+
idx = nil
|
21
|
+
found = nil
|
22
|
+
groups.each_with_index do |tok, i|
|
23
|
+
if (found = tok)
|
24
|
+
idx = i
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
typ = case idx
|
29
|
+
when 0
|
30
|
+
:FuncTree
|
31
|
+
when 1
|
32
|
+
case found
|
33
|
+
when '(', ')'
|
34
|
+
:Delimiter
|
35
|
+
when ','
|
36
|
+
:Separator
|
37
|
+
end
|
38
|
+
when 2
|
39
|
+
:BinopTree
|
40
|
+
when 3
|
41
|
+
:UnopTree
|
42
|
+
when 4
|
43
|
+
:QString
|
44
|
+
when 5
|
45
|
+
:FPNumber
|
46
|
+
when 6
|
47
|
+
:Qualit
|
48
|
+
when 7
|
49
|
+
:Literal
|
50
|
+
when 8
|
51
|
+
:unmatchedQuote
|
52
|
+
end
|
53
|
+
yield found, typ
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
require_relative './error.rb'
|
2
|
+
|
3
|
+
module OData
|
4
|
+
module Filter
|
5
|
+
# Base class for Leaves, Trees, RootTrees etc
|
6
|
+
class Node
|
7
|
+
attr_reader :value
|
8
|
+
def initialize(val, &block)
|
9
|
+
@value = val
|
10
|
+
instance_eval(&block) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
@value == other.value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Leaves are Nodes with a parent but no children
|
19
|
+
class Leave < Node
|
20
|
+
attr_accessor :parent
|
21
|
+
def accept?(tok, typ)
|
22
|
+
[false, Parser::ErrorInvalidToken(tok, typ)]
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_types; end
|
26
|
+
end
|
27
|
+
|
28
|
+
# RootTrees have childrens but no parent
|
29
|
+
class RootTree < Node
|
30
|
+
attr_reader :children
|
31
|
+
attr_accessor :state
|
32
|
+
def initialize(val: :root, &block)
|
33
|
+
@children = []
|
34
|
+
super(val, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def attach(child)
|
38
|
+
child.parent = self
|
39
|
+
@children << child
|
40
|
+
end
|
41
|
+
|
42
|
+
def detach(child)
|
43
|
+
child.parent = nil
|
44
|
+
@children.delete(child)
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
super(other) && (@children == other.children)
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_state(tok, typ) end
|
52
|
+
|
53
|
+
def accept?(tok, typ)
|
54
|
+
case typ
|
55
|
+
when :Literal, :Qualit, :QString, :FuncTree, :ArgTree, :UnopTree, :FPNumber
|
56
|
+
true
|
57
|
+
when :Delimiter
|
58
|
+
if tok == '('
|
59
|
+
true
|
60
|
+
else
|
61
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
62
|
+
end
|
63
|
+
else
|
64
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_types
|
69
|
+
@children.each(&:check_types)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Tree's have Parent and children
|
74
|
+
class Tree < RootTree
|
75
|
+
attr_accessor :parent
|
76
|
+
|
77
|
+
def initialize(val)
|
78
|
+
super(val: val)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# For functions... should have a single child---> the argument list
|
83
|
+
class FuncTree < Tree
|
84
|
+
def initialize(val)
|
85
|
+
super(val.downcase.to_sym)
|
86
|
+
end
|
87
|
+
|
88
|
+
def args
|
89
|
+
@children.first.children
|
90
|
+
end
|
91
|
+
|
92
|
+
def arity_full?(cursize)
|
93
|
+
cursize >= max_arity
|
94
|
+
end
|
95
|
+
|
96
|
+
def max_arity
|
97
|
+
case @value
|
98
|
+
when :replace
|
99
|
+
3
|
100
|
+
when :concat, :substringof, :substring, :endswith, :startswith
|
101
|
+
2
|
102
|
+
else
|
103
|
+
1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def edm_type
|
108
|
+
case @value
|
109
|
+
when :concat, :substring
|
110
|
+
:string
|
111
|
+
when :length
|
112
|
+
:int
|
113
|
+
when :substringof, :endswith, :startswith
|
114
|
+
:bool
|
115
|
+
else
|
116
|
+
:any
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def accept?(tok, typ)
|
121
|
+
case typ
|
122
|
+
when :BinopTree
|
123
|
+
true
|
124
|
+
else
|
125
|
+
super(tok, typ)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_types
|
130
|
+
case @value
|
131
|
+
when :length
|
132
|
+
argtyp = args.first.edm_type
|
133
|
+
if (argtyp != :any) && (argtyp != :string)
|
134
|
+
raise Parser::ErrorInvalidArgumentType.new(self,
|
135
|
+
expected: :string,
|
136
|
+
actual: argtyp)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
super
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Indentity Func to use as "parent" func of parenthesis expressions
|
144
|
+
# --> allow to handle generically parenthesis always as argument of
|
145
|
+
# some function
|
146
|
+
class IdentityFuncTree < FuncTree
|
147
|
+
def initialize
|
148
|
+
super(:__indentity)
|
149
|
+
end
|
150
|
+
|
151
|
+
# we can have parenthesis with one expression inside everywhere
|
152
|
+
# only in FuncTree this is redefined for the function's arity
|
153
|
+
def arity_full?(cursize)
|
154
|
+
cursize >= 1
|
155
|
+
end
|
156
|
+
|
157
|
+
def edm_type
|
158
|
+
@children.first.edm_type
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# unary op eg. NOT
|
163
|
+
class UnopTree < Tree
|
164
|
+
def initialize(val)
|
165
|
+
super(val.downcase.to_sym)
|
166
|
+
end
|
167
|
+
|
168
|
+
# reference:
|
169
|
+
# OData v4 par 5.1.1.9 Operator Precedence
|
170
|
+
def precedence
|
171
|
+
case @value
|
172
|
+
when :not
|
173
|
+
7
|
174
|
+
else
|
175
|
+
999
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def edm_type
|
180
|
+
case @value
|
181
|
+
when :not
|
182
|
+
:bool
|
183
|
+
else
|
184
|
+
:any
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Bin ops
|
190
|
+
class BinopTree < Tree
|
191
|
+
def initialize(val)
|
192
|
+
@state = :open
|
193
|
+
super(val.downcase.to_sym)
|
194
|
+
end
|
195
|
+
|
196
|
+
# reference:
|
197
|
+
# OData v4 par 5.1.1.9 Operator Precedence
|
198
|
+
def precedence
|
199
|
+
case @value
|
200
|
+
when :or
|
201
|
+
1
|
202
|
+
when :and
|
203
|
+
2
|
204
|
+
when :eq, :ne
|
205
|
+
3
|
206
|
+
when :gt, :ge, :lt, :le
|
207
|
+
4
|
208
|
+
else
|
209
|
+
999
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def update_state(_tok, typ)
|
214
|
+
case typ
|
215
|
+
when :Literal, :Qualit, :QString, :FuncTree, :BinopTree, :UnopTree, :FPNumber
|
216
|
+
@state = :closed
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def edm_type
|
221
|
+
case @value
|
222
|
+
when :or, :and, :eq, :ne, :gt, :ge, :lt, :le
|
223
|
+
:bool
|
224
|
+
else
|
225
|
+
:any
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Arguments or lists
|
231
|
+
class ArgTree < Tree
|
232
|
+
attr_reader :type
|
233
|
+
def initialize(val)
|
234
|
+
@type = :expression
|
235
|
+
@state = :open
|
236
|
+
super
|
237
|
+
end
|
238
|
+
|
239
|
+
def update_state(_tok, typ)
|
240
|
+
case typ
|
241
|
+
when :Delimiter
|
242
|
+
@state = :closed
|
243
|
+
when :Separator
|
244
|
+
@state = :sep
|
245
|
+
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
246
|
+
@state = :val
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def accept?(tok, typ)
|
251
|
+
case typ
|
252
|
+
when :Delimiter
|
253
|
+
if @value == '(' && tok == ')' && @state != :closed
|
254
|
+
if @parent.arity_full?(@children.size)
|
255
|
+
true
|
256
|
+
else
|
257
|
+
[false, Parser::ErrorInvalidArity.new(tok, typ, self)]
|
258
|
+
end
|
259
|
+
else
|
260
|
+
[false, Parser::ErrorUnmatchedClose.new(tok, typ, self)]
|
261
|
+
end
|
262
|
+
when :Separator
|
263
|
+
if @value == '(' && tok == ',' && @state == :val
|
264
|
+
true
|
265
|
+
else
|
266
|
+
if (@state == :sep)
|
267
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
268
|
+
else
|
269
|
+
true
|
270
|
+
end
|
271
|
+
end
|
272
|
+
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
273
|
+
if (@state == :open) || (@state == :sep)
|
274
|
+
if @parent.arity_full?(@children.size)
|
275
|
+
[false, Parser::ErrorInvalidArity.new(tok, typ, self)]
|
276
|
+
else
|
277
|
+
true
|
278
|
+
end
|
279
|
+
else
|
280
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
281
|
+
end
|
282
|
+
when :BinopTree
|
283
|
+
true
|
284
|
+
else
|
285
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def ==(other)
|
290
|
+
super(other) && @type == other.type && @state == other.state
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Numbers (floating point, ints, dec)
|
295
|
+
class FPNumber < Leave
|
296
|
+
def accept?(tok, typ)
|
297
|
+
case typ
|
298
|
+
when :Delimiter, :Separator, :BinopTree
|
299
|
+
true
|
300
|
+
else
|
301
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# TODO
|
306
|
+
def edm_type
|
307
|
+
:number
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Literals are unquoted words without /
|
312
|
+
class Literal < Leave
|
313
|
+
def accept?(tok, typ)
|
314
|
+
case typ
|
315
|
+
when :Delimiter, :Separator, :BinopTree
|
316
|
+
true
|
317
|
+
else
|
318
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def edm_type
|
323
|
+
:any
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Qualit (qualified lits) are words separated by /
|
328
|
+
# path/path/path/attrib
|
329
|
+
class Qualit < Literal
|
330
|
+
REGEXP = /((?:\w+\/)+)(\w+)/.freeze
|
331
|
+
attr_reader :path
|
332
|
+
attr_reader :attrib
|
333
|
+
def initialize(val)
|
334
|
+
super(val)
|
335
|
+
# split into path + attrib
|
336
|
+
if (md = REGEXP.match(val))
|
337
|
+
@path = md[1].chomp('/')
|
338
|
+
@attrib = md[2]
|
339
|
+
else
|
340
|
+
raise Parser::Error.new(self, Qualit)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Quoted Strings
|
346
|
+
class QString < Leave
|
347
|
+
DoubleQuote = "''".freeze
|
348
|
+
SingleQuote = "'".freeze
|
349
|
+
def initialize(val)
|
350
|
+
# unescape double quotes
|
351
|
+
super(val.gsub(DoubleQuote, SingleQuote))
|
352
|
+
end
|
353
|
+
|
354
|
+
def accept?(tok, typ)
|
355
|
+
case typ
|
356
|
+
when :Delimiter, :Separator, :BinopTree
|
357
|
+
true
|
358
|
+
else
|
359
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def edm_type
|
364
|
+
:string
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|