safrano 0.4.1 → 0.4.6

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