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.
@@ -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