safrano 0.3.4 → 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 (57) 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 +15 -10
  14. data/lib/odata/batch.rb +17 -15
  15. data/lib/odata/collection.rb +141 -500
  16. data/lib/odata/collection_filter.rb +44 -37
  17. data/lib/odata/collection_media.rb +193 -43
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +39 -12
  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 +201 -176
  23. data/lib/odata/error.rb +186 -33
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +69 -0
  26. data/lib/odata/filter/error.rb +55 -6
  27. data/lib/odata/filter/parse.rb +38 -36
  28. data/lib/odata/filter/sequel.rb +121 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +15 -11
  31. data/lib/odata/filter/tree.rb +110 -60
  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 +50 -32
  35. data/lib/odata/relations.rb +7 -7
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +18 -28
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +43 -0
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
  46. data/lib/safrano/rack_app.rb +175 -0
  47. data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
  48. data/lib/{request.rb → safrano/request.rb} +102 -50
  49. data/lib/{response.rb → safrano/response.rb} +5 -4
  50. data/lib/safrano/sequel_join_by_paths.rb +5 -0
  51. data/lib/{service.rb → safrano/service.rb} +257 -188
  52. data/lib/safrano/version.rb +5 -0
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +53 -17
  55. data/lib/rack_app.rb +0 -174
  56. data/lib/sequel_join_by_paths.rb +0 -5
  57. data/lib/version.rb +0 -4
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ODataCommonLogger < CommonLogger
3
5
  def call(env)
@@ -5,21 +7,46 @@ module Rack
5
7
  super
6
8
  end
7
9
 
10
+ # Handle https://github.com/rack/rack/pull/1526
11
+ # new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
12
+
13
+ MSG_FUNC = if FORMAT.count('%') == 10
14
+ 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
+ ]
27
+ }
28
+ elsif FORMAT.count('%') == 11
29
+ 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
+ ]
43
+ }
44
+ end
45
+
8
46
  def batch_log(env, status, header, began_at)
9
47
  length = extract_content_length(header)
10
48
 
11
- msg = FORMAT % [
12
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
13
- env["REMOTE_USER"] || "-",
14
- Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
15
- env[REQUEST_METHOD],
16
- env[PATH_INFO],
17
- env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
18
- env[SERVER_PROTOCOL],
19
- status.to_s[0..3],
20
- length,
21
- Utils.clock_time - began_at
22
- ]
49
+ msg = MSG_FUNC.call(env, length, status, began_at)
23
50
 
24
51
  logger = @logger || env[RACK_ERRORS]
25
52
  # Standard library logger doesn't support write but it supports << which actually
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module FunctionImport
5
+ class ResultDefinition
6
+ D = 'd'.freeze
7
+ DJ_OPEN = '{"d":'.freeze
8
+ DJ_CLOSE = '}'.freeze
9
+ METAK = '__metadata'.freeze
10
+ TYPEK = 'type'.freeze
11
+ VALUEK = 'value'.freeze
12
+ RESULTSK = 'results'.freeze
13
+ COLLECTION = 'Collection'.freeze
14
+
15
+ def initialize(klassmod)
16
+ @klassmod = klassmod
17
+ end
18
+
19
+ def to_odata_json(result, _req)
20
+ "#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
21
+ end
22
+
23
+ def type_metadata
24
+ @klassmod.type_name
25
+ end
26
+ end
27
+ class ResultAsComplexType < ResultDefinition
28
+ end
29
+ class ResultAsComplexTypeColl < ResultDefinition
30
+ def type_metadata
31
+ "Collection(#{@klassmod.type_name})"
32
+ end
33
+
34
+ def to_odata_json(coll, _req)
35
+ "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
36
+ end
37
+ end
38
+ class ResultAsEntity < ResultDefinition
39
+ def to_odata_json(result_entity, req)
40
+ result_entity.instance_exec do
41
+ copy_request_infos(req)
42
+ to_odata_json(request: req)
43
+ end
44
+ end
45
+ end
46
+ class ResultAsEntityColl < ResultDefinition
47
+ def type_metadata
48
+ "Collection(#{@klassmod.type_name})"
49
+ end
50
+
51
+ def to_odata_json(result_dataset, req)
52
+ coll = Safrano::OData::Collection.new(@klassmod)
53
+ coll.instance_exec do
54
+ @params = req.params
55
+ initialize_dataset(result_dataset)
56
+ end
57
+ coll.to_odata_json(request: req)
58
+ end
59
+ end
60
+ class ResultAsPrimitiveType < ResultDefinition
61
+ def to_odata_json(result, _req)
62
+ { D => { METAK => { TYPEK => type_metadata },
63
+ VALUEK => @klassmod.odata_value(result) } }.to_json
64
+ end
65
+ end
66
+ class ResultAsPrimitiveTypeColl < ResultDefinition
67
+ def type_metadata
68
+ "Collection(#{@klassmod.type_name})"
69
+ end
70
+
71
+ def to_odata_json(result, _req)
72
+ { D => { METAK => { TYPEK => type_metadata },
73
+ RESULTSK => @klassmod.odata_collection(result) } }.to_json
74
+ end
75
+ end
76
+ end
77
+
78
+ # a generic Struct like ruby's standard Struct, but implemented with a
79
+ # @values Hash, similar to Sequel models and
80
+ # with added OData functionality
81
+ class ComplexType
82
+ attr_reader :values
83
+
84
+ @namespace = nil
85
+ def self.namespace
86
+ @namespace
87
+ end
88
+
89
+ def self.props
90
+ @props
91
+ end
92
+
93
+ def self.type_name
94
+ "#{@namespace}.#{self.to_s}"
95
+ end
96
+
97
+ def initialize
98
+ @values = {}
99
+ end
100
+ METAK = '__metadata'.freeze
101
+ TYPEK = 'type'.freeze
102
+
103
+ def odata_h
104
+ ret = { METAK => { TYPEK => self.class.type_name } }
105
+ @values.each { |k, v|
106
+ ret[k] = if v.respond_to? :odata_h
107
+ v.odata_h
108
+ else
109
+ v
110
+ end
111
+ }
112
+ ret
113
+ end
114
+
115
+ def self.return_as_collection_descriptor
116
+ FunctionImport::ResultAsComplexTypeColl.new(self)
117
+ end
118
+
119
+ def self.return_as_instance_descriptor
120
+ FunctionImport::ResultAsComplexType.new(self)
121
+ end
122
+
123
+ # add metadata xml to the passed REXML schema object
124
+ def self.add_metadata_rexml(schema)
125
+ ctty = schema.add_element('ComplexType', 'Name' => to_s)
126
+
127
+ # with their properties
128
+ @props.each do |prop, rbtype|
129
+ attrs = { 'Name' => prop.to_s,
130
+ 'Type' => rbtype.type_name }
131
+ ctty.add_element('Property', attrs)
132
+ end
133
+ ctty
134
+ end
135
+ end
136
+
137
+ def Safrano.ComplexType(**props)
138
+ Class.new(Safrano::ComplexType) do
139
+ @props = props
140
+ props.each { |a, klassmod|
141
+ asym = a.to_sym
142
+ define_method(asym) do @values[asym] end
143
+ define_method("#{a}=") do |val| @values[asym] = val end
144
+ }
145
+ define_method :initialize do |*p, **kwvals|
146
+ super()
147
+ p.zip(props.keys).each { |val, a| @values[a] = val } if p
148
+ kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
149
+ end
150
+ end
151
+ end
152
+ end
@@ -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,41 +1,26 @@
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 ??
7
+ require_relative 'navigation_attribute'
5
8
 
6
- module OData
9
+ module Safrano
7
10
  # this will be mixed in the Model classes (subclasses of Sequel Model)
8
11
  module EntityBase
9
12
  attr_reader :params
10
- attr_reader :uribase
13
+
14
+ include Safrano::NavigationInfo
11
15
 
12
16
  # methods related to transitions to next state (cf. walker)
13
17
  module Transitions
14
18
  def allowed_transitions
15
- alltr = [
16
- Safrano::TransitionEnd,
17
- Safrano::TransitionCount,
18
- Safrano::TransitionLinks,
19
- Safrano::TransitionValue,
20
- Safrano::Transition.new(self.class.transition_attribute_regexp,
21
- trans: 'transition_attribute')
22
- ]
23
- if (ncurgx = self.class.nav_collection_url_regexp)
24
- alltr <<
25
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
26
- trans: 'transition_nav_collection')
27
-
28
- end
29
- if (neurgx = self.class.nav_entity_url_regexp)
30
- alltr <<
31
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
32
- trans: 'transition_nav_entity')
33
- end
34
- alltr
19
+ self.class.entity_allowed_transitions
35
20
  end
36
21
 
37
22
  def transition_end(_match_result)
38
- [nil, :end]
23
+ Safrano::Transition::RESULT_END
39
24
  end
40
25
 
41
26
  def transition_count(_match_result)
@@ -53,8 +38,7 @@ module OData
53
38
 
54
39
  def transition_attribute(match_result)
55
40
  attrib = match_result[1]
56
- # [values[attrib.to_sym], :run]
57
- [OData::Attribute.new(self, attrib), :run]
41
+ [Safrano::Attribute.new(self, attrib), :run]
58
42
  end
59
43
 
60
44
  def transition_nav_collection(match_result)
@@ -66,17 +50,26 @@ module OData
66
50
  attrib = match_result[1]
67
51
  [get_related_entity(attrib), :run]
68
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
69
58
  end
70
59
 
71
60
  include Transitions
72
61
 
62
+ # for testing only?
63
+ def ==(other)
64
+ ((self.class.type_name == other.class.type_name) and (@values == other.values))
65
+ end
66
+
73
67
  def nav_values
74
68
  @nav_values = {}
75
69
 
76
70
  self.class.nav_entity_attribs&.each_key do |na_str|
77
71
  @nav_values[na_str.to_sym] = send(na_str)
78
72
  end
79
-
80
73
  @nav_values
81
74
  end
82
75
 
@@ -88,43 +81,42 @@ module OData
88
81
  @nav_coll
89
82
  end
90
83
 
91
- def uri(uriba)
92
- "#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
84
+ def uri
85
+ @odata_pk ||= "(#{pk_uri})"
86
+ "#{self.class.uri}#{@odata_pk}"
93
87
  end
88
+
94
89
  D = 'd'.freeze
95
- DJopen = '{"d":'.freeze
96
- DJclose = '}'.freeze
90
+ DJ_OPEN = '{"d":'.freeze
91
+ DJ_CLOSE = '}'.freeze
92
+
97
93
  # Json formatter for a single entity (probably OData V1/V2 like)
98
- def to_odata_json(service:)
99
- innerj = service.get_entity_odata_h(entity: self,
100
- expand: @params['$expand'],
101
- # links: @do_links,
102
- uribase: @uribase).to_json
103
- "#{DJopen}#{innerj}#{DJclose}"
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
99
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
104
100
  end
105
101
 
106
- # needed for proper datetime output
107
- # TODO design/performance
108
- def casted_values
109
- # WARNING; this code is duplicated in attribute.rb
110
- # (and the inverted transformation is in test/client.rb)
111
- # will require a more systematic solution some day
112
- values_for_odata.transform_values! { |v|
113
- case v
114
- when Time
115
- # try to get back the database time zone and value
116
- (v + v.gmt_offset).utc.to_datetime
117
- else
118
- v
119
- end
120
- }
102
+ # Json formatter for a single entity reached by navigation $links
103
+ def to_odata_onelink_json(service:)
104
+ innerj = service.get_entity_odata_link_h(entity: self).to_json
105
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
106
+ end
107
+
108
+ def selected_values_for_odata(cols)
109
+ allvals = values_for_odata
110
+ selvals = {}
111
+ cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
112
+ selvals
121
113
  end
122
114
 
123
115
  # post paylod expects the new entity in an array
124
116
  def to_odata_post_json(service:)
125
117
  innerj = service.get_coll_odata_h(array: [self],
126
- uribase: @uribase).to_json
127
- "#{DJopen}#{innerj}#{DJclose}"
118
+ template: self.class.default_template).to_json
119
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
128
120
  end
129
121
 
130
122
  def type_name
@@ -133,38 +125,59 @@ module OData
133
125
 
134
126
  def copy_request_infos(req)
135
127
  @params = req.params
136
- @uribase = req.uribase
137
128
  @do_links = req.walker.do_links
129
+ @uparms = UrlParameters4Single.new(self, @params)
138
130
  end
139
131
 
140
- # Finally Process REST verbs...
141
- def odata_get(req)
142
- copy_request_infos(req)
143
-
132
+ def odata_get_output(req)
144
133
  if req.walker.media_value
145
134
  odata_media_value_get(req)
146
135
  elsif req.accept?(APPJSON)
147
- [200, CT_JSON, [to_odata_json(service: req.service)]]
136
+ # json is default content type so we dont need to specify it here again
137
+ if req.walker.do_links
138
+ [200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
139
+ else
140
+ [200, EMPTY_HASH, [to_odata_json(request: req)]]
141
+ end
148
142
  else # TODO: other formats
149
143
  415
150
144
  end
151
145
  end
152
146
 
153
- def odata_delete(req)
154
- if req.accept?(APPJSON)
155
- delete
156
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
157
- else # TODO: other formats
158
- 415
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
+
154
+ DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
155
+ Safrano.remove_nav_relation(assoc, parent)
156
+ entity.destroy(transaction: false)
157
+ end
158
+
159
+ def odata_delete_relation_and_entity(req, assoc, parent)
160
+ if parent
161
+ if req.in_changeset
162
+ # in-changeset requests get their own transaction
163
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
164
+ else
165
+ db.transaction do
166
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
167
+ end
168
+ end
169
+ else
170
+ destroy(transaction: false)
159
171
  end
172
+ rescue StandardError => e
173
+ raise SequelAdapterError.new(e)
160
174
  end
161
175
 
162
176
  # TODO: differentiate between POST/PUT/PATCH/MERGE
163
177
  def odata_post(req)
164
- data = JSON.parse(req.body.read)
165
- @uribase = req.uribase
166
-
167
- if req.accept?(APPJSON)
178
+ if req.walker.media_value
179
+ odata_media_value_put(req)
180
+ elsif req.accept?(APPJSON)
168
181
  data.delete('__metadata')
169
182
 
170
183
  if req.in_changeset
@@ -174,7 +187,7 @@ module OData
174
187
  update_fields(data, self.class.data_fields, missing: :skip)
175
188
  end
176
189
 
177
- [202, {}, to_odata_post_json(service: req.service)]
190
+ [202, EMPTY_HASH, to_odata_post_json(service: req.service)]
178
191
  else # TODO: other formats
179
192
  415
180
193
  end
@@ -183,23 +196,20 @@ module OData
183
196
  def odata_put(req)
184
197
  if req.walker.media_value
185
198
  odata_media_value_put(req)
186
- else
187
- if req.accept?(APPJSON)
188
- data = JSON.parse(req.body.read)
189
- @uribase = req.uribase
190
- data.delete('__metadata')
191
-
192
- if req.in_changeset
193
- set_fields(data, self.class.data_fields, missing: :skip)
194
- save(transaction: false)
195
- else
196
- update_fields(data, self.class.data_fields, missing: :skip)
197
- end
199
+ elsif req.accept?(APPJSON)
200
+ data = JSON.parse(req.body.read)
201
+ data.delete('__metadata')
198
202
 
199
- [204, {}, []]
200
- else # TODO: other formats
201
- 415
203
+ if req.in_changeset
204
+ set_fields(data, self.class.data_fields, missing: :skip)
205
+ save(transaction: false)
206
+ else
207
+ update_fields(data, self.class.data_fields, missing: :skip)
202
208
  end
209
+
210
+ ARY_204_EMPTY_HASH_ARY
211
+ else # TODO: other formats
212
+ 415
203
213
  end
204
214
  end
205
215
 
@@ -209,14 +219,12 @@ module OData
209
219
 
210
220
  # validate payload column names
211
221
  if (invalid = self.class.invalid_hash_data?(data))
212
- ::OData::Request::ON_CGST_ERROR.call(req)
213
- return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
222
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
223
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
214
224
  end
215
225
  # TODO: check values/types
216
226
 
217
227
  my_data_fields = self.class.data_fields
218
- @uribase = req.uribase
219
- # if req.accept?('application/json')
220
228
 
221
229
  if req.in_changeset
222
230
  set_fields(data, my_data_fields, missing: :skip)
@@ -225,68 +233,15 @@ module OData
225
233
  update_fields(data, my_data_fields, missing: :skip)
226
234
  end
227
235
  # patch should return 204 + no content
228
- [204, {}, []]
236
+ ARY_204_EMPTY_HASH_ARY
229
237
  end
230
238
  end
231
239
 
232
- # redefinitions of the main methods for a navigated collection
233
- # (eg. all Books of Author[2] is Author[2].Books.all )
234
- module NavigationRedefinitions
235
- def all
236
- @child_method.call
237
- end
238
-
239
- def count
240
- @child_method.call.count
241
- end
242
-
243
- def dataset
244
- @child_dataset_method.call
245
- end
246
-
247
- def navigated_dataset
248
- @child_dataset_method.call
249
- end
250
-
251
- def each
252
- y = @child_method.call
253
- y.each { |enty| yield enty }
254
- end
255
-
256
- def type_name
257
- superclass.type_name
258
- end
259
-
260
- def media_handler
261
- superclass.media_handler
262
- end
263
-
264
- def to_a
265
- y = @child_method.call
266
- y.to_a
267
- end
268
- end
269
- # GetRelated that returns a anonymous Class (ie. representing a collection)
270
- # subtype of the related object Class ( childklass )
240
+ # GetRelated that returns a collection object representing
241
+ # wrapping the related object Class ( childklass )
271
242
  # (...to_many relationship )
272
243
  def get_related(childattrib)
273
- parent = self
274
- childklass = self.class.nav_collection_attribs[childattrib]
275
- Class.new(childklass) do
276
- # this makes use of Sequel's Model relationships; eg this is
277
- # 'Race[12].Edition'
278
- # where Race[12] would be our self and 'Edition' is the
279
- # childattrib(collection)
280
- @child_method = parent.method(childattrib.to_sym)
281
- @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
282
- @nav_parent = parent
283
- @navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
284
- prepare_pk
285
- prepare_fields
286
- # Now in this anonymous Class we can refine the "all, count and []
287
- # methods, to take into account the relationship
288
- extend NavigationRedefinitions
289
- end
244
+ Safrano::OData::NavigatedCollection.new(childattrib, self)
290
245
  end
291
246
 
292
247
  # GetRelatedEntity that returns an single related Entity
@@ -301,69 +256,125 @@ module OData
301
256
  # then we return a Nil... wrapper object. This object then
302
257
  # allows to receive a POST operation that would actually create the nav attribute entity
303
258
 
304
- ret = method(childattrib.to_sym).call ||
305
- OData::NilNavigationAttribute.new(self, childattrib)
259
+ ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
260
+
261
+ ret.set_relation_info(self, childattrib)
306
262
 
307
263
  ret
308
264
  end
309
265
  end
310
- # end of module ODataEntity
266
+ # end of module SafranoEntity
311
267
  module Entity
312
268
  include EntityBase
313
269
  end
314
270
 
315
271
  module NonMediaEntity
316
272
  # non media entity metadata for json h
317
- def metadata_h(uribase:)
318
- { uri: uri(uribase),
273
+ def metadata_h
274
+ { uri: uri,
319
275
  type: type_name }
320
276
  end
321
277
 
322
278
  def values_for_odata
323
- values.dup
279
+ values
280
+ end
281
+
282
+ def odata_delete(req)
283
+ if req.accept?(APPJSON)
284
+ # delete
285
+ begin
286
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
287
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
288
+ rescue SequelAdapterError => e
289
+ BadRequestSequelAdapterError.new(e).odata_get(req)
290
+ end
291
+ else # TODO: other formats
292
+ 415
293
+ end
324
294
  end
325
295
 
326
296
  # in case of a non media entity, we have to return an error on $value request
327
- def odata_media_value_get(req)
328
- return BadRequestNonMediaValue.odata_get
297
+ def odata_media_value_get(_req)
298
+ BadRequestNonMediaValue.odata_get
329
299
  end
330
300
 
331
301
  # in case of a non media entity, we have to return an error on $value PUT
332
- def odata_media_value_put(req)
333
- return BadRequestNonMediaValue.odata_get
302
+ def odata_media_value_put(_req)
303
+ BadRequestNonMediaValue.odata_get
304
+ end
305
+ end
306
+
307
+ module MappingBeforeOutput
308
+ # needed for proper datetime output
309
+ def casted_values(cols = nil)
310
+ vals = case cols
311
+ when nil
312
+ # we need to dup the model values as we need to change it before passing to_json,
313
+ # but we dont want to interfere with Sequel's owned data
314
+ # (eg because then in worst case it could happen that we write back changed values to DB)
315
+ values_for_odata.dup
316
+ else
317
+ selected_values_for_odata(cols)
318
+ end
319
+ self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
320
+ vals
321
+ end
322
+ end
323
+ module NoMappingBeforeOutput
324
+ # current model does not have eg. Time fields--> no special mapping, just to_json is fine
325
+ # --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
326
+ # need to change it, just output as is
327
+ def casted_values(cols = nil)
328
+ case cols
329
+ when nil
330
+ values_for_odata
331
+ else
332
+ selected_values_for_odata(cols)
333
+ end
334
334
  end
335
335
  end
336
336
 
337
337
  module MediaEntity
338
338
  # media entity metadata for json h
339
- def metadata_h(uribase:)
340
- { uri: uri(uribase),
339
+ def metadata_h
340
+ { uri: uri,
341
341
  type: type_name,
342
- media_src: media_src(uribase),
343
- edit_media: media_src(uribase),
342
+ media_src: media_src,
343
+ edit_media: edit_media,
344
344
  content_type: @values[:content_type] }
345
345
  end
346
346
 
347
- def media_src(urbase)
348
- "#{uri(urbase)}/$value"
347
+ def media_src
348
+ version = self.class.media_handler.ressource_version(self)
349
+ "#{uri}/$value?version=#{version}"
349
350
  end
350
351
 
351
- # directory where to put/find the media files for this entity-type
352
- def klass_dir
353
- type_name
352
+ def edit_media
353
+ "#{uri}/$value"
354
354
  end
355
355
 
356
- # # this is just ModelKlass/pk as a single string
357
- # def qualified_media_path_id
358
- # "#{self.class}/#{media_path_id}"
359
- # end
360
-
361
356
  def values_for_odata
362
357
  ret = values.dup
363
358
  ret.delete(:content_type)
364
359
  ret
365
360
  end
366
361
 
362
+ def odata_delete(req)
363
+ if req.accept?(APPJSON)
364
+ # delete the MR
365
+ # delegate to the media handler on collection(ie class) level
366
+ # TODO error handling
367
+
368
+ self.class.media_handler.odata_delete(entity: self)
369
+ # delete the relation(s) to parent(s) (if any) and then entity
370
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
371
+ # result
372
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
373
+ else # TODO: other formats
374
+ 415
375
+ end
376
+ end
377
+
367
378
  # real implementation for returning $value for a media entity
368
379
  def odata_media_value_get(req)
369
380
  # delegate to the media handler on collection(ie class) level
@@ -374,17 +385,20 @@ module OData
374
385
  def odata_media_value_put(req)
375
386
  model = self.class
376
387
  req.with_media_data do |data, mimetype, filename|
377
- emdata = { :content_type => mimetype }
388
+ emdata = { content_type: mimetype }
378
389
  if req.in_changeset
379
390
  set_fields(emdata, model.data_fields, missing: :skip)
380
391
  save(transaction: false)
381
392
  else
393
+
382
394
  update_fields(emdata, model.data_fields, missing: :skip)
395
+
383
396
  end
384
397
  model.media_handler.replace_file(data: data,
385
398
  entity: self,
386
399
  filename: filename)
387
- [204, {}, []]
400
+
401
+ ARY_204_EMPTY_HASH_ARY
388
402
  end
389
403
  end
390
404
  end
@@ -399,18 +413,29 @@ module OData
399
413
  def media_path_id
400
414
  pk.to_s
401
415
  end
416
+
417
+ def media_path_ids
418
+ [pk]
419
+ end
402
420
  end
403
421
 
404
422
  # for multiple key
405
423
  module EntityMultiPK
406
424
  include Entity
407
425
  def pk_uri
408
- # pk_hash is provided by Sequel
409
- self.pk_hash.map { |k, v| "#{k}='#{v}'" }.join(',')
426
+ pku = +''
427
+ self.class.odata_upk_parts.each_with_index { |upart, i|
428
+ pku = "#{pku}#{upart}#{pk[i]}"
429
+ }
430
+ pku
410
431
  end
411
432
 
412
433
  def media_path_id
413
- self.pk_hash.values.join('_')
434
+ pk_hash.values.join(SPACE)
435
+ end
436
+
437
+ def media_path_ids
438
+ pk_hash.values
414
439
  end
415
440
  end
416
441
  end