safrano 0.4.2 → 0.4.3
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 +4 -4
- data/lib/odata/attribute.rb +8 -7
- data/lib/odata/collection.rb +86 -34
- data/lib/odata/entity.rb +65 -69
- data/lib/odata/error.rb +18 -0
- data/lib/odata/filter/base.rb +66 -0
- data/lib/odata/filter/error.rb +27 -0
- data/lib/odata/filter/parse.rb +2 -0
- data/lib/odata/filter/sequel.rb +32 -17
- data/lib/odata/filter/sequel_function_adapter.rb +147 -0
- data/lib/odata/filter/token.rb +5 -1
- data/lib/odata/filter/tree.rb +34 -14
- data/lib/odata/navigation_attribute.rb +4 -2
- data/lib/odata/walker.rb +6 -4
- data/lib/safrano/core.rb +0 -2
- data/lib/safrano/multipart.rb +0 -9
- data/lib/safrano/odata_rack_builder.rb +0 -1
- data/lib/safrano/rack_app.rb +8 -6
- data/lib/safrano/request.rb +0 -7
- data/lib/safrano/service.rb +111 -47
- data/lib/safrano/version.rb +1 -1
- metadata +4 -2
data/lib/odata/error.rb
CHANGED
@@ -129,9 +129,27 @@ module OData
|
|
129
129
|
HTTP_CODE = 501
|
130
130
|
@msg = 'Not implemented: OData batch'
|
131
131
|
end
|
132
|
+
|
132
133
|
# error in filter parsing (Safrano specific)
|
133
134
|
class FilterParseError < BadRequestError
|
134
135
|
extend ErrorClass
|
135
136
|
HTTP_CODE = 400
|
136
137
|
end
|
138
|
+
|
139
|
+
class FilterFunctionNotImplementedError < BadRequestError
|
140
|
+
extend ErrorClass
|
141
|
+
include ErrorInstance
|
142
|
+
@msg = 'the requested $filter function is Not implemented'
|
143
|
+
HTTP_CODE = 400
|
144
|
+
def initialize(exception)
|
145
|
+
@msg = exception.to_s
|
146
|
+
end
|
147
|
+
end
|
148
|
+
class FilterInvalidFunctionError < BadRequestError
|
149
|
+
include ErrorInstance
|
150
|
+
HTTP_CODE = 400
|
151
|
+
def initialize(exception)
|
152
|
+
@msg = exception.to_s
|
153
|
+
end
|
154
|
+
end
|
137
155
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OData
|
4
|
+
module Filter
|
5
|
+
# Base class for Leaves, Trees, RootTrees etc
|
6
|
+
class Node
|
7
|
+
end
|
8
|
+
|
9
|
+
# Leaves are Nodes with a parent but no children
|
10
|
+
class Leave < Node
|
11
|
+
end
|
12
|
+
|
13
|
+
# RootTrees have childrens but no parent
|
14
|
+
class RootTree < Node
|
15
|
+
end
|
16
|
+
|
17
|
+
# Tree's have Parent and children
|
18
|
+
class Tree < RootTree
|
19
|
+
end
|
20
|
+
|
21
|
+
# For functions... should have a single child---> the argument list
|
22
|
+
class FuncTree < Tree
|
23
|
+
end
|
24
|
+
|
25
|
+
# Indentity Func to use as "parent" func of parenthesis expressions
|
26
|
+
# --> allow to handle generically parenthesis always as argument of
|
27
|
+
# some function
|
28
|
+
class IdentityFuncTree < FuncTree
|
29
|
+
end
|
30
|
+
|
31
|
+
# unary op eg. NOT
|
32
|
+
class UnopTree < Tree
|
33
|
+
end
|
34
|
+
|
35
|
+
# Bin ops
|
36
|
+
class BinopTree < Tree
|
37
|
+
end
|
38
|
+
|
39
|
+
class BinopBool < BinopTree
|
40
|
+
end
|
41
|
+
|
42
|
+
class BinopArithm < BinopTree
|
43
|
+
end
|
44
|
+
|
45
|
+
# Arguments or lists
|
46
|
+
class ArgTree < Tree
|
47
|
+
end
|
48
|
+
|
49
|
+
# Numbers (floating point, ints, dec)
|
50
|
+
class FPNumber < Leave
|
51
|
+
end
|
52
|
+
|
53
|
+
# Literals are unquoted words without /
|
54
|
+
class Literal < Leave
|
55
|
+
end
|
56
|
+
|
57
|
+
# Qualit (qualified lits) are words separated by /
|
58
|
+
# path/path/path/attrib
|
59
|
+
class Qualit < Literal
|
60
|
+
end
|
61
|
+
|
62
|
+
# Quoted Strings
|
63
|
+
class QString < Leave
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/odata/filter/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative '../error'
|
2
|
+
|
1
3
|
module OData
|
2
4
|
class SequelAdapterError < StandardError
|
3
5
|
attr_reader :inner
|
@@ -5,7 +7,22 @@ module OData
|
|
5
7
|
@inner = err
|
6
8
|
end
|
7
9
|
end
|
10
|
+
|
11
|
+
# exception to OData error bridge
|
12
|
+
module ErrorBridge
|
13
|
+
# return an odata error object wrapping the exception
|
14
|
+
# the odata error object should respond to odata_get for output
|
15
|
+
def odata_error
|
16
|
+
self.class.const_get('ODATA_ERROR_KLASS').new(self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
8
20
|
module Filter
|
21
|
+
class FunctionNotImplemented < StandardError
|
22
|
+
ODATA_ERROR_KLASS = OData::FilterFunctionNotImplementedError
|
23
|
+
include ::OData::ErrorBridge
|
24
|
+
end
|
25
|
+
|
9
26
|
class Parser
|
10
27
|
# Parser errors
|
11
28
|
class Error < StandardError
|
@@ -35,6 +52,16 @@ module OData
|
|
35
52
|
class ErrorWrongColumnName < StandardError
|
36
53
|
end
|
37
54
|
|
55
|
+
# attempt to add a child to a Leave
|
56
|
+
class ErrorLeaveChild < StandardError
|
57
|
+
end
|
58
|
+
|
59
|
+
# invalid function error (literal attach to IdentityFuncTree)
|
60
|
+
class ErrorInvalidFunction < StandardError
|
61
|
+
ODATA_ERROR_KLASS = OData::FilterInvalidFunctionError
|
62
|
+
include ::OData::ErrorBridge
|
63
|
+
end
|
64
|
+
|
38
65
|
# Invalid function arity
|
39
66
|
class ErrorInvalidArity < Error
|
40
67
|
end
|
data/lib/odata/filter/parse.rb
CHANGED
data/lib/odata/filter/sequel.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './base.rb'
|
4
|
+
require_relative './sequel_function_adapter.rb'
|
5
|
+
|
2
6
|
module OData
|
3
7
|
module Filter
|
4
8
|
# Base class for Leaves, Trees, RootTrees etc
|
5
|
-
class Node
|
6
|
-
end
|
9
|
+
# class Node
|
10
|
+
# end
|
7
11
|
|
8
12
|
# Leaves are Nodes with a parent but no children
|
9
|
-
class Leave < Node
|
10
|
-
end
|
13
|
+
# class Leave < Node
|
14
|
+
# end
|
11
15
|
|
12
16
|
# RootTrees have childrens but no parent
|
13
17
|
class RootTree
|
@@ -26,6 +30,8 @@ module OData
|
|
26
30
|
end
|
27
31
|
|
28
32
|
# For functions... should have a single child---> the argument list
|
33
|
+
# note: Adapter specific function helpers like year() or substringof_sig2()
|
34
|
+
# need to be mixed in on startup (eg. on publish finalize)
|
29
35
|
class FuncTree < Tree
|
30
36
|
def leuqes(jh)
|
31
37
|
case @value
|
@@ -48,18 +54,7 @@ module OData
|
|
48
54
|
Sequel.like(args[1].leuqes(jh),
|
49
55
|
args[0].leuqes_substringof_sig1(jh))
|
50
56
|
elsif args[1].is_a?(QString)
|
51
|
-
|
52
|
-
# '__Route du Rhum__' contains name as a substring
|
53
|
-
# TODO... check if the database supports instr (how?)
|
54
|
-
# othewise use substr(postgresql) or whatevr?
|
55
|
-
instr_substr_func = if Sequel::Model.db.adapter_scheme == :postgres
|
56
|
-
Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
|
57
|
-
else
|
58
|
-
Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
59
|
-
end
|
60
|
-
|
61
|
-
Sequel::SQL::BooleanExpression.new(:>, instr_substr_func, 0)
|
62
|
-
|
57
|
+
substringof_sig2(jh) # adapter specific
|
63
58
|
else
|
64
59
|
# TODO... actually not supported?
|
65
60
|
raise OData::Filter::Parser::ErrorFunctionArgumentType
|
@@ -75,6 +70,26 @@ module OData
|
|
75
70
|
Sequel.function(:upper, args.first.leuqes(jh))
|
76
71
|
when :tolower
|
77
72
|
Sequel.function(:lower, args.first.leuqes(jh))
|
73
|
+
# all datetime funcs are adapter specific (because sqlite does not have extract)
|
74
|
+
when :year
|
75
|
+
year(jh)
|
76
|
+
when :month
|
77
|
+
month(jh)
|
78
|
+
when :second
|
79
|
+
second(jh)
|
80
|
+
when :minute
|
81
|
+
minute(jh)
|
82
|
+
when :hour
|
83
|
+
hour(jh)
|
84
|
+
when :day
|
85
|
+
day(jh)
|
86
|
+
# math functions
|
87
|
+
when :round
|
88
|
+
Sequel.function(:round, args.first.leuqes(jh))
|
89
|
+
when :floor
|
90
|
+
floor(jh)
|
91
|
+
when :ceiling
|
92
|
+
ceiling(jh)
|
78
93
|
else
|
79
94
|
raise OData::FilterParseError
|
80
95
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './tree.rb'
|
4
|
+
require_relative './sequel.rb'
|
5
|
+
|
6
|
+
module OData
|
7
|
+
module Filter
|
8
|
+
# sqlite adapter specific function handler
|
9
|
+
module FuncTreeSqlite
|
10
|
+
def substringof_sig2(jh)
|
11
|
+
# substringof(name, '__Route du Rhum__') -->
|
12
|
+
# '__Route du Rhum__' contains name as a substring
|
13
|
+
# sqlite uses instr()
|
14
|
+
|
15
|
+
substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
16
|
+
|
17
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
18
|
+
end
|
19
|
+
# %d day of month: 00
|
20
|
+
# %f fractional seconds: SS.SSS
|
21
|
+
# %H hour: 00-24
|
22
|
+
# %j day of year: 001-366
|
23
|
+
# %J Julian day number
|
24
|
+
# %m month: 01-12
|
25
|
+
# %M minute: 00-59
|
26
|
+
# %s seconds since 1970-01-01
|
27
|
+
# %S seconds: 00-59
|
28
|
+
# %w day of week 0-6 with Sunday==0
|
29
|
+
# %W week of year: 00-53
|
30
|
+
# %Y year: 0000-9999
|
31
|
+
# %% %
|
32
|
+
|
33
|
+
# sqlite does not have extract but recommends to use strftime
|
34
|
+
def year(jh)
|
35
|
+
Sequel.function(:strftime, '%Y', args.first.leuqes(jh)).cast(:integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def month(jh)
|
39
|
+
Sequel.function(:strftime, '%m', args.first.leuqes(jh)).cast(:integer)
|
40
|
+
end
|
41
|
+
|
42
|
+
def second(jh)
|
43
|
+
Sequel.function(:strftime, '%S', args.first.leuqes(jh)).cast(:integer)
|
44
|
+
end
|
45
|
+
|
46
|
+
def minute(jh)
|
47
|
+
Sequel.function(:strftime, '%M', args.first.leuqes(jh)).cast(:integer)
|
48
|
+
end
|
49
|
+
|
50
|
+
def hour(jh)
|
51
|
+
Sequel.function(:strftime, '%H', args.first.leuqes(jh)).cast(:integer)
|
52
|
+
end
|
53
|
+
|
54
|
+
def day(jh)
|
55
|
+
Sequel.function(:strftime, '%d', args.first.leuqes(jh)).cast(:integer)
|
56
|
+
end
|
57
|
+
|
58
|
+
def floor(jh)
|
59
|
+
raise OData::Filter::FunctionNotImplemented, "$filter function 'floor' is not implemented in sqlite adapter"
|
60
|
+
end
|
61
|
+
|
62
|
+
def ceiling(jh)
|
63
|
+
raise OData::Filter::FunctionNotImplemented, "$filter function 'ceiling' is not implemented in sqlite adapter"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
# re-useable module with math floor/ceil functions for those adapters having these SQL funcs
|
67
|
+
module MathFloorCeilFuncTree
|
68
|
+
def floor(jh)
|
69
|
+
Sequel.function(:floor, args.first.leuqes(jh))
|
70
|
+
end
|
71
|
+
|
72
|
+
def ceiling(jh)
|
73
|
+
Sequel.function(:ceil, args.first.leuqes(jh))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# re-useable module with Datetime functions with extract()
|
78
|
+
module DateTimeFuncTreeExtract
|
79
|
+
def year(jh)
|
80
|
+
args.first.leuqes(jh).extract(:year)
|
81
|
+
end
|
82
|
+
|
83
|
+
def year(jh)
|
84
|
+
args.first.leuqes(jh).extract(:year)
|
85
|
+
end
|
86
|
+
|
87
|
+
def month(jh)
|
88
|
+
args.first.leuqes(jh).extract(:month)
|
89
|
+
end
|
90
|
+
|
91
|
+
def second(jh)
|
92
|
+
args.first.leuqes(jh).extract(:second)
|
93
|
+
end
|
94
|
+
|
95
|
+
def minute(jh)
|
96
|
+
args.first.leuqes(jh).extract(:minute)
|
97
|
+
end
|
98
|
+
|
99
|
+
def hour(jh)
|
100
|
+
args.first.leuqes(jh).extract(:hour)
|
101
|
+
end
|
102
|
+
|
103
|
+
def day(jh)
|
104
|
+
args.first.leuqes(jh).extract(:day)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# postgresql adapter specific function handler
|
109
|
+
module FuncTreePostgres
|
110
|
+
def substringof_sig2(jh)
|
111
|
+
# substringof(name, '__Route du Rhum__') -->
|
112
|
+
# '__Route du Rhum__' contains name as a substring
|
113
|
+
# postgres does not know instr() but has strpos
|
114
|
+
substr_func = Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
|
115
|
+
|
116
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
117
|
+
end
|
118
|
+
|
119
|
+
# postgres uses extract()
|
120
|
+
include DateTimeFuncTreeExtract
|
121
|
+
|
122
|
+
# postgres has floor/ceil funcs
|
123
|
+
include MathFloorCeilFuncTree
|
124
|
+
end
|
125
|
+
|
126
|
+
# default adapter function handler for all others... try to use the most common version
|
127
|
+
# :substring --> instr because here is seems Postgres is special
|
128
|
+
# datetime funcs --> exctract, here sqlite is special(uses format)
|
129
|
+
# note: we dont test this, provided as an example/template, might work eg for mysql
|
130
|
+
module FuncTreeDefault
|
131
|
+
def substringof_sig2(jh)
|
132
|
+
# substringof(name, '__Route du Rhum__') -->
|
133
|
+
# '__Route du Rhum__' contains name as a substring
|
134
|
+
# instr() seems to be the most common substring func
|
135
|
+
substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
136
|
+
|
137
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
138
|
+
end
|
139
|
+
|
140
|
+
# XYZ uses extract() ?
|
141
|
+
include DateTimeFuncTreeExtract
|
142
|
+
|
143
|
+
# ... assume SQL
|
144
|
+
include MathFloorCeilFuncTree
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/odata/filter/token.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# top level OData namespace
|
2
4
|
module OData
|
3
5
|
module Filter
|
@@ -5,7 +7,9 @@ module OData
|
|
5
7
|
# Input tokenizer
|
6
8
|
module Token
|
7
9
|
FUNCNAMES = %w[concat substringof endswith startswith length indexof
|
8
|
-
replace substring trim toupper tolower
|
10
|
+
replace substring trim toupper tolower
|
11
|
+
day hour minute month second year
|
12
|
+
round floor ceiling].freeze
|
9
13
|
FUNCRGX = FUNCNAMES.join('|').freeze
|
10
14
|
QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
11
15
|
BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'.freeze
|
data/lib/odata/filter/tree.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './base.rb'
|
1
4
|
require_relative './error.rb'
|
2
5
|
|
3
6
|
module OData
|
@@ -16,17 +19,22 @@ module OData
|
|
16
19
|
end
|
17
20
|
|
18
21
|
# Leaves are Nodes with a parent but no children
|
19
|
-
class Leave
|
22
|
+
class Leave
|
20
23
|
attr_accessor :parent
|
21
24
|
def accept?(tok, typ)
|
22
25
|
[false, Parser::ErrorInvalidToken(tok, typ)]
|
23
26
|
end
|
24
27
|
|
25
28
|
def check_types; end
|
29
|
+
|
30
|
+
def attach(child)
|
31
|
+
# TODO better reporting of error infos
|
32
|
+
raise ErrorLeaveChild
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
# RootTrees have childrens but no parent
|
29
|
-
class RootTree
|
37
|
+
class RootTree
|
30
38
|
attr_reader :children
|
31
39
|
attr_accessor :state
|
32
40
|
def initialize(val: :root, &block)
|
@@ -71,7 +79,7 @@ module OData
|
|
71
79
|
end
|
72
80
|
|
73
81
|
# Tree's have Parent and children
|
74
|
-
class Tree
|
82
|
+
class Tree
|
75
83
|
attr_accessor :parent
|
76
84
|
|
77
85
|
def initialize(val)
|
@@ -80,7 +88,7 @@ module OData
|
|
80
88
|
end
|
81
89
|
|
82
90
|
# For functions... should have a single child---> the argument list
|
83
|
-
class FuncTree
|
91
|
+
class FuncTree
|
84
92
|
def initialize(val)
|
85
93
|
super(val.downcase.to_sym)
|
86
94
|
end
|
@@ -143,7 +151,7 @@ module OData
|
|
143
151
|
# Indentity Func to use as "parent" func of parenthesis expressions
|
144
152
|
# --> allow to handle generically parenthesis always as argument of
|
145
153
|
# some function
|
146
|
-
class IdentityFuncTree
|
154
|
+
class IdentityFuncTree
|
147
155
|
def initialize
|
148
156
|
super(:__indentity)
|
149
157
|
end
|
@@ -160,7 +168,7 @@ module OData
|
|
160
168
|
end
|
161
169
|
|
162
170
|
# unary op eg. NOT
|
163
|
-
class UnopTree
|
171
|
+
class UnopTree
|
164
172
|
def initialize(val)
|
165
173
|
super(val.downcase.to_sym)
|
166
174
|
end
|
@@ -187,7 +195,7 @@ module OData
|
|
187
195
|
end
|
188
196
|
|
189
197
|
# Bin ops
|
190
|
-
class BinopTree
|
198
|
+
class BinopTree
|
191
199
|
def initialize(val)
|
192
200
|
@state = :open
|
193
201
|
super(val.downcase.to_sym)
|
@@ -201,7 +209,7 @@ module OData
|
|
201
209
|
end
|
202
210
|
end
|
203
211
|
|
204
|
-
class BinopBool
|
212
|
+
class BinopBool
|
205
213
|
# reference:
|
206
214
|
# OData v4 par 5.1.1.9 Operator Precedence
|
207
215
|
def precedence
|
@@ -224,7 +232,7 @@ module OData
|
|
224
232
|
end
|
225
233
|
end
|
226
234
|
|
227
|
-
class BinopArithm
|
235
|
+
class BinopArithm
|
228
236
|
# reference:
|
229
237
|
# OData v4 par 5.1.1.9 Operator Precedence
|
230
238
|
def precedence
|
@@ -245,7 +253,7 @@ module OData
|
|
245
253
|
end
|
246
254
|
|
247
255
|
# Arguments or lists
|
248
|
-
class ArgTree
|
256
|
+
class ArgTree
|
249
257
|
attr_reader :type
|
250
258
|
def initialize(val)
|
251
259
|
@type = :expression
|
@@ -307,7 +315,7 @@ module OData
|
|
307
315
|
end
|
308
316
|
|
309
317
|
# Numbers (floating point, ints, dec)
|
310
|
-
class FPNumber
|
318
|
+
class FPNumber
|
311
319
|
def accept?(tok, typ)
|
312
320
|
case typ
|
313
321
|
when :Delimiter, :Separator, :BinopBool, :BinopArithm
|
@@ -324,7 +332,7 @@ module OData
|
|
324
332
|
end
|
325
333
|
|
326
334
|
# Literals are unquoted words without /
|
327
|
-
class Literal
|
335
|
+
class Literal
|
328
336
|
def accept?(tok, typ)
|
329
337
|
case typ
|
330
338
|
when :Delimiter, :Separator, :BinopBool, :BinopArithm
|
@@ -337,11 +345,23 @@ module OData
|
|
337
345
|
def edm_type
|
338
346
|
:any
|
339
347
|
end
|
348
|
+
|
349
|
+
# error, Literal are leaves
|
350
|
+
# when the child is a IdentityFuncTree then this looks like
|
351
|
+
# an attempt to use a unknown function, eg. ceil(Total)
|
352
|
+
# instead of ceiling(Total)
|
353
|
+
def attach(child)
|
354
|
+
if child.kind_of? OData::Filter::IdentityFuncTree
|
355
|
+
raise Parser::ErrorInvalidFunction.new("Error in $filter expr.: invalid function #{self.value}")
|
356
|
+
else
|
357
|
+
super
|
358
|
+
end
|
359
|
+
end
|
340
360
|
end
|
341
361
|
|
342
362
|
# Qualit (qualified lits) are words separated by /
|
343
363
|
# path/path/path/attrib
|
344
|
-
class Qualit
|
364
|
+
class Qualit
|
345
365
|
REGEXP = /((?:\w+\/)+)(\w+)/.freeze
|
346
366
|
attr_reader :path
|
347
367
|
attr_reader :attrib
|
@@ -356,7 +376,7 @@ module OData
|
|
356
376
|
end
|
357
377
|
|
358
378
|
# Quoted Strings
|
359
|
-
class QString
|
379
|
+
class QString
|
360
380
|
DBL_QO = "''".freeze
|
361
381
|
SI_QO = "'".freeze
|
362
382
|
def initialize(val)
|