safrano 0.3.3 → 0.4.3

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 (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