safrano 0.4.3 → 0.5.1

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 +8 -4
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +139 -642
  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 +196 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +78 -123
  23. data/lib/odata/error.rb +170 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +9 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +39 -25
  28. data/lib/odata/filter/sequel.rb +112 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +21 -18
  31. data/lib/odata/filter/tree.rb +78 -44
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +641 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +18 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +141 -0
  42. data/lib/safrano/core.rb +24 -106
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -24
  46. data/lib/safrano/rack_app.rb +62 -63
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +96 -38
  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 +156 -110
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +30 -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
@@ -81,20 +56,17 @@ module OData
81
56
  NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
82
57
  'Expires' => '-1',
83
58
  'Pragma' => 'no-cache' }.freeze
84
- DATASERVICEVERSION = 'DataServiceVersion'.freeze
59
+ DATASERVICEVERSION = 'DataServiceVersion'
85
60
  include MethodHandlers
61
+
86
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
- myhdrs = NOCACHE_HDRS.dup
96
- myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
97
- headers myhdrs
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
98
70
  end
99
71
 
100
72
  # dispatch for all methods requiring parsing of the path
@@ -119,6 +91,11 @@ module OData
119
91
  end
120
92
  end
121
93
 
94
+ def dispatch_error(err)
95
+ @response.status, rsph, @response.body = err.odata_get(@request)
96
+ headers rsph
97
+ end
98
+
122
99
  def dispatch
123
100
  req_ret = if @request.request_method !~ METHODS_REGEXP
124
101
  [404, EMPTY_HASH, ['Did you get lost?']]
@@ -132,13 +109,23 @@ module OData
132
109
  end
133
110
 
134
111
  def call(env)
135
- @request = OData::Request.new(env)
136
- @response = OData::Response.new
112
+ # for thread safety
113
+ dup._call(env)
114
+ end
137
115
 
138
- before
116
+ def _call(env)
117
+ begin
118
+ @request = Safrano::Request.new(env)
119
+ @response = Safrano::Response.new
139
120
 
140
- dispatch
121
+ before.tap_error { |err| dispatch_error(err) }
122
+ .tap_valid { |res| dispatch }
141
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
142
129
  @response.finish
143
130
  end
144
131
 
@@ -167,10 +154,22 @@ module OData
167
154
  end
168
155
 
169
156
  def self.publish_service(&block)
170
- sbase = OData::ServiceBase.new
157
+ sbase = Safrano::ServiceBase.new
171
158
  sbase.instance_eval(&block) if block_given?
172
159
  sbase.finalize_publishing
173
160
  set_servicebase(sbase)
174
161
  end
175
162
  end
176
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
@@ -15,3 +17,18 @@ module Rack
15
17
  end
16
18
  end
17
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
@@ -157,44 +162,97 @@ module OData
157
162
  end
158
163
  end
159
164
 
160
- def negotiate_service_version
161
- maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
162
- OData::ServiceBase.parse_data_service_version(rqv)
163
- else
164
- OData::MAX_DATASERVICE_VERSION
165
- end
166
- return OData::BadRequestError if maxv.nil?
167
- # client request an too old version --> 501
168
- return OData::NotImplementedError if maxv < OData::MIN_DATASERVICE_VERSION
169
-
170
- minv = if (rqv = env['HTTP_MINDATASERVICEVERSION'])
171
- OData::ServiceBase.parse_data_service_version(rqv)
172
- else
173
- OData::MIN_DATASERVICE_VERSION
174
- end
175
- return OData::BadRequestError if minv.nil?
176
- # client request an too new version --> 501
177
- return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
178
- return OData::BadRequestError if minv > maxv
179
-
180
- v = if (rqv = env['HTTP_DATASERVICEVERSION'])
181
- 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
182
178
  else
183
- OData::MAX_DATASERVICE_VERSION
179
+ Contract.valid(maxv)
184
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
185
189
 
186
- return OData::BadRequestError if v.nil?
187
- return OData::NotImplementedError if v > OData::MAX_DATASERVICE_VERSION
188
- return OData::NotImplementedError if v < OData::MIN_DATASERVICE_VERSION
189
-
190
- @service = nil
191
- @service = case maxv
192
- when '1'
193
- @service_base.v1
194
- when '2', '3', '4'
195
- @service_base.v2
196
- end
197
- nil
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
200
+ else
201
+ Contract.valid(v)
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
232
+
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
198
256
  end
199
257
  end
200
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,53 +1,36 @@
1
- require 'rexml/document'
2
- require 'odata/relations.rb'
3
- require 'odata/batch.rb'
4
- require 'odata/error.rb'
5
- require 'odata/filter/sequel.rb'
1
+ # frozen_string_literal: true
6
2
 
7
- 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
8
14
  # this module has all methods related to expand/defered output preparation
9
15
  # and will be included in Service class
10
16
  module ExpandHandler
11
- PATH_SPLITTER = %r{\A(\w+)\/?(.*)\z}.freeze
12
- DEFERRED = '__deferred'.freeze
13
- URI = 'uri'.freeze
17
+ PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
18
+ DEFERRED = '__deferred'
19
+ URI = 'uri'
14
20
 
15
21
  def get_deferred_odata_h(entity_uri:, attrib:)
16
22
  { DEFERRED => { URI => "#{entity_uri}/#{attrib}" } }
17
23
  end
18
24
 
19
- # split expand argument into first and rest part
20
- def split_entity_expand_arg(exp_one)
21
- if exp_one.include?('/')
22
- m = PATH_SPLITTER.match(exp_one)
23
- cur_exp = m[1].strip
24
- rest_exp = m[2]
25
- # TODO: check errorhandling
26
- raise OData::ServerError if cur_exp.nil?
27
-
28
- k_s = cur_exp
29
-
30
- else
31
- k_s = exp_one.strip
32
- rest_exp = nil
33
- end
34
- k = k_s.to_sym
35
- yield k, k_s, rest_exp
36
- end
37
-
38
25
  # default v2
39
26
  # overriden in ServiceV1
40
- RESULTS_K = 'results'.freeze
41
- COUNT_K = '__count'.freeze
27
+ RESULTS_K = 'results'
28
+ COUNT_K = '__count'
42
29
  def get_coll_odata_h(array:, template:, icount: nil)
43
30
  array.map! do |w|
44
31
  get_entity_odata_h(entity: w, template: template)
45
32
  end
46
- if icount
47
- { RESULTS_K => array, COUNT_K => icount }
48
- else
49
- { RESULTS_K => array }
50
- end
33
+ icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
51
34
  end
52
35
 
53
36
  # handle $links ... Note: $expand seems to be ignored when $links
@@ -57,7 +40,7 @@ module OData
57
40
  end
58
41
 
59
42
  EMPTYH = {}.freeze
60
- METADATA_K = '__metadata'.freeze
43
+ METADATA_K = '__metadata'
61
44
  def get_entity_odata_h(entity:, template:)
62
45
  # start with metadata
63
46
  hres = { METADATA_K => entity.metadata_h }
@@ -66,8 +49,10 @@ module OData
66
49
  case elmt
67
50
  when :all_values
68
51
  hres.merge! entity.casted_values
52
+
69
53
  when :selected_vals
70
54
  hres.merge! entity.casted_values(arg)
55
+
71
56
  when :expand_e
72
57
 
73
58
  arg.each do |attr, templ|
@@ -79,6 +64,7 @@ module OData
79
64
  EMPTYH
80
65
  end
81
66
  end
67
+
82
68
  when :expand_c
83
69
  arg.each do |attr, templ|
84
70
  next unless (encoll = entity.send(attr))
@@ -87,6 +73,7 @@ module OData
87
73
  hres[attr] = get_coll_odata_h(array: encoll, template: templ)
88
74
  # else error ?
89
75
  end
76
+
90
77
  when :deferr
91
78
  euri = entity.uri
92
79
  arg.each do |attr|
@@ -99,33 +86,33 @@ module OData
99
86
  end
100
87
  end
101
88
 
102
- module OData
89
+ module Safrano
103
90
  # xml namespace constants needed for the output of XML service and
104
91
  # and metadata
105
92
  module XMLNS
106
- MSFT_ADO = 'http://schemas.microsoft.com/ado'.freeze
107
- MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm".freeze
108
- MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx".freeze
109
- MSFT_ADO_2007_META = MSFT_ADO + \
110
- '/2007/08/dataservices/metadata'.freeze
111
-
112
- W3_2005_ATOM = 'http://www.w3.org/2005/Atom'.freeze
113
- 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'
114
100
  end
115
101
  end
116
102
 
117
103
  # Link to Model
118
- module OData
119
- MAX_DATASERVICE_VERSION = '2'.freeze
120
- 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
121
109
  include XMLNS
122
110
  # Base class for service. Subclass will be for V1, V2 etc...
123
111
  class ServiceBase
124
112
  include Safrano
125
113
  include ExpandHandler
126
114
 
127
- XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
128
- "\r\n".freeze
115
+ XML_PREAMBLE = %(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
129
116
 
130
117
  # This is just a hash of entity Set Names to the corresponding Class
131
118
  # Example
@@ -148,6 +135,9 @@ module OData
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,37 +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
163
+ DEFAULT_PATH_PREFIX = '/'
173
164
  DEFAULT_SERVER_URL = 'http://localhost:9494'
174
165
 
175
- # input is the DataServiceVersion request header string, eg.
176
- # '2.0;blabla' ---> Version -> 2
177
- def self.parse_data_service_version(inp)
178
- m = DATASERVICEVERSION_RGX.match(inp)
179
- m[1] if m
180
- end
181
-
182
166
  def enable_batch
183
- @batch_handler = OData::Batch::EnabledHandler.new
167
+ @batch_handler = Safrano::Batch::EnabledHandler.new
184
168
  (@v1.batch_handler = @batch_handler) if @v1
185
169
  (@v2.batch_handler = @batch_handler) if @v2
186
170
  end
187
171
 
188
172
  def enable_v1_service
189
- @v1 = OData::ServiceV1.new
173
+ @v1 = Safrano::ServiceV1.new
190
174
  copy_attribs_to @v1
191
175
  end
192
176
 
193
177
  def enable_v2_service
194
- @v2 = OData::ServiceV2.new
178
+ @v2 = Safrano::ServiceV2.new
195
179
  copy_attribs_to @v2
196
180
  end
197
181
 
@@ -220,17 +204,21 @@ module OData
220
204
  (@v2.xserver_url = @xserver_url) if @v2
221
205
  end
222
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
+
223
213
  # end public API
224
214
 
225
215
  def set_uribase
226
216
  @uribase = if @xpath_prefix.empty?
227
217
  @xserver_url
218
+ elsif @xpath_prefix[0] == '/'
219
+ "#{@xserver_url}#{@xpath_prefix}"
228
220
  else
229
- if @xpath_prefix[0] == '/'
230
- @xserver_url + @xpath_prefix
231
- else
232
- @xserver_url + '/' + @xpath_prefix
233
- end
221
+ "#{@xserver_url}/#{@xpath_prefix}"
234
222
  end
235
223
  (@v1.uribase = @uribase) if @v1
236
224
  (@v2.uribase = @uribase) if @v2
@@ -249,6 +237,8 @@ module OData
249
237
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
250
238
  other.relman = @relman
251
239
  other.batch_handler = @batch_handler
240
+ other.complex_types = @complex_types
241
+ other.function_imports = @function_imports
252
242
  other
253
243
  end
254
244
 
@@ -261,38 +251,43 @@ module OData
261
251
  def register_model(modelklass, entity_set_name = nil, is_media = false)
262
252
  # check that the provided klass is a Sequel Model
263
253
 
264
- 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
265
255
 
266
- if modelklass.ancestors.include? OData::Entity
256
+ if modelklass.ancestors.include? Safrano::Entity
267
257
  # modules were already added previously;
268
258
  # cleanup state to avoid having data from previous calls
269
259
  # mostly usefull for testing (eg API)
270
260
  modelklass.reset
271
261
  elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
272
- modelklass.extend OData::EntityClassMultiPK
273
- modelklass.include OData::EntityMultiPK
262
+ modelklass.extend Safrano::EntityClassMultiPK
263
+ modelklass.include Safrano::EntityMultiPK
274
264
  else
275
- modelklass.extend OData::EntityClassSinglePK
276
- modelklass.include OData::EntitySinglePK
265
+ modelklass.extend Safrano::EntityClassSinglePK
266
+ modelklass.include Safrano::EntitySinglePK
277
267
  end
268
+
278
269
  # Media/Non-media
279
270
  if is_media
280
- modelklass.extend OData::EntityClassMedia
271
+ modelklass.extend Safrano::EntityClassMedia
281
272
  # set default media handler . Can be overridden later with the
282
273
  # "use HandlerKlass, options" API
283
274
 
284
275
  modelklass.set_default_media_handler
285
276
  modelklass.api_check_media_fields
286
- modelklass.include OData::MediaEntity
277
+ modelklass.include Safrano::MediaEntity
287
278
  else
288
- modelklass.extend OData::EntityClassNonMedia
289
- modelklass.include OData::NonMediaEntity
279
+ modelklass.extend Safrano::EntityClassNonMedia
280
+ modelklass.include Safrano::NonMediaEntity
290
281
  end
291
282
 
292
283
  modelklass.prepare_pk
293
284
  modelklass.prepare_fields
294
285
  esname = (entity_set_name || modelklass).to_s.freeze
295
- 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
296
291
  @cmap[esname] = modelklass
297
292
  set_collections_sorted(@cmap.values)
298
293
  end
@@ -313,6 +308,23 @@ module OData
313
308
  modelklass.deferred_iblock = block if block_given?
314
309
  end
315
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
+
316
328
  def cmap=(imap)
317
329
  @cmap = imap
318
330
  set_collections_sorted(@cmap.values)
@@ -352,14 +364,13 @@ module OData
352
364
  @collections.each(&:finalize_publishing)
353
365
 
354
366
  # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
355
- @collections.each { |klass|
367
+ @collections.each do |klass|
356
368
  klass.build_uri(@uribase)
357
- if klass.time_cols.empty?
358
- klass.include OData::NoMappingBeforeOutput
359
- else
360
- klass.include OData::MappingBeforeOutput
361
- end
362
- }
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
363
374
 
364
375
  # build allowed transitions (requires that @collections are filled and sorted for having a
365
376
  # correct base_url_regexp)
@@ -368,11 +379,11 @@ module OData
368
379
  # mixin adapter specific modules where needed
369
380
  case Sequel::Model.db.adapter_scheme
370
381
  when :postgres
371
- OData::Filter::FuncTree.include OData::Filter::FuncTreePostgres
382
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
372
383
  when :sqlite
373
- OData::Filter::FuncTree.include OData::Filter::FuncTreeSqlite
384
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
374
385
  else
375
- OData::Filter::FuncTree.include OData::Filter::FuncTreeDefault
386
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
376
387
  end
377
388
  end
378
389
 
@@ -389,19 +400,24 @@ module OData
389
400
  @collections.map(&:entity_set_name).join('|')
390
401
  end
391
402
 
403
+ def base_url_func_regexp
404
+ @function_imports.keys.join('|')
405
+ end
406
+
392
407
  def service
393
408
  hres = {}
394
409
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
395
410
  hres
396
411
  end
397
412
 
398
- def service_xml(req)
413
+ def service_xml(_req)
399
414
  doc = REXML::Document.new
400
415
  # separator required ? ?
401
416
  root = doc.add_element('service', 'xml:base' => @uribase)
402
417
 
403
418
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
404
419
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
420
+
405
421
  # this generates the main xmlns attribute
406
422
  root.add_namespace(XMLNS::W3_2007_APP)
407
423
  wp = root.add_element 'workspace'
@@ -412,7 +428,7 @@ module OData
412
428
  @collections.each do |klass|
413
429
  col = wp.add_element('collection', 'href' => klass.entity_set_name)
414
430
  ct = col.add_element('atom:title')
415
- ct.text = klass.type_name
431
+ ct.text = klass.entity_set_name
416
432
  end
417
433
 
418
434
  XML_PREAMBLE + doc.to_pretty_xml
@@ -421,10 +437,18 @@ module OData
421
437
  def add_metadata_xml_entity_type(schema)
422
438
  @collections.each do |klass|
423
439
  enty = klass.add_metadata_rexml(schema)
424
- klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
440
+ klass.add_metadata_navs_rexml(enty, @relman)
425
441
  end
426
442
  end
427
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
+
428
452
  def add_metadata_xml_associations(schema)
429
453
  @relman.each_rel do |rel|
430
454
  rel.with_metadata_info(@xnamespace) do |name, bdinfo|
@@ -447,7 +471,7 @@ module OData
447
471
  # 3.a Entity set's
448
472
  ec.add_element('EntitySet',
449
473
  'Name' => klass.entity_set_name,
450
- 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
474
+ 'EntityType' => klass.type_name)
451
475
  end
452
476
  # 3.b Association set's
453
477
  @relman.each_rel do |rel|
@@ -461,6 +485,9 @@ module OData
461
485
  assoc.add_element('End', assoend)
462
486
  end
463
487
  end
488
+
489
+ # 4 function imports
490
+ add_metadata_xml_function_imports(ec)
464
491
  end
465
492
 
466
493
  def metadata_xml(_req)
@@ -494,9 +521,12 @@ module OData
494
521
  schema = serv.add_element('Schema',
495
522
  'Namespace' => @xnamespace,
496
523
  'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
497
- # 1. all EntityType
524
+ # 1. a. all EntityType
498
525
  add_metadata_xml_entity_type(schema)
499
526
 
527
+ # 1. b. all ComplexType
528
+ add_metadata_xml_complex_types(schema)
529
+
500
530
  # 2. Associations
501
531
  add_metadata_xml_associations(schema)
502
532
 
@@ -508,7 +538,7 @@ module OData
508
538
 
509
539
  # methods related to transitions to next state (cf. walker)
510
540
  module Transitions
511
- DOLLAR_ID_REGEXP = Regexp.new('\A\/\$').freeze
541
+ DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
512
542
  ALLOWED_TRANSITIONS_FIXED = [
513
543
  Safrano::TransitionEnd,
514
544
  Safrano::TransitionMetadata,
@@ -517,18 +547,36 @@ module OData
517
547
  ].freeze
518
548
 
519
549
  def build_allowed_transitions
520
- @allowed_transitions = (ALLOWED_TRANSITIONS_FIXED + [
521
- Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
522
- trans: 'transition_collection')
523
- ]).freeze
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
524
563
  end
525
564
 
526
565
  def allowed_transitions
527
566
  @allowed_transitions
528
567
  end
529
568
 
569
+ def thread_safe_collection(collklass)
570
+ Safrano::OData::Collection.new(collklass)
571
+ end
572
+
530
573
  def transition_collection(match_result)
531
- [@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]
532
580
  end
533
581
 
534
582
  def transition_batch(_match_result)
@@ -544,7 +592,7 @@ module OData
544
592
  end
545
593
 
546
594
  def transition_end(_match_result)
547
- [nil, :end]
595
+ Safrano::Transition::RESULT_END
548
596
  end
549
597
  end
550
598
 
@@ -552,14 +600,11 @@ module OData
552
600
 
553
601
  def odata_get(req)
554
602
  if req.accept?(APPXML)
555
- # app.headers 'Content-Type' => 'application/xml;charset=utf-8'
556
- # Doc: 2.2.3.7.1 Service Document As per [RFC5023], AtomPub Service
557
- # Documents MUST be
558
- # identified with the "application/atomsvc+xml" media type (see
559
- # [RFC5023] section 8).
560
- [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)]]
561
606
  else
562
- # 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
563
608
  415
564
609
  end
565
610
  end
@@ -615,6 +660,7 @@ module OData
615
660
  # a virtual entity for the service metadata
616
661
  class ServiceMeta
617
662
  attr_accessor :service
663
+
618
664
  def initialize(service)
619
665
  @service = service
620
666
  end
@@ -625,7 +671,7 @@ module OData
625
671
  end
626
672
 
627
673
  def transition_end(_match_result)
628
- [nil, :end]
674
+ Safrano::Transition::RESULT_END
629
675
  end
630
676
 
631
677
  def odata_get(req)