safrano 0.4.0 → 0.4.5

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 +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +145 -74
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  24. data/lib/odata/expand.rb +126 -0
  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 +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  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 +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +637 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -1,44 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'rexml/document'
3
5
  require 'safrano.rb'
4
- require 'odata/collection.rb' # required for self.class.entity_type_name ??
6
+ require 'odata/model_ext.rb' # required for self.class.entity_type_name ??
5
7
  require_relative 'navigation_attribute'
6
8
 
7
- module OData
9
+ module Safrano
8
10
  # this will be mixed in the Model classes (subclasses of Sequel Model)
9
11
  module EntityBase
10
12
  attr_reader :params
11
- attr_reader :uribase
12
13
 
13
- include EntityBase::NavigationInfo
14
-
14
+ include Safrano::NavigationInfo
15
+
15
16
  # methods related to transitions to next state (cf. walker)
16
17
  module Transitions
17
18
  def allowed_transitions
18
- alltr = [
19
- Safrano::TransitionEnd,
20
- Safrano::TransitionCount,
21
- Safrano::TransitionLinks,
22
- Safrano::TransitionValue,
23
- Safrano::Transition.new(self.class.transition_attribute_regexp,
24
- trans: 'transition_attribute')
25
- ]
26
- if (ncurgx = self.class.nav_collection_url_regexp)
27
- alltr <<
28
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
29
- trans: 'transition_nav_collection')
30
-
31
- end
32
- if (neurgx = self.class.nav_entity_url_regexp)
33
- alltr <<
34
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
35
- trans: 'transition_nav_entity')
36
- end
37
- alltr
19
+ self.class.entity_allowed_transitions
38
20
  end
39
21
 
40
22
  def transition_end(_match_result)
41
- [nil, :end]
23
+ Safrano::Transition::RESULT_END
42
24
  end
43
25
 
44
26
  def transition_count(_match_result)
@@ -56,8 +38,7 @@ module OData
56
38
 
57
39
  def transition_attribute(match_result)
58
40
  attrib = match_result[1]
59
- # [values[attrib.to_sym], :run]
60
- [OData::Attribute.new(self, attrib), :run]
41
+ [Safrano::Attribute.new(self, attrib), :run]
61
42
  end
62
43
 
63
44
  def transition_nav_collection(match_result)
@@ -69,17 +50,26 @@ module OData
69
50
  attrib = match_result[1]
70
51
  [get_related_entity(attrib), :run]
71
52
  end
53
+
54
+ def transition_invalid_attribute(match_result)
55
+ invalid_attrib = match_result[1]
56
+ [nil, :error, Safrano::ErrorNotFoundSegment.new(invalid_attrib)]
57
+ end
72
58
  end
73
59
 
74
60
  include Transitions
75
61
 
62
+ # for testing only?
63
+ def ==(other)
64
+ ((self.class.type_name == other.class.type_name) and (@values == other.values))
65
+ end
66
+
76
67
  def nav_values
77
68
  @nav_values = {}
78
69
 
79
70
  self.class.nav_entity_attribs&.each_key do |na_str|
80
71
  @nav_values[na_str.to_sym] = send(na_str)
81
72
  end
82
-
83
73
  @nav_values
84
74
  end
85
75
 
@@ -91,50 +81,42 @@ module OData
91
81
  @nav_coll
92
82
  end
93
83
 
94
- def uri(uriba)
95
- "#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
84
+ def uri
85
+ @odata_pk ||= "(#{pk_uri})"
86
+ "#{self.class.uri}#{@odata_pk}"
96
87
  end
88
+
97
89
  D = 'd'.freeze
98
- DJopen = '{"d":'.freeze
99
- DJclose = '}'.freeze
90
+ DJ_OPEN = '{"d":'.freeze
91
+ DJ_CLOSE = '}'.freeze
92
+
100
93
  # Json formatter for a single entity (probably OData V1/V2 like)
101
- def to_odata_json(service:)
102
- innerj = service.get_entity_odata_h(entity: self,
103
- expand: @params['$expand'],
104
- uribase: @uribase).to_json
105
- "#{DJopen}#{innerj}#{DJclose}"
94
+ def to_odata_json(request:)
95
+ template = self.class.output_template(expand_list: @uparms.expand.template,
96
+ select: @uparms.select)
97
+ innerj = request.service.get_entity_odata_h(entity: self,
98
+ template: template).to_json
99
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
106
100
  end
107
101
 
108
102
  # Json formatter for a single entity reached by navigation $links
109
103
  def to_odata_onelink_json(service:)
110
- innerj = service.get_entity_odata_link_h(entity: self,
111
- uribase: @uribase).to_json
112
- "#{DJopen}#{innerj}#{DJclose}"
104
+ innerj = service.get_entity_odata_link_h(entity: self).to_json
105
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
113
106
  end
114
107
 
115
-
116
- # needed for proper datetime output
117
- # TODO design/performance
118
- def casted_values
119
- # WARNING; this code is duplicated in attribute.rb
120
- # (and the inverted transformation is in test/client.rb)
121
- # will require a more systematic solution some day
122
- values_for_odata.transform_values! { |v|
123
- case v
124
- when Time
125
- # try to get back the database time zone and value
126
- (v + v.gmt_offset).utc.to_datetime
127
- else
128
- v
129
- end
130
- }
108
+ def selected_values_for_odata(cols)
109
+ allvals = values_for_odata
110
+ selvals = {}
111
+ cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
112
+ selvals
131
113
  end
132
114
 
133
115
  # post paylod expects the new entity in an array
134
116
  def to_odata_post_json(service:)
135
117
  innerj = service.get_coll_odata_h(array: [self],
136
- uribase: @uribase).to_json
137
- "#{DJopen}#{innerj}#{DJclose}"
118
+ template: self.class.default_template).to_json
119
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
138
120
  end
139
121
 
140
122
  def type_name
@@ -143,32 +125,37 @@ module OData
143
125
 
144
126
  def copy_request_infos(req)
145
127
  @params = req.params
146
- @uribase = req.uribase
147
128
  @do_links = req.walker.do_links
129
+ @uparms = UrlParameters4Single.new(self, @params)
148
130
  end
149
131
 
150
- # Finally Process REST verbs...
151
- def odata_get(req)
152
- copy_request_infos(req)
153
-
132
+ def odata_get_output(req)
154
133
  if req.walker.media_value
155
134
  odata_media_value_get(req)
156
135
  elsif req.accept?(APPJSON)
136
+ # json is default content type so we dont need to specify it here again
157
137
  if req.walker.do_links
158
- [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
138
+ [200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
159
139
  else
160
- [200, CT_JSON, [to_odata_json(service: req.service)]]
140
+ [200, EMPTY_HASH, [to_odata_json(request: req)]]
161
141
  end
162
142
  else # TODO: other formats
163
143
  415
164
144
  end
165
145
  end
166
-
146
+
147
+ # Finally Process REST verbs...
148
+ def odata_get(req)
149
+ copy_request_infos(req)
150
+ @uparms.check_all.tap_valid { return odata_get_output(req) }
151
+ .tap_error { |e| return e.odata_get(req) }
152
+ end
153
+
167
154
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
168
- OData.remove_nav_relation(entity, assoc, parent)
155
+ Safrano.remove_nav_relation(assoc, parent)
169
156
  entity.destroy(transaction: false)
170
157
  end
171
-
158
+
172
159
  def odata_delete_relation_and_entity(req, assoc, parent)
173
160
  if parent
174
161
  if req.in_changeset
@@ -182,16 +169,15 @@ module OData
182
169
  else
183
170
  destroy(transaction: false)
184
171
  end
185
- rescue StandardError => e
186
- raise SequelAdapterError.new(e)
172
+ rescue StandardError => e
173
+ raise SequelAdapterError.new(e)
187
174
  end
188
-
175
+
189
176
  # TODO: differentiate between POST/PUT/PATCH/MERGE
190
177
  def odata_post(req)
191
- data = JSON.parse(req.body.read)
192
- @uribase = req.uribase
193
-
194
- if req.accept?(APPJSON)
178
+ if req.walker.media_value
179
+ odata_media_value_put(req)
180
+ elsif req.accept?(APPJSON)
195
181
  data.delete('__metadata')
196
182
 
197
183
  if req.in_changeset
@@ -201,7 +187,7 @@ module OData
201
187
  update_fields(data, self.class.data_fields, missing: :skip)
202
188
  end
203
189
 
204
- [202, {}, to_odata_post_json(service: req.service)]
190
+ [202, EMPTY_HASH, to_odata_post_json(service: req.service)]
205
191
  else # TODO: other formats
206
192
  415
207
193
  end
@@ -210,23 +196,20 @@ module OData
210
196
  def odata_put(req)
211
197
  if req.walker.media_value
212
198
  odata_media_value_put(req)
213
- else
214
- if req.accept?(APPJSON)
215
- data = JSON.parse(req.body.read)
216
- @uribase = req.uribase
217
- data.delete('__metadata')
218
-
219
- if req.in_changeset
220
- set_fields(data, self.class.data_fields, missing: :skip)
221
- save(transaction: false)
222
- else
223
- update_fields(data, self.class.data_fields, missing: :skip)
224
- end
199
+ elsif req.accept?(APPJSON)
200
+ data = JSON.parse(req.body.read)
201
+ data.delete('__metadata')
225
202
 
226
- [204, {}, []]
227
- else # TODO: other formats
228
- 415
203
+ if req.in_changeset
204
+ set_fields(data, self.class.data_fields, missing: :skip)
205
+ save(transaction: false)
206
+ else
207
+ update_fields(data, self.class.data_fields, missing: :skip)
229
208
  end
209
+
210
+ ARY_204_EMPTY_HASH_ARY
211
+ else # TODO: other formats
212
+ 415
230
213
  end
231
214
  end
232
215
 
@@ -236,14 +219,12 @@ module OData
236
219
 
237
220
  # validate payload column names
238
221
  if (invalid = self.class.invalid_hash_data?(data))
239
- ::OData::Request::ON_CGST_ERROR.call(req)
240
- return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
222
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
223
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
241
224
  end
242
225
  # TODO: check values/types
243
226
 
244
227
  my_data_fields = self.class.data_fields
245
- @uribase = req.uribase
246
- # if req.accept?('application/json')
247
228
 
248
229
  if req.in_changeset
249
230
  set_fields(data, my_data_fields, missing: :skip)
@@ -252,75 +233,17 @@ module OData
252
233
  update_fields(data, my_data_fields, missing: :skip)
253
234
  end
254
235
  # patch should return 204 + no content
255
- [204, {}, []]
236
+ ARY_204_EMPTY_HASH_ARY
256
237
  end
257
238
  end
258
239
 
259
- # redefinitions of the main methods for a navigated collection
260
- # (eg. all Books of Author[2] is Author[2].Books.all )
261
- module NavigationRedefinitions
262
- def all
263
- @child_method.call
264
- end
265
-
266
- def count
267
- @child_method.call.count
268
- end
269
-
270
- def dataset
271
- @child_dataset_method.call
272
- end
273
-
274
- def navigated_dataset
275
- @child_dataset_method.call
276
- end
277
-
278
- def each
279
- y = @child_method.call
280
- y.each { |enty| yield enty }
281
- end
282
-
283
- # TODO design... this is not DRY
284
- def slug_field
285
- superclass.slug_field
286
- end
287
-
288
- def type_name
289
- superclass.type_name
290
- end
291
-
292
- def media_handler
293
- superclass.media_handler
294
- end
295
-
296
- def to_a
297
- y = @child_method.call
298
- y.to_a
299
- end
300
- end
301
- # GetRelated that returns a anonymous Class (ie. representing a collection)
302
- # subtype of the related object Class ( childklass )
240
+ # GetRelated that returns a collection object representing
241
+ # wrapping the related object Class ( childklass )
303
242
  # (...to_many relationship )
304
243
  def get_related(childattrib)
305
- parent = self
306
- childklass = self.class.nav_collection_attribs[childattrib]
307
- Class.new(childklass) do
308
- # this makes use of Sequel's Model relationships; eg this is
309
- # 'Race[12].Edition'
310
- # where Race[12] would be our self and 'Edition' is the
311
- # childattrib(collection)
312
- @child_method = parent.method(childattrib.to_sym)
313
- @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
314
- @nav_parent = parent
315
- @navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
316
- prepare_pk
317
- prepare_fields
318
- # Now in this anonymous Class we can refine the "all, count and []
319
- # methods, to take into account the relationship
320
- extend NavigationRedefinitions
321
- end
244
+ Safrano::OData::NavigatedCollection.new(childattrib, self)
322
245
  end
323
-
246
+
324
247
  # GetRelatedEntity that returns an single related Entity
325
248
  # (...to_one relationship )
326
249
  def get_related_entity(childattrib)
@@ -333,35 +256,35 @@ module OData
333
256
  # then we return a Nil... wrapper object. This object then
334
257
  # allows to receive a POST operation that would actually create the nav attribute entity
335
258
 
336
- ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
337
-
259
+ ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
260
+
338
261
  ret.set_relation_info(self, childattrib)
339
-
262
+
340
263
  ret
341
264
  end
342
265
  end
343
- # end of module ODataEntity
266
+ # end of module SafranoEntity
344
267
  module Entity
345
268
  include EntityBase
346
269
  end
347
270
 
348
271
  module NonMediaEntity
349
272
  # non media entity metadata for json h
350
- def metadata_h(uribase:)
351
- { uri: uri(uribase),
273
+ def metadata_h
274
+ { uri: uri,
352
275
  type: type_name }
353
276
  end
354
277
 
355
278
  def values_for_odata
356
- values.dup
279
+ values
357
280
  end
358
281
 
359
282
  def odata_delete(req)
360
283
  if req.accept?(APPJSON)
361
- # delete
284
+ # delete
362
285
  begin
363
286
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
364
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
287
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
365
288
  rescue SequelAdapterError => e
366
289
  BadRequestSequelAdapterError.new(e).odata_get(req)
367
290
  end
@@ -371,40 +294,65 @@ module OData
371
294
  end
372
295
 
373
296
  # in case of a non media entity, we have to return an error on $value request
374
- def odata_media_value_get(req)
375
- return BadRequestNonMediaValue.odata_get
297
+ def odata_media_value_get(_req)
298
+ BadRequestNonMediaValue.odata_get
376
299
  end
377
300
 
378
301
  # in case of a non media entity, we have to return an error on $value PUT
379
- def odata_media_value_put(req)
380
- return BadRequestNonMediaValue.odata_get
302
+ def odata_media_value_put(_req)
303
+ BadRequestNonMediaValue.odata_get
304
+ end
305
+ end
306
+
307
+ module MappingBeforeOutput
308
+ # needed for proper datetime output
309
+ def casted_values(cols = nil)
310
+ vals = case cols
311
+ when nil
312
+ # we need to dup the model values as we need to change it before passing to_json,
313
+ # but we dont want to interfere with Sequel's owned data
314
+ # (eg because then in worst case it could happen that we write back changed values to DB)
315
+ values_for_odata.dup
316
+ else
317
+ selected_values_for_odata(cols)
318
+ end
319
+ self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
320
+ vals
321
+ end
322
+ end
323
+ module NoMappingBeforeOutput
324
+ # current model does not have eg. Time fields--> no special mapping, just to_json is fine
325
+ # --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
326
+ # need to change it, just output as is
327
+ def casted_values(cols = nil)
328
+ case cols
329
+ when nil
330
+ values_for_odata
331
+ else
332
+ selected_values_for_odata(cols)
333
+ end
381
334
  end
382
335
  end
383
336
 
384
337
  module MediaEntity
385
338
  # media entity metadata for json h
386
- def metadata_h(uribase:)
387
- { uri: uri(uribase),
339
+ def metadata_h
340
+ { uri: uri,
388
341
  type: type_name,
389
- media_src: media_src(uribase),
390
- edit_media: media_src(uribase),
342
+ media_src: media_src,
343
+ edit_media: edit_media,
391
344
  content_type: @values[:content_type] }
392
345
  end
393
346
 
394
- def media_src(urbase)
395
- "#{uri(urbase)}/$value"
347
+ def media_src
348
+ version = self.class.media_handler.ressource_version(self)
349
+ "#{uri}/$value?version=#{version}"
396
350
  end
397
351
 
398
- # directory where to put/find the media files for this entity-type
399
- def klass_dir
400
- type_name
352
+ def edit_media
353
+ "#{uri}/$value"
401
354
  end
402
355
 
403
- # # this is just ModelKlass/pk as a single string
404
- # def qualified_media_path_id
405
- # "#{self.class}/#{media_path_id}"
406
- # end
407
-
408
356
  def values_for_odata
409
357
  ret = values.dup
410
358
  ret.delete(:content_type)
@@ -416,13 +364,12 @@ module OData
416
364
  # delete the MR
417
365
  # delegate to the media handler on collection(ie class) level
418
366
  # TODO error handling
419
-
420
-
421
- self.class.media_handler.odata_delete(request: req, entity: self)
367
+
368
+ self.class.media_handler.odata_delete(entity: self)
422
369
  # delete the relation(s) to parent(s) (if any) and then entity
423
370
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
424
371
  # result
425
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
372
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
426
373
  else # TODO: other formats
427
374
  415
428
375
  end
@@ -438,17 +385,20 @@ module OData
438
385
  def odata_media_value_put(req)
439
386
  model = self.class
440
387
  req.with_media_data do |data, mimetype, filename|
441
- emdata = { :content_type => mimetype }
388
+ emdata = { content_type: mimetype }
442
389
  if req.in_changeset
443
390
  set_fields(emdata, model.data_fields, missing: :skip)
444
391
  save(transaction: false)
445
392
  else
393
+
446
394
  update_fields(emdata, model.data_fields, missing: :skip)
395
+
447
396
  end
448
397
  model.media_handler.replace_file(data: data,
449
398
  entity: self,
450
399
  filename: filename)
451
- [204, {}, []]
400
+
401
+ ARY_204_EMPTY_HASH_ARY
452
402
  end
453
403
  end
454
404
  end
@@ -463,6 +413,7 @@ module OData
463
413
  def media_path_id
464
414
  pk.to_s
465
415
  end
416
+
466
417
  def media_path_ids
467
418
  [pk]
468
419
  end
@@ -472,16 +423,19 @@ module OData
472
423
  module EntityMultiPK
473
424
  include Entity
474
425
  def pk_uri
475
- # pk_hash is provided by Sequel
476
- self.pk_hash.map { |k, v| "#{k}='#{v}'" }.join(',')
426
+ pku = +''
427
+ self.class.odata_upk_parts.each_with_index { |upart, i|
428
+ pku = "#{pku}#{upart}#{pk[i]}"
429
+ }
430
+ pku
477
431
  end
478
432
 
479
433
  def media_path_id
480
- self.pk_hash.values.join('_')
434
+ pk_hash.values.join(SPACE)
481
435
  end
482
-
436
+
483
437
  def media_path_ids
484
- self.pk_hash.values
438
+ pk_hash.values
485
439
  end
486
440
  end
487
441
  end