safrano 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 +17 -15
  15. data/lib/odata/collection.rb +141 -500
  16. data/lib/odata/collection_filter.rb +44 -37
  17. data/lib/odata/collection_media.rb +193 -43
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +39 -12
  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 +201 -176
  23. data/lib/odata/error.rb +186 -33
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +69 -0
  26. data/lib/odata/filter/error.rb +55 -6
  27. data/lib/odata/filter/parse.rb +38 -36
  28. data/lib/odata/filter/sequel.rb +121 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +15 -11
  31. data/lib/odata/filter/tree.rb +110 -60
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +50 -32
  35. data/lib/odata/relations.rb +7 -7
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +18 -28
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +43 -0
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
  46. data/lib/safrano/rack_app.rb +175 -0
  47. data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
  48. data/lib/{request.rb → safrano/request.rb} +102 -50
  49. data/lib/{response.rb → safrano/response.rb} +5 -4
  50. data/lib/safrano/sequel_join_by_paths.rb +5 -0
  51. data/lib/{service.rb → safrano/service.rb} +257 -188
  52. data/lib/safrano/version.rb +5 -0
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +53 -17
  55. data/lib/rack_app.rb +0 -174
  56. data/lib/sequel_join_by_paths.rb +0 -5
  57. data/lib/version.rb +0 -4
@@ -1,12 +1,31 @@
1
- module OData
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Safrano
6
+ class SequelAdapterError < StandardError
7
+ attr_reader :inner
8
+
9
+ def initialize(err)
10
+ @inner = err
11
+ end
12
+ end
13
+
2
14
  module Filter
3
15
  class Parser
4
16
  # Parser errors
5
- class Error < StandardError
17
+
18
+ class Error
19
+ def Error.http_code
20
+ const_get(:HTTP_CODE)
21
+ end
22
+ HTTP_CODE = 400
23
+
6
24
  attr_reader :tok
7
25
  attr_reader :typ
8
26
  attr_reader :cur_val
9
27
  attr_reader :cur_typ
28
+
10
29
  def initialize(tok, typ, cur)
11
30
  @tok = tok
12
31
  @typ = typ
@@ -18,31 +37,61 @@ module OData
18
37
  end
19
38
  # Invalid Tokens
20
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
21
45
  end
22
46
  # Unmached closed
23
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
53
+ end
54
+
55
+ class ErrorFunctionArgumentType
56
+ include ::Safrano::ErrorInstance
24
57
  end
25
58
 
26
- class ErrorFunctionArgumentType < StandardError
59
+ class ErrorWrongColumnName
60
+ include ::Safrano::ErrorInstance
27
61
  end
28
62
 
29
- class ErrorWrongColumnName < StandardError
63
+ # attempt to add a child to a Leave
64
+ class ErrorLeaveChild
65
+ include ::Safrano::ErrorInstance
30
66
  end
31
67
 
32
68
  # Invalid function arity
33
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
34
75
  end
35
76
  # Invalid separator in this context (missing parenthesis?)
36
77
  class ErrorInvalidSeparator < Error
78
+ include ::Safrano::ErrorInstance
37
79
  end
38
80
 
39
81
  # unmatched quot3
40
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
41
88
  end
42
89
 
43
90
  # wrong type of function argument
44
- class ErrorInvalidArgumentType < StandardError
45
- def initialize(tree, expected:, actual:)
91
+ class ErrorInvalidArgumentType < Error
92
+ include ::Safrano::ErrorInstance
93
+
94
+ def initialize(tree, expected:, actual:)
46
95
  @tree = tree
47
96
  @expected = expected
48
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)
@@ -168,19 +168,21 @@ module OData
168
168
  @cursor.update_state(tok, typ)
169
169
  grow_at_cursor(FPNumber.new(tok))
170
170
  end
171
+
171
172
  when :unmatchedQuote
172
173
  break unmatched_quote_error(tok, typ)
174
+
175
+ when :space
176
+ with_accepted(tok, typ) do
177
+ @cursor.update_state(tok, typ)
178
+ end
173
179
  else
174
- raise 'Severe Error'
180
+ server_error
175
181
  end
176
- break if @error
177
- end
178
- begin
179
- @tree.check_types unless @error
180
- rescue ErrorInvalidArgumentType => e
181
- @error = e
182
+ break(@error) if @error
182
183
  end
183
- @error || @tree
184
+ (@error = @tree.check_types) unless @error
185
+ @error ? @error : Contract.valid(@tree)
184
186
  end
185
187
  end
186
188
  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,12 @@ 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
+ Sequel::SQL::BooleanExpression.new(leuqes_op, c0, c1)
187
+ end
132
188
  end
133
189
  end
134
190
 
@@ -147,12 +203,12 @@ module OData
147
203
  when :mod
148
204
  :%
149
205
  else
150
- raise OData::FilterParseError
206
+ return Safrano::FilterParseError
151
207
  end
152
-
153
- Sequel::SQL::NumericExpression.new(leuqes_op,
154
- @children[0].leuqes(jh),
155
- @children[1].leuqes(jh))
208
+ Contract.collect_result!(@children[0].leuqes(jh),
209
+ @children[1].leuqes(jh)) do |c0, c1|
210
+ Sequel::SQL::NumericExpression.new(leuqes_op, c0, c1)
211
+ end
156
212
  end
157
213
  end
158
214
 
@@ -163,44 +219,42 @@ module OData
163
219
  # Numbers (floating point, ints, dec)
164
220
  class FPNumber
165
221
  def leuqes(_jh)
166
- Sequel.lit(@value)
222
+ success Sequel.lit(@value)
167
223
  end
168
224
 
169
225
  def leuqes_starts_like(_jh)
170
- "#{@value.to_s}%"
226
+ success "#{@value}%"
171
227
  end
172
228
 
173
229
  def leuqes_ends_like(_jh)
174
- "%#{@value.to_s}"
230
+ success "%#{@value}"
175
231
  end
176
232
 
177
233
  def leuqes_substringof_sig1(_jh)
178
- "%#{@value.to_s}%"
234
+ success "%#{@value}%"
179
235
  end
180
236
  end
181
237
 
182
238
  # Literals are unquoted words
183
239
  class Literal
184
240
  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
241
+ return Safrano::FilterParseErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
242
+
243
+ success Sequel[jh.start_model.table_name][@value.to_sym]
190
244
  end
191
245
 
192
246
  # non stantard extensions to support things like
193
247
  # substringof(Rhum, name) ????
194
248
  def leuqes_starts_like(_jh)
195
- "#{@value}%"
249
+ success "#{@value}%"
196
250
  end
197
251
 
198
252
  def leuqes_ends_like(_jh)
199
- "%#{@value}"
253
+ success "%#{@value}"
200
254
  end
201
255
 
202
256
  def leuqes_substringof_sig1(_jh)
203
- "%#{@value}%"
257
+ success "%#{@value}%"
204
258
  end
205
259
 
206
260
  def as_string
@@ -213,26 +267,26 @@ module OData
213
267
  def leuqes(jh)
214
268
  jh.add(@path)
215
269
  talias = jh.start_model.get_alias_sym(@path)
216
- Sequel[talias][@attrib.to_sym]
270
+ success Sequel[talias][@attrib.to_sym]
217
271
  end
218
272
  end
219
273
 
220
274
  # Quoted Strings
221
275
  class QString
222
276
  def leuqes(_jh)
223
- @value
277
+ success @value
224
278
  end
225
279
 
226
280
  def leuqes_starts_like(_jh)
227
- "#{@value}%"
281
+ success "#{@value}%"
228
282
  end
229
283
 
230
284
  def leuqes_ends_like(_jh)
231
- "%#{@value}"
285
+ success "%#{@value}"
232
286
  end
233
287
 
234
288
  def leuqes_substringof_sig1(_jh)
235
- "%#{@value}%"
289
+ success "%#{@value}%"
236
290
  end
237
291
  end
238
292
  end