safrano 0.4.1 → 0.4.2

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.
@@ -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