safrano 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 +17 -15
  15. data/lib/odata/collection.rb +141 -500
  16. data/lib/odata/collection_filter.rb +44 -37
  17. data/lib/odata/collection_media.rb +193 -43
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +39 -12
  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 +201 -176
  23. data/lib/odata/error.rb +186 -33
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +69 -0
  26. data/lib/odata/filter/error.rb +55 -6
  27. data/lib/odata/filter/parse.rb +38 -36
  28. data/lib/odata/filter/sequel.rb +121 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +15 -11
  31. data/lib/odata/filter/tree.rb +110 -60
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +50 -32
  35. data/lib/odata/relations.rb +7 -7
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +18 -28
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +43 -0
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
  46. data/lib/safrano/rack_app.rb +175 -0
  47. data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
  48. data/lib/{request.rb → safrano/request.rb} +102 -50
  49. data/lib/{response.rb → safrano/response.rb} +5 -4
  50. data/lib/safrano/sequel_join_by_paths.rb +5 -0
  51. data/lib/{service.rb → safrano/service.rb} +257 -188
  52. data/lib/safrano/version.rb +5 -0
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +53 -17
  55. data/lib/rack_app.rb +0 -174
  56. data/lib/sequel_join_by_paths.rb +0 -5
  57. data/lib/version.rb +0 -4
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require_relative '../odata/walker'
5
+ require_relative 'request'
6
+ require_relative 'response'
7
+
8
+ module Safrano
9
+ # handle GET PUT etc
10
+ module MethodHandlers
11
+ def odata_options
12
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
13
+ .if_valid do |context|
14
+ # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
15
+ headers.delete('Content-Type')
16
+ @response.headers.delete('Content-Type')
17
+ @response.headers['Content-Type'] = ''
18
+ [200, EMPTY_HASH, '']
19
+ end
20
+ end
21
+
22
+ def odata_delete
23
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
24
+ .if_valid { |context| context.odata_delete(@request) }
25
+ end
26
+
27
+ def odata_put
28
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
29
+ .if_valid { |context| context.odata_put(@request) }
30
+ end
31
+
32
+ def odata_patch
33
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
34
+ .if_valid { |context| context.odata_patch(@request) }
35
+ end
36
+
37
+ def odata_get
38
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
39
+ .if_valid { |context| context.odata_get(@request) }
40
+ end
41
+
42
+ def odata_post
43
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
44
+ .if_valid { |context| context.odata_post(@request) }
45
+ end
46
+
47
+ def odata_head
48
+ [200, EMPTY_HASH, [EMPTY_STRING]]
49
+ end
50
+ end
51
+
52
+ # the main Rack server app. Source: the Rack docu/examples and partly
53
+ # inspired from Sinatra
54
+ class ServerApp
55
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
56
+ NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
57
+ 'Expires' => '-1',
58
+ 'Pragma' => 'no-cache' }.freeze
59
+ DATASERVICEVERSION = 'DataServiceVersion'.freeze
60
+ include MethodHandlers
61
+
62
+ def before
63
+ @request.service_base = self.class.get_service_base
64
+
65
+ @request.negotiate_service_version.tap_valid do
66
+ myhdrs = NOCACHE_HDRS.dup
67
+ myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
68
+ headers myhdrs
69
+ end
70
+ end
71
+
72
+ # dispatch for all methods requiring parsing of the path
73
+ # with walker (ie. allmost all excepted HEAD)
74
+ def dispatch_with_walker
75
+ @walker = @request.create_odata_walker
76
+ case @request.request_method
77
+ when 'GET'
78
+ odata_get
79
+ when 'POST'
80
+ odata_post
81
+ when 'DELETE'
82
+ odata_delete
83
+ when 'OPTIONS'
84
+ odata_options
85
+ when 'PUT'
86
+ odata_put
87
+ when 'PATCH', 'MERGE'
88
+ odata_patch
89
+ else
90
+ raise Error
91
+ end
92
+ end
93
+
94
+ def dispatch_error(err)
95
+ @response.status, rsph, @response.body = err.odata_get(@request)
96
+ headers rsph
97
+ end
98
+
99
+ def dispatch
100
+ req_ret = if @request.request_method !~ METHODS_REGEXP
101
+ [404, EMPTY_HASH, ['Did you get lost?']]
102
+ elsif @request.request_method == 'HEAD'
103
+ odata_head
104
+ else
105
+ dispatch_with_walker
106
+ end
107
+ @response.status, rsph, @response.body = req_ret
108
+ headers rsph
109
+ end
110
+
111
+ def call(env)
112
+ # for thread safety
113
+ dup._call(env)
114
+ end
115
+
116
+ def _call(env)
117
+ begin
118
+ @request = Safrano::Request.new(env)
119
+ @response = Safrano::Response.new
120
+
121
+ before.tap_error { |err| dispatch_error(err) }
122
+ .tap_valid { |res| dispatch }
123
+
124
+ # handle remaining Sequel errors that we couldnt prevent with our
125
+ # own pre-checks
126
+ rescue Sequel::Error => e
127
+ dispatch_error(SequelExceptionError.new(e))
128
+ end
129
+ @response.finish
130
+ end
131
+
132
+ # Set multiple response headers with Hash.
133
+ def headers(hash = nil)
134
+ @response.headers.merge! hash if hash
135
+ @response.headers
136
+ end
137
+
138
+ def self.enable_batch
139
+ @service_base.enable_batch
140
+ end
141
+
142
+ def self.path_prefix(path_pr)
143
+ @service_base.path_prefix path_pr
144
+ end
145
+
146
+ def self.get_service_base
147
+ @service_base
148
+ end
149
+
150
+ def self.set_servicebase(sbase)
151
+ @service_base = sbase
152
+ @service_base.enable_v1_service
153
+ @service_base.enable_v2_service
154
+ end
155
+
156
+ def self.publish_service(&block)
157
+ sbase = Safrano::ServiceBase.new
158
+ sbase.instance_eval(&block) if block_given?
159
+ sbase.finalize_publishing
160
+ set_servicebase(sbase)
161
+ end
162
+ end
163
+ end
164
+
165
+ # deprecated
166
+ # REMOVE 0.6
167
+ module OData
168
+ class ServerApp < Safrano::ServerApp
169
+ def self.publish_service(&block)
170
+ ::Safrano::Deprecation.deprecate('OData::ServerApp',
171
+ 'Use Safrano::ServerApp instead')
172
+ super
173
+ end
174
+ end
175
+ end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'rack/cors'
3
5
 
4
6
  module Rack
5
- module OData
7
+ module Safrano
6
8
  # just a Wrapper to ensure (force?) that mandatory middlewares are acutally
7
9
  # used
8
10
  class Builder < ::Rack::Builder
@@ -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,8 +1,9 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
+ require 'rfc2047'
4
5
 
5
- module OData
6
+ module Safrano
6
7
  # monkey patch deactivate Rack/multipart because it does not work on simple
7
8
  # OData $batch requests when the content-length
8
9
  # is not passed
@@ -16,6 +17,7 @@ module OData
16
17
  class AcceptEntry
17
18
  attr_accessor :params
18
19
  attr_reader :entry
20
+
19
21
  def initialize(entry)
20
22
  params = entry.scan(HEADER_PARAM).map! do |s|
21
23
  key, value = s.strip.split('=', 2)
@@ -95,7 +97,9 @@ module OData
95
97
  end
96
98
 
97
99
  def create_odata_walker
98
- @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)
99
103
  end
100
104
 
101
105
  def accept
@@ -109,13 +113,6 @@ module OData
109
113
  end
110
114
  end
111
115
 
112
- def uribase
113
- return @uribase if @uribase
114
-
115
- @uribase =
116
- "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
117
- end
118
-
119
116
  def accept?(type)
120
117
  preferred_type(type).to_s.include?(type)
121
118
  end
@@ -136,11 +133,13 @@ module OData
136
133
  def with_media_data
137
134
  if (filename = @env['HTTP_SLUG'])
138
135
 
139
- yield @env['rack.input'], content_type.split(';').first, filename
136
+ yield @env['rack.input'],
137
+ content_type.split(';').first,
138
+ Rfc2047.decode(filename)
140
139
 
141
140
  else
142
141
  ON_CGST_ERROR.call(self)
143
- return [400, {}, ['File upload error: Missing SLUG']]
142
+ [400, EMPTY_HASH, ['File upload error: Missing SLUG']]
144
143
  end
145
144
  end
146
145
 
@@ -151,56 +150,109 @@ module OData
151
150
  data = JSON.parse(body.read)
152
151
  rescue JSON::ParserError => e
153
152
  ON_CGST_ERROR.call(self)
154
- return [400, {}, ['JSON Parser Error while parsing payload : ',
155
- e.message]]
153
+ return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
154
+ e.message]]
156
155
  end
157
156
 
158
157
  yield data
159
158
 
160
159
  else # TODO: other formats
161
160
 
162
- [415, {}, []]
161
+ [415, EMPTY_HASH, EMPTY_ARRAY]
163
162
  end
164
163
  end
165
164
 
166
- def negotiate_service_version
167
- maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
168
- OData::ServiceBase.parse_data_service_version(rqv)
169
- else
170
- OData::MAX_DATASERVICE_VERSION
171
- end
172
- return OData::BadRequestError if maxv.nil?
173
- # client request an too old version --> 501
174
- return OData::NotImplementedError if maxv < OData::MIN_DATASERVICE_VERSION
175
-
176
- minv = if (rqv = env['HTTP_MINDATASERVICEVERSION'])
177
- OData::ServiceBase.parse_data_service_version(rqv)
178
- else
179
- OData::MIN_DATASERVICE_VERSION
180
- end
181
- return OData::BadRequestError if minv.nil?
182
- # client request an too new version --> 501
183
- return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
184
- return OData::BadRequestError if minv > maxv
185
-
186
- v = if (rqv = env['HTTP_DATASERVICEVERSION'])
187
- 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
188
178
  else
189
- OData::MAX_DATASERVICE_VERSION
179
+ Contract.valid(maxv)
190
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
191
189
 
192
- return OData::BadRequestError if v.nil?
193
- return OData::NotImplementedError if v > OData::MAX_DATASERVICE_VERSION
194
- return OData::NotImplementedError if v < OData::MIN_DATASERVICE_VERSION
195
-
196
- @service = nil
197
- @service = case maxv
198
- when '1'
199
- @service_base.v1
200
- when '2', '3', '4'
201
- @service_base.v2
202
- end
203
- nil
190
+ DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
191
+ 'DataServiceVersion could not be parsed'
192
+ ).freeze
193
+ def get_version
194
+ if (rqv = env['HTTP_DATASERVICEVERSION'])
195
+ if (m = DATASERVICEVERSION_RGX.match(rqv))
196
+ # client request an too new version --> 501
197
+ if ((v = m[1]) > Safrano::MAX_DATASERVICE_VERSION) or
198
+ (v < Safrano::MIN_DATASERVICE_VERSION)
199
+ Safrano::VersionNotImplementedError
200
+ else
201
+ Contract.valid(v)
202
+ end
203
+ else
204
+ DTSV_PARSE_ERROR
205
+ end
206
+ else
207
+ # not provided in request header --> take our maxv
208
+ Safrano::CV_MAX_DATASERVICE_VERSION
209
+ end
210
+ end
211
+
212
+ MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
213
+ 'MinDataServiceVersion could not be parsed'
214
+ ).freeze
215
+ def get_minversion
216
+ if (rqv = env['HTTP_MINDATASERVICEVERSION'])
217
+ if (m = DATASERVICEVERSION_RGX.match(rqv))
218
+ # client request an too new version --> 501
219
+ if (minv = m[1]) > Safrano::MAX_DATASERVICE_VERSION
220
+ Safrano::VersionNotImplementedError
221
+ else
222
+ Contract.valid(minv)
223
+ end
224
+ else
225
+ MIN_DTSV_PARSE_ERROR
226
+ end
227
+ else
228
+ # not provided in request header --> take ours
229
+ Safrano::CV_MIN_DATASERVICE_VERSION
230
+ end
231
+ end
232
+
233
+ MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
234
+ 'MinDataServiceVersion is larger as MaxDataServiceVersion'
235
+ ).freeze
236
+ def negotiate_service_version
237
+ get_maxversion.if_valid do |maxv|
238
+ get_minversion.if_valid do |minv|
239
+ return MAX_LT_MIN_DTSV_ERROR if minv > maxv
240
+
241
+ get_version.if_valid do |v|
242
+ @service = nil
243
+ @service = case maxv
244
+ when '1'
245
+ @service_base.v1
246
+ when '2', '3', '4'
247
+ @service_base.v2
248
+ else
249
+ return Safrano::VersionNotImplementedError
250
+ end
251
+
252
+ Contract::OK
253
+ end # valid get_version
254
+ end # valid get_minversion
255
+ end # valid get_maxversion
204
256
  end
205
257
  end
206
258
  end
@@ -1,9 +1,10 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
+
2
3
  require 'rack'
3
4
 
4
5
  # monkey patch deactivate Rack/multipart because it does not work on simple
5
6
  # OData $batch requests when the content-length is not passed
6
- module OData
7
+ module Safrano
7
8
  # borrowed fro Sinatra
8
9
  # The response object. See Rack::Response and Rack::Response::Helpers for
9
10
  # more info:
@@ -13,7 +14,7 @@ module OData
13
14
  DROP_BODY_RESPONSES = [204, 205, 304].freeze
14
15
  def initialize(*)
15
16
  super
16
- headers['Content-Type'] ||= 'text/html'
17
+ headers[CONTENT_TYPE] ||= APPJSON_UTF8
17
18
  end
18
19
 
19
20
  def body=(value)
@@ -35,7 +36,7 @@ module OData
35
36
 
36
37
  if drop_body?
37
38
  close
38
- result = []
39
+ result = EMPTY_ARRAY
39
40
  end
40
41
 
41
42
  if calculate_content_length?
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../sequel/plugins/join_by_paths'
4
+
5
+ Sequel::Model.plugin Sequel::Plugins::JoinByPaths