safrano 0.3.4 → 0.4.4

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 (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