safrano 0.4.2 → 0.5.0

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 +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  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 +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  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 +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  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 +106 -52
  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 +13 -26
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -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,12 +64,7 @@ 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
@@ -69,15 +72,19 @@ module OData
69
72
  case typ
70
73
  when :FuncTree
71
74
  with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
75
+
72
76
  when :Delimiter
73
77
  case tok
74
78
  when '('
75
79
  with_accepted(tok, typ) do
76
80
  grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
77
- openarg = ArgTree.new('(')
78
- @stack << openarg
79
- grow_at_cursor(openarg)
81
+ unless @error
82
+ openarg = ArgTree.new('(')
83
+ @stack << openarg
84
+ grow_at_cursor(openarg)
85
+ end
80
86
  end
87
+
81
88
  when ')'
82
89
  break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
83
90
 
@@ -121,6 +128,7 @@ module OData
121
128
  end
122
129
  insert_before_cursor(binoptr)
123
130
  end
131
+
124
132
  when :BinopArithm
125
133
  with_accepted(tok, typ) do
126
134
  binoptr = BinopArithm.new(tok)
@@ -143,6 +151,12 @@ module OData
143
151
  grow_at_cursor(Literal.new(tok))
144
152
  end
145
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
+
146
160
  when :Qualit
147
161
  with_accepted(tok, typ) do
148
162
  @cursor.update_state(tok, typ)
@@ -160,19 +174,21 @@ module OData
160
174
  @cursor.update_state(tok, typ)
161
175
  grow_at_cursor(FPNumber.new(tok))
162
176
  end
177
+
163
178
  when :unmatchedQuote
164
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
165
185
  else
166
- raise 'Severe Error'
186
+ server_error
167
187
  end
168
- break if @error
169
- end
170
- begin
171
- @tree.check_types unless @error
172
- rescue ErrorInvalidArgumentType => e
173
- @error = e
188
+ break(@error) if @error
174
189
  end
175
- @error || @tree
190
+ (@error = @tree.check_types) unless @error
191
+ @error ? @error : Contract.valid(@tree)
176
192
  end
177
193
  end
178
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
- jh.dataset(dtcx).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
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
63
  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)
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,42 +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}%"
233
+ success "#{@value}%"
171
234
  end
172
235
 
173
236
  def leuqes_ends_like(_jh)
174
- "%#{@value}"
237
+ success "%#{@value}"
175
238
  end
176
239
 
177
240
  def leuqes_substringof_sig1(_jh)
178
- "%#{@value}%"
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
- raise OData::Filter::Parser::ErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
248
+ return Safrano::FilterParseErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
186
249
 
187
- Sequel[jh.start_model.table_name][@value.to_sym]
250
+ success Sequel[jh.start_model.table_name][@value.to_sym]
188
251
  end
189
252
 
190
253
  # non stantard extensions to support things like
191
254
  # substringof(Rhum, name) ????
192
255
  def leuqes_starts_like(_jh)
193
- "#{@value}%"
256
+ success "#{@value}%"
194
257
  end
195
258
 
196
259
  def leuqes_ends_like(_jh)
197
- "%#{@value}"
260
+ success "%#{@value}"
198
261
  end
199
262
 
200
263
  def leuqes_substringof_sig1(_jh)
201
- "%#{@value}%"
264
+ success "%#{@value}%"
202
265
  end
203
266
 
204
267
  def as_string
@@ -206,31 +269,39 @@ module OData
206
269
  end
207
270
  end
208
271
 
272
+ # Null
273
+ class NullLiteral
274
+ def leuqes(jh)
275
+ # Sequel's representation of NULL
276
+ success LEUQES
277
+ end
278
+ end
279
+
209
280
  # Qualit (qualified lits) are words separated by /
210
281
  class Qualit
211
282
  def leuqes(jh)
212
283
  jh.add(@path)
213
284
  talias = jh.start_model.get_alias_sym(@path)
214
- Sequel[talias][@attrib.to_sym]
285
+ success Sequel[talias][@attrib.to_sym]
215
286
  end
216
287
  end
217
288
 
218
289
  # Quoted Strings
219
290
  class QString
220
291
  def leuqes(_jh)
221
- @value
292
+ success @value
222
293
  end
223
294
 
224
295
  def leuqes_starts_like(_jh)
225
- "#{@value}%"
296
+ success "#{@value}%"
226
297
  end
227
298
 
228
299
  def leuqes_ends_like(_jh)
229
- "%#{@value}"
300
+ success "%#{@value}"
230
301
  end
231
302
 
232
303
  def leuqes_substringof_sig1(_jh)
233
- "%#{@value}%"
304
+ success "%#{@value}%"
234
305
  end
235
306
  end
236
307
  end