safrano 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  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 +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  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 +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 +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  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 +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -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
@@ -83,18 +58,15 @@ module OData
83
58
  'Pragma' => 'no-cache' }.freeze
84
59
  DATASERVICEVERSION = 'DataServiceVersion'.freeze
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,7 +1,9 @@
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
@@ -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) or
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,14 +1,20 @@
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
17
+ PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
12
18
  DEFERRED = '__deferred'.freeze
13
19
  URI = 'uri'.freeze
14
20
 
@@ -16,25 +22,6 @@ module OData
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
27
  RESULTS_K = 'results'.freeze
@@ -43,11 +30,7 @@ module OData
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
@@ -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,7 +86,7 @@ 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
@@ -115,17 +102,18 @@ module OData
115
102
  end
116
103
 
117
104
  # Link to Model
118
- module OData
105
+ module Safrano
119
106
  MAX_DATASERVICE_VERSION = '2'.freeze
120
107
  MIN_DATASERVICE_VERSION = '1'.freeze
108
+ CV_MAX_DATASERVICE_VERSION = Contract.valid(MAX_DATASERVICE_VERSION).freeze
109
+ CV_MIN_DATASERVICE_VERSION = Contract.valid(MIN_DATASERVICE_VERSION).freeze
121
110
  include XMLNS
122
111
  # Base class for service. Subclass will be for V1, V2 etc...
123
112
  class ServiceBase
124
113
  include Safrano
125
114
  include ExpandHandler
126
115
 
127
- XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
128
- "\r\n".freeze
116
+ XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n).freeze
129
117
 
130
118
  # This is just a hash of entity Set Names to the corresponding Class
131
119
  # Example
@@ -148,6 +136,9 @@ module OData
148
136
  attr_accessor :meta
149
137
  attr_accessor :batch_handler
150
138
  attr_accessor :relman
139
+ attr_accessor :complex_types
140
+ attr_accessor :function_imports
141
+
151
142
  # Instance attributes for specialized Version specific Instances
152
143
  attr_accessor :v1
153
144
  attr_accessor :v2
@@ -161,37 +152,31 @@ module OData
161
152
  # because of the version subclasses that dont use "super" initialise
162
153
  # (todo: why not??)
163
154
  @meta = ServiceMeta.new(self)
164
- @batch_handler = OData::Batch::DisabledHandler.new
165
- @relman = OData::RelationManager.new
155
+ @batch_handler = Safrano::Batch::DisabledHandler.new
156
+ @relman = Safrano::RelationManager.new
157
+ @complex_types = Set.new
158
+ @function_imports = {}
166
159
  @cmap = {}
167
160
  instance_eval(&block) if block_given?
168
161
  end
169
162
 
170
- DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
171
163
  TRAILING_SLASH = %r{/\z}.freeze
172
164
  DEFAULT_PATH_PREFIX = '/'.freeze
173
- DEFAULT_SERVER_URL = 'http://localhost:9494'
174
-
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
165
+ DEFAULT_SERVER_URL = 'http://localhost:9494'.freeze
181
166
 
182
167
  def enable_batch
183
- @batch_handler = OData::Batch::EnabledHandler.new
168
+ @batch_handler = Safrano::Batch::EnabledHandler.new
184
169
  (@v1.batch_handler = @batch_handler) if @v1
185
170
  (@v2.batch_handler = @batch_handler) if @v2
186
171
  end
187
172
 
188
173
  def enable_v1_service
189
- @v1 = OData::ServiceV1.new
174
+ @v1 = Safrano::ServiceV1.new
190
175
  copy_attribs_to @v1
191
176
  end
192
177
 
193
178
  def enable_v2_service
194
- @v2 = OData::ServiceV2.new
179
+ @v2 = Safrano::ServiceV2.new
195
180
  copy_attribs_to @v2
196
181
  end
197
182
 
@@ -225,12 +210,10 @@ module OData
225
210
  def set_uribase
226
211
  @uribase = if @xpath_prefix.empty?
227
212
  @xserver_url
213
+ elsif @xpath_prefix[0] == '/'
214
+ "#{@xserver_url}#{@xpath_prefix}"
228
215
  else
229
- if @xpath_prefix[0] == '/'
230
- @xserver_url + @xpath_prefix
231
- else
232
- @xserver_url + '/' + @xpath_prefix
233
- end
216
+ "#{@xserver_url}/#{@xpath_prefix}"
234
217
  end
235
218
  (@v1.uribase = @uribase) if @v1
236
219
  (@v2.uribase = @uribase) if @v2
@@ -249,6 +232,8 @@ module OData
249
232
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
250
233
  other.relman = @relman
251
234
  other.batch_handler = @batch_handler
235
+ other.complex_types = @complex_types
236
+ other.function_imports = @function_imports
252
237
  other
253
238
  end
254
239
 
@@ -261,38 +246,43 @@ module OData
261
246
  def register_model(modelklass, entity_set_name = nil, is_media = false)
262
247
  # check that the provided klass is a Sequel Model
263
248
 
264
- raise(OData::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
249
+ raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
265
250
 
266
- if modelklass.ancestors.include? OData::Entity
251
+ if modelklass.ancestors.include? Safrano::Entity
267
252
  # modules were already added previously;
268
253
  # cleanup state to avoid having data from previous calls
269
254
  # mostly usefull for testing (eg API)
270
255
  modelklass.reset
271
256
  elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
272
- modelklass.extend OData::EntityClassMultiPK
273
- modelklass.include OData::EntityMultiPK
257
+ modelklass.extend Safrano::EntityClassMultiPK
258
+ modelklass.include Safrano::EntityMultiPK
274
259
  else
275
- modelklass.extend OData::EntityClassSinglePK
276
- modelklass.include OData::EntitySinglePK
260
+ modelklass.extend Safrano::EntityClassSinglePK
261
+ modelklass.include Safrano::EntitySinglePK
277
262
  end
263
+
278
264
  # Media/Non-media
279
265
  if is_media
280
- modelklass.extend OData::EntityClassMedia
266
+ modelklass.extend Safrano::EntityClassMedia
281
267
  # set default media handler . Can be overridden later with the
282
268
  # "use HandlerKlass, options" API
283
269
 
284
270
  modelklass.set_default_media_handler
285
271
  modelklass.api_check_media_fields
286
- modelklass.include OData::MediaEntity
272
+ modelklass.include Safrano::MediaEntity
287
273
  else
288
- modelklass.extend OData::EntityClassNonMedia
289
- modelklass.include OData::NonMediaEntity
274
+ modelklass.extend Safrano::EntityClassNonMedia
275
+ modelklass.include Safrano::NonMediaEntity
290
276
  end
291
277
 
292
278
  modelklass.prepare_pk
293
279
  modelklass.prepare_fields
294
280
  esname = (entity_set_name || modelklass).to_s.freeze
295
- modelklass.instance_eval { @entity_set_name = esname }
281
+ serv_namespace = @xnamespace
282
+ modelklass.instance_eval do
283
+ @entity_set_name = esname
284
+ @namespace = serv_namespace
285
+ end
296
286
  @cmap[esname] = modelklass
297
287
  set_collections_sorted(@cmap.values)
298
288
  end
@@ -313,6 +303,23 @@ module OData
313
303
  modelklass.deferred_iblock = block if block_given?
314
304
  end
315
305
 
306
+ def publish_complex_type(ctklass)
307
+ # check that the provided klass is a Safrano ComplexType
308
+
309
+ raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType
310
+
311
+ serv_namespace = @xnamespace
312
+ ctklass.instance_eval { @namespace = serv_namespace }
313
+
314
+ @complex_types.add ctklass
315
+ end
316
+
317
+ def function_import(name)
318
+ funcimp = Safrano::FunctionImport(name)
319
+ @function_imports[name] = funcimp
320
+ funcimp
321
+ end
322
+
316
323
  def cmap=(imap)
317
324
  @cmap = imap
318
325
  set_collections_sorted(@cmap.values)
@@ -352,14 +359,10 @@ module OData
352
359
  @collections.each(&:finalize_publishing)
353
360
 
354
361
  # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
355
- @collections.each { |klass|
362
+ @collections.each do |klass|
356
363
  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
- }
364
+ klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
365
+ end
363
366
 
364
367
  # build allowed transitions (requires that @collections are filled and sorted for having a
365
368
  # correct base_url_regexp)
@@ -368,11 +371,11 @@ module OData
368
371
  # mixin adapter specific modules where needed
369
372
  case Sequel::Model.db.adapter_scheme
370
373
  when :postgres
371
- OData::Filter::FuncTree.include OData::Filter::FuncTreePostgres
374
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
372
375
  when :sqlite
373
- OData::Filter::FuncTree.include OData::Filter::FuncTreeSqlite
376
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
374
377
  else
375
- OData::Filter::FuncTree.include OData::Filter::FuncTreeDefault
378
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
376
379
  end
377
380
  end
378
381
 
@@ -389,19 +392,24 @@ module OData
389
392
  @collections.map(&:entity_set_name).join('|')
390
393
  end
391
394
 
395
+ def base_url_func_regexp
396
+ @function_imports.keys.join('|')
397
+ end
398
+
392
399
  def service
393
400
  hres = {}
394
401
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
395
402
  hres
396
403
  end
397
404
 
398
- def service_xml(req)
405
+ def service_xml(_req)
399
406
  doc = REXML::Document.new
400
407
  # separator required ? ?
401
408
  root = doc.add_element('service', 'xml:base' => @uribase)
402
409
 
403
410
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
404
411
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
412
+
405
413
  # this generates the main xmlns attribute
406
414
  root.add_namespace(XMLNS::W3_2007_APP)
407
415
  wp = root.add_element 'workspace'
@@ -412,7 +420,7 @@ module OData
412
420
  @collections.each do |klass|
413
421
  col = wp.add_element('collection', 'href' => klass.entity_set_name)
414
422
  ct = col.add_element('atom:title')
415
- ct.text = klass.type_name
423
+ ct.text = klass.entity_set_name
416
424
  end
417
425
 
418
426
  XML_PREAMBLE + doc.to_pretty_xml
@@ -421,10 +429,18 @@ module OData
421
429
  def add_metadata_xml_entity_type(schema)
422
430
  @collections.each do |klass|
423
431
  enty = klass.add_metadata_rexml(schema)
424
- klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
432
+ klass.add_metadata_navs_rexml(enty, @relman)
425
433
  end
426
434
  end
427
435
 
436
+ def add_metadata_xml_complex_types(schema)
437
+ @complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) }
438
+ end
439
+
440
+ def add_metadata_xml_function_imports(ec)
441
+ @function_imports.each_value { |func| func.add_metadata_rexml(ec) }
442
+ end
443
+
428
444
  def add_metadata_xml_associations(schema)
429
445
  @relman.each_rel do |rel|
430
446
  rel.with_metadata_info(@xnamespace) do |name, bdinfo|
@@ -447,7 +463,7 @@ module OData
447
463
  # 3.a Entity set's
448
464
  ec.add_element('EntitySet',
449
465
  'Name' => klass.entity_set_name,
450
- 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
466
+ 'EntityType' => klass.type_name)
451
467
  end
452
468
  # 3.b Association set's
453
469
  @relman.each_rel do |rel|
@@ -461,6 +477,9 @@ module OData
461
477
  assoc.add_element('End', assoend)
462
478
  end
463
479
  end
480
+
481
+ # 4 function imports
482
+ add_metadata_xml_function_imports(ec)
464
483
  end
465
484
 
466
485
  def metadata_xml(_req)
@@ -494,9 +513,12 @@ module OData
494
513
  schema = serv.add_element('Schema',
495
514
  'Namespace' => @xnamespace,
496
515
  'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
497
- # 1. all EntityType
516
+ # 1. a. all EntityType
498
517
  add_metadata_xml_entity_type(schema)
499
518
 
519
+ # 1. b. all ComplexType
520
+ add_metadata_xml_complex_types(schema)
521
+
500
522
  # 2. Associations
501
523
  add_metadata_xml_associations(schema)
502
524
 
@@ -517,18 +539,36 @@ module OData
517
539
  ].freeze
518
540
 
519
541
  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
542
+ @allowed_transitions = if @function_imports.empty?
543
+ (ALLOWED_TRANSITIONS_FIXED + [
544
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
545
+ trans: 'transition_collection')
546
+ ]).freeze
547
+ else
548
+ (ALLOWED_TRANSITIONS_FIXED + [
549
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
550
+ trans: 'transition_collection'),
551
+ Safrano::Transition.new(%r{\A/(#{base_url_func_regexp})(.*)},
552
+ trans: 'transition_service_op')
553
+ ]).freeze
554
+ end
524
555
  end
525
556
 
526
557
  def allowed_transitions
527
558
  @allowed_transitions
528
559
  end
529
560
 
561
+ def thread_safe_collection(collklass)
562
+ Safrano::OData::Collection.new(collklass)
563
+ end
564
+
530
565
  def transition_collection(match_result)
531
- [@cmap[match_result[1]], :run] if match_result[1]
566
+ [thread_safe_collection(@cmap[match_result[1]]), :run] if match_result[1]
567
+ # [@cmap[match_result[1]], :run] if match_result[1]
568
+ end
569
+
570
+ def transition_service_op(match_result)
571
+ [@function_imports[match_result[1]], :run] if match_result[1]
532
572
  end
533
573
 
534
574
  def transition_batch(_match_result)
@@ -544,7 +584,7 @@ module OData
544
584
  end
545
585
 
546
586
  def transition_end(_match_result)
547
- [nil, :end]
587
+ Safrano::Transition::RESULT_END
548
588
  end
549
589
  end
550
590
 
@@ -552,14 +592,11 @@ module OData
552
592
 
553
593
  def odata_get(req)
554
594
  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)]]
595
+ # OData V2 reference service implementations are returning app-xml-u8
596
+ # so we do
597
+ [200, CT_APPXML, [service_xml(req)]]
561
598
  else
562
- # this is returned by http://services.odata.org/V2/OData/OData.svc
599
+ # this is returned by http://services.odata.org/V2/OData/Safrano.svc
563
600
  415
564
601
  end
565
602
  end
@@ -615,6 +652,7 @@ module OData
615
652
  # a virtual entity for the service metadata
616
653
  class ServiceMeta
617
654
  attr_accessor :service
655
+
618
656
  def initialize(service)
619
657
  @service = service
620
658
  end
@@ -625,7 +663,7 @@ module OData
625
663
  end
626
664
 
627
665
  def transition_end(_match_result)
628
- [nil, :end]
666
+ Safrano::Transition::RESULT_END
629
667
  end
630
668
 
631
669
  def odata_get(req)