safrano 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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) do
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
 
@@ -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
- dtcx = jh.dataset.where(filtexpr).select_all(jh.start_model.table_name)
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 (args[0].is_a?(QString))
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 (args[0].is_a?(Literal) && args[1].is_a?(Literal))
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 (args[1].is_a?(QString))
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 (Sequel::Model.db.adapter_scheme == :postgres)
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.to_s}%"
170
+ "#{@value}%"
171
171
  end
172
172
 
173
173
  def leuqes_ends_like(_jh)
174
- "%#{@value.to_s}"
174
+ "%#{@value}"
175
175
  end
176
176
 
177
177
  def leuqes_substringof_sig1(_jh)
178
- "%#{@value.to_s}%"
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
- if jh.start_model.db_schema.has_key?(@value.to_sym)
186
- Sequel[jh.start_model.table_name][@value.to_sym]
187
- else
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
@@ -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
- if (@state == :sep)
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
- if (md = REGEXP.match(val))
354
- @path = md[1].chomp('/')
355
- @attrib = md[2]
356
- else
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
- DoubleQuote = "''".freeze
365
- SingleQuote = "'".freeze
360
+ DBL_QO = "''".freeze
361
+ SI_QO = "'".freeze
366
362
  def initialize(val)
367
363
  # unescape double quotes
368
- super(val.gsub(DoubleQuote, SingleQuote))
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 OData.remove_nav_relation(entity, assoc, parent)
8
+ def self.remove_nav_relation(assoc, parent)
10
9
  return unless assoc
11
10
 
12
- case assoc[:type]
13
- when :one_to_many, :one_to_one
14
- when :many_to_one
15
- # removes/clear the FK values in parent
16
- # thus deleting the "link" between the entity and the parent
17
- # Note: This is called if we have to delete the child--> can only be
18
- # done after removing the FK in parent (if allowed!)
19
- lks = [assoc[:key]].flatten
20
- lks.each{|lk|
21
- parent.set(lk => nil )
22
- parent.save(transaction: false)
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 OData.create_nav_relation(child, assoc, parent)
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 { |rk, lk|
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
- # TODO
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 { |rk, lk|
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 (parent.values[lk] != child.pk_hash[rk]) # error...
71
- # TODO
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
- # delegate to the class method
120
- @nav_klass.odata_create_entity_and_relation(req,
121
- @navattr_reflection,
122
- @nav_parent)
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' => {} }.to_json
125
+ { 'd' => EMPTY_HASH }.to_json
130
126
  end
131
127
 
132
128
  # for testing purpose (assert_equal ...)
@@ -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
@@ -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 UrlParameters
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
- def initialize(jh, params)
10
- @jh = jh
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 unless @params['$orderby']
44
+ return BadRequestOrderParseError if @ordby.parse_error?
45
+ end
26
46
 
27
- pordlist = @params['$orderby'].dup
28
- pordlist.split(',').each do |pord|
29
- pord.strip!
30
- qualfn, dir = pord.split(/\s/)
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
- @ordby = Order.new_by_parse(@params['$orderby'], @jh)
52
+ def apply_expand_to_dataset(dtcx)
53
+ return dtcx if @expand.empty?
38
54
 
39
- # nil is the expected return for no errors
40
- nil
55
+ @expand.apply_to_dataset(dtcx)
41
56
  end
42
57
 
43
- def apply_to_dataset(dtcx)
44
- return dtcx if (@filt.nil? && @ordby.nil?)
45
-
46
- if @filt.nil?
47
- dtcx = @jh.dataset.select_all(@jh.start_model.table_name)
48
-
49
- @ordby.apply_to_dataset(dtcx)
50
- elsif @ordby.nil?
51
- @filt.apply_to_dataset(dtcx)
52
- else
53
- filtexpr = @filt.sequel_expr
54
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
55
- @ordby.apply_to_dataset(dtcx)
56
- end
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
@@ -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) {|x|
38
- next if ( ( x == '.' ) or ( x == '..' ) )
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