safrano 0.4.3 → 0.5.1

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