safrano 0.4.2 → 0.5.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 (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 +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  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 +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  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 +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  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 +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -1,72 +1,47 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
- require_relative '../odata/walker.rb'
5
- require_relative 'request.rb'
6
- require_relative 'response.rb'
4
+ require_relative '../odata/walker'
5
+ require_relative 'request'
6
+ require_relative 'response'
7
7
 
8
- module OData
8
+ module Safrano
9
9
  # handle GET PUT etc
10
10
  module MethodHandlers
11
11
  def odata_options
12
- # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
13
-
14
- x = if @walker.status == :end
15
- headers.delete('Content-Type')
16
- @response.headers.delete('Content-Type')
17
- [200, EMPTY_HASH, '']
18
- else
19
- odata_error
20
- end
21
- @response.headers['Content-Type'] = ''
22
- x
23
- end
24
-
25
- def odata_error
26
- return @walker.error.odata_get(@request) unless @walker.error.nil?
27
-
28
- # this is too critical; raise a real Exception
29
- raise 'Walker construction failed with a unknown Error '
12
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
13
+ .if_valid do |context|
14
+ # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
15
+ headers.delete('Content-Type')
16
+ @response.headers.delete('Content-Type')
17
+ @response.headers['Content-Type'] = ''
18
+ [200, EMPTY_HASH, '']
19
+ end
30
20
  end
31
21
 
32
22
  def odata_delete
33
- if @walker.status == :end
34
- @walker.end_context.odata_delete(@request)
35
- else
36
- odata_error
37
- end
23
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
24
+ .if_valid { |context| context.odata_delete(@request) }
38
25
  end
39
26
 
40
27
  def odata_put
41
- if @walker.status == :end
42
- @walker.end_context.odata_put(@request)
43
- else
44
- odata_error
45
- end
28
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
29
+ .if_valid { |context| context.odata_put(@request) }
46
30
  end
47
31
 
48
32
  def odata_patch
49
- if @walker.status == :end
50
- @walker.end_context.odata_patch(@request)
51
- else
52
- odata_error
53
- end
33
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
34
+ .if_valid { |context| context.odata_patch(@request) }
54
35
  end
55
36
 
56
37
  def odata_get
57
- if @walker.status == :end
58
- @walker.end_context.odata_get(@request)
59
- else
60
- odata_error
61
- end
38
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
39
+ .if_valid { |context| context.odata_get(@request) }
62
40
  end
63
41
 
64
42
  def odata_post
65
- if @walker.status == :end
66
- @walker.end_context.odata_post(@request)
67
- else
68
- odata_error
69
- end
43
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
44
+ .if_valid { |context| context.odata_post(@request) }
70
45
  end
71
46
 
72
47
  def odata_head
@@ -77,22 +52,21 @@ module OData
77
52
  # the main Rack server app. Source: the Rack docu/examples and partly
78
53
  # inspired from Sinatra
79
54
  class ServerApp
80
- METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE')
55
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
56
+ NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
57
+ 'Expires' => '-1',
58
+ 'Pragma' => 'no-cache' }.freeze
59
+ DATASERVICEVERSION = 'DataServiceVersion'
81
60
  include MethodHandlers
82
- def before
83
- headers 'Cache-Control' => 'no-cache'
84
- headers 'Expires' => '-1'
85
- headers 'Pragma' => 'no-cache'
86
61
 
62
+ def before
87
63
  @request.service_base = self.class.get_service_base
88
64
 
89
- neg_error = @request.negotiate_service_version
90
-
91
- raise RuntimeError if neg_error
92
-
93
- return false unless @request.service
94
-
95
- headers 'DataServiceVersion' => @request.service.data_service_version
65
+ @request.negotiate_service_version.tap_valid do
66
+ myhdrs = NOCACHE_HDRS.dup
67
+ myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
68
+ headers myhdrs
69
+ end
96
70
  end
97
71
 
98
72
  # dispatch for all methods requiring parsing of the path
@@ -117,6 +91,11 @@ module OData
117
91
  end
118
92
  end
119
93
 
94
+ def dispatch_error(err)
95
+ @response.status, rsph, @response.body = err.odata_get(@request)
96
+ headers rsph
97
+ end
98
+
120
99
  def dispatch
121
100
  req_ret = if @request.request_method !~ METHODS_REGEXP
122
101
  [404, EMPTY_HASH, ['Did you get lost?']]
@@ -130,13 +109,23 @@ module OData
130
109
  end
131
110
 
132
111
  def call(env)
133
- @request = OData::Request.new(env)
134
- @response = OData::Response.new
112
+ # for thread safety
113
+ dup._call(env)
114
+ end
135
115
 
136
- before
116
+ def _call(env)
117
+ begin
118
+ @request = Safrano::Request.new(env)
119
+ @response = Safrano::Response.new
137
120
 
138
- dispatch
121
+ before.tap_error { |err| dispatch_error(err) }
122
+ .tap_valid { |res| dispatch }
139
123
 
124
+ # handle remaining Sequel errors that we couldnt prevent with our
125
+ # own pre-checks
126
+ rescue Sequel::Error => e
127
+ dispatch_error(SequelExceptionError.new(e))
128
+ end
140
129
  @response.finish
141
130
  end
142
131
 
@@ -165,10 +154,22 @@ module OData
165
154
  end
166
155
 
167
156
  def self.publish_service(&block)
168
- sbase = OData::ServiceBase.new
157
+ sbase = Safrano::ServiceBase.new
169
158
  sbase.instance_eval(&block) if block_given?
170
159
  sbase.finalize_publishing
171
160
  set_servicebase(sbase)
172
161
  end
173
162
  end
174
163
  end
164
+
165
+ # deprecated
166
+ # REMOVE 0.6
167
+ module OData
168
+ class ServerApp < Safrano::ServerApp
169
+ def self.publish_service(&block)
170
+ ::Safrano::Deprecation.deprecate('OData::ServerApp',
171
+ 'Use Safrano::ServerApp instead')
172
+ super
173
+ end
174
+ end
175
+ end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'rack/cors'
3
5
 
4
6
  module Rack
5
- module OData
7
+ module Safrano
6
8
  # just a Wrapper to ensure (force?) that mandatory middlewares are acutally
7
9
  # used
8
10
  class Builder < ::Rack::Builder
@@ -10,9 +12,23 @@ module Rack
10
12
  super(default_app) {}
11
13
  use ::Rack::Cors
12
14
  instance_eval(&block) if block_given?
13
- use ::Rack::Lint
14
15
  use ::Rack::ContentLength
15
16
  end
16
17
  end
17
18
  end
18
19
  end
20
+
21
+ # deprecated
22
+ # REMOVE 0.6
23
+ module Rack
24
+ module OData
25
+ class Builder < ::Rack::Safrano::Builder
26
+ def initialize(default_app = nil, &block)
27
+ ::Safrano::Deprecation.deprecate('Rack::OData::Builder',
28
+ 'Use Rack::Safrano::Builder instead')
29
+
30
+ super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'rfc2047'
3
5
 
4
- module OData
6
+ module Safrano
5
7
  # monkey patch deactivate Rack/multipart because it does not work on simple
6
8
  # OData $batch requests when the content-length
7
9
  # is not passed
8
10
  class Request < Rack::Request
9
11
  HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
10
- HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
12
+ HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'
11
13
  HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
12
14
  ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
13
15
 
@@ -15,6 +17,7 @@ module OData
15
17
  class AcceptEntry
16
18
  attr_accessor :params
17
19
  attr_reader :entry
20
+
18
21
  def initialize(entry)
19
22
  params = entry.scan(HEADER_PARAM).map! do |s|
20
23
  key, value = s.strip.split('=', 2)
@@ -94,7 +97,9 @@ module OData
94
97
  end
95
98
 
96
99
  def create_odata_walker
97
- @env['safrano.walker'] = @walker = Walker.new(@service, path_info, @content_id_references)
100
+ @env['safrano.walker'] = @walker = Walker.new(@service,
101
+ path_info,
102
+ @content_id_references)
98
103
  end
99
104
 
100
105
  def accept
@@ -108,13 +113,6 @@ module OData
108
113
  end
109
114
  end
110
115
 
111
- def uribase
112
- return @uribase if @uribase
113
-
114
- @uribase =
115
- "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
116
- end
117
-
118
116
  def accept?(type)
119
117
  preferred_type(type).to_s.include?(type)
120
118
  end
@@ -164,44 +162,97 @@ module OData
164
162
  end
165
163
  end
166
164
 
167
- def negotiate_service_version
168
- maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
169
- OData::ServiceBase.parse_data_service_version(rqv)
170
- else
171
- OData::MAX_DATASERVICE_VERSION
172
- end
173
- return OData::BadRequestError if maxv.nil?
174
- # client request an too old version --> 501
175
- return OData::NotImplementedError if maxv < OData::MIN_DATASERVICE_VERSION
176
-
177
- minv = if (rqv = env['HTTP_MINDATASERVICEVERSION'])
178
- OData::ServiceBase.parse_data_service_version(rqv)
179
- else
180
- OData::MIN_DATASERVICE_VERSION
181
- end
182
- return OData::BadRequestError if minv.nil?
183
- # client request an too new version --> 501
184
- return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
185
- return OData::BadRequestError if minv > maxv
186
-
187
- v = if (rqv = env['HTTP_DATASERVICEVERSION'])
188
- OData::ServiceBase.parse_data_service_version(rqv)
165
+ # input is the DataServiceVersion request header string, eg.
166
+ # '2.0;blabla' ---> Version -> 2
167
+ DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
168
+
169
+ MAX_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
170
+ 'MaxDataServiceVersion could not be parsed'
171
+ ).freeze
172
+ def get_maxversion
173
+ if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
174
+ if (m = DATASERVICEVERSION_RGX.match(rqv))
175
+ # client request an too old version --> 501
176
+ if (maxv = m[1]) < Safrano::MIN_DATASERVICE_VERSION
177
+ Safrano::VersionNotImplementedError
178
+ else
179
+ Contract.valid(maxv)
180
+ end
181
+ else
182
+ MAX_DTSV_PARSE_ERROR
183
+ end
184
+ else
185
+ # not provided in request header --> take ours
186
+ Safrano::CV_MAX_DATASERVICE_VERSION
187
+ end
188
+ end
189
+
190
+ DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
191
+ 'DataServiceVersion could not be parsed'
192
+ ).freeze
193
+ def get_version
194
+ if (rqv = env['HTTP_DATASERVICEVERSION'])
195
+ if (m = DATASERVICEVERSION_RGX.match(rqv))
196
+ # client request an too new version --> 501
197
+ if ((v = m[1]) > Safrano::MAX_DATASERVICE_VERSION) ||
198
+ (v < Safrano::MIN_DATASERVICE_VERSION)
199
+ Safrano::VersionNotImplementedError
189
200
  else
190
- OData::MAX_DATASERVICE_VERSION
201
+ Contract.valid(v)
191
202
  end
203
+ else
204
+ DTSV_PARSE_ERROR
205
+ end
206
+ else
207
+ # not provided in request header --> take our maxv
208
+ Safrano::CV_MAX_DATASERVICE_VERSION
209
+ end
210
+ end
211
+
212
+ MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
213
+ 'MinDataServiceVersion could not be parsed'
214
+ ).freeze
215
+ def get_minversion
216
+ if (rqv = env['HTTP_MINDATASERVICEVERSION'])
217
+ if (m = DATASERVICEVERSION_RGX.match(rqv))
218
+ # client request an too new version --> 501
219
+ if (minv = m[1]) > Safrano::MAX_DATASERVICE_VERSION
220
+ Safrano::VersionNotImplementedError
221
+ else
222
+ Contract.valid(minv)
223
+ end
224
+ else
225
+ MIN_DTSV_PARSE_ERROR
226
+ end
227
+ else
228
+ # not provided in request header --> take ours
229
+ Safrano::CV_MIN_DATASERVICE_VERSION
230
+ end
231
+ end
192
232
 
193
- return OData::BadRequestError if v.nil?
194
- return OData::NotImplementedError if v > OData::MAX_DATASERVICE_VERSION
195
- return OData::NotImplementedError if v < OData::MIN_DATASERVICE_VERSION
196
-
197
- @service = nil
198
- @service = case maxv
199
- when '1'
200
- @service_base.v1
201
- when '2', '3', '4'
202
- @service_base.v2
203
- end
204
- nil
233
+ MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
234
+ 'MinDataServiceVersion is larger as MaxDataServiceVersion'
235
+ ).freeze
236
+ def negotiate_service_version
237
+ get_maxversion.if_valid do |maxv|
238
+ get_minversion.if_valid do |minv|
239
+ return MAX_LT_MIN_DTSV_ERROR if minv > maxv
240
+
241
+ get_version.if_valid do |v|
242
+ @service = nil
243
+ @service = case maxv
244
+ when '1'
245
+ @service_base.v1
246
+ when '2', '3', '4'
247
+ @service_base.v2
248
+ else
249
+ return Safrano::VersionNotImplementedError
250
+ end
251
+
252
+ Contract::OK
253
+ end # valid get_version
254
+ end # valid get_minversion
255
+ end # valid get_maxversion
205
256
  end
206
257
  end
207
258
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
 
3
5
  # monkey patch deactivate Rack/multipart because it does not work on simple
4
6
  # OData $batch requests when the content-length is not passed
5
- module OData
7
+ module Safrano
6
8
  # borrowed fro Sinatra
7
9
  # The response object. See Rack::Response and Rack::Response::Helpers for
8
10
  # more info:
@@ -12,7 +14,7 @@ module OData
12
14
  DROP_BODY_RESPONSES = [204, 205, 304].freeze
13
15
  def initialize(*)
14
16
  super
15
- headers['Content-Type'] ||= 'text/html'
17
+ headers[CONTENT_TYPE] ||= APPJSON_UTF8
16
18
  end
17
19
 
18
20
  def body=(value)
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
- require_relative '../sequel/plugins/join_by_paths.rb'
3
+ require_relative '../sequel/plugins/join_by_paths'
4
4
 
5
5
  Sequel::Model.plugin Sequel::Plugins::JoinByPaths
@@ -1,97 +1,83 @@
1
- require 'rexml/document'
2
- require 'odata/relations.rb'
3
- require 'odata/batch.rb'
4
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
5
2
 
6
- module OData
3
+ require 'rexml/document'
4
+ require 'odata/relations'
5
+ require 'odata/batch'
6
+ require 'odata/complex_type'
7
+ require 'odata/function_import'
8
+ require 'odata/error'
9
+ require 'odata/filter/sequel'
10
+ require 'set'
11
+ require 'odata/collection'
12
+
13
+ module Safrano
7
14
  # this module has all methods related to expand/defered output preparation
8
15
  # and will be included in Service class
9
16
  module ExpandHandler
10
- PATH_SPLITTER = %r{\A(\w+)\/?(.*)\z}.freeze
11
- def get_deferred_odata_h(entity:, attrib:, uribase:)
12
- { '__deferred' => { 'uri' => "#{entity.uri(uribase)}/#{attrib}" } }
13
- end
14
-
15
- # split expand argument into first and rest part
16
- def split_entity_expand_arg(exp_one)
17
- if exp_one.include?('/')
18
- m = PATH_SPLITTER.match(exp_one)
19
- cur_exp = m[1].strip
20
- rest_exp = m[2]
21
- # TODO: check errorhandling
22
- raise OData::ServerError if cur_exp.nil?
17
+ PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
18
+ DEFERRED = '__deferred'
19
+ URI = 'uri'
23
20
 
24
- k_s = cur_exp
25
-
26
- else
27
- k_s = exp_one.strip
28
- rest_exp = nil
29
- end
30
- k = k_s.to_sym
31
- yield k, k_s, rest_exp
21
+ def get_deferred_odata_h(entity_uri:, attrib:)
22
+ { DEFERRED => { URI => "#{entity_uri}/#{attrib}" } }
32
23
  end
33
24
 
34
25
  # default v2
35
26
  # overriden in ServiceV1
36
- def get_coll_odata_h(array:, template:, uribase:, icount: nil)
37
- res = array.map do |w|
38
- get_entity_odata_h(entity: w,
39
- template: template,
40
- uribase: uribase)
41
- end
42
- if icount
43
- { 'results' => res, '__count' => icount }
44
- else
45
- { 'results' => res }
27
+ RESULTS_K = 'results'
28
+ COUNT_K = '__count'
29
+ def get_coll_odata_h(array:, template:, icount: nil)
30
+ array.map! do |w|
31
+ get_entity_odata_h(entity: w, template: template)
46
32
  end
33
+ icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
47
34
  end
48
35
 
49
36
  # handle $links ... Note: $expand seems to be ignored when $links
50
37
  # are requested
51
- def get_entity_odata_link_h(entity:, uribase:)
52
- { uri: entity.uri(uribase) }
38
+ def get_entity_odata_link_h(entity:)
39
+ { uri: entity.uri }
53
40
  end
54
41
 
55
42
  EMPTYH = {}.freeze
56
- def get_entity_odata_h(entity:, template:, uribase:)
43
+ METADATA_K = '__metadata'
44
+ def get_entity_odata_h(entity:, template:)
57
45
  # start with metadata
58
- hres = { '__metadata' => entity.metadata_h(uribase: uribase) }
46
+ hres = { METADATA_K => entity.metadata_h }
59
47
 
60
48
  template.each do |elmt, arg|
61
49
  case elmt
62
50
  when :all_values
63
51
  hres.merge! entity.casted_values
52
+
64
53
  when :selected_vals
65
54
  hres.merge! entity.casted_values(arg)
55
+
66
56
  when :expand_e
67
57
 
68
58
  arg.each do |attr, templ|
69
59
  enval = entity.send(attr)
70
60
  hres[attr] = if enval
71
-
72
- get_entity_odata_h(entity: enval,
73
- template: templ,
74
- uribase: uribase)
61
+ get_entity_odata_h(entity: enval, template: templ)
75
62
  else
76
63
  # FK is NULL --> nav_value is nil --> return empty json
77
64
  EMPTYH
78
65
  end
79
66
  end
67
+
80
68
  when :expand_c
81
69
  arg.each do |attr, templ|
82
70
  next unless (encoll = entity.send(attr))
83
71
 
84
72
  # nav attributes that are a collection (x..n)
85
- hres[attr] = get_coll_odata_h(array: encoll,
86
- template: templ,
87
- uribase: uribase)
73
+ hres[attr] = get_coll_odata_h(array: encoll, template: templ)
88
74
  # else error ?
89
75
  end
76
+
90
77
  when :deferr
78
+ euri = entity.uri
91
79
  arg.each do |attr|
92
- hres[attr] = get_deferred_odata_h(entity: entity,
93
- attrib: attr,
94
- uribase: uribase)
80
+ hres[attr] = get_deferred_odata_h(entity_uri: euri, attrib: attr)
95
81
  end
96
82
  end
97
83
  end
@@ -100,33 +86,33 @@ module OData
100
86
  end
101
87
  end
102
88
 
103
- module OData
89
+ module Safrano
104
90
  # xml namespace constants needed for the output of XML service and
105
91
  # and metadata
106
92
  module XMLNS
107
- MSFT_ADO = 'http://schemas.microsoft.com/ado'.freeze
108
- MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm".freeze
109
- MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx".freeze
110
- MSFT_ADO_2007_META = MSFT_ADO + \
111
- '/2007/08/dataservices/metadata'.freeze
112
-
113
- W3_2005_ATOM = 'http://www.w3.org/2005/Atom'.freeze
114
- W3_2007_APP = 'http://www.w3.org/2007/app'.freeze
93
+ MSFT_ADO = 'http://schemas.microsoft.com/ado'
94
+ MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm"
95
+ MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx"
96
+ MSFT_ADO_2007_META = "#{MSFT_ADO}/2007/08/dataservices/metadata"
97
+
98
+ W3_2005_ATOM = 'http://www.w3.org/2005/Atom'
99
+ W3_2007_APP = 'http://www.w3.org/2007/app'
115
100
  end
116
101
  end
117
102
 
118
103
  # Link to Model
119
- module OData
120
- MAX_DATASERVICE_VERSION = '2'.freeze
121
- MIN_DATASERVICE_VERSION = '1'.freeze
104
+ module Safrano
105
+ MAX_DATASERVICE_VERSION = '2'
106
+ MIN_DATASERVICE_VERSION = '1'
107
+ CV_MAX_DATASERVICE_VERSION = Contract.valid(MAX_DATASERVICE_VERSION).freeze
108
+ CV_MIN_DATASERVICE_VERSION = Contract.valid(MIN_DATASERVICE_VERSION).freeze
122
109
  include XMLNS
123
110
  # Base class for service. Subclass will be for V1, V2 etc...
124
111
  class ServiceBase
125
112
  include Safrano
126
113
  include ExpandHandler
127
114
 
128
- XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
129
- "\r\n".freeze
115
+ XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
130
116
 
131
117
  # This is just a hash of entity Set Names to the corresponding Class
132
118
  # Example
@@ -144,10 +130,14 @@ module OData
144
130
  attr_accessor :xname
145
131
  attr_accessor :xnamespace
146
132
  attr_accessor :xpath_prefix
147
- # attr_accessor :xuribase
133
+ attr_accessor :xserver_url
134
+ attr_accessor :uribase
148
135
  attr_accessor :meta
149
136
  attr_accessor :batch_handler
150
137
  attr_accessor :relman
138
+ attr_accessor :complex_types
139
+ attr_accessor :function_imports
140
+
151
141
  # Instance attributes for specialized Version specific Instances
152
142
  attr_accessor :v1
153
143
  attr_accessor :v2
@@ -161,36 +151,31 @@ module OData
161
151
  # because of the version subclasses that dont use "super" initialise
162
152
  # (todo: why not??)
163
153
  @meta = ServiceMeta.new(self)
164
- @batch_handler = OData::Batch::DisabledHandler.new
165
- @relman = OData::RelationManager.new
154
+ @batch_handler = Safrano::Batch::DisabledHandler.new
155
+ @relman = Safrano::RelationManager.new
156
+ @complex_types = Set.new
157
+ @function_imports = {}
166
158
  @cmap = {}
167
159
  instance_eval(&block) if block_given?
168
160
  end
169
161
 
170
- DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
171
162
  TRAILING_SLASH = %r{/\z}.freeze
172
- DEFAULT_PATH_PREFIX = '/'.freeze
173
-
174
- # input is the DataServiceVersion request header string, eg.
175
- # '2.0;blabla' ---> Version -> 2
176
- def self.parse_data_service_version(inp)
177
- m = DATASERVICEVERSION_RGX.match(inp)
178
- m[1] if m
179
- end
163
+ DEFAULT_PATH_PREFIX = '/'
164
+ DEFAULT_SERVER_URL = 'http://localhost:9494'
180
165
 
181
166
  def enable_batch
182
- @batch_handler = OData::Batch::EnabledHandler.new
167
+ @batch_handler = Safrano::Batch::EnabledHandler.new
183
168
  (@v1.batch_handler = @batch_handler) if @v1
184
169
  (@v2.batch_handler = @batch_handler) if @v2
185
170
  end
186
171
 
187
172
  def enable_v1_service
188
- @v1 = OData::ServiceV1.new
173
+ @v1 = Safrano::ServiceV1.new
189
174
  copy_attribs_to @v1
190
175
  end
191
176
 
192
177
  def enable_v2_service
193
- @v2 = OData::ServiceV2.new
178
+ @v2 = Safrano::ServiceV2.new
194
179
  copy_attribs_to @v2
195
180
  end
196
181
 
@@ -212,56 +197,97 @@ module OData
212
197
  (@v1.xpath_prefix = @xpath_prefix) if @v1
213
198
  (@v2.xpath_prefix = @xpath_prefix) if @v2
214
199
  end
200
+
201
+ def server_url(surl)
202
+ @xserver_url = surl.sub(TRAILING_SLASH, '')
203
+ (@v1.xserver_url = @xserver_url) if @v1
204
+ (@v2.xserver_url = @xserver_url) if @v2
205
+ end
206
+
207
+ # keep the bug active for now, but allow to activate the fix,
208
+ # later we will change the default to be fixed
209
+ def bugfix_create_response(bool = false)
210
+ @bugfix_create_response = bool
211
+ end
212
+
215
213
  # end public API
216
214
 
215
+ def set_uribase
216
+ @uribase = if @xpath_prefix.empty?
217
+ @xserver_url
218
+ elsif @xpath_prefix[0] == '/'
219
+ "#{@xserver_url}#{@xpath_prefix}"
220
+ else
221
+ "#{@xserver_url}/#{@xpath_prefix}"
222
+ end
223
+ (@v1.uribase = @uribase) if @v1
224
+ (@v2.uribase = @uribase) if @v2
225
+ end
226
+
217
227
  def copy_attribs_to(other)
218
228
  other.cmap = @cmap
219
229
  other.collections = @collections
230
+ other.allowed_transitions = @allowed_transitions
220
231
  other.xtitle = @xtitle
221
232
  other.xname = @xname
222
233
  other.xnamespace = @xnamespace
223
234
  other.xpath_prefix = @xpath_prefix
235
+ other.xserver_url = @xserver_url
236
+ other.uribase = @uribase
224
237
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
225
238
  other.relman = @relman
226
239
  other.batch_handler = @batch_handler
240
+ other.complex_types = @complex_types
241
+ other.function_imports = @function_imports
227
242
  other
228
243
  end
229
244
 
245
+ # this is a central place. We extend Sequel models with OData functionality
246
+ # The included/extended modules depends on the properties(eg, pks, field types) of the model
247
+ # we differentiate
248
+ # * Single/Multi PK
249
+ # * Media/Non-Media entity
250
+ # Putting this logic here in modules loaded once on start shall result in less runtime overhead
230
251
  def register_model(modelklass, entity_set_name = nil, is_media = false)
231
252
  # check that the provided klass is a Sequel Model
232
253
 
233
- raise(OData::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
254
+ raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
234
255
 
235
- if modelklass.ancestors.include? OData::Entity
256
+ if modelklass.ancestors.include? Safrano::Entity
236
257
  # modules were already added previously;
237
258
  # cleanup state to avoid having data from previous calls
238
259
  # mostly usefull for testing (eg API)
239
260
  modelklass.reset
240
261
  elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
241
- modelklass.extend OData::EntityClassMultiPK
242
- modelklass.include OData::EntityMultiPK
262
+ modelklass.extend Safrano::EntityClassMultiPK
263
+ modelklass.include Safrano::EntityMultiPK
243
264
  else
244
- modelklass.extend OData::EntityClassSinglePK
245
- modelklass.include OData::EntitySinglePK
265
+ modelklass.extend Safrano::EntityClassSinglePK
266
+ modelklass.include Safrano::EntitySinglePK
246
267
  end
268
+
247
269
  # Media/Non-media
248
270
  if is_media
249
- modelklass.extend OData::EntityClassMedia
271
+ modelklass.extend Safrano::EntityClassMedia
250
272
  # set default media handler . Can be overridden later with the
251
273
  # "use HandlerKlass, options" API
252
274
 
253
275
  modelklass.set_default_media_handler
254
276
  modelklass.api_check_media_fields
255
- modelklass.include OData::MediaEntity
277
+ modelklass.include Safrano::MediaEntity
256
278
  else
257
- modelklass.extend OData::EntityClassNonMedia
258
- modelklass.include OData::NonMediaEntity
279
+ modelklass.extend Safrano::EntityClassNonMedia
280
+ modelklass.include Safrano::NonMediaEntity
259
281
  end
260
282
 
261
283
  modelklass.prepare_pk
262
284
  modelklass.prepare_fields
263
285
  esname = (entity_set_name || modelklass).to_s.freeze
264
- modelklass.instance_eval { @entity_set_name = esname }
286
+ serv_namespace = @xnamespace
287
+ modelklass.instance_eval do
288
+ @entity_set_name = esname
289
+ @namespace = serv_namespace
290
+ end
265
291
  @cmap[esname] = modelklass
266
292
  set_collections_sorted(@cmap.values)
267
293
  end
@@ -282,6 +308,23 @@ module OData
282
308
  modelklass.deferred_iblock = block if block_given?
283
309
  end
284
310
 
311
+ def publish_complex_type(ctklass)
312
+ # check that the provided klass is a Safrano ComplexType
313
+
314
+ raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType
315
+
316
+ serv_namespace = @xnamespace
317
+ ctklass.instance_eval { @namespace = serv_namespace }
318
+
319
+ @complex_types.add ctklass
320
+ end
321
+
322
+ def function_import(name)
323
+ funcimp = Safrano::FunctionImport(name)
324
+ @function_imports[name] = funcimp
325
+ funcimp
326
+ end
327
+
285
328
  def cmap=(imap)
286
329
  @cmap = imap
287
330
  set_collections_sorted(@cmap.values)
@@ -313,10 +356,35 @@ module OData
313
356
  # set default path prefix if path_prefix was not called
314
357
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
315
358
 
359
+ # set default server url if server_url was not called
360
+ server_url(DEFAULT_SERVER_URL) unless @xserver_url
361
+
362
+ set_uribase
363
+
316
364
  @collections.each(&:finalize_publishing)
317
365
 
318
- # finalize the media handlers
319
- @collections.each { |klass| }
366
+ # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
367
+ @collections.each do |klass|
368
+ klass.build_uri(@uribase)
369
+ klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
370
+
371
+ # Output create (POST) as single entity (Standard) or as array (non-standard buggy)
372
+ klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)
373
+ end
374
+
375
+ # build allowed transitions (requires that @collections are filled and sorted for having a
376
+ # correct base_url_regexp)
377
+ build_allowed_transitions
378
+
379
+ # mixin adapter specific modules where needed
380
+ case Sequel::Model.db.adapter_scheme
381
+ when :postgres
382
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
383
+ when :sqlite
384
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
385
+ else
386
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
387
+ end
320
388
  end
321
389
 
322
390
  def execute_deferred_iblocks
@@ -332,19 +400,24 @@ module OData
332
400
  @collections.map(&:entity_set_name).join('|')
333
401
  end
334
402
 
403
+ def base_url_func_regexp
404
+ @function_imports.keys.join('|')
405
+ end
406
+
335
407
  def service
336
408
  hres = {}
337
409
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
338
410
  hres
339
411
  end
340
412
 
341
- def service_xml(req)
413
+ def service_xml(_req)
342
414
  doc = REXML::Document.new
343
415
  # separator required ? ?
344
- root = doc.add_element('service', 'xml:base' => req.uribase)
416
+ root = doc.add_element('service', 'xml:base' => @uribase)
345
417
 
346
418
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
347
419
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
420
+
348
421
  # this generates the main xmlns attribute
349
422
  root.add_namespace(XMLNS::W3_2007_APP)
350
423
  wp = root.add_element 'workspace'
@@ -355,7 +428,7 @@ module OData
355
428
  @collections.each do |klass|
356
429
  col = wp.add_element('collection', 'href' => klass.entity_set_name)
357
430
  ct = col.add_element('atom:title')
358
- ct.text = klass.type_name
431
+ ct.text = klass.entity_set_name
359
432
  end
360
433
 
361
434
  XML_PREAMBLE + doc.to_pretty_xml
@@ -364,10 +437,18 @@ module OData
364
437
  def add_metadata_xml_entity_type(schema)
365
438
  @collections.each do |klass|
366
439
  enty = klass.add_metadata_rexml(schema)
367
- klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
440
+ klass.add_metadata_navs_rexml(enty, @relman)
368
441
  end
369
442
  end
370
443
 
444
+ def add_metadata_xml_complex_types(schema)
445
+ @complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) }
446
+ end
447
+
448
+ def add_metadata_xml_function_imports(ec)
449
+ @function_imports.each_value { |func| func.add_metadata_rexml(ec) }
450
+ end
451
+
371
452
  def add_metadata_xml_associations(schema)
372
453
  @relman.each_rel do |rel|
373
454
  rel.with_metadata_info(@xnamespace) do |name, bdinfo|
@@ -390,7 +471,7 @@ module OData
390
471
  # 3.a Entity set's
391
472
  ec.add_element('EntitySet',
392
473
  'Name' => klass.entity_set_name,
393
- 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
474
+ 'EntityType' => klass.type_name)
394
475
  end
395
476
  # 3.b Association set's
396
477
  @relman.each_rel do |rel|
@@ -404,6 +485,9 @@ module OData
404
485
  assoc.add_element('End', assoend)
405
486
  end
406
487
  end
488
+
489
+ # 4 function imports
490
+ add_metadata_xml_function_imports(ec)
407
491
  end
408
492
 
409
493
  def metadata_xml(_req)
@@ -437,9 +521,12 @@ module OData
437
521
  schema = serv.add_element('Schema',
438
522
  'Namespace' => @xnamespace,
439
523
  'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
440
- # 1. all EntityType
524
+ # 1. a. all EntityType
441
525
  add_metadata_xml_entity_type(schema)
442
526
 
527
+ # 1. b. all ComplexType
528
+ add_metadata_xml_complex_types(schema)
529
+
443
530
  # 2. Associations
444
531
  add_metadata_xml_associations(schema)
445
532
 
@@ -451,20 +538,45 @@ module OData
451
538
 
452
539
  # methods related to transitions to next state (cf. walker)
453
540
  module Transitions
454
- DOLLAR_ID_REGEXP = Regexp.new('\A\/\$').freeze
541
+ DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
542
+ ALLOWED_TRANSITIONS_FIXED = [
543
+ Safrano::TransitionEnd,
544
+ Safrano::TransitionMetadata,
545
+ Safrano::TransitionBatch,
546
+ Safrano::TransitionContentId
547
+ ].freeze
548
+
549
+ def build_allowed_transitions
550
+ @allowed_transitions = if @function_imports.empty?
551
+ (ALLOWED_TRANSITIONS_FIXED + [
552
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
553
+ trans: 'transition_collection')
554
+ ]).freeze
555
+ else
556
+ (ALLOWED_TRANSITIONS_FIXED + [
557
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
558
+ trans: 'transition_collection'),
559
+ Safrano::Transition.new(%r{\A/(#{base_url_func_regexp})(.*)},
560
+ trans: 'transition_service_op')
561
+ ]).freeze
562
+ end
563
+ end
564
+
455
565
  def allowed_transitions
456
- @allowed_transitions = [
457
- Safrano::TransitionEnd,
458
- Safrano::TransitionMetadata,
459
- Safrano::TransitionBatch,
460
- Safrano::TransitionContentId,
461
- Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
462
- trans: 'transition_collection')
463
- ]
566
+ @allowed_transitions
567
+ end
568
+
569
+ def thread_safe_collection(collklass)
570
+ Safrano::OData::Collection.new(collklass)
464
571
  end
465
572
 
466
573
  def transition_collection(match_result)
467
- [@cmap[match_result[1]], :run] if match_result[1]
574
+ [thread_safe_collection(@cmap[match_result[1]]), :run] if match_result[1]
575
+ # [@cmap[match_result[1]], :run] if match_result[1]
576
+ end
577
+
578
+ def transition_service_op(match_result)
579
+ [@function_imports[match_result[1]], :run] if match_result[1]
468
580
  end
469
581
 
470
582
  def transition_batch(_match_result)
@@ -480,7 +592,7 @@ module OData
480
592
  end
481
593
 
482
594
  def transition_end(_match_result)
483
- [nil, :end]
595
+ Safrano::Transition::RESULT_END
484
596
  end
485
597
  end
486
598
 
@@ -488,14 +600,11 @@ module OData
488
600
 
489
601
  def odata_get(req)
490
602
  if req.accept?(APPXML)
491
- # app.headers 'Content-Type' => 'application/xml;charset=utf-8'
492
- # Doc: 2.2.3.7.1 Service Document As per [RFC5023], AtomPub Service
493
- # Documents MUST be
494
- # identified with the "application/atomsvc+xml" media type (see
495
- # [RFC5023] section 8).
496
- [200, CT_ATOMXML, [service_xml(req)]]
603
+ # OData V2 reference service implementations are returning app-xml-u8
604
+ # so we do
605
+ [200, CT_APPXML, [service_xml(req)]]
497
606
  else
498
- # this is returned by http://services.odata.org/V2/OData/OData.svc
607
+ # this is returned by http://services.odata.org/V2/OData/Safrano.svc
499
608
  415
500
609
  end
501
610
  end
@@ -507,18 +616,18 @@ module OData
507
616
  @data_service_version = '1.0'
508
617
  end
509
618
 
510
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
619
+ def get_coll_odata_links_h(array:, icount: nil)
511
620
  array.map do |w|
512
- get_entity_odata_link_h(entity: w, uribase: uribase)
621
+ get_entity_odata_link_h(entity: w)
513
622
  end
514
623
  end
515
624
 
516
- def get_coll_odata_h(array:, template:, uribase:, icount: nil)
517
- array.map do |w|
625
+ def get_coll_odata_h(array:, template:, icount: nil)
626
+ array.map! do |w|
518
627
  get_entity_odata_h(entity: w,
519
- template: template,
520
- uribase: uribase)
628
+ template: template)
521
629
  end
630
+ array
522
631
  end
523
632
 
524
633
  def get_emptycoll_odata_h
@@ -532,36 +641,37 @@ module OData
532
641
  @data_service_version = '2.0'
533
642
  end
534
643
 
535
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
644
+ def get_coll_odata_links_h(array:, icount: nil)
536
645
  res = array.map do |w|
537
- get_entity_odata_link_h(entity: w, uribase: uribase)
646
+ get_entity_odata_link_h(entity: w)
538
647
  end
539
648
  if icount
540
- { 'results' => res, '__count' => icount }
649
+ { RESULTS_K => res, COUNT_K => icount }
541
650
  else
542
- { 'results' => res }
651
+ { RESULTS_K => res }
543
652
  end
544
653
  end
545
654
 
546
655
  def get_emptycoll_odata_h
547
- { 'results' => EMPTY_HASH_IN_ARY }
656
+ { RESULTS_K => EMPTY_HASH_IN_ARY }
548
657
  end
549
658
  end
550
659
 
551
660
  # a virtual entity for the service metadata
552
661
  class ServiceMeta
553
662
  attr_accessor :service
663
+
554
664
  def initialize(service)
555
665
  @service = service
556
666
  end
557
667
 
668
+ ALLOWED_TRANSITIONS_FIXED = [Safrano::TransitionEnd].freeze
558
669
  def allowed_transitions
559
- @allowed_transitions = [Safrano::Transition.new('',
560
- trans: 'transition_end')]
670
+ ALLOWED_TRANSITIONS_FIXED
561
671
  end
562
672
 
563
673
  def transition_end(_match_result)
564
- [nil, :end]
674
+ Safrano::Transition::RESULT_END
565
675
  end
566
676
 
567
677
  def odata_get(req)