safrano 0.4.1 → 0.4.6

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -11
@@ -1,18 +1,31 @@
1
- module OData
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Safrano
2
6
  class SequelAdapterError < StandardError
3
7
  attr_reader :inner
8
+
4
9
  def initialize(err)
5
10
  @inner = err
6
11
  end
7
12
  end
13
+
8
14
  module Filter
9
15
  class Parser
10
16
  # Parser errors
11
- class Error < StandardError
17
+
18
+ class Error
19
+ def Error.http_code
20
+ const_get(:HTTP_CODE)
21
+ end
22
+ HTTP_CODE = 400
23
+
12
24
  attr_reader :tok
13
25
  attr_reader :typ
14
26
  attr_reader :cur_val
15
27
  attr_reader :cur_typ
28
+
16
29
  def initialize(tok, typ, cur)
17
30
  @tok = tok
18
31
  @typ = typ
@@ -24,31 +37,61 @@ module OData
24
37
  end
25
38
  # Invalid Tokens
26
39
  class ErrorInvalidToken < Error
40
+ include ::Safrano::ErrorInstance
41
+ def initialize(tok, typ, cur)
42
+ super
43
+ @msg = "Bad Request: invalid token #{tok} in $filter"
44
+ end
27
45
  end
28
46
  # Unmached closed
29
47
  class ErrorUnmatchedClose < Error
48
+ include ::Safrano::ErrorInstance
49
+ def initialize(tok, typ, cur)
50
+ super
51
+ @msg = "Bad Request: unmatched #{tok} in $filter"
52
+ end
30
53
  end
31
54
 
32
- class ErrorFunctionArgumentType < StandardError
55
+ class ErrorFunctionArgumentType
56
+ include ::Safrano::ErrorInstance
33
57
  end
34
58
 
35
- class ErrorWrongColumnName < StandardError
59
+ class ErrorWrongColumnName
60
+ include ::Safrano::ErrorInstance
61
+ end
62
+
63
+ # attempt to add a child to a Leave
64
+ class ErrorLeaveChild
65
+ include ::Safrano::ErrorInstance
36
66
  end
37
67
 
38
68
  # Invalid function arity
39
69
  class ErrorInvalidArity < Error
70
+ include ::Safrano::ErrorInstance
71
+ def initialize(tok, typ, cur)
72
+ super
73
+ @msg = "Bad Request: wrong number of parameters for function #{cur.parent.value.to_s} in $filter"
74
+ end
40
75
  end
41
76
  # Invalid separator in this context (missing parenthesis?)
42
77
  class ErrorInvalidSeparator < Error
78
+ include ::Safrano::ErrorInstance
43
79
  end
44
80
 
45
81
  # unmatched quot3
46
82
  class UnmatchedQuoteError < Error
83
+ include ::Safrano::ErrorInstance
84
+ def initialize(tok, typ, cur)
85
+ super
86
+ @msg = "Bad Request: unbalanced quotes #{tok} in $filter"
87
+ end
47
88
  end
48
89
 
49
90
  # wrong type of function argument
50
- class ErrorInvalidArgumentType < StandardError
51
- def initialize(tree, expected:, actual:)
91
+ class ErrorInvalidArgumentType < Error
92
+ include ::Safrano::ErrorInstance
93
+
94
+ def initialize(tree, expected:, actual:)
52
95
  @tree = tree
53
96
  @expected = expected
54
97
  @actual = actual
@@ -1,15 +1,19 @@
1
- require_relative './token.rb'
2
- require_relative './tree.rb'
3
- require_relative './error.rb'
1
+ # frozen_string_literal: true
4
2
 
5
- # top level OData namespace
6
- module OData
3
+ require_relative './token'
4
+ require_relative './tree'
5
+ require_relative './error'
6
+
7
+ # top level Safrano namespace
8
+ module Safrano
7
9
  # for handling $filter
8
10
  module Filter
9
11
  # Parser for $filter input
10
12
  class Parser
11
13
  include Token
12
14
  attr_reader :cursor
15
+ attr_reader :error
16
+
13
17
  def initialize(input)
14
18
  @tree = RootTree.new
15
19
  @cursor = @tree
@@ -18,10 +22,14 @@ module OData
18
22
  @binop_stack = []
19
23
  end
20
24
 
25
+ def server_error
26
+ (@error = ::Safrano::ServerError)
27
+ end
28
+
21
29
  def grow_at_cursor(child)
22
- raise 'unknown BroGrammingError' if @cursor.nil?
30
+ return server_error if @cursor.nil?
23
31
 
24
- @cursor.attach(child)
32
+ @cursor.attach(child).tap_error { |err| return (@error = err) }
25
33
  @cursor = child
26
34
  end
27
35
 
@@ -40,7 +48,7 @@ module OData
40
48
  def insert_before_cursor(node)
41
49
  left = detach_cursor
42
50
  grow_at_cursor(node)
43
- @cursor.attach(left)
51
+ @cursor.attach(left).tap_error { |err| return (@error = err) }
44
52
  end
45
53
 
46
54
  def invalid_separator_error(tok, typ)
@@ -56,36 +64,29 @@ module OData
56
64
  end
57
65
 
58
66
  def with_accepted(tok, typ)
59
- acc, err = @cursor.accept?(tok, typ)
60
- if acc
61
- yield
62
- else
63
- @error = err
64
- end
67
+ (err = @cursor.accept?(tok, typ)) ? (@error = err) : yield
65
68
  end
66
69
 
67
70
  def build
68
71
  each_typed_token(@input) do |tok, typ|
69
72
  case typ
70
73
  when :FuncTree
71
- with_accepted(tok, typ) do
72
- grow_at_cursor(FuncTree.new(tok))
73
- end
74
+ with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
75
+
74
76
  when :Delimiter
75
77
  case tok
76
78
  when '('
77
79
  with_accepted(tok, typ) do
78
- unless @cursor.is_a? FuncTree
79
- grow_at_cursor(IdentityFuncTree.new)
80
+ grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
81
+ unless @error
82
+ openarg = ArgTree.new('(')
83
+ @stack << openarg
84
+ grow_at_cursor(openarg)
80
85
  end
81
- openarg = ArgTree.new('(')
82
- @stack << openarg
83
- grow_at_cursor(openarg)
84
86
  end
87
+
85
88
  when ')'
86
- unless (@cursor = @stack.pop)
87
- break invalid_closing_delimiter_error(tok, typ)
88
- end
89
+ break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
89
90
 
90
91
  with_accepted(tok, typ) do
91
92
  @cursor.update_state(tok, typ)
@@ -94,9 +95,7 @@ module OData
94
95
  end
95
96
 
96
97
  when :Separator
97
- unless (@cursor = @stack.last)
98
- break invalid_separator_error(tok, typ)
99
- end
98
+ break invalid_separator_error(tok, typ) unless (@cursor = @stack.last)
100
99
 
101
100
  with_accepted(tok, typ) { @cursor.update_state(tok, typ) }
102
101
 
@@ -129,6 +128,7 @@ module OData
129
128
  end
130
129
  insert_before_cursor(binoptr)
131
130
  end
131
+
132
132
  when :BinopArithm
133
133
  with_accepted(tok, typ) do
134
134
  binoptr = BinopArithm.new(tok)
@@ -151,6 +151,12 @@ module OData
151
151
  grow_at_cursor(Literal.new(tok))
152
152
  end
153
153
 
154
+ when :NullLiteral
155
+ with_accepted(tok, typ) do
156
+ @cursor.update_state(tok, typ)
157
+ grow_at_cursor(NullLiteral.new(tok))
158
+ end
159
+
154
160
  when :Qualit
155
161
  with_accepted(tok, typ) do
156
162
  @cursor.update_state(tok, typ)
@@ -168,19 +174,21 @@ module OData
168
174
  @cursor.update_state(tok, typ)
169
175
  grow_at_cursor(FPNumber.new(tok))
170
176
  end
177
+
171
178
  when :unmatchedQuote
172
179
  break unmatched_quote_error(tok, typ)
180
+
181
+ when :space
182
+ with_accepted(tok, typ) do
183
+ @cursor.update_state(tok, typ)
184
+ end
173
185
  else
174
- raise 'Severe Error'
186
+ server_error
175
187
  end
176
- break if @error
177
- end
178
- begin
179
- @tree.check_types unless @error
180
- rescue ErrorInvalidArgumentType => e
181
- @error = e
188
+ break(@error) if @error
182
189
  end
183
- @error || @tree
190
+ (@error = @tree.check_types) unless @error
191
+ @error ? @error : Contract.valid(@tree)
184
192
  end
185
193
  end
186
194
  end
@@ -1,19 +1,24 @@
1
- require_relative './tree.rb'
2
- module OData
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base'
4
+ require_relative './sequel_function_adapter'
5
+
6
+ module Safrano
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
14
18
  def apply_to_dataset(dtcx, jh)
15
- filtexpr = @children.first.leuqes(jh)
16
- dtcx = jh.dataset.where(filtexpr).select_all(jh.start_model.table_name)
19
+ @children.first.leuqes(jh).if_valid do |filtexpr|
20
+ jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
21
+ end
17
22
  end
18
23
 
19
24
  def sequel_expr(jh)
@@ -26,57 +31,108 @@ module OData
26
31
  end
27
32
 
28
33
  # For functions... should have a single child---> the argument list
34
+ # note: Adapter specific function helpers like year() or substringof_sig2()
35
+ # need to be mixed in on startup (eg. on publish finalize)
29
36
  class FuncTree < Tree
30
37
  def leuqes(jh)
31
38
  case @value
32
39
  when :startswith
33
- Sequel.like(args[0].leuqes(jh),
34
- args[1].leuqes_starts_like(jh))
40
+ Contract.collect_result!(args[0].leuqes(jh),
41
+ args[1].leuqes_starts_like(jh)) do |l0, l1|
42
+ Sequel.like(l0, l1)
43
+ end
44
+
35
45
  when :endswith
36
- Sequel.like(args[0].leuqes(jh),
37
- args[1].leuqes_ends_like(jh))
46
+ Contract.collect_result!(args[0].leuqes(jh),
47
+ args[1].leuqes_ends_like(jh)) do |l0, l1|
48
+ Sequel.like(l0, l1)
49
+ end
50
+
38
51
  when :substringof
39
52
 
40
53
  # there are multiple possible argument types (but all should return edm.string)
41
- if (args[0].is_a?(QString))
54
+ if args[0].is_a?(QString)
42
55
  # substringof('Rhum', name) -->
43
56
  # name contains substr 'Rhum'
44
- Sequel.like(args[1].leuqes(jh),
45
- args[0].leuqes_substringof_sig1(jh))
57
+ Contract.collect_result!(args[1].leuqes(jh),
58
+ args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
59
+ Sequel.like(l1, l0)
60
+ end
61
+
46
62
  # special non standard (ui5 client) case ?
47
- elsif (args[0].is_a?(Literal) && args[1].is_a?(Literal))
48
- Sequel.like(args[1].leuqes(jh),
49
- args[0].leuqes_substringof_sig1(jh))
50
- elsif (args[1].is_a?(QString))
51
- # substringof(name, '__Route du Rhum__') -->
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)
63
+ elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
64
+ Contract.collect_result!(args[1].leuqes(jh),
65
+ args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
66
+ Sequel.like(l1, l0)
67
+ end
62
68
 
69
+ elsif args[1].is_a?(QString)
70
+ substringof_sig2(jh) # adapter specific
63
71
  else
64
72
  # TODO... actually not supported?
65
- raise OData::Filter::Parser::ErrorFunctionArgumentType
73
+ raise Safrano::Filter::Parser::ErrorFunctionArgumentType
66
74
  end
67
75
  when :concat
68
- Sequel.join([args[0].leuqes(jh),
69
- args[1].leuqes(jh)])
76
+ Contract.collect_result!(args[0].leuqes(jh),
77
+ args[1].leuqes(jh)) do |l0, l1|
78
+ Sequel.join([l0, l1])
79
+ end
80
+
70
81
  when :length
71
- Sequel.char_length(args.first.leuqes(jh))
82
+ args.first.leuqes(jh)
83
+ .map_result! { |l| Sequel.char_length(l) }
84
+
72
85
  when :trim
73
- Sequel.trim(args.first.leuqes(jh))
86
+ args.first.leuqes(jh)
87
+ .map_result! { |l| Sequel.trim(l) }
88
+
74
89
  when :toupper
75
- Sequel.function(:upper, args.first.leuqes(jh))
90
+ args.first.leuqes(jh)
91
+ .map_result! { |l| Sequel.function(:upper, l) }
92
+
76
93
  when :tolower
77
- Sequel.function(:lower, args.first.leuqes(jh))
94
+ args.first.leuqes(jh)
95
+ .map_result! { |l| Sequel.function(:lower, l) }
96
+
97
+ # all datetime funcs are adapter specific (because sqlite does not have extract)
98
+ when :year
99
+ args.first.leuqes(jh)
100
+ .map_result! { |l| year(l) }
101
+
102
+ when :month
103
+ args.first.leuqes(jh)
104
+ .map_result! { |l| month(l) }
105
+
106
+ when :second
107
+ args.first.leuqes(jh)
108
+ .map_result! { |l| second(l) }
109
+
110
+ when :minute
111
+ args.first.leuqes(jh)
112
+ .map_result! { |l| minute(l) }
113
+
114
+ when :hour
115
+ args.first.leuqes(jh)
116
+ .map_result! { |l| hour(l) }
117
+
118
+ when :day
119
+ args.first.leuqes(jh)
120
+ .map_result! { |l| day(l) }
121
+
122
+ # math functions
123
+ when :round
124
+ args.first.leuqes(jh)
125
+ .map_result! { |l| Sequel.function(:round, l) }
126
+
127
+ when :floor
128
+ args.first.leuqes(jh)
129
+ .if_valid { |l| floor(l) }
130
+
131
+ when :ceiling
132
+ args.first.leuqes(jh)
133
+ .if_valid { |l| ceiling(l) }
78
134
  else
79
- raise OData::FilterParseError
135
+ Safrano::FilterParseError
80
136
  end
81
137
  end
82
138
  end
@@ -95,9 +151,9 @@ module OData
95
151
  def leuqes(jh)
96
152
  case @value
97
153
  when :not
98
- Sequel.~(@children.first.leuqes(jh))
154
+ @children.first.leuqes(jh).map_result! { |l| Sequel.~(l) }
99
155
  else
100
- raise OData::FilterParseError
156
+ Safrano::FilterParseError
101
157
  end
102
158
  end
103
159
  end
@@ -123,12 +179,19 @@ module OData
123
179
  when :and
124
180
  :AND
125
181
  else
126
- raise OData::FilterParseError
182
+ return Safrano::FilterParseError
127
183
  end
128
-
129
- Sequel::SQL::BooleanExpression.new(leuqes_op,
130
- @children[0].leuqes(jh),
131
- @children[1].leuqes(jh))
184
+ Contract.collect_result!(@children[0].leuqes(jh),
185
+ @children[1].leuqes(jh)) do |c0, c1|
186
+ if c1 == NullLiteral::LEUQES
187
+ if @value == :eq
188
+ leuqes_op = :IS
189
+ elsif @value == :ne
190
+ leuqes_op = :'IS NOT'
191
+ end
192
+ end
193
+ Sequel::SQL::BooleanExpression.new(leuqes_op, c0, c1)
194
+ end
132
195
  end
133
196
  end
134
197
 
@@ -147,12 +210,12 @@ module OData
147
210
  when :mod
148
211
  :%
149
212
  else
150
- raise OData::FilterParseError
213
+ return Safrano::FilterParseError
151
214
  end
152
-
153
- Sequel::SQL::NumericExpression.new(leuqes_op,
154
- @children[0].leuqes(jh),
155
- @children[1].leuqes(jh))
215
+ Contract.collect_result!(@children[0].leuqes(jh),
216
+ @children[1].leuqes(jh)) do |c0, c1|
217
+ Sequel::SQL::NumericExpression.new(leuqes_op, c0, c1)
218
+ end
156
219
  end
157
220
  end
158
221
 
@@ -163,44 +226,42 @@ module OData
163
226
  # Numbers (floating point, ints, dec)
164
227
  class FPNumber
165
228
  def leuqes(_jh)
166
- Sequel.lit(@value)
229
+ success Sequel.lit(@value)
167
230
  end
168
231
 
169
232
  def leuqes_starts_like(_jh)
170
- "#{@value.to_s}%"
233
+ success "#{@value}%"
171
234
  end
172
235
 
173
236
  def leuqes_ends_like(_jh)
174
- "%#{@value.to_s}"
237
+ success "%#{@value}"
175
238
  end
176
239
 
177
240
  def leuqes_substringof_sig1(_jh)
178
- "%#{@value.to_s}%"
241
+ success "%#{@value}%"
179
242
  end
180
243
  end
181
244
 
182
245
  # Literals are unquoted words
183
246
  class Literal
184
247
  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
248
+ return Safrano::FilterParseErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
249
+
250
+ success Sequel[jh.start_model.table_name][@value.to_sym]
190
251
  end
191
252
 
192
253
  # non stantard extensions to support things like
193
254
  # substringof(Rhum, name) ????
194
255
  def leuqes_starts_like(_jh)
195
- "#{@value}%"
256
+ success "#{@value}%"
196
257
  end
197
258
 
198
259
  def leuqes_ends_like(_jh)
199
- "%#{@value}"
260
+ success "%#{@value}"
200
261
  end
201
262
 
202
263
  def leuqes_substringof_sig1(_jh)
203
- "%#{@value}%"
264
+ success "%#{@value}%"
204
265
  end
205
266
 
206
267
  def as_string
@@ -208,31 +269,39 @@ module OData
208
269
  end
209
270
  end
210
271
 
272
+ # Null
273
+ class NullLiteral
274
+ def leuqes(jh)
275
+ # Sequel's representation of NULL
276
+ success LEUQES
277
+ end
278
+ end
279
+
211
280
  # Qualit (qualified lits) are words separated by /
212
281
  class Qualit
213
282
  def leuqes(jh)
214
283
  jh.add(@path)
215
284
  talias = jh.start_model.get_alias_sym(@path)
216
- Sequel[talias][@attrib.to_sym]
285
+ success Sequel[talias][@attrib.to_sym]
217
286
  end
218
287
  end
219
288
 
220
289
  # Quoted Strings
221
290
  class QString
222
291
  def leuqes(_jh)
223
- @value
292
+ success @value
224
293
  end
225
294
 
226
295
  def leuqes_starts_like(_jh)
227
- "#{@value}%"
296
+ success "#{@value}%"
228
297
  end
229
298
 
230
299
  def leuqes_ends_like(_jh)
231
- "%#{@value}"
300
+ success "%#{@value}"
232
301
  end
233
302
 
234
303
  def leuqes_substringof_sig1(_jh)
235
- "%#{@value}%"
304
+ success "%#{@value}%"
236
305
  end
237
306
  end
238
307
  end