safrano 0.4.0 → 0.4.5

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 +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +145 -74
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  24. data/lib/odata/expand.rb +126 -0
  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 +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  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 +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +637 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -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
@@ -134,14 +132,14 @@ module OData
134
132
 
135
133
  def with_media_data
136
134
  if (filename = @env['HTTP_SLUG'])
137
-
138
- yield @env['rack.input'],
135
+
136
+ yield @env['rack.input'],
139
137
  content_type.split(';').first,
140
138
  Rfc2047.decode(filename)
141
139
 
142
140
  else
143
141
  ON_CGST_ERROR.call(self)
144
- return [400, {}, ['File upload error: Missing SLUG']]
142
+ [400, EMPTY_HASH, ['File upload error: Missing SLUG']]
145
143
  end
146
144
  end
147
145
 
@@ -152,56 +150,109 @@ module OData
152
150
  data = JSON.parse(body.read)
153
151
  rescue JSON::ParserError => e
154
152
  ON_CGST_ERROR.call(self)
155
- return [400, {}, ['JSON Parser Error while parsing payload : ',
156
- e.message]]
153
+ return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
154
+ e.message]]
157
155
  end
158
156
 
159
157
  yield data
160
158
 
161
159
  else # TODO: other formats
162
160
 
163
- [415, {}, []]
161
+ [415, EMPTY_HASH, EMPTY_ARRAY]
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
189
178
  else
190
- OData::MAX_DATASERVICE_VERSION
179
+ Contract.valid(maxv)
191
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
192
189
 
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
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
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)
@@ -34,7 +36,7 @@ module OData
34
36
 
35
37
  if drop_body?
36
38
  close
37
- result = []
39
+ result = EMPTY_ARRAY
38
40
  end
39
41
 
40
42
  if calculate_content_length?
@@ -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,184 +1,118 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rexml/document'
4
- require 'odata/relations.rb'
5
- require 'odata/batch.rb'
6
- require 'odata/error.rb'
7
-
8
- module OData
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
9
14
  # this module has all methods related to expand/defered output preparation
10
15
  # and will be included in Service class
11
16
  module ExpandHandler
12
- PATH_SPLITTER = %r{\A(\w+)\/?(.*)\z}.freeze
13
- def get_deferred_odata_h(entity:, attrib:, uribase:)
14
- { '__deferred' => { 'uri' => "#{entity.uri(uribase)}/#{attrib}" } }
15
- end
16
-
17
- # split expand argument into first and rest part
18
- def split_entity_expand_arg(exp_one)
19
- if exp_one.include?('/')
20
- m = PATH_SPLITTER.match(exp_one)
21
- cur_exp = m[1].strip
22
- rest_exp = m[2]
23
- # TODO: check errorhandling
24
- raise OData::ServerError if cur_exp.nil?
17
+ PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
18
+ DEFERRED = '__deferred'
19
+ URI = 'uri'
25
20
 
26
- k = cur_exp.to_sym
27
- else
28
- k = exp_one.strip.to_sym
29
- rest_exp = nil
30
- end
31
- yield k, 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:, expand: nil, uribase:, icount: nil)
37
- res = array.map do |w|
38
- get_entity_odata_h(entity: w,
39
- expand: expand,
40
- uribase: uribase)
41
- end
42
- if icount
43
- { 'results' => res, '__count' => icount }
44
- else
45
- { 'results' => res }
46
- end
47
- end
48
-
49
- # for expand 1..n nav attributes
50
- # actually same as v1 get_coll_odata_h
51
- # def get_expandcoll_odata_h(array:, expand: nil, uribase:, icount: nil)
52
- # array.map do |w|
53
- # get_entity_odata_h(entity: w,
54
- # expand: expand,
55
- # uribase: uribase)
56
- # end
57
- # end
58
-
59
- # handle a single expand
60
- def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
61
- uribase:)
62
-
63
-
64
- split_entity_expand_arg(exp_one) do |first, rest_exp|
65
- if ( entity.nav_values.has_key?(first) )
66
- if (enval = entity.nav_values[first])
67
- nav_values_h[first.to_s] = get_entity_odata_h(entity: enval,
68
- expand: rest_exp,
69
- uribase: uribase)
70
- else
71
- # FK is NULL --> nav_value is nil --> return empty json
72
- nav_values_h[first.to_s] = {}
73
- end
74
- elsif (encoll = entity.nav_coll[first])
75
- # nav attributes that are a collection (x..n)
76
- nav_coll_h[first.to_s] = get_coll_odata_h(array: encoll,
77
- expand: rest_exp,
78
- uribase: uribase)
79
- # nav_coll_h[first.to_s] = get_expandcoll_odata_h(array: encoll,
80
- # expand: rest_exp,
81
- # uribase: uribase)
82
-
83
-
84
- end
85
- end
86
- end
87
-
88
- def handle_entity_expand(entity:, expand:, nav_values_h:,
89
- nav_coll_h:, uribase:)
90
- expand.strip!
91
- explist = expand.split(',')
92
- # handle multiple expands
93
- explist.each do |exp|
94
- handle_entity_expand_one(entity: entity,
95
- exp_one: exp,
96
- nav_values_h: nav_values_h,
97
- nav_coll_h: nav_coll_h,
98
- uribase: uribase)
99
- end
100
- end
101
-
102
- def handle_entity_deferred_attribs(entity:, nav_values_h:,
103
- nav_coll_h:, uribase:)
104
- entity.nav_values.each_key do |ksy|
105
- ks = ksy.to_s
106
- next if nav_values_h.key?(ks)
107
-
108
- nav_values_h[ks] = get_deferred_odata_h(entity: entity,
109
- attrib: ks, uribase: uribase)
110
- end
111
- entity.nav_coll.each_key do |ksy|
112
- ks = ksy.to_s
113
- next if nav_coll_h.key?(ks)
114
-
115
- nav_coll_h[ks] = get_deferred_odata_h(entity: entity, attrib: ks,
116
- uribase: uribase)
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)
117
32
  end
33
+ icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
118
34
  end
119
35
 
120
36
  # handle $links ... Note: $expand seems to be ignored when $links
121
37
  # are requested
122
- def get_entity_odata_link_h(entity:, uribase:)
123
- { uri: entity.uri(uribase) }
124
- end
38
+ def get_entity_odata_link_h(entity:)
39
+ { uri: entity.uri }
40
+ end
41
+
42
+ EMPTYH = {}.freeze
43
+ METADATA_K = '__metadata'
44
+ def get_entity_odata_h(entity:, template:)
45
+ # start with metadata
46
+ hres = { METADATA_K => entity.metadata_h }
47
+
48
+ template.each do |elmt, arg|
49
+ case elmt
50
+ when :all_values
51
+ hres.merge! entity.casted_values
52
+
53
+ when :selected_vals
54
+ hres.merge! entity.casted_values(arg)
55
+
56
+ when :expand_e
57
+
58
+ arg.each do |attr, templ|
59
+ enval = entity.send(attr)
60
+ hres[attr] = if enval
61
+ get_entity_odata_h(entity: enval, template: templ)
62
+ else
63
+ # FK is NULL --> nav_value is nil --> return empty json
64
+ EMPTYH
65
+ end
66
+ end
125
67
 
126
- def get_entity_odata_h(entity:, expand: nil, uribase:)
127
- hres = entity.casted_values
128
- hres['__metadata'] = entity.metadata_h(uribase: uribase)
68
+ when :expand_c
69
+ arg.each do |attr, templ|
70
+ next unless (encoll = entity.send(attr))
129
71
 
130
- nav_values_h = {}
131
- nav_coll_h = {}
72
+ # nav attributes that are a collection (x..n)
73
+ hres[attr] = get_coll_odata_h(array: encoll, template: templ)
74
+ # else error ?
75
+ end
132
76
 
133
- # handle expanded nav attributes
134
- unless expand.nil?
135
- handle_entity_expand(entity: entity, expand: expand,
136
- nav_values_h: nav_values_h,
137
- nav_coll_h: nav_coll_h,
138
- uribase: uribase)
77
+ when :deferr
78
+ euri = entity.uri
79
+ arg.each do |attr|
80
+ hres[attr] = get_deferred_odata_h(entity_uri: euri, attrib: attr)
81
+ end
82
+ end
139
83
  end
140
-
141
- # handle not expanded (deferred) nav attributes
142
- handle_entity_deferred_attribs(entity: entity,
143
- nav_values_h: nav_values_h,
144
- nav_coll_h: nav_coll_h,
145
- uribase: uribase)
146
- # merge ...
147
- hres.merge!(nav_values_h)
148
- hres.merge!(nav_coll_h)
149
-
150
84
  hres
151
85
  end
152
86
  end
153
87
  end
154
88
 
155
- module OData
89
+ module Safrano
156
90
  # xml namespace constants needed for the output of XML service and
157
91
  # and metadata
158
92
  module XMLNS
159
- MSFT_ADO = 'http://schemas.microsoft.com/ado'.freeze
160
- MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm".freeze
161
- MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx".freeze
162
- MSFT_ADO_2007_META = MSFT_ADO + \
163
- '/2007/08/dataservices/metadata'.freeze
164
-
165
- W3_2005_ATOM = 'http://www.w3.org/2005/Atom'.freeze
166
- 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'
167
100
  end
168
101
  end
169
102
 
170
103
  # Link to Model
171
- module OData
172
- MAX_DATASERVICE_VERSION = '2'.freeze
173
- 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
174
109
  include XMLNS
175
110
  # Base class for service. Subclass will be for V1, V2 etc...
176
111
  class ServiceBase
177
112
  include Safrano
178
113
  include ExpandHandler
179
114
 
180
- XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
181
- "\r\n".freeze
115
+ XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
182
116
 
183
117
  # This is just a hash of entity Set Names to the corresponding Class
184
118
  # Example
@@ -196,10 +130,14 @@ module OData
196
130
  attr_accessor :xname
197
131
  attr_accessor :xnamespace
198
132
  attr_accessor :xpath_prefix
199
- # attr_accessor :xuribase
133
+ attr_accessor :xserver_url
134
+ attr_accessor :uribase
200
135
  attr_accessor :meta
201
136
  attr_accessor :batch_handler
202
137
  attr_accessor :relman
138
+ attr_accessor :complex_types
139
+ attr_accessor :function_imports
140
+
203
141
  # Instance attributes for specialized Version specific Instances
204
142
  attr_accessor :v1
205
143
  attr_accessor :v2
@@ -213,36 +151,31 @@ module OData
213
151
  # because of the version subclasses that dont use "super" initialise
214
152
  # (todo: why not??)
215
153
  @meta = ServiceMeta.new(self)
216
- @batch_handler = OData::Batch::DisabledHandler.new
217
- @relman = OData::RelationManager.new
154
+ @batch_handler = Safrano::Batch::DisabledHandler.new
155
+ @relman = Safrano::RelationManager.new
156
+ @complex_types = Set.new
157
+ @function_imports = {}
218
158
  @cmap = {}
219
159
  instance_eval(&block) if block_given?
220
160
  end
221
161
 
222
- DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
223
162
  TRAILING_SLASH = %r{/\z}.freeze
224
163
  DEFAULT_PATH_PREFIX = '/'
225
-
226
- # input is the DataServiceVersion request header string, eg.
227
- # '2.0;blabla' ---> Version -> 2
228
- def self.parse_data_service_version(inp)
229
- m = DATASERVICEVERSION_RGX.match(inp)
230
- m[1] if m
231
- end
164
+ DEFAULT_SERVER_URL = 'http://localhost:9494'
232
165
 
233
166
  def enable_batch
234
- @batch_handler = OData::Batch::EnabledHandler.new
167
+ @batch_handler = Safrano::Batch::EnabledHandler.new
235
168
  (@v1.batch_handler = @batch_handler) if @v1
236
169
  (@v2.batch_handler = @batch_handler) if @v2
237
170
  end
238
171
 
239
172
  def enable_v1_service
240
- @v1 = OData::ServiceV1.new
173
+ @v1 = Safrano::ServiceV1.new
241
174
  copy_attribs_to @v1
242
175
  end
243
176
 
244
177
  def enable_v2_service
245
- @v2 = OData::ServiceV2.new
178
+ @v2 = Safrano::ServiceV2.new
246
179
  copy_attribs_to @v2
247
180
  end
248
181
 
@@ -264,59 +197,91 @@ module OData
264
197
  (@v1.xpath_prefix = @xpath_prefix) if @v1
265
198
  (@v2.xpath_prefix = @xpath_prefix) if @v2
266
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
+
267
207
  # end public API
268
208
 
209
+ def set_uribase
210
+ @uribase = if @xpath_prefix.empty?
211
+ @xserver_url
212
+ elsif @xpath_prefix[0] == '/'
213
+ "#{@xserver_url}#{@xpath_prefix}"
214
+ else
215
+ "#{@xserver_url}/#{@xpath_prefix}"
216
+ end
217
+ (@v1.uribase = @uribase) if @v1
218
+ (@v2.uribase = @uribase) if @v2
219
+ end
220
+
269
221
  def copy_attribs_to(other)
270
222
  other.cmap = @cmap
271
223
  other.collections = @collections
224
+ other.allowed_transitions = @allowed_transitions
272
225
  other.xtitle = @xtitle
273
226
  other.xname = @xname
274
227
  other.xnamespace = @xnamespace
275
228
  other.xpath_prefix = @xpath_prefix
229
+ other.xserver_url = @xserver_url
230
+ other.uribase = @uribase
276
231
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
277
232
  other.relman = @relman
278
233
  other.batch_handler = @batch_handler
234
+ other.complex_types = @complex_types
235
+ other.function_imports = @function_imports
279
236
  other
280
237
  end
281
238
 
239
+ # this is a central place. We extend Sequel models with OData functionality
240
+ # The included/extended modules depends on the properties(eg, pks, field types) of the model
241
+ # we differentiate
242
+ # * Single/Multi PK
243
+ # * Media/Non-Media entity
244
+ # Putting this logic here in modules loaded once on start shall result in less runtime overhead
282
245
  def register_model(modelklass, entity_set_name = nil, is_media = false)
283
246
  # check that the provided klass is a Sequel Model
284
- unless modelklass.is_a? Sequel::Model::ClassMethods
285
- raise OData::API::ModelNameError, modelklass
286
- end
287
247
 
288
- if modelklass.ancestors.include? OData::Entity
248
+ raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
249
+
250
+ if modelklass.ancestors.include? Safrano::Entity
289
251
  # modules were already added previously;
290
252
  # cleanup state to avoid having data from previous calls
291
253
  # mostly usefull for testing (eg API)
292
254
  modelklass.reset
293
- else # first API call... (normal non-testing case)
294
- if modelklass.primary_key.is_a?(Array)
295
- modelklass.extend OData::EntityClassMultiPK
296
- modelklass.include OData::EntityMultiPK
297
- else
298
- modelklass.extend OData::EntityClassSinglePK
299
- modelklass.include OData::EntitySinglePK
300
- end
255
+ elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
256
+ modelklass.extend Safrano::EntityClassMultiPK
257
+ modelklass.include Safrano::EntityMultiPK
258
+ else
259
+ modelklass.extend Safrano::EntityClassSinglePK
260
+ modelklass.include Safrano::EntitySinglePK
301
261
  end
262
+
302
263
  # Media/Non-media
303
264
  if is_media
304
- modelklass.extend OData::EntityClassMedia
265
+ modelklass.extend Safrano::EntityClassMedia
305
266
  # set default media handler . Can be overridden later with the
306
267
  # "use HandlerKlass, options" API
307
268
 
308
269
  modelklass.set_default_media_handler
309
270
  modelklass.api_check_media_fields
310
- modelklass.include OData::MediaEntity
271
+ modelklass.include Safrano::MediaEntity
311
272
  else
312
- modelklass.extend OData::EntityClassNonMedia
313
- modelklass.include OData::NonMediaEntity
273
+ modelklass.extend Safrano::EntityClassNonMedia
274
+ modelklass.include Safrano::NonMediaEntity
314
275
  end
315
276
 
316
277
  modelklass.prepare_pk
317
278
  modelklass.prepare_fields
318
279
  esname = (entity_set_name || modelklass).to_s.freeze
319
- modelklass.instance_eval { @entity_set_name = esname }
280
+ serv_namespace = @xnamespace
281
+ modelklass.instance_eval do
282
+ @entity_set_name = esname
283
+ @namespace = serv_namespace
284
+ end
320
285
  @cmap[esname] = modelklass
321
286
  set_collections_sorted(@cmap.values)
322
287
  end
@@ -337,6 +302,23 @@ module OData
337
302
  modelklass.deferred_iblock = block if block_given?
338
303
  end
339
304
 
305
+ def publish_complex_type(ctklass)
306
+ # check that the provided klass is a Safrano ComplexType
307
+
308
+ raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType
309
+
310
+ serv_namespace = @xnamespace
311
+ ctklass.instance_eval { @namespace = serv_namespace }
312
+
313
+ @complex_types.add ctklass
314
+ end
315
+
316
+ def function_import(name)
317
+ funcimp = Safrano::FunctionImport(name)
318
+ @function_imports[name] = funcimp
319
+ funcimp
320
+ end
321
+
340
322
  def cmap=(imap)
341
323
  @cmap = imap
342
324
  set_collections_sorted(@cmap.values)
@@ -347,9 +329,7 @@ module OData
347
329
  # example: CrewMember must be matched before Crew otherwise we get error
348
330
  def set_collections_sorted(coll_data)
349
331
  @collections = coll_data
350
- if @collections
351
- @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse!
352
- end
332
+ @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
353
333
  @collections
354
334
  end
355
335
 
@@ -370,10 +350,32 @@ module OData
370
350
  # set default path prefix if path_prefix was not called
371
351
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
372
352
 
353
+ # set default server url if server_url was not called
354
+ server_url(DEFAULT_SERVER_URL) unless @xserver_url
355
+
356
+ set_uribase
357
+
373
358
  @collections.each(&:finalize_publishing)
374
-
375
- #finalize the media handlers
376
- @collections.each{|klass| }
359
+
360
+ # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
361
+ @collections.each do |klass|
362
+ klass.build_uri(@uribase)
363
+ klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
364
+ end
365
+
366
+ # build allowed transitions (requires that @collections are filled and sorted for having a
367
+ # correct base_url_regexp)
368
+ build_allowed_transitions
369
+
370
+ # mixin adapter specific modules where needed
371
+ case Sequel::Model.db.adapter_scheme
372
+ when :postgres
373
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
374
+ when :sqlite
375
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
376
+ else
377
+ Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
378
+ end
377
379
  end
378
380
 
379
381
  def execute_deferred_iblocks
@@ -389,19 +391,24 @@ module OData
389
391
  @collections.map(&:entity_set_name).join('|')
390
392
  end
391
393
 
394
+ def base_url_func_regexp
395
+ @function_imports.keys.join('|')
396
+ end
397
+
392
398
  def service
393
399
  hres = {}
394
400
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
395
401
  hres
396
402
  end
397
403
 
398
- def service_xml(req)
404
+ def service_xml(_req)
399
405
  doc = REXML::Document.new
400
406
  # separator required ? ?
401
- root = doc.add_element('service', 'xml:base' => req.uribase)
407
+ root = doc.add_element('service', 'xml:base' => @uribase)
402
408
 
403
409
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
404
410
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
411
+
405
412
  # this generates the main xmlns attribute
406
413
  root.add_namespace(XMLNS::W3_2007_APP)
407
414
  wp = root.add_element 'workspace'
@@ -412,7 +419,7 @@ module OData
412
419
  @collections.each do |klass|
413
420
  col = wp.add_element('collection', 'href' => klass.entity_set_name)
414
421
  ct = col.add_element('atom:title')
415
- ct.text = klass.type_name
422
+ ct.text = klass.entity_set_name
416
423
  end
417
424
 
418
425
  XML_PREAMBLE + doc.to_pretty_xml
@@ -421,10 +428,18 @@ module OData
421
428
  def add_metadata_xml_entity_type(schema)
422
429
  @collections.each do |klass|
423
430
  enty = klass.add_metadata_rexml(schema)
424
- klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
431
+ klass.add_metadata_navs_rexml(enty, @relman)
425
432
  end
426
433
  end
427
434
 
435
+ def add_metadata_xml_complex_types(schema)
436
+ @complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) }
437
+ end
438
+
439
+ def add_metadata_xml_function_imports(ec)
440
+ @function_imports.each_value { |func| func.add_metadata_rexml(ec) }
441
+ end
442
+
428
443
  def add_metadata_xml_associations(schema)
429
444
  @relman.each_rel do |rel|
430
445
  rel.with_metadata_info(@xnamespace) do |name, bdinfo|
@@ -447,7 +462,7 @@ module OData
447
462
  # 3.a Entity set's
448
463
  ec.add_element('EntitySet',
449
464
  'Name' => klass.entity_set_name,
450
- 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
465
+ 'EntityType' => klass.type_name)
451
466
  end
452
467
  # 3.b Association set's
453
468
  @relman.each_rel do |rel|
@@ -461,6 +476,9 @@ module OData
461
476
  assoc.add_element('End', assoend)
462
477
  end
463
478
  end
479
+
480
+ # 4 function imports
481
+ add_metadata_xml_function_imports(ec)
464
482
  end
465
483
 
466
484
  def metadata_xml(_req)
@@ -494,9 +512,12 @@ module OData
494
512
  schema = serv.add_element('Schema',
495
513
  'Namespace' => @xnamespace,
496
514
  'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
497
- # 1. all EntityType
515
+ # 1. a. all EntityType
498
516
  add_metadata_xml_entity_type(schema)
499
517
 
518
+ # 1. b. all ComplexType
519
+ add_metadata_xml_complex_types(schema)
520
+
500
521
  # 2. Associations
501
522
  add_metadata_xml_associations(schema)
502
523
 
@@ -508,20 +529,45 @@ module OData
508
529
 
509
530
  # methods related to transitions to next state (cf. walker)
510
531
  module Transitions
511
- DOLLAR_ID_REGEXP = Regexp.new('\A\/\$').freeze
532
+ DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
533
+ ALLOWED_TRANSITIONS_FIXED = [
534
+ Safrano::TransitionEnd,
535
+ Safrano::TransitionMetadata,
536
+ Safrano::TransitionBatch,
537
+ Safrano::TransitionContentId
538
+ ].freeze
539
+
540
+ def build_allowed_transitions
541
+ @allowed_transitions = if @function_imports.empty?
542
+ (ALLOWED_TRANSITIONS_FIXED + [
543
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
544
+ trans: 'transition_collection')
545
+ ]).freeze
546
+ else
547
+ (ALLOWED_TRANSITIONS_FIXED + [
548
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
549
+ trans: 'transition_collection'),
550
+ Safrano::Transition.new(%r{\A/(#{base_url_func_regexp})(.*)},
551
+ trans: 'transition_service_op')
552
+ ]).freeze
553
+ end
554
+ end
555
+
512
556
  def allowed_transitions
513
- @allowed_transitions = [
514
- Safrano::TransitionEnd,
515
- Safrano::TransitionMetadata,
516
- Safrano::TransitionBatch,
517
- Safrano::TransitionContentId,
518
- Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
519
- trans: 'transition_collection')
520
- ]
557
+ @allowed_transitions
558
+ end
559
+
560
+ def thread_safe_collection(collklass)
561
+ Safrano::OData::Collection.new(collklass)
521
562
  end
522
563
 
523
564
  def transition_collection(match_result)
524
- [@cmap[match_result[1]], :run] if match_result[1]
565
+ [thread_safe_collection(@cmap[match_result[1]]), :run] if match_result[1]
566
+ # [@cmap[match_result[1]], :run] if match_result[1]
567
+ end
568
+
569
+ def transition_service_op(match_result)
570
+ [@function_imports[match_result[1]], :run] if match_result[1]
525
571
  end
526
572
 
527
573
  def transition_batch(_match_result)
@@ -537,7 +583,7 @@ module OData
537
583
  end
538
584
 
539
585
  def transition_end(_match_result)
540
- [nil, :end]
586
+ Safrano::Transition::RESULT_END
541
587
  end
542
588
  end
543
589
 
@@ -545,14 +591,11 @@ module OData
545
591
 
546
592
  def odata_get(req)
547
593
  if req.accept?(APPXML)
548
- # app.headers 'Content-Type' => 'application/xml;charset=utf-8'
549
- # Doc: 2.2.3.7.1 Service Document As per [RFC5023], AtomPub Service
550
- # Documents MUST be
551
- # identified with the "application/atomsvc+xml" media type (see
552
- # [RFC5023] section 8).
553
- [200, CT_ATOMXML, [service_xml(req)]]
594
+ # OData V2 reference service implementations are returning app-xml-u8
595
+ # so we do
596
+ [200, CT_APPXML, [service_xml(req)]]
554
597
  else
555
- # this is returned by http://services.odata.org/V2/OData/OData.svc
598
+ # this is returned by http://services.odata.org/V2/OData/Safrano.svc
556
599
  415
557
600
  end
558
601
  end
@@ -564,22 +607,22 @@ module OData
564
607
  @data_service_version = '1.0'
565
608
  end
566
609
 
567
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
610
+ def get_coll_odata_links_h(array:, icount: nil)
568
611
  array.map do |w|
569
- get_entity_odata_link_h(entity: w, uribase: uribase)
612
+ get_entity_odata_link_h(entity: w)
570
613
  end
571
614
  end
572
615
 
573
- def get_coll_odata_h(array:, expand: nil, uribase:, icount: nil)
574
- array.map do |w|
616
+ def get_coll_odata_h(array:, template:, icount: nil)
617
+ array.map! do |w|
575
618
  get_entity_odata_h(entity: w,
576
- expand: expand,
577
- uribase: uribase)
619
+ template: template)
578
620
  end
621
+ array
579
622
  end
580
623
 
581
624
  def get_emptycoll_odata_h
582
- [{}]
625
+ EMPTY_HASH_IN_ARY
583
626
  end
584
627
  end
585
628
 
@@ -589,36 +632,37 @@ module OData
589
632
  @data_service_version = '2.0'
590
633
  end
591
634
 
592
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
635
+ def get_coll_odata_links_h(array:, icount: nil)
593
636
  res = array.map do |w|
594
- get_entity_odata_link_h(entity: w, uribase: uribase)
637
+ get_entity_odata_link_h(entity: w)
595
638
  end
596
639
  if icount
597
- { 'results' => res, '__count' => icount }
640
+ { RESULTS_K => res, COUNT_K => icount }
598
641
  else
599
- { 'results' => res }
642
+ { RESULTS_K => res }
600
643
  end
601
644
  end
602
645
 
603
646
  def get_emptycoll_odata_h
604
- { 'results' => [{}] }
647
+ { RESULTS_K => EMPTY_HASH_IN_ARY }
605
648
  end
606
649
  end
607
650
 
608
651
  # a virtual entity for the service metadata
609
652
  class ServiceMeta
610
653
  attr_accessor :service
654
+
611
655
  def initialize(service)
612
656
  @service = service
613
657
  end
614
658
 
659
+ ALLOWED_TRANSITIONS_FIXED = [Safrano::TransitionEnd].freeze
615
660
  def allowed_transitions
616
- @allowed_transitions = [Safrano::Transition.new('',
617
- trans: 'transition_end')]
661
+ ALLOWED_TRANSITIONS_FIXED
618
662
  end
619
663
 
620
664
  def transition_end(_match_result)
621
- [nil, :end]
665
+ Safrano::Transition::RESULT_END
622
666
  end
623
667
 
624
668
  def odata_get(req)