safrano 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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