safrano 0.3.4 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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