safrano 0.4.3 → 0.4.4

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