safrano 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,7 +11,7 @@ module OData
11
11
  attr_reader :uribase
12
12
 
13
13
  include EntityBase::NavigationInfo
14
-
14
+
15
15
  # methods related to transitions to next state (cf. walker)
16
16
  module Transitions
17
17
  def allowed_transitions
@@ -79,7 +79,6 @@ module OData
79
79
  self.class.nav_entity_attribs&.each_key do |na_str|
80
80
  @nav_values[na_str.to_sym] = send(na_str)
81
81
  end
82
-
83
82
  @nav_values
84
83
  end
85
84
 
@@ -95,31 +94,47 @@ module OData
95
94
  "#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
96
95
  end
97
96
  D = 'd'.freeze
98
- DJopen = '{"d":'.freeze
99
- DJclose = '}'.freeze
97
+ DJ_OPEN = '{"d":'.freeze
98
+ DJ_CLOSE = '}'.freeze
99
+
100
100
  # Json formatter for a single entity (probably OData V1/V2 like)
101
101
  def to_odata_json(service:)
102
+ template = self.class.output_template(@uparms)
102
103
  innerj = service.get_entity_odata_h(entity: self,
103
- expand: @params['$expand'],
104
+ template: template,
104
105
  uribase: @uribase).to_json
105
- "#{DJopen}#{innerj}#{DJclose}"
106
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
106
107
  end
107
108
 
108
109
  # Json formatter for a single entity reached by navigation $links
109
110
  def to_odata_onelink_json(service:)
110
111
  innerj = service.get_entity_odata_link_h(entity: self,
111
112
  uribase: @uribase).to_json
112
- "#{DJopen}#{innerj}#{DJclose}"
113
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
113
114
  end
114
115
 
116
+ def selected_values_for_odata(cols)
117
+ allvals = values_for_odata
118
+ selvals = {}
119
+ cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
120
+ selvals
121
+ end
115
122
 
116
123
  # needed for proper datetime output
117
- # TODO design/performance
118
- def casted_values
124
+ # TODO: design/performance
125
+ def casted_values(cols = nil)
126
+ vals = case cols
127
+ when nil
128
+ values_for_odata
129
+ else
130
+ selected_values_for_odata(cols)
131
+ end
132
+
119
133
  # WARNING; this code is duplicated in attribute.rb
120
134
  # (and the inverted transformation is in test/client.rb)
121
135
  # will require a more systematic solution some day
122
- values_for_odata.transform_values! { |v|
136
+
137
+ vals.transform_values! do |v|
123
138
  case v
124
139
  when Time
125
140
  # try to get back the database time zone and value
@@ -127,14 +142,15 @@ module OData
127
142
  else
128
143
  v
129
144
  end
130
- }
145
+ end
131
146
  end
132
147
 
133
148
  # post paylod expects the new entity in an array
134
149
  def to_odata_post_json(service:)
135
150
  innerj = service.get_coll_odata_h(array: [self],
151
+ template: self.class.default_template,
136
152
  uribase: @uribase).to_json
137
- "#{DJopen}#{innerj}#{DJclose}"
153
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
138
154
  end
139
155
 
140
156
  def type_name
@@ -145,12 +161,12 @@ module OData
145
161
  @params = req.params
146
162
  @uribase = req.uribase
147
163
  @do_links = req.walker.do_links
164
+ @uparms = UrlParameters4Single.new(@params)
148
165
  end
149
166
 
150
167
  # Finally Process REST verbs...
151
168
  def odata_get(req)
152
169
  copy_request_infos(req)
153
-
154
170
  if req.walker.media_value
155
171
  odata_media_value_get(req)
156
172
  elsif req.accept?(APPJSON)
@@ -163,12 +179,12 @@ module OData
163
179
  415
164
180
  end
165
181
  end
166
-
182
+
167
183
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
168
- OData.remove_nav_relation(entity, assoc, parent)
184
+ OData.remove_nav_relation(assoc, parent)
169
185
  entity.destroy(transaction: false)
170
186
  end
171
-
187
+
172
188
  def odata_delete_relation_and_entity(req, assoc, parent)
173
189
  if parent
174
190
  if req.in_changeset
@@ -182,52 +198,48 @@ module OData
182
198
  else
183
199
  destroy(transaction: false)
184
200
  end
185
- rescue StandardError => e
186
- raise SequelAdapterError.new(e)
201
+ rescue StandardError => e
202
+ raise SequelAdapterError.new(e)
187
203
  end
188
-
204
+
189
205
  # TODO: differentiate between POST/PUT/PATCH/MERGE
190
206
  def odata_post(req)
191
207
  if req.walker.media_value
192
208
  odata_media_value_put(req)
193
- else
194
- if req.accept?(APPJSON)
195
- data.delete('__metadata')
196
-
197
- if req.in_changeset
198
- set_fields(data, self.class.data_fields, missing: :skip)
199
- save(transaction: false)
200
- else
201
- update_fields(data, self.class.data_fields, missing: :skip)
202
- end
203
-
204
- [202, {}, to_odata_post_json(service: req.service)]
205
- else # TODO: other formats
206
- 415
209
+ elsif req.accept?(APPJSON)
210
+ data.delete('__metadata')
211
+
212
+ if req.in_changeset
213
+ set_fields(data, self.class.data_fields, missing: :skip)
214
+ save(transaction: false)
215
+ else
216
+ update_fields(data, self.class.data_fields, missing: :skip)
207
217
  end
218
+
219
+ [202, EMPTY_HASH, to_odata_post_json(service: req.service)]
220
+ else # TODO: other formats
221
+ 415
208
222
  end
209
223
  end
210
224
 
211
225
  def odata_put(req)
212
226
  if req.walker.media_value
213
227
  odata_media_value_put(req)
214
- else
215
- if req.accept?(APPJSON)
216
- data = JSON.parse(req.body.read)
217
- @uribase = req.uribase
218
- data.delete('__metadata')
219
-
220
- if req.in_changeset
221
- set_fields(data, self.class.data_fields, missing: :skip)
222
- save(transaction: false)
223
- else
224
- update_fields(data, self.class.data_fields, missing: :skip)
225
- end
228
+ elsif req.accept?(APPJSON)
229
+ data = JSON.parse(req.body.read)
230
+ @uribase = req.uribase
231
+ data.delete('__metadata')
226
232
 
227
- [204, {}, []]
228
- else # TODO: other formats
229
- 415
233
+ if req.in_changeset
234
+ set_fields(data, self.class.data_fields, missing: :skip)
235
+ save(transaction: false)
236
+ else
237
+ update_fields(data, self.class.data_fields, missing: :skip)
230
238
  end
239
+
240
+ ARY_204_EMPTY_HASH_ARY
241
+ else # TODO: other formats
242
+ 415
231
243
  end
232
244
  end
233
245
 
@@ -238,7 +250,7 @@ module OData
238
250
  # validate payload column names
239
251
  if (invalid = self.class.invalid_hash_data?(data))
240
252
  ::OData::Request::ON_CGST_ERROR.call(req)
241
- return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
253
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
242
254
  end
243
255
  # TODO: check values/types
244
256
 
@@ -253,7 +265,7 @@ module OData
253
265
  update_fields(data, my_data_fields, missing: :skip)
254
266
  end
255
267
  # patch should return 204 + no content
256
- [204, {}, []]
268
+ ARY_204_EMPTY_HASH_ARY
257
269
  end
258
270
  end
259
271
 
@@ -281,7 +293,7 @@ module OData
281
293
  y.each { |enty| yield enty }
282
294
  end
283
295
 
284
- # TODO design... this is not DRY
296
+ # TODO: design... this is not DRY
285
297
  def slug_field
286
298
  superclass.slug_field
287
299
  end
@@ -294,6 +306,10 @@ module OData
294
306
  superclass.media_handler
295
307
  end
296
308
 
309
+ def default_template
310
+ superclass.default_template
311
+ end
312
+
297
313
  def to_a
298
314
  y = @child_method.call
299
315
  y.to_a
@@ -321,7 +337,7 @@ module OData
321
337
  extend NavigationRedefinitions
322
338
  end
323
339
  end
324
-
340
+
325
341
  # GetRelatedEntity that returns an single related Entity
326
342
  # (...to_one relationship )
327
343
  def get_related_entity(childattrib)
@@ -335,9 +351,9 @@ module OData
335
351
  # allows to receive a POST operation that would actually create the nav attribute entity
336
352
 
337
353
  ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
338
-
354
+
339
355
  ret.set_relation_info(self, childattrib)
340
-
356
+
341
357
  ret
342
358
  end
343
359
  end
@@ -359,7 +375,7 @@ module OData
359
375
 
360
376
  def odata_delete(req)
361
377
  if req.accept?(APPJSON)
362
- # delete
378
+ # delete
363
379
  begin
364
380
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
365
381
  [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
@@ -372,13 +388,13 @@ module OData
372
388
  end
373
389
 
374
390
  # in case of a non media entity, we have to return an error on $value request
375
- def odata_media_value_get(req)
376
- return BadRequestNonMediaValue.odata_get
391
+ def odata_media_value_get(_req)
392
+ BadRequestNonMediaValue.odata_get
377
393
  end
378
394
 
379
395
  # in case of a non media entity, we have to return an error on $value PUT
380
- def odata_media_value_put(req)
381
- return BadRequestNonMediaValue.odata_get
396
+ def odata_media_value_put(_req)
397
+ BadRequestNonMediaValue.odata_get
382
398
  end
383
399
  end
384
400
 
@@ -396,11 +412,11 @@ module OData
396
412
  version = self.class.media_handler.ressource_version(self)
397
413
  "#{uri(uribase)}/$value?version=#{version}"
398
414
  end
399
-
415
+
400
416
  def edit_media(uribase)
401
417
  "#{uri(uribase)}/$value"
402
418
  end
403
-
419
+
404
420
  # directory where to put/find the media files for this entity-type
405
421
  def klass_dir
406
422
  type_name
@@ -422,9 +438,8 @@ module OData
422
438
  # delete the MR
423
439
  # delegate to the media handler on collection(ie class) level
424
440
  # TODO error handling
425
-
426
-
427
- self.class.media_handler.odata_delete(request: req, entity: self)
441
+
442
+ self.class.media_handler.odata_delete(entity: self)
428
443
  # delete the relation(s) to parent(s) (if any) and then entity
429
444
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
430
445
  # result
@@ -444,7 +459,7 @@ module OData
444
459
  def odata_media_value_put(req)
445
460
  model = self.class
446
461
  req.with_media_data do |data, mimetype, filename|
447
- emdata = { :content_type => mimetype }
462
+ emdata = { content_type: mimetype }
448
463
  if req.in_changeset
449
464
  set_fields(emdata, model.data_fields, missing: :skip)
450
465
  save(transaction: false)
@@ -454,7 +469,7 @@ module OData
454
469
  model.media_handler.replace_file(data: data,
455
470
  entity: self,
456
471
  filename: filename)
457
- [204, {}, []]
472
+ ARY_204_EMPTY_HASH_ARY
458
473
  end
459
474
  end
460
475
  end
@@ -469,6 +484,7 @@ module OData
469
484
  def media_path_id
470
485
  pk.to_s
471
486
  end
487
+
472
488
  def media_path_ids
473
489
  [pk]
474
490
  end
@@ -479,15 +495,15 @@ module OData
479
495
  include Entity
480
496
  def pk_uri
481
497
  # pk_hash is provided by Sequel
482
- self.pk_hash.map { |k, v| "#{k}='#{v}'" }.join(',')
498
+ pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
483
499
  end
484
500
 
485
501
  def media_path_id
486
- self.pk_hash.values.join('_')
502
+ pk_hash.values.join(SPACE)
487
503
  end
488
-
504
+
489
505
  def media_path_ids
490
- self.pk_hash.values
506
+ pk_hash.values
491
507
  end
492
508
  end
493
509
  end
@@ -40,7 +40,7 @@ module OData
40
40
  end
41
41
  end
42
42
  end
43
-
43
+
44
44
  # base module for HTTP errors, when used as an Error instance
45
45
  module ErrorInstance
46
46
  def odata_get(req)
@@ -52,7 +52,7 @@ module OData
52
52
  end
53
53
  end
54
54
  end
55
-
55
+
56
56
  # http Bad Req.
57
57
  class BadRequestError
58
58
  extend ErrorClass
@@ -76,12 +76,25 @@ module OData
76
76
  @msg = err.inner.message
77
77
  end
78
78
  end
79
-
79
+
80
80
  # for Syntax error in Filtering
81
81
  class BadRequestFilterParseError < BadRequestError
82
82
  HTTP_CODE = 400
83
- @msg = 'Bad Request: Syntax error in Filter'
83
+ @msg = 'Bad Request: Syntax error in $filter'
84
+ end
85
+
86
+ # for Syntax error in $expand param
87
+ class BadRequestExpandParseError < BadRequestError
88
+ HTTP_CODE = 400
89
+ @msg = 'Bad Request: Syntax error in $expand'
84
90
  end
91
+
92
+ # for Syntax error in $orderby param
93
+ class BadRequestOrderParseError < BadRequestError
94
+ HTTP_CODE = 400
95
+ @msg = 'Bad Request: Syntax error in $orderby'
96
+ end
97
+
85
98
  # for $inlinecount error
86
99
  class BadRequestInlineCountParamError < BadRequestError
87
100
  HTTP_CODE = 400
@@ -0,0 +1,123 @@
1
+ require 'odata/error.rb'
2
+
3
+ # all dataset expanding related classes in our OData module
4
+ # ie do eager loading
5
+ module OData
6
+ # base class for expanding
7
+ class ExpandBase
8
+ EmptyExpand = new # re-useable empty expanding (idempotent)
9
+ EMPTYH = {}.freeze
10
+
11
+ def self.factory(expandstr)
12
+ expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
13
+ end
14
+
15
+ # output template
16
+ attr_reader :template
17
+
18
+ def apply_to_dataset(dtcx)
19
+ dtcx
20
+ end
21
+
22
+ def empty?
23
+ true
24
+ end
25
+
26
+ def parse_error?
27
+ false
28
+ end
29
+
30
+ def template
31
+ EMPTYH
32
+ end
33
+ end
34
+
35
+ # single expand
36
+ class Expand < ExpandBase
37
+ # sequel eager arg.
38
+ attr_reader :arg
39
+ attr_reader :template
40
+
41
+ # used for Sequel eager argument
42
+ # Recursive array to deep hash
43
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
44
+ # [1] --> 1
45
+ DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
46
+
47
+ # used for building output template
48
+ # Recursive array to deep hash
49
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
50
+ # [1] --> { 1 => {} }
51
+ DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
52
+
53
+ NODESEP = '/'.freeze
54
+
55
+ def initialize(exstr)
56
+ exstr.strip!
57
+ @expandp = exstr
58
+ @nodes = @expandp.split(NODESEP)
59
+ build_arg
60
+ end
61
+
62
+ def apply_to_dataset(dtcx)
63
+ dtcx
64
+ end
65
+
66
+ def build_arg
67
+ # 'a/b/c/d' ==> {a: {b:{c: :d}}}
68
+ # 'xy' ==> :xy
69
+ @arg = DEEPH_0.call(@nodes.map(&:to_sym))
70
+ @template = DEEPH_1.call(@nodes)
71
+ end
72
+
73
+ def parse_error?
74
+ # todo
75
+ false
76
+ end
77
+
78
+ def empty?
79
+ false
80
+ end
81
+ end
82
+
83
+ # Multi expanding logic
84
+ class MultiExpand < ExpandBase
85
+ COMASPLIT = /\s*,\s*/.freeze
86
+ attr_reader :template
87
+
88
+ def initialize(expandstr)
89
+ expandstr.strip!
90
+ @expandp = expandstr
91
+ @exlist = []
92
+
93
+ @exlist = expandstr.split(COMASPLIT).map { |exstr| Expand.new(exstr) }
94
+ build_template
95
+ end
96
+
97
+ def apply_to_dataset(dtcx)
98
+ # use eager loading for each used association
99
+ @exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
100
+ dtcx
101
+ end
102
+
103
+ def build_template
104
+ # 'a/b/c/d,xy' ==> [ {'a' =>{ 'b' => {'c' => {'d' => {} } }}},
105
+ # { 'xy' => {} }]
106
+ #
107
+ @template = @exlist.map(&:template)
108
+
109
+ # { 'a' => { 'b' => {'c' => 'd' }},
110
+ # 'xy' => {} }
111
+ @template = @template.inject({}) { |mrg, elmt| mrg.merge elmt }
112
+ end
113
+
114
+ def parse_error?
115
+ # todo
116
+ false
117
+ end
118
+
119
+ def empty?
120
+ false
121
+ end
122
+ end
123
+ end