safrano 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -2,36 +2,19 @@ require 'json'
2
2
  require 'rexml/document'
3
3
  require 'safrano.rb'
4
4
  require 'odata/collection.rb' # required for self.class.entity_type_name ??
5
+ require_relative 'navigation_attribute'
5
6
 
6
7
  module OData
7
8
  # this will be mixed in the Model classes (subclasses of Sequel Model)
8
9
  module EntityBase
9
10
  attr_reader :params
10
- attr_reader :uribase
11
+
12
+ include EntityBase::NavigationInfo
11
13
 
12
14
  # methods related to transitions to next state (cf. walker)
13
15
  module Transitions
14
16
  def allowed_transitions
15
- alltr = [
16
- Safrano::TransitionEnd,
17
- Safrano::TransitionCount,
18
- Safrano::TransitionLinks,
19
- Safrano::TransitionValue,
20
- Safrano::Transition.new(self.class.transition_attribute_regexp,
21
- trans: 'transition_attribute')
22
- ]
23
- if (ncurgx = self.class.nav_collection_url_regexp)
24
- alltr <<
25
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
26
- trans: 'transition_nav_collection')
27
-
28
- end
29
- if (neurgx = self.class.nav_entity_url_regexp)
30
- alltr <<
31
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
32
- trans: 'transition_nav_entity')
33
- end
34
- alltr
17
+ self.class.entity_allowed_transitions
35
18
  end
36
19
 
37
20
  def transition_end(_match_result)
@@ -76,7 +59,6 @@ module OData
76
59
  self.class.nav_entity_attribs&.each_key do |na_str|
77
60
  @nav_values[na_str.to_sym] = send(na_str)
78
61
  end
79
-
80
62
  @nav_values
81
63
  end
82
64
 
@@ -88,43 +70,41 @@ module OData
88
70
  @nav_coll
89
71
  end
90
72
 
91
- def uri(uriba)
92
- "#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
73
+ def uri
74
+ @odata_pk ||= "(#{pk_uri})"
75
+ "#{self.class.uri}#{@odata_pk}"
93
76
  end
77
+
94
78
  D = 'd'.freeze
95
- DJopen = '{"d":'.freeze
96
- DJclose = '}'.freeze
79
+ DJ_OPEN = '{"d":'.freeze
80
+ DJ_CLOSE = '}'.freeze
81
+
97
82
  # Json formatter for a single entity (probably OData V1/V2 like)
98
83
  def to_odata_json(service:)
84
+ template = self.class.output_template(@uparms)
99
85
  innerj = service.get_entity_odata_h(entity: self,
100
- expand: @params['$expand'],
101
- # links: @do_links,
102
- uribase: @uribase).to_json
103
- "#{DJopen}#{innerj}#{DJclose}"
86
+ template: template).to_json
87
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
104
88
  end
105
89
 
106
- # needed for proper datetime output
107
- # TODO design/performance
108
- def casted_values
109
- # WARNING; this code is duplicated in attribute.rb
110
- # (and the inverted transformation is in test/client.rb)
111
- # will require a more systematic solution some day
112
- values_for_odata.transform_values! { |v|
113
- case v
114
- when Time
115
- # try to get back the database time zone and value
116
- (v + v.gmt_offset).utc.to_datetime
117
- else
118
- v
119
- end
120
- }
90
+ # Json formatter for a single entity reached by navigation $links
91
+ def to_odata_onelink_json(service:)
92
+ innerj = service.get_entity_odata_link_h(entity: self).to_json
93
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
94
+ end
95
+
96
+ def selected_values_for_odata(cols)
97
+ allvals = values_for_odata
98
+ selvals = {}
99
+ cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
100
+ selvals
121
101
  end
122
102
 
123
103
  # post paylod expects the new entity in an array
124
104
  def to_odata_post_json(service:)
125
105
  innerj = service.get_coll_odata_h(array: [self],
126
- uribase: @uribase).to_json
127
- "#{DJopen}#{innerj}#{DJclose}"
106
+ template: self.class.default_template).to_json
107
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
128
108
  end
129
109
 
130
110
  def type_name
@@ -133,38 +113,53 @@ module OData
133
113
 
134
114
  def copy_request_infos(req)
135
115
  @params = req.params
136
- @uribase = req.uribase
137
116
  @do_links = req.walker.do_links
117
+ @uparms = UrlParameters4Single.new(@params)
138
118
  end
139
119
 
140
120
  # Finally Process REST verbs...
141
121
  def odata_get(req)
142
122
  copy_request_infos(req)
143
-
144
123
  if req.walker.media_value
145
124
  odata_media_value_get(req)
146
125
  elsif req.accept?(APPJSON)
147
- [200, CT_JSON, [to_odata_json(service: req.service)]]
126
+ if req.walker.do_links
127
+ [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
128
+ else
129
+ [200, CT_JSON, [to_odata_json(service: req.service)]]
130
+ end
148
131
  else # TODO: other formats
149
132
  415
150
133
  end
151
134
  end
152
135
 
153
- def odata_delete(req)
154
- if req.accept?(APPJSON)
155
- delete
156
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
157
- else # TODO: other formats
158
- 415
136
+ DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
137
+ OData.remove_nav_relation(assoc, parent)
138
+ entity.destroy(transaction: false)
139
+ end
140
+
141
+ def odata_delete_relation_and_entity(req, assoc, parent)
142
+ if parent
143
+ if req.in_changeset
144
+ # in-changeset requests get their own transaction
145
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
146
+ else
147
+ db.transaction do
148
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
149
+ end
150
+ end
151
+ else
152
+ destroy(transaction: false)
159
153
  end
154
+ rescue StandardError => e
155
+ raise SequelAdapterError.new(e)
160
156
  end
161
157
 
162
158
  # TODO: differentiate between POST/PUT/PATCH/MERGE
163
159
  def odata_post(req)
164
- data = JSON.parse(req.body.read)
165
- @uribase = req.uribase
166
-
167
- if req.accept?(APPJSON)
160
+ if req.walker.media_value
161
+ odata_media_value_put(req)
162
+ elsif req.accept?(APPJSON)
168
163
  data.delete('__metadata')
169
164
 
170
165
  if req.in_changeset
@@ -174,7 +169,7 @@ module OData
174
169
  update_fields(data, self.class.data_fields, missing: :skip)
175
170
  end
176
171
 
177
- [202, {}, to_odata_post_json(service: req.service)]
172
+ [202, EMPTY_HASH, to_odata_post_json(service: req.service)]
178
173
  else # TODO: other formats
179
174
  415
180
175
  end
@@ -183,23 +178,20 @@ module OData
183
178
  def odata_put(req)
184
179
  if req.walker.media_value
185
180
  odata_media_value_put(req)
186
- else
187
- if req.accept?(APPJSON)
188
- data = JSON.parse(req.body.read)
189
- @uribase = req.uribase
190
- data.delete('__metadata')
191
-
192
- if req.in_changeset
193
- set_fields(data, self.class.data_fields, missing: :skip)
194
- save(transaction: false)
195
- else
196
- update_fields(data, self.class.data_fields, missing: :skip)
197
- end
181
+ elsif req.accept?(APPJSON)
182
+ data = JSON.parse(req.body.read)
183
+ data.delete('__metadata')
198
184
 
199
- [204, {}, []]
200
- else # TODO: other formats
201
- 415
185
+ if req.in_changeset
186
+ set_fields(data, self.class.data_fields, missing: :skip)
187
+ save(transaction: false)
188
+ else
189
+ update_fields(data, self.class.data_fields, missing: :skip)
202
190
  end
191
+
192
+ ARY_204_EMPTY_HASH_ARY
193
+ else # TODO: other formats
194
+ 415
203
195
  end
204
196
  end
205
197
 
@@ -210,13 +202,11 @@ module OData
210
202
  # validate payload column names
211
203
  if (invalid = self.class.invalid_hash_data?(data))
212
204
  ::OData::Request::ON_CGST_ERROR.call(req)
213
- return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
205
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
214
206
  end
215
207
  # TODO: check values/types
216
208
 
217
209
  my_data_fields = self.class.data_fields
218
- @uribase = req.uribase
219
- # if req.accept?('application/json')
220
210
 
221
211
  if req.in_changeset
222
212
  set_fields(data, my_data_fields, missing: :skip)
@@ -225,7 +215,7 @@ module OData
225
215
  update_fields(data, my_data_fields, missing: :skip)
226
216
  end
227
217
  # patch should return 204 + no content
228
- [204, {}, []]
218
+ ARY_204_EMPTY_HASH_ARY
229
219
  end
230
220
  end
231
221
 
@@ -253,14 +243,39 @@ module OData
253
243
  y.each { |enty| yield enty }
254
244
  end
255
245
 
246
+ # TODO: design... this is not DRY
247
+ def slug_field
248
+ superclass.slug_field
249
+ end
250
+
256
251
  def type_name
257
252
  superclass.type_name
258
253
  end
259
254
 
255
+ def time_cols
256
+ superclass.time_cols
257
+ end
258
+
260
259
  def media_handler
261
260
  superclass.media_handler
262
261
  end
263
262
 
263
+ def uri
264
+ superclass.uri
265
+ end
266
+
267
+ def default_template
268
+ superclass.default_template
269
+ end
270
+
271
+ def allowed_transitions
272
+ superclass.allowed_transitions
273
+ end
274
+
275
+ def entity_allowed_transitions
276
+ superclass.entity_allowed_transitions
277
+ end
278
+
264
279
  def to_a
265
280
  y = @child_method.call
266
281
  y.to_a
@@ -301,8 +316,9 @@ module OData
301
316
  # then we return a Nil... wrapper object. This object then
302
317
  # allows to receive a POST operation that would actually create the nav attribute entity
303
318
 
304
- ret = method(childattrib.to_sym).call ||
305
- OData::NilNavigationAttribute.new(self, childattrib)
319
+ ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
320
+
321
+ ret.set_relation_info(self, childattrib)
306
322
 
307
323
  ret
308
324
  end
@@ -314,38 +330,87 @@ module OData
314
330
 
315
331
  module NonMediaEntity
316
332
  # non media entity metadata for json h
317
- def metadata_h(uribase:)
318
- { uri: uri(uribase),
333
+ def metadata_h
334
+ { uri: uri,
319
335
  type: type_name }
320
336
  end
321
337
 
322
338
  def values_for_odata
323
- values.dup
339
+ values
340
+ end
341
+
342
+ def odata_delete(req)
343
+ if req.accept?(APPJSON)
344
+ # delete
345
+ begin
346
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
347
+ [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
348
+ rescue SequelAdapterError => e
349
+ BadRequestSequelAdapterError.new(e).odata_get(req)
350
+ end
351
+ else # TODO: other formats
352
+ 415
353
+ end
324
354
  end
325
355
 
326
356
  # in case of a non media entity, we have to return an error on $value request
327
- def odata_media_value_get(req)
328
- return BadRequestNonMediaValue.odata_get
357
+ def odata_media_value_get(_req)
358
+ BadRequestNonMediaValue.odata_get
329
359
  end
330
360
 
331
361
  # in case of a non media entity, we have to return an error on $value PUT
332
- def odata_media_value_put(req)
333
- return BadRequestNonMediaValue.odata_get
362
+ def odata_media_value_put(_req)
363
+ BadRequestNonMediaValue.odata_get
364
+ end
365
+ end
366
+
367
+ module MappingBeforeOutput
368
+ # needed for proper datetime output
369
+ def casted_values(cols = nil)
370
+ vals = case cols
371
+ when nil
372
+ # we need to dup the model values as we need to change it before passing to_json,
373
+ # but we dont want to interfere with Sequel's owned data
374
+ # (eg because then in worst case it could happen that we write back changed values to DB)
375
+ values_for_odata.dup
376
+ else
377
+ selected_values_for_odata(cols)
378
+ end
379
+ self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
380
+ vals
381
+ end
382
+ end
383
+ module NoMappingBeforeOutput
384
+ # current model does not have eg. Time fields--> no special mapping, just to_json is fine
385
+ # --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
386
+ # need to change it, just output as is
387
+ def casted_values(cols = nil)
388
+ case cols
389
+ when nil
390
+ values_for_odata
391
+ else
392
+ selected_values_for_odata(cols)
393
+ end
334
394
  end
335
395
  end
336
396
 
337
397
  module MediaEntity
338
398
  # media entity metadata for json h
339
- def metadata_h(uribase:)
340
- { uri: uri(uribase),
399
+ def metadata_h
400
+ { uri: uri,
341
401
  type: type_name,
342
- media_src: media_src(uribase),
343
- edit_media: media_src(uribase),
402
+ media_src: media_src,
403
+ edit_media: edit_media,
344
404
  content_type: @values[:content_type] }
345
405
  end
346
406
 
347
- def media_src(urbase)
348
- "#{uri(urbase)}/$value"
407
+ def media_src
408
+ version = self.class.media_handler.ressource_version(self)
409
+ "#{uri}/$value?version=#{version}"
410
+ end
411
+
412
+ def edit_media
413
+ "#{uri}/$value"
349
414
  end
350
415
 
351
416
  # directory where to put/find the media files for this entity-type
@@ -364,6 +429,22 @@ module OData
364
429
  ret
365
430
  end
366
431
 
432
+ def odata_delete(req)
433
+ if req.accept?(APPJSON)
434
+ # delete the MR
435
+ # delegate to the media handler on collection(ie class) level
436
+ # TODO error handling
437
+
438
+ self.class.media_handler.odata_delete(entity: self)
439
+ # delete the relation(s) to parent(s) (if any) and then entity
440
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
441
+ # result
442
+ [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
443
+ else # TODO: other formats
444
+ 415
445
+ end
446
+ end
447
+
367
448
  # real implementation for returning $value for a media entity
368
449
  def odata_media_value_get(req)
369
450
  # delegate to the media handler on collection(ie class) level
@@ -374,7 +455,7 @@ module OData
374
455
  def odata_media_value_put(req)
375
456
  model = self.class
376
457
  req.with_media_data do |data, mimetype, filename|
377
- emdata = { :content_type => mimetype }
458
+ emdata = { content_type: mimetype }
378
459
  if req.in_changeset
379
460
  set_fields(emdata, model.data_fields, missing: :skip)
380
461
  save(transaction: false)
@@ -384,7 +465,7 @@ module OData
384
465
  model.media_handler.replace_file(data: data,
385
466
  entity: self,
386
467
  filename: filename)
387
- [204, {}, []]
468
+ ARY_204_EMPTY_HASH_ARY
388
469
  end
389
470
  end
390
471
  end
@@ -399,6 +480,10 @@ module OData
399
480
  def media_path_id
400
481
  pk.to_s
401
482
  end
483
+
484
+ def media_path_ids
485
+ [pk]
486
+ end
402
487
  end
403
488
 
404
489
  # for multiple key
@@ -406,11 +491,15 @@ module OData
406
491
  include Entity
407
492
  def pk_uri
408
493
  # pk_hash is provided by Sequel
409
- self.pk_hash.map { |k, v| "#{k}='#{v}'" }.join(',')
494
+ pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
410
495
  end
411
496
 
412
497
  def media_path_id
413
- self.pk_hash.values.join('_')
498
+ pk_hash.values.join(SPACE)
499
+ end
500
+
501
+ def media_path_ids
502
+ pk_hash.values
414
503
  end
415
504
  end
416
505
  end