safrano 0.5.3 → 0.6.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Date/format.rb +47 -0
  3. data/lib/core_ext/DateTime/format.rb +54 -0
  4. data/lib/core_ext/Dir/iter.rb +7 -5
  5. data/lib/core_ext/Hash/transform.rb +14 -6
  6. data/lib/core_ext/Numeric/convert.rb +25 -0
  7. data/lib/core_ext/Time/format.rb +71 -0
  8. data/lib/core_ext/date.rb +5 -0
  9. data/lib/core_ext/datetime.rb +5 -0
  10. data/lib/core_ext/numeric.rb +3 -0
  11. data/lib/core_ext/time.rb +5 -0
  12. data/lib/odata/attribute.rb +8 -6
  13. data/lib/odata/batch.rb +3 -3
  14. data/lib/odata/collection.rb +9 -9
  15. data/lib/odata/collection_filter.rb +3 -1
  16. data/lib/odata/collection_media.rb +4 -27
  17. data/lib/odata/collection_order.rb +1 -1
  18. data/lib/odata/common_logger.rb +5 -27
  19. data/lib/odata/complex_type.rb +61 -67
  20. data/lib/odata/edm/primitive_types.rb +110 -42
  21. data/lib/odata/entity.rb +14 -47
  22. data/lib/odata/error.rb +7 -7
  23. data/lib/odata/expand.rb +2 -2
  24. data/lib/odata/filter/base.rb +10 -1
  25. data/lib/odata/filter/error.rb +2 -2
  26. data/lib/odata/filter/parse.rb +16 -2
  27. data/lib/odata/filter/sequel.rb +31 -4
  28. data/lib/odata/filter/sequel_datetime_adapter.rb +21 -0
  29. data/lib/odata/filter/token.rb +18 -5
  30. data/lib/odata/filter/tree.rb +83 -9
  31. data/lib/odata/function_import.rb +19 -18
  32. data/lib/odata/model_ext.rb +96 -38
  33. data/lib/odata/request/json.rb +171 -0
  34. data/lib/odata/transition.rb +13 -9
  35. data/lib/odata/url_parameters.rb +3 -3
  36. data/lib/odata/walker.rb +9 -9
  37. data/lib/safrano/multipart.rb +1 -3
  38. data/lib/safrano/rack_app.rb +2 -14
  39. data/lib/safrano/rack_builder.rb +0 -15
  40. data/lib/safrano/request.rb +3 -3
  41. data/lib/safrano/response.rb +3 -3
  42. data/lib/safrano/service.rb +43 -12
  43. data/lib/safrano/type_mapping.rb +149 -0
  44. data/lib/safrano/version.rb +1 -2
  45. data/lib/safrano.rb +3 -0
  46. metadata +54 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64699787371f5e9f696461faac80382cf2d53fe78d387742d8650e5a4b2c54df
4
- data.tar.gz: 530e7619b4b27a1a50a63f832d4f3a8894850c6cea36e638769cdffbc6b7d942
3
+ metadata.gz: 26b1634021c6ab2117b0b2fe4fb8c49242d4a045b6afafec604ef61e26c3bc48
4
+ data.tar.gz: 86adac4fb2ab8ad02d570f373325b7e1c4945941a01e2328fc50ff10c9460abd
5
5
  SHA512:
6
- metadata.gz: e5d118f15a3a3ba78f5385867c82370e36612585b7f83329343babb2e3a8a7220d1059ad766fbe7c9e1f7f74a57945c086e60624d0301259c1427f0053b59052
7
- data.tar.gz: 52b283be3e8902226f14864a74eae30bc62c65908be748ea37c5a150f574f3130c8957f26251e1958e298eba221eeada9f0155fcc73ed2591034e97466f1e629
6
+ metadata.gz: c19b30f23c22127742d8c557a23feec6de75a91d20b1b8b0033fb85e4f1549e92de300e17cb3d80564f92ed0dcd81096b5b4aca1a268eba803b6bbd6a4c0a75c
7
+ data.tar.gz: ee3a9d73e9e1d5862cb1a699786e9f81c5a58d7937678b5a138eecae08ccc10e49248f70066ede55e270fb361744978ca4b5b9a75bbd6e62a2c4685e6fc2ae46
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreExt
5
+ module Date
6
+ module Format
7
+ # Date is a DateTime with time 00:00:00 thus the milliseconds are ALWAYS 000
8
+ # excepted when we get Date(0) = Epoch date
9
+ REGEX = /\/Date\((?:(-?\d+)000|0+)\)\//.freeze
10
+ # Input: /Date(86400000)/
11
+ # Match groups
12
+ # 1. 86400
13
+ # Input: /Date(0)/
14
+ # Match groups
15
+ # 1.
16
+
17
+ def from_edm_json(instr)
18
+ return unless (md = instr.match(REGEX))
19
+
20
+ if md[1].nil? # no offset relative to Epoch
21
+ ::Date.new(1970, 1, 1).freeze
22
+ else
23
+ # parse as seconds since epoch
24
+ ::Date.strptime(md[1], '%s')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ module CoreIncl
31
+ module Date
32
+ module Format
33
+ # https://www.odata.org/documentation/odata-version-2-0/json-format/
34
+ # Edm.DateTime
35
+ # "/Date(<ticks>["+" | "-" <offset>)/"
36
+ # <ticks> = number of milliseconds since midnight Jan 1, 1970
37
+ # <offset> = number of minutes to add or subtract
38
+ # https://stackoverflow.com/questions/10286204/what-is-the-right-json-date-format/10286228#10286228
39
+ def to_edm_json
40
+ # no offset
41
+ # --> %Q milliseconds since epoch
42
+ strftime('/Date(%Q)/')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreExt
5
+ module DateTime
6
+ module Format
7
+ REGEX = /\/Date\((-?\d+)(?:([-+])(\d+))?\)\//.freeze
8
+ # Input: /Date(120000+150)/
9
+ # Match groups
10
+ # 1. 120000
11
+ # 2. +
12
+ # 3. 150
13
+
14
+ def from_edm_json(instr)
15
+ return unless (md = instr.match(REGEX))
16
+
17
+ if md[3].nil? # no offset
18
+ ::DateTime.strptime(md[1], '%Q')
19
+ else
20
+ # offset in hours / mins
21
+ off_h, off_m = md[3].to_i.divmod(60)
22
+ # DateTime.strptime("120000+2:30", '%Q%z') milliseconds since epoch with a z-offset
23
+ # --> #<DateTime: 1970-01-01T02:32:00+02:30 ((2440588j,120s,0n),+9000s,2299161j)>
24
+ ::DateTime.strptime("#{md[1]}#{md[2]}#{off_h}:#{off_m}", '%Q%z')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ module CoreIncl
31
+ module DateTime
32
+ module Format
33
+ # https://www.odata.org/documentation/odata-version-2-0/json-format/
34
+ # Edm.DateTime
35
+ # "/Date(<ticks>["+" | "-" <offset>)/"
36
+ # <ticks> = number of milliseconds since midnight Jan 1, 1970
37
+ # <offset> = number of minutes to add or subtract
38
+ # https://stackoverflow.com/questions/10286204/what-is-the-right-json-date-format/10286228#10286228
39
+ def to_edm_json
40
+ if offset.zero?
41
+ # no offset
42
+ # --> %Q milliseconds since epoch
43
+ strftime('/Date(%Q)/')
44
+ else
45
+ # same as above with GMT offset in minutes
46
+ # DateTime offset is Rational ; fraction of hours per Day --> *24*60
47
+ min_off_s = (min_off = (offset * 60 * 24).to_i) > 0 ? "+#{min_off}" : min_off.to_s
48
+ "/Date(#{strftime('%Q')}#{min_off_s})/"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -5,13 +5,15 @@ module Safrano
5
5
  module CoreExt
6
6
  module Dir
7
7
  module Iter
8
- def each_child(dir)
9
- ::Dir.foreach(dir) do |x|
10
- next if (x == '.') || (x == '..')
8
+ unless ::Dir.respond_to? :each_child
9
+ def each_child(dir)
10
+ ::Dir.foreach(dir) do |x|
11
+ next if (x == '.') || (x == '..')
11
12
 
12
- yield x
13
+ yield x
14
+ end
13
15
  end
14
- end unless ::Dir.respond_to? :each_child
16
+ end
15
17
  end
16
18
  end
17
19
  end
@@ -5,15 +5,23 @@ module Safrano
5
5
  module CoreIncl
6
6
  module Hash
7
7
  module Transform
8
- def transform_keys!
9
- keys.each do |key|
10
- self[yield(key)] = delete(key)
8
+ unless method_defined? :transform_keys!
9
+ def transform_keys!
10
+ keys.each do |key|
11
+ self[yield(key)] = delete(key)
12
+ end
13
+ self
11
14
  end
12
- self
13
- end unless method_defined? :transform_keys!
15
+ end
14
16
 
15
17
  def symbolize_keys!
16
- transform_keys! { |key| key.to_sym rescue key }
18
+ transform_keys! do |key|
19
+ begin
20
+ key.to_sym
21
+ rescue StandardError
22
+ key
23
+ end
24
+ end
17
25
  end
18
26
  end
19
27
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'bigdecimal/util'
4
+
5
+ module Safrano
6
+ module CoreIncl
7
+ module Numeric
8
+ module Convert
9
+ def toDecimalString
10
+ BigDecimal(to_s).to_s('F')
11
+ end
12
+
13
+ def toDecimalPrecisionString(precision)
14
+ p = Integer(precision)
15
+ BigDecimal(self, p).to_s('F')
16
+ end
17
+
18
+ def toDecimalPrecisionScaleString(precision, scale)
19
+ p = Integer(precision)
20
+ format("%#{p + 2}.#{scale}f", self).strip
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreExt
5
+ module Time
6
+ module Format
7
+ REGEX = /\/Date\((-?\d+)(?:([-+])(\d+))?\)\//.freeze
8
+ # Input: /Date(120000+150)/
9
+ # Match groups
10
+ # 1. 120000
11
+ # 2. +
12
+ # 3. 150
13
+
14
+ def from_edm_json(instr)
15
+ return unless (md = instr.match(REGEX))
16
+
17
+ sec, milli = md[1].to_i.divmod(1000)
18
+ secm = milli.zero? ? sec : sec + Float(milli) / 1000
19
+ if md[3].nil? # no offset
20
+ # ::Time.at(sec, milli, :millisecond) # not supported in ruby 2.4
21
+ ::Time.gm(1970, 1, 1) + secm
22
+ else
23
+ # offset in hours / mins
24
+ off_h, off_m = md[3].to_i.divmod(60)
25
+ # add leading 0 because Time.at is expecting "+HH:MM", "-HH:MM"
26
+ off_h = off_h < 10 ? "0#{off_h}" : off_h.to_s
27
+ off_m = off_m < 10 ? "0#{off_m}" : off_m.to_s
28
+ # Time.strptime("120000+2:30", '%Q%z') milliseconds since epoch with a z-offset
29
+ # --> #<DateTime: 1970-01-01T02:32:00+02:30 ((2440588j,120s,0n),+9000s,2299161j)>
30
+ # ::Time.at(sec, milli, :millisecond, in: "#{md[2]}#{off_h}:#{off_m}") # not supported in ruby 2.4
31
+ offset_str = "#{md[2]}#{off_h}:#{off_m}"
32
+ ::Time.new(1970, 1, 1, 0, 0, 0, offset_str) + secm + ::Time.zone_offset(offset_str)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module CoreIncl
40
+ module Time
41
+ module Format
42
+ # Welcome to Hell
43
+ # https://www.odata.org/documentation/odata-version-2-0/json-format/
44
+ # Edm.DateTime
45
+ # "/Date(<ticks>["+" | "-" <offset>)/"
46
+ # <ticks> = number of milliseconds since midnight Jan 1, 1970
47
+ # <offset> = number of minutes to add or subtract
48
+ # https://stackoverflow.com/questions/10286204/what-is-the-right-json-date-format/10286228#10286228
49
+ # also https://www.hanselman.com/blog/on-the-nightmare-that-is-json-dates-plus-jsonnet-and-aspnet-web-api
50
+ # and https://itecnote.com/tecnote/c-use-json-net-to-parse-json-date-of-format-dateepochtime-offset/
51
+ # https://docs.microsoft.com/de-de/dotnet/standard/datetime/system-text-json-support
52
+ # https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/stand-alone-json-serialization#datestimes-and-json
53
+ # https://blogs.sap.com/2017/01/05/date-and-time-in-sap-gateway-foundation/
54
+ def to_edm_json
55
+ if utc? || (gmt_offset == 0)
56
+ # no offset
57
+ # %s : seconds since unix epoch
58
+ # %L : milliseconds 000-999
59
+ # --> %S%L milliseconds since epoch
60
+ strftime('/Date(%s%L)/')
61
+ else
62
+ # same as above with GMT offset in minutes
63
+
64
+ min_off_s = (min_off = gmt_offset / 60) > 0 ? "+#{min_off}" : min_off.to_s
65
+ "/Date(#{strftime('%s%L')}#{min_off_s})/"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'Date/format'
2
+ require 'date'
3
+
4
+ Date.include Safrano::CoreIncl::Date::Format
5
+ Date.extend Safrano::CoreExt::Date::Format
@@ -0,0 +1,5 @@
1
+ require_relative 'DateTime/format'
2
+ require 'date'
3
+
4
+ DateTime.include Safrano::CoreIncl::DateTime::Format
5
+ DateTime.extend Safrano::CoreExt::DateTime::Format
@@ -0,0 +1,3 @@
1
+ require_relative 'Numeric/convert'
2
+
3
+ Numeric.include Safrano::CoreIncl::Numeric::Convert
@@ -0,0 +1,5 @@
1
+ require_relative 'Time/format'
2
+ require 'time'
3
+
4
+ Time.include Safrano::CoreIncl::Time::Format
5
+ Time.extend Safrano::CoreExt::Time::Format
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative '../safrano/core.rb'
5
- require_relative './entity.rb'
4
+ require_relative '../safrano/core'
5
+ require_relative './entity'
6
6
 
7
7
  module Safrano
8
8
  # Represents a named and valued attribute of an Entity
@@ -18,12 +18,14 @@ module Safrano
18
18
  def value
19
19
  # WARNING ... this require more work to handle the timezones topci
20
20
  # currently it is just set to make some minimal testcase work
21
+ # See also model_ext.rb
21
22
  case (v = @entity.values[@name.to_sym])
23
+ when Date
24
+ v.to_edm_json
22
25
  when Time
23
- # try to get back the database time zone and value
24
- # (v + v.gmt_offset).utc.to_datetime
25
- v.iso8601
26
-
26
+ v.to_edm_json
27
+ when DateTime
28
+ v.to_edm_json
27
29
  else
28
30
  v
29
31
  end
data/lib/odata/batch.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../safrano/rack_app.rb'
4
- require_relative '../safrano/core.rb'
3
+ require_relative '../safrano/rack_app'
4
+ require_relative '../safrano/core'
5
5
  require 'rack/body_proxy'
6
- require_relative './common_logger.rb'
6
+ require_relative './common_logger'
7
7
 
8
8
  module Safrano
9
9
  # Support for OData multipart $batch Requests
@@ -60,13 +60,13 @@ module Safrano
60
60
  end
61
61
 
62
62
  def initialize_dataset(dtset = nil)
63
- @cx = @cx || dtset || @modelk
63
+ @cx = @cx || dtset || @modelk
64
64
  end
65
-
65
+
66
66
  def initialize_uparms
67
67
  @uparms = UrlParameters4Coll.new(@cx, @params)
68
68
  end
69
-
69
+
70
70
  def odata_get_apply_params
71
71
  @uparms.apply_to_dataset(@cx).map_result! do |dataset|
72
72
  @cx = dataset
@@ -134,14 +134,14 @@ module Safrano
134
134
 
135
135
  # on model class level we return the collection
136
136
  def odata_get(req)
137
- @params = @params || req.params
138
- initialize_dataset
137
+ @params ||= req.params
138
+ initialize_dataset
139
139
  initialize_uparms
140
- @uparms.check_all.if_valid { |_ret|
141
- odata_get_apply_params.if_valid { |_ret|
140
+ @uparms.check_all.if_valid do |_ret|
141
+ odata_get_apply_params.if_valid do |_ret|
142
142
  odata_get_output(req)
143
- }
144
- }.tap_error { |e| return e.odata_get(req) }.result
143
+ end
144
+ end.tap_error { |e| return e.odata_get(req) }.result
145
145
  end
146
146
 
147
147
  def odata_post(req)
@@ -20,7 +20,9 @@ module Safrano
20
20
  end
21
21
 
22
22
  # finalize
23
- def finalize(_jh) Contract::OK end
23
+ def finalize(_jh)
24
+ Contract::OK
25
+ end
24
26
 
25
27
  def empty?
26
28
  true
@@ -143,8 +143,8 @@ module Safrano
143
143
  # with directory Tree structure
144
144
 
145
145
  class StaticTree < Static
146
- SEP = '/00/'.freeze
147
- VERS = '/v'.freeze
146
+ SEP = '/00/'
147
+ VERS = '/v'
148
148
 
149
149
  def self.path_builder(ids)
150
150
  ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
@@ -276,7 +276,7 @@ module Safrano
276
276
 
277
277
  media_handler.check_before_create(data: data,
278
278
  entity: new_entity,
279
- filename: filename).if_valid { |_ret|
279
+ filename: filename).if_valid do |_ret|
280
280
  # to_one rels are create with FK data set on the parent entity
281
281
  if parent
282
282
  odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
@@ -302,7 +302,7 @@ module Safrano
302
302
  # Contract.valid([201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)])
303
303
  # TODO quirks array mode !
304
304
  Contract.valid([201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)])
305
- }.tap_error { |e| return e.odata_get(req) }.result
305
+ end.tap_error { |e| return e.odata_get(req) }.result
306
306
 
307
307
  else # TODO: other formats
308
308
  415
@@ -311,26 +311,3 @@ module Safrano
311
311
  end
312
312
  end
313
313
  end
314
-
315
- # deprecated
316
- # REMOVE 0.6
317
- module OData
318
- module Media
319
- class Static < ::Safrano::Media::Static
320
- def initialize(root: nil, mediaklass:)
321
- ::Safrano::Deprecation.deprecate('OData::Media::Static',
322
- 'Use Safrano::Media::Static instead')
323
-
324
- super
325
- end
326
- end
327
- class StaticTree < ::Safrano::Media::StaticTree
328
- def initialize(root: nil, mediaklass:)
329
- ::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
330
- 'Use Safrano::Media::StaticTree instead')
331
-
332
- super
333
- end
334
- end
335
- end
336
- end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'odata/error.rb'
3
+ require 'odata/error'
4
4
 
5
5
  # all ordering related classes in our OData module
6
6
  module Safrano
@@ -10,36 +10,14 @@ module Rack
10
10
  # Handle https://github.com/rack/rack/pull/1526
11
11
  # new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
12
12
 
13
- MSG_FUNC = if FORMAT.count('%') == 10
13
+ MSG_FUNC = case FORMAT.count('%')
14
+ when 10
14
15
  lambda { |env, length, status, began_at|
15
- FORMAT % [
16
- env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
17
- env['REMOTE_USER'] || '-',
18
- Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
19
- env[REQUEST_METHOD],
20
- env[SCRIPT_NAME] + env[PATH_INFO],
21
- env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
22
- env[SERVER_PROTOCOL],
23
- status.to_s[0..3],
24
- length,
25
- Utils.clock_time - began_at
26
- ]
16
+ format(FORMAT, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME] + env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
27
17
  }
28
- elsif FORMAT.count('%') == 11
18
+ when 11
29
19
  lambda { |env, length, status, began_at|
30
- FORMAT % [
31
- env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
32
- env['REMOTE_USER'] || '-',
33
- Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
34
- env[REQUEST_METHOD],
35
- env[SCRIPT_NAME],
36
- env[PATH_INFO],
37
- env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
38
- env[SERVER_PROTOCOL],
39
- status.to_s[0..3],
40
- length,
41
- Utils.clock_time - began_at
42
- ]
20
+ format(FORMAT, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME], env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
43
21
  }
44
22
  end
45
23