safrano 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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