safrano 0.4.0 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
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)