safrano 0.4.3 → 0.5.1

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 (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 +8 -4
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +139 -642
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +196 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +78 -123
  23. data/lib/odata/error.rb +170 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +9 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +39 -25
  28. data/lib/odata/filter/sequel.rb +112 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +21 -18
  31. data/lib/odata/filter/tree.rb +78 -44
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +641 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +18 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +141 -0
  42. data/lib/safrano/core.rb +24 -106
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -24
  46. data/lib/safrano/rack_app.rb +62 -63
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +96 -38
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +156 -110
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +30 -11
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Safrano
6
+ # Type mapping DB --> Edm
7
+ # TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
8
+ # "STRING" => "Edm.String"}
9
+ # Todo: complete mapping... this is just for the most common ones
10
+
11
+ # TODO: use Sequel GENERIC_TYPES: -->
12
+ # Constants
13
+ # GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
14
+ # Time File TrueClass FalseClass'.freeze
15
+ # Classes specifying generic types that Sequel will convert to
16
+ # database-specific types.
17
+ DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
18
+
19
+ # used in $metadata
20
+ # cf. Sequel Database column_schema_default_to_ruby_value
21
+ # schema_column_type
22
+ # https://www.odata.org/documentation/odata-version-2-0/overview/
23
+ def self.default_edm_type(ruby_type:)
24
+ case ruby_type
25
+ when :integer
26
+ 'Edm.Int32'
27
+ when :string
28
+ 'Edm.String'
29
+ when :date, :datetime,
30
+ 'Edm.DateTime'
31
+ when :time
32
+ 'Edm.Time'
33
+ when :boolean
34
+ 'Edm.Boolean'
35
+ when :float
36
+ 'Edm.Double'
37
+ when :decimal
38
+ 'Edm.Decimal'
39
+ when :blob
40
+ 'Edm.Binary'
41
+ end
42
+ end
43
+
44
+ # use Edm twice so that we can do include Safrano::Edm and then
45
+ # have Edm::Int32 etc... availabe
46
+ # and we can have Edm::String different from ::String
47
+ module Edm
48
+ module Edm
49
+ module OutputClassMethods
50
+ def type_name
51
+ "Edm.#{name.split('::').last}"
52
+ end
53
+
54
+ def odata_collection(array)
55
+ array
56
+ end
57
+
58
+ def odata_value(instance)
59
+ instance
60
+ end
61
+ end
62
+
63
+ class Null < NilClass
64
+ extend OutputClassMethods
65
+ # nil --> null convertion is done by to_json
66
+ def self.odata_value(instance)
67
+ nil
68
+ end
69
+
70
+ def self.convert_from_urlparam(v)
71
+ return Contract::NOK unless (v == 'null')
72
+
73
+ Contract.valid(nil)
74
+ end
75
+ end
76
+
77
+ # Binary is a String with the BINARY encoding
78
+ class Binary < String
79
+ extend OutputClassMethods
80
+
81
+ def self.convert_from_urlparam(v)
82
+ Contract.valid(v.dup.force_encoding('BINARY'))
83
+ end
84
+ end
85
+
86
+ # an object alwys evaluates to
87
+ # true ([true, anything not false & not nil objs])
88
+ # or false([nil, false])
89
+ class Boolean < Object
90
+ extend OutputClassMethods
91
+ def Boolean.odata_value(instance)
92
+ instance ? true : false
93
+ end
94
+
95
+ def self.odata_collection(array)
96
+ array.map { |v| odata_value(v) }
97
+ end
98
+
99
+ def self.convert_from_urlparam(v)
100
+ return Contract::NOK unless ['true', 'false'].include?(v)
101
+
102
+ Contract.valid(v == 'true')
103
+ end
104
+ end
105
+
106
+ # Bytes are usualy represented as Intger in ruby,
107
+ # eg.String.bytes --> Array of ints
108
+ class Byte < Integer
109
+ extend OutputClassMethods
110
+
111
+ def self.convert_from_urlparam(v)
112
+ return Contract::NOK unless ((bytev = v.to_i) < 256)
113
+
114
+ Contract.valid(bytev)
115
+ end
116
+ end
117
+
118
+ class DateTime < ::DateTime
119
+ extend OutputClassMethods
120
+ def DateTime.odata_value(instance)
121
+ instance.to_datetime
122
+ end
123
+
124
+ def self.odata_collection(array)
125
+ array.map { |v| odata_value(v) }
126
+ end
127
+
128
+ def self.convert_from_urlparam(v)
129
+ begin
130
+ Contract.valid(DateTime.parse(v))
131
+ rescue
132
+ return convertion_error(v)
133
+ end
134
+ end
135
+ end
136
+
137
+ class String < ::String
138
+ extend OutputClassMethods
139
+
140
+ def self.convert_from_urlparam(v)
141
+ Contract.valid(v)
142
+ end
143
+ end
144
+
145
+ class Int32 < Integer
146
+ extend OutputClassMethods
147
+
148
+ def self.convert_from_urlparam(v)
149
+ return Contract::NOK unless (ret = number_or_nil(v))
150
+
151
+ Contract.valid(ret)
152
+ end
153
+ end
154
+
155
+ class Int64 < Integer
156
+ extend OutputClassMethods
157
+
158
+ def self.convert_from_urlparam(v)
159
+ return Contract::NOK unless (ret = number_or_nil(v))
160
+
161
+ Contract.valid(ret)
162
+ end
163
+ end
164
+
165
+ class Double < Float
166
+ extend OutputClassMethods
167
+
168
+ def self.convert_from_urlparam(v)
169
+ begin
170
+ Contract.valid(v.to_f)
171
+ rescue
172
+ return Contract::NOK
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # include Safrano
181
+
182
+ # x = Edm::String.new('xxx')
183
+
184
+ # pp x
data/lib/odata/entity.rb CHANGED
@@ -1,15 +1,17 @@
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
13
 
12
- include EntityBase::NavigationInfo
14
+ include Safrano::NavigationInfo
13
15
 
14
16
  # methods related to transitions to next state (cf. walker)
15
17
  module Transitions
@@ -18,7 +20,7 @@ module OData
18
20
  end
19
21
 
20
22
  def transition_end(_match_result)
21
- [nil, :end]
23
+ Safrano::Transition::RESULT_END
22
24
  end
23
25
 
24
26
  def transition_count(_match_result)
@@ -36,8 +38,7 @@ module OData
36
38
 
37
39
  def transition_attribute(match_result)
38
40
  attrib = match_result[1]
39
- # [values[attrib.to_sym], :run]
40
- [OData::Attribute.new(self, attrib), :run]
41
+ [Safrano::Attribute.new(self, attrib), :run]
41
42
  end
42
43
 
43
44
  def transition_nav_collection(match_result)
@@ -49,10 +50,20 @@ module OData
49
50
  attrib = match_result[1]
50
51
  [get_related_entity(attrib), :run]
51
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
52
58
  end
53
59
 
54
60
  include Transitions
55
61
 
62
+ # for testing only?
63
+ def ==(other)
64
+ ((self.class.type_name == other.class.type_name) and (@values == other.values))
65
+ end
66
+
56
67
  def nav_values
57
68
  @nav_values = {}
58
69
 
@@ -80,10 +91,11 @@ module OData
80
91
  DJ_CLOSE = '}'.freeze
81
92
 
82
93
  # Json formatter for a single entity (probably OData V1/V2 like)
83
- def to_odata_json(service:)
84
- template = self.class.output_template(@uparms)
85
- innerj = service.get_entity_odata_h(entity: self,
86
- template: template).to_json
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
87
99
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
88
100
  end
89
101
 
@@ -100,10 +112,11 @@ module OData
100
112
  selvals
101
113
  end
102
114
 
103
- # post paylod expects the new entity in an array
104
- def to_odata_post_json(service:)
105
- innerj = service.get_coll_odata_h(array: [self],
106
- template: self.class.default_template).to_json
115
+ # some clients wrongly expect post payload with the new entity in an array
116
+ # TODO quirks array mode !
117
+ def to_odata_array_json(request:)
118
+ innerj = request.service.get_coll_odata_h(array: [self],
119
+ template: self.class.default_template).to_json
107
120
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
108
121
  end
109
122
 
@@ -114,27 +127,33 @@ module OData
114
127
  def copy_request_infos(req)
115
128
  @params = req.params
116
129
  @do_links = req.walker.do_links
117
- @uparms = UrlParameters4Single.new(@params)
130
+ @uparms = UrlParameters4Single.new(self, @params)
118
131
  end
119
132
 
120
- # Finally Process REST verbs...
121
- def odata_get(req)
122
- copy_request_infos(req)
133
+ def odata_get_output(req)
123
134
  if req.walker.media_value
124
135
  odata_media_value_get(req)
125
136
  elsif req.accept?(APPJSON)
137
+ # json is default content type so we dont need to specify it here again
126
138
  if req.walker.do_links
127
- [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
139
+ [200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
128
140
  else
129
- [200, CT_JSON, [to_odata_json(service: req.service)]]
141
+ [200, EMPTY_HASH, [to_odata_json(request: req)]]
130
142
  end
131
143
  else # TODO: other formats
132
144
  415
133
145
  end
134
146
  end
135
147
 
148
+ # Finally Process REST verbs...
149
+ def odata_get(req)
150
+ copy_request_infos(req)
151
+ @uparms.check_all.tap_valid { return odata_get_output(req) }
152
+ .tap_error { |e| return e.odata_get(req) }
153
+ end
154
+
136
155
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
137
- OData.remove_nav_relation(assoc, parent)
156
+ Safrano.remove_nav_relation(assoc, parent)
138
157
  entity.destroy(transaction: false)
139
158
  end
140
159
 
@@ -201,7 +220,7 @@ module OData
201
220
 
202
221
  # validate payload column names
203
222
  if (invalid = self.class.invalid_hash_data?(data))
204
- ::OData::Request::ON_CGST_ERROR.call(req)
223
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
205
224
  return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
206
225
  end
207
226
  # TODO: check values/types
@@ -219,89 +238,11 @@ module OData
219
238
  end
220
239
  end
221
240
 
222
- # redefinitions of the main methods for a navigated collection
223
- # (eg. all Books of Author[2] is Author[2].Books.all )
224
- module NavigationRedefinitions
225
- def all
226
- @child_method.call
227
- end
228
-
229
- def count
230
- @child_method.call.count
231
- end
232
-
233
- def dataset
234
- @child_dataset_method.call
235
- end
236
-
237
- def navigated_dataset
238
- @child_dataset_method.call
239
- end
240
-
241
- def each
242
- y = @child_method.call
243
- y.each { |enty| yield enty }
244
- end
245
-
246
- # TODO: design... this is not DRY
247
- def slug_field
248
- superclass.slug_field
249
- end
250
-
251
- def type_name
252
- superclass.type_name
253
- end
254
-
255
- def time_cols
256
- superclass.time_cols
257
- end
258
-
259
- def media_handler
260
- superclass.media_handler
261
- end
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
-
279
- def to_a
280
- y = @child_method.call
281
- y.to_a
282
- end
283
- end
284
- # GetRelated that returns a anonymous Class (ie. representing a collection)
285
- # subtype of the related object Class ( childklass )
241
+ # GetRelated that returns a collection object representing
242
+ # wrapping the related object Class ( childklass )
286
243
  # (...to_many relationship )
287
244
  def get_related(childattrib)
288
- parent = self
289
- childklass = self.class.nav_collection_attribs[childattrib]
290
- Class.new(childklass) do
291
- # this makes use of Sequel's Model relationships; eg this is
292
- # 'Race[12].Edition'
293
- # where Race[12] would be our self and 'Edition' is the
294
- # childattrib(collection)
295
- @child_method = parent.method(childattrib.to_sym)
296
- @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
297
- @nav_parent = parent
298
- @navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
299
- prepare_pk
300
- prepare_fields
301
- # Now in this anonymous Class we can refine the "all, count and []
302
- # methods, to take into account the relationship
303
- extend NavigationRedefinitions
304
- end
245
+ Safrano::OData::NavigatedCollection.new(childattrib, self)
305
246
  end
306
247
 
307
248
  # GetRelatedEntity that returns an single related Entity
@@ -316,14 +257,15 @@ module OData
316
257
  # then we return a Nil... wrapper object. This object then
317
258
  # allows to receive a POST operation that would actually create the nav attribute entity
318
259
 
319
- ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
260
+ ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
320
261
 
321
262
  ret.set_relation_info(self, childattrib)
322
263
 
323
264
  ret
324
265
  end
325
266
  end
326
- # end of module ODataEntity
267
+
268
+ # end of module SafranoEntity
327
269
  module Entity
328
270
  include EntityBase
329
271
  end
@@ -344,7 +286,7 @@ module OData
344
286
  # delete
345
287
  begin
346
288
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
347
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
289
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
348
290
  rescue SequelAdapterError => e
349
291
  BadRequestSequelAdapterError.new(e).odata_get(req)
350
292
  end
@@ -375,7 +317,7 @@ module OData
375
317
  values_for_odata.dup
376
318
  else
377
319
  selected_values_for_odata(cols)
378
- end
320
+ end
379
321
  self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
380
322
  vals
381
323
  end
@@ -390,7 +332,7 @@ module OData
390
332
  values_for_odata
391
333
  else
392
334
  selected_values_for_odata(cols)
393
- end
335
+ end
394
336
  end
395
337
  end
396
338
 
@@ -413,16 +355,6 @@ module OData
413
355
  "#{uri}/$value"
414
356
  end
415
357
 
416
- # directory where to put/find the media files for this entity-type
417
- def klass_dir
418
- type_name
419
- end
420
-
421
- # # this is just ModelKlass/pk as a single string
422
- # def qualified_media_path_id
423
- # "#{self.class}/#{media_path_id}"
424
- # end
425
-
426
358
  def values_for_odata
427
359
  ret = values.dup
428
360
  ret.delete(:content_type)
@@ -439,7 +371,7 @@ module OData
439
371
  # delete the relation(s) to parent(s) (if any) and then entity
440
372
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
441
373
  # result
442
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
374
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
443
375
  else # TODO: other formats
444
376
  415
445
377
  end
@@ -460,11 +392,14 @@ module OData
460
392
  set_fields(emdata, model.data_fields, missing: :skip)
461
393
  save(transaction: false)
462
394
  else
395
+
463
396
  update_fields(emdata, model.data_fields, missing: :skip)
397
+
464
398
  end
465
399
  model.media_handler.replace_file(data: data,
466
400
  entity: self,
467
401
  filename: filename)
402
+
468
403
  ARY_204_EMPTY_HASH_ARY
469
404
  end
470
405
  end
@@ -490,8 +425,11 @@ module OData
490
425
  module EntityMultiPK
491
426
  include Entity
492
427
  def pk_uri
493
- # pk_hash is provided by Sequel
494
- pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
428
+ pku = +''
429
+ self.class.odata_upk_parts.each_with_index { |upart, i|
430
+ pku = "#{pku}#{upart}#{pk[i]}"
431
+ }
432
+ pku
495
433
  end
496
434
 
497
435
  def media_path_id
@@ -502,5 +440,22 @@ module OData
502
440
  pk_hash.values
503
441
  end
504
442
  end
505
- end
506
- # end of Module OData
443
+
444
+ module EntityCreateStandardOutput
445
+ # Json formatter for a create entity POST call / Standard version; return as json object
446
+ def to_odata_create_json(request:)
447
+ # TODO Perf: reduce method call overhead
448
+ # we added this redirection for readability and flexibility
449
+ to_odata_json(request: request)
450
+ end
451
+ end
452
+
453
+ module EntityCreateArrayOutput
454
+ # Json formatter for a create entity POST call Array version
455
+ def to_odata_create_json(request:)
456
+ # TODO Perf: reduce method call overhead
457
+ # we added this redirection for readability and flexibility
458
+ to_odata_array_json(request: request)
459
+ end
460
+ end
461
+ end # end of Module OData