safrano 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/odata/batch.rb +6 -6
- data/lib/odata/collection.rb +134 -74
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +53 -54
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +34 -34
- data/lib/odata/entity.rb +86 -70
- data/lib/odata/error.rb +17 -4
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/parse.rb +4 -12
- data/lib/odata/filter/sequel.rb +11 -13
- data/lib/odata/filter/tree.rb +11 -15
- data/lib/odata/navigation_attribute.rb +36 -40
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/safrano.rb +5 -5
- data/lib/safrano/core.rb +10 -1
- data/lib/safrano/multipart.rb +16 -16
- data/lib/safrano/rack_app.rb +3 -3
- data/lib/safrano/request.rb +6 -6
- data/lib/safrano/response.rb +1 -1
- data/lib/safrano/service.rb +64 -119
- data/lib/safrano/version.rb +1 -1
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +5 -3
data/lib/odata/filter/parse.rb
CHANGED
@@ -68,24 +68,18 @@ module OData
|
|
68
68
|
each_typed_token(@input) do |tok, typ|
|
69
69
|
case typ
|
70
70
|
when :FuncTree
|
71
|
-
with_accepted(tok, typ)
|
72
|
-
grow_at_cursor(FuncTree.new(tok))
|
73
|
-
end
|
71
|
+
with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
|
74
72
|
when :Delimiter
|
75
73
|
case tok
|
76
74
|
when '('
|
77
75
|
with_accepted(tok, typ) do
|
78
|
-
unless @cursor.is_a? FuncTree
|
79
|
-
grow_at_cursor(IdentityFuncTree.new)
|
80
|
-
end
|
76
|
+
grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
|
81
77
|
openarg = ArgTree.new('(')
|
82
78
|
@stack << openarg
|
83
79
|
grow_at_cursor(openarg)
|
84
80
|
end
|
85
81
|
when ')'
|
86
|
-
unless (@cursor = @stack.pop)
|
87
|
-
break invalid_closing_delimiter_error(tok, typ)
|
88
|
-
end
|
82
|
+
break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
|
89
83
|
|
90
84
|
with_accepted(tok, typ) do
|
91
85
|
@cursor.update_state(tok, typ)
|
@@ -94,9 +88,7 @@ module OData
|
|
94
88
|
end
|
95
89
|
|
96
90
|
when :Separator
|
97
|
-
unless (@cursor = @stack.last)
|
98
|
-
break invalid_separator_error(tok, typ)
|
99
|
-
end
|
91
|
+
break invalid_separator_error(tok, typ) unless (@cursor = @stack.last)
|
100
92
|
|
101
93
|
with_accepted(tok, typ) { @cursor.update_state(tok, typ) }
|
102
94
|
|
data/lib/odata/filter/sequel.rb
CHANGED
@@ -13,7 +13,7 @@ module OData
|
|
13
13
|
class RootTree
|
14
14
|
def apply_to_dataset(dtcx, jh)
|
15
15
|
filtexpr = @children.first.leuqes(jh)
|
16
|
-
|
16
|
+
jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
|
17
17
|
end
|
18
18
|
|
19
19
|
def sequel_expr(jh)
|
@@ -38,21 +38,21 @@ module OData
|
|
38
38
|
when :substringof
|
39
39
|
|
40
40
|
# there are multiple possible argument types (but all should return edm.string)
|
41
|
-
if
|
41
|
+
if args[0].is_a?(QString)
|
42
42
|
# substringof('Rhum', name) -->
|
43
43
|
# name contains substr 'Rhum'
|
44
44
|
Sequel.like(args[1].leuqes(jh),
|
45
45
|
args[0].leuqes_substringof_sig1(jh))
|
46
46
|
# special non standard (ui5 client) case ?
|
47
|
-
elsif
|
47
|
+
elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
|
48
48
|
Sequel.like(args[1].leuqes(jh),
|
49
49
|
args[0].leuqes_substringof_sig1(jh))
|
50
|
-
elsif
|
50
|
+
elsif args[1].is_a?(QString)
|
51
51
|
# substringof(name, '__Route du Rhum__') -->
|
52
52
|
# '__Route du Rhum__' contains name as a substring
|
53
53
|
# TODO... check if the database supports instr (how?)
|
54
54
|
# othewise use substr(postgresql) or whatevr?
|
55
|
-
instr_substr_func = if
|
55
|
+
instr_substr_func = if Sequel::Model.db.adapter_scheme == :postgres
|
56
56
|
Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
|
57
57
|
else
|
58
58
|
Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
@@ -167,26 +167,24 @@ module OData
|
|
167
167
|
end
|
168
168
|
|
169
169
|
def leuqes_starts_like(_jh)
|
170
|
-
"#{@value
|
170
|
+
"#{@value}%"
|
171
171
|
end
|
172
172
|
|
173
173
|
def leuqes_ends_like(_jh)
|
174
|
-
"%#{@value
|
174
|
+
"%#{@value}"
|
175
175
|
end
|
176
176
|
|
177
177
|
def leuqes_substringof_sig1(_jh)
|
178
|
-
"%#{@value
|
178
|
+
"%#{@value}%"
|
179
179
|
end
|
180
180
|
end
|
181
181
|
|
182
182
|
# Literals are unquoted words
|
183
183
|
class Literal
|
184
184
|
def leuqes(jh)
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
raise OData::Filter::Parser::ErrorWrongColumnName
|
189
|
-
end
|
185
|
+
raise OData::Filter::Parser::ErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
|
186
|
+
|
187
|
+
Sequel[jh.start_model.table_name][@value.to_sym]
|
190
188
|
end
|
191
189
|
|
192
190
|
# non stantard extensions to support things like
|
data/lib/odata/filter/tree.rb
CHANGED
@@ -238,7 +238,7 @@ module OData
|
|
238
238
|
end
|
239
239
|
end
|
240
240
|
|
241
|
-
# TODO different num types?
|
241
|
+
# TODO: different num types?
|
242
242
|
def edm_type
|
243
243
|
:any
|
244
244
|
end
|
@@ -279,12 +279,10 @@ module OData
|
|
279
279
|
when :Separator
|
280
280
|
if @value == '(' && tok == ',' && @state == :val
|
281
281
|
true
|
282
|
+
elsif @state == :sep
|
283
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
282
284
|
else
|
283
|
-
|
284
|
-
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
285
|
-
else
|
286
|
-
true
|
287
|
-
end
|
285
|
+
true
|
288
286
|
end
|
289
287
|
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
290
288
|
if (@state == :open) || (@state == :sep)
|
@@ -350,22 +348,20 @@ module OData
|
|
350
348
|
def initialize(val)
|
351
349
|
super(val)
|
352
350
|
# split into path + attrib
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
raise Parser::Error.new(self, Qualit)
|
358
|
-
end
|
351
|
+
raise Parser::Error.new(self, Qualit) unless (md = REGEXP.match(val))
|
352
|
+
|
353
|
+
@path = md[1].chomp('/')
|
354
|
+
@attrib = md[2]
|
359
355
|
end
|
360
356
|
end
|
361
357
|
|
362
358
|
# Quoted Strings
|
363
359
|
class QString < Leave
|
364
|
-
|
365
|
-
|
360
|
+
DBL_QO = "''".freeze
|
361
|
+
SI_QO = "'".freeze
|
366
362
|
def initialize(val)
|
367
363
|
# unescape double quotes
|
368
|
-
super(val.gsub(
|
364
|
+
super(val.gsub(DBL_QO, SI_QO))
|
369
365
|
end
|
370
366
|
|
371
367
|
def accept?(tok, typ)
|
@@ -3,31 +3,27 @@ require_relative '../safrano/core.rb'
|
|
3
3
|
require_relative './entity.rb'
|
4
4
|
|
5
5
|
module OData
|
6
|
-
|
7
|
-
# remove the relation between entity and parent by clearing
|
6
|
+
# remove the relation between entity and parent by clearing
|
8
7
|
# the FK field(s) (if allowed)
|
9
|
-
def
|
8
|
+
def self.remove_nav_relation(assoc, parent)
|
10
9
|
return unless assoc
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
}
|
24
|
-
|
25
|
-
end
|
11
|
+
return unless assoc[:type] == :many_to_one
|
12
|
+
|
13
|
+
# removes/clear the FK values in parent
|
14
|
+
# thus deleting the "link" between the entity and the parent
|
15
|
+
# Note: This is called if we have to delete the child--> can only be
|
16
|
+
# done after removing the FK in parent (if allowed!)
|
17
|
+
lks = [assoc[:key]].flatten
|
18
|
+
lks.each do |lk|
|
19
|
+
parent.set(lk => nil)
|
20
|
+
parent.save(transaction: false)
|
21
|
+
end
|
26
22
|
end
|
27
|
-
|
23
|
+
|
28
24
|
# link newly created entities(child) to an existing parent
|
29
25
|
# by following the association_reflection rules
|
30
|
-
def
|
26
|
+
def self.create_nav_relation(child, assoc, parent)
|
31
27
|
return unless assoc
|
32
28
|
|
33
29
|
# Note: this coding shares some bits from our sequel/plugins/join_by_paths,
|
@@ -46,16 +42,16 @@ module OData
|
|
46
42
|
lks = [leftm.primary_key].flatten
|
47
43
|
rks = [assoc[:key]].flatten
|
48
44
|
join_cond = rks.zip(lks).to_h
|
49
|
-
join_cond.each
|
45
|
+
join_cond.each do |rk, lk|
|
50
46
|
if child.values[rk] # FK in new entity from payload not nil, only check consistency
|
51
47
|
# with the parent - id(s)
|
52
|
-
if (child.values[rk] != parent.pk_hash[lk]) # error...
|
53
|
-
|
54
|
-
end
|
48
|
+
# if (child.values[rk] != parent.pk_hash[lk]) # error...
|
49
|
+
# TODO
|
50
|
+
# end
|
55
51
|
else # we can set the FK value, thus creating the "link"
|
56
52
|
child.set(rk => parent.pk_hash[lk])
|
57
53
|
end
|
58
|
-
|
54
|
+
end
|
59
55
|
when :many_to_one
|
60
56
|
# sets the FK values in parent to corresponding related child key-values
|
61
57
|
# thus creating the "link" between the new entity and the parent
|
@@ -64,16 +60,16 @@ module OData
|
|
64
60
|
lks = [assoc[:key]].flatten
|
65
61
|
rks = [child.class.primary_key].flatten
|
66
62
|
join_cond = rks.zip(lks).to_h
|
67
|
-
join_cond.each
|
63
|
+
join_cond.each do |rk, lk|
|
68
64
|
if parent.values[lk] # FK in parent not nil, only check consistency
|
69
65
|
# with the child - id(s)
|
70
|
-
if
|
71
|
-
|
72
|
-
end
|
66
|
+
# if parent.values[lk] != child.pk_hash[rk] # error...
|
67
|
+
# TODO
|
68
|
+
# end
|
73
69
|
else # we can set the FK value, thus creating the "link"
|
74
70
|
parent.set(lk => child.pk_hash[rk])
|
75
71
|
end
|
76
|
-
|
72
|
+
end
|
77
73
|
end
|
78
74
|
end
|
79
75
|
|
@@ -82,7 +78,7 @@ module OData
|
|
82
78
|
attr_reader :nav_parent
|
83
79
|
attr_reader :navattr_reflection
|
84
80
|
attr_reader :nav_name
|
85
|
-
def set_relation_info(parent,name)
|
81
|
+
def set_relation_info(parent, name)
|
86
82
|
@nav_parent = parent
|
87
83
|
@nav_name = name
|
88
84
|
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
@@ -108,25 +104,25 @@ module OData
|
|
108
104
|
# create the nav. entity
|
109
105
|
def odata_post(req)
|
110
106
|
# delegate to the class method
|
111
|
-
@nav_klass.odata_create_entity_and_relation(req,
|
112
|
-
@navattr_reflection,
|
107
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
108
|
+
@navattr_reflection,
|
113
109
|
@nav_parent)
|
114
110
|
end
|
115
111
|
|
116
112
|
# create the nav. entity
|
117
113
|
def odata_put(req)
|
118
|
-
# if req.walker.raw_value
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
# else
|
124
|
-
# end
|
114
|
+
# if req.walker.raw_value
|
115
|
+
# delegate to the class method
|
116
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
117
|
+
@navattr_reflection,
|
118
|
+
@nav_parent)
|
119
|
+
# else
|
120
|
+
# end
|
125
121
|
end
|
126
122
|
|
127
123
|
# empty output as OData json (v2)
|
128
124
|
def to_odata_json(*)
|
129
|
-
{ 'd' =>
|
125
|
+
{ 'd' => EMPTY_HASH }.to_json
|
130
126
|
end
|
131
127
|
|
132
128
|
# for testing purpose (assert_equal ...)
|
data/lib/odata/select.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'odata/error.rb'
|
2
|
+
|
3
|
+
# all dataset selecting related classes in our OData module
|
4
|
+
# ie do eager loading
|
5
|
+
module OData
|
6
|
+
# base class for selecting. We have to distinguish between
|
7
|
+
# fields of the current entity, and the navigation properties
|
8
|
+
# we can have one special case
|
9
|
+
# empty, ie no $select specified --> return all fields and all nav props
|
10
|
+
# ==> SelectAll
|
11
|
+
|
12
|
+
class SelectBase
|
13
|
+
ALL = new # re-useable selecting-all handler
|
14
|
+
|
15
|
+
def self.factory(selectstr)
|
16
|
+
case selectstr&.strip
|
17
|
+
when nil, '', '*'
|
18
|
+
ALL
|
19
|
+
else
|
20
|
+
Select.new(selectstr)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def all_props?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def ALL.all_props?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# single select
|
34
|
+
class Select < SelectBase
|
35
|
+
COMASPLIT = /\s*,\s*/.freeze
|
36
|
+
attr_reader :props
|
37
|
+
def initialize(selstr)
|
38
|
+
@selectp = selstr.strip
|
39
|
+
@props = @selectp.split(COMASPLIT)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/odata/url_parameters.rb
CHANGED
@@ -3,57 +3,72 @@ require 'odata/error.rb'
|
|
3
3
|
# url parameters processing . Mostly delegates to specialised classes
|
4
4
|
# (filter, order...) to convert into Sequel exprs.
|
5
5
|
module OData
|
6
|
-
class
|
6
|
+
class UrlParametersBase
|
7
|
+
attr_reader :expand
|
8
|
+
attr_reader :select
|
9
|
+
|
10
|
+
def check_expand
|
11
|
+
return BadRequestExpandParseError if @expand.parse_error?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# url parameters for a single entity expand/select
|
16
|
+
class UrlParameters4Single < UrlParametersBase
|
17
|
+
def initialize(params)
|
18
|
+
@params = params
|
19
|
+
@expand = ExpandBase.factory(@params['$expand'])
|
20
|
+
@select = SelectBase.factory(@params['$select'])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# url parameters for a collection expand/select + filter/order
|
25
|
+
class UrlParameters4Coll < UrlParametersBase
|
7
26
|
attr_reader :filt
|
8
27
|
attr_reader :ordby
|
9
|
-
|
10
|
-
|
28
|
+
|
29
|
+
def initialize(model, params)
|
30
|
+
# join helper is only needed for odering or filtering
|
31
|
+
@jh = model.join_by_paths_helper if params['$orderby'] || params['$filter']
|
11
32
|
@params = params
|
33
|
+
@ordby = OrderBase.factory(@params['$orderby'], @jh)
|
34
|
+
@filt = FilterBase.factory(@params['$filter'])
|
35
|
+
@expand = ExpandBase.factory(@params['$expand'])
|
36
|
+
@select = SelectBase.factory(@params['$select'])
|
12
37
|
end
|
13
38
|
|
14
39
|
def check_filter
|
15
|
-
return unless @params['$filter']
|
16
|
-
|
17
|
-
@filt = FilterByParse.new(@params['$filter'], @jh)
|
18
40
|
return BadRequestFilterParseError if @filt.parse_error?
|
19
|
-
|
20
|
-
# nil is the expected return for no errors
|
21
|
-
nil
|
22
41
|
end
|
23
42
|
|
24
43
|
def check_order
|
25
|
-
return
|
44
|
+
return BadRequestOrderParseError if @ordby.parse_error?
|
45
|
+
end
|
26
46
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
qualfn.strip!
|
32
|
-
dir.strip! if dir
|
33
|
-
return BadRequestError unless @jh.start_model.attrib_path_valid? qualfn
|
34
|
-
return BadRequestError unless [nil, 'asc', 'desc'].include? dir
|
35
|
-
end
|
47
|
+
def apply_to_dataset(dtcx)
|
48
|
+
dtcx = apply_expand_to_dataset(dtcx)
|
49
|
+
apply_filter_order_to_dataset(dtcx)
|
50
|
+
end
|
36
51
|
|
37
|
-
|
52
|
+
def apply_expand_to_dataset(dtcx)
|
53
|
+
return dtcx if @expand.empty?
|
38
54
|
|
39
|
-
|
40
|
-
nil
|
55
|
+
@expand.apply_to_dataset(dtcx)
|
41
56
|
end
|
42
57
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
# Warning, the @ordby and @filt objects are coupled by way of the join helper
|
59
|
+
def apply_filter_order_to_dataset(dtcx)
|
60
|
+
return dtcx if @filt.empty? && @ordby.empty?
|
61
|
+
|
62
|
+
# filter object and join-helper need to be finalized after filter has been parsed and checked
|
63
|
+
@filt.finalize(@jh)
|
64
|
+
|
65
|
+
# start with the join
|
66
|
+
dtcx = @jh.dataset(dtcx)
|
67
|
+
|
68
|
+
dtcx = @filt.apply_to_dataset(dtcx)
|
69
|
+
dtcx = @ordby.apply_to_dataset(dtcx)
|
70
|
+
|
71
|
+
dtcx.select_all(@jh.start_model.table_name)
|
57
72
|
end
|
58
73
|
end
|
59
74
|
end
|
data/lib/safrano.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require 'json'
|
3
2
|
require 'rexml/document'
|
4
3
|
require_relative 'safrano/multipart.rb'
|
@@ -34,9 +33,10 @@ end
|
|
34
33
|
# needed for ruby < 2.5
|
35
34
|
class Dir
|
36
35
|
def self.each_child(dir)
|
37
|
-
Dir.foreach(dir)
|
38
|
-
next if (
|
36
|
+
Dir.foreach(dir) do |x|
|
37
|
+
next if (x == '.') || (x == '..')
|
38
|
+
|
39
39
|
yield x
|
40
|
-
|
40
|
+
end
|
41
41
|
end unless respond_to? :each_child
|
42
|
-
end
|
42
|
+
end
|