safrano 0.4.1 → 0.4.2
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/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
|