safrano 0.4.3 → 0.4.4

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 +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  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 +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -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 +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  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 +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -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
@@ -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
 
@@ -114,27 +126,33 @@ module OData
114
126
  def copy_request_infos(req)
115
127
  @params = req.params
116
128
  @do_links = req.walker.do_links
117
- @uparms = UrlParameters4Single.new(@params)
129
+ @uparms = UrlParameters4Single.new(self, @params)
118
130
  end
119
131
 
120
- # Finally Process REST verbs...
121
- def odata_get(req)
122
- copy_request_infos(req)
132
+ def odata_get_output(req)
123
133
  if req.walker.media_value
124
134
  odata_media_value_get(req)
125
135
  elsif req.accept?(APPJSON)
136
+ # json is default content type so we dont need to specify it here again
126
137
  if req.walker.do_links
127
- [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
138
+ [200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
128
139
  else
129
- [200, CT_JSON, [to_odata_json(service: req.service)]]
140
+ [200, EMPTY_HASH, [to_odata_json(request: req)]]
130
141
  end
131
142
  else # TODO: other formats
132
143
  415
133
144
  end
134
145
  end
135
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
+
136
154
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
137
- OData.remove_nav_relation(assoc, parent)
155
+ Safrano.remove_nav_relation(assoc, parent)
138
156
  entity.destroy(transaction: false)
139
157
  end
140
158
 
@@ -201,7 +219,7 @@ module OData
201
219
 
202
220
  # validate payload column names
203
221
  if (invalid = self.class.invalid_hash_data?(data))
204
- ::OData::Request::ON_CGST_ERROR.call(req)
222
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
205
223
  return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
206
224
  end
207
225
  # TODO: check values/types
@@ -219,89 +237,11 @@ module OData
219
237
  end
220
238
  end
221
239
 
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 )
240
+ # GetRelated that returns a collection object representing
241
+ # wrapping the related object Class ( childklass )
286
242
  # (...to_many relationship )
287
243
  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
244
+ Safrano::OData::NavigatedCollection.new(childattrib, self)
305
245
  end
306
246
 
307
247
  # GetRelatedEntity that returns an single related Entity
@@ -316,14 +256,14 @@ module OData
316
256
  # then we return a Nil... wrapper object. This object then
317
257
  # allows to receive a POST operation that would actually create the nav attribute entity
318
258
 
319
- ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
259
+ ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
320
260
 
321
261
  ret.set_relation_info(self, childattrib)
322
262
 
323
263
  ret
324
264
  end
325
265
  end
326
- # end of module ODataEntity
266
+ # end of module SafranoEntity
327
267
  module Entity
328
268
  include EntityBase
329
269
  end
@@ -344,7 +284,7 @@ module OData
344
284
  # delete
345
285
  begin
346
286
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
347
- [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]]
348
288
  rescue SequelAdapterError => e
349
289
  BadRequestSequelAdapterError.new(e).odata_get(req)
350
290
  end
@@ -375,7 +315,7 @@ module OData
375
315
  values_for_odata.dup
376
316
  else
377
317
  selected_values_for_odata(cols)
378
- end
318
+ end
379
319
  self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
380
320
  vals
381
321
  end
@@ -390,7 +330,7 @@ module OData
390
330
  values_for_odata
391
331
  else
392
332
  selected_values_for_odata(cols)
393
- end
333
+ end
394
334
  end
395
335
  end
396
336
 
@@ -413,16 +353,6 @@ module OData
413
353
  "#{uri}/$value"
414
354
  end
415
355
 
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
356
  def values_for_odata
427
357
  ret = values.dup
428
358
  ret.delete(:content_type)
@@ -439,7 +369,7 @@ module OData
439
369
  # delete the relation(s) to parent(s) (if any) and then entity
440
370
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
441
371
  # result
442
- [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]]
443
373
  else # TODO: other formats
444
374
  415
445
375
  end
@@ -460,11 +390,14 @@ module OData
460
390
  set_fields(emdata, model.data_fields, missing: :skip)
461
391
  save(transaction: false)
462
392
  else
393
+
463
394
  update_fields(emdata, model.data_fields, missing: :skip)
395
+
464
396
  end
465
397
  model.media_handler.replace_file(data: data,
466
398
  entity: self,
467
399
  filename: filename)
400
+
468
401
  ARY_204_EMPTY_HASH_ARY
469
402
  end
470
403
  end
@@ -490,8 +423,11 @@ module OData
490
423
  module EntityMultiPK
491
424
  include Entity
492
425
  def pk_uri
493
- # pk_hash is provided by Sequel
494
- pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
426
+ pku = +''
427
+ self.class.odata_upk_parts.each_with_index { |upart, i|
428
+ pku = "#{pku}#{upart}#{pk[i]}"
429
+ }
430
+ pku
495
431
  end
496
432
 
497
433
  def media_path_id