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
@@ -0,0 +1,73 @@
1
+ # frozen-string-literal: true
2
+
3
+ # shamelessly copied from Sequel...
4
+
5
+ module Safrano
6
+ # This module makes it easy to print deprecation warnings with optional backtraces to a given stream.
7
+ # There are a two accessors you can use to change how/where the deprecation methods are printed
8
+ # and whether/how backtraces should be included:
9
+ #
10
+ # Safrano::Deprecation.output = $stderr # print deprecation messages to standard error (default)
11
+ # Safrano::Deprecation.output = File.open('deprecated_calls.txt', 'wb') # use a file instead
12
+ # Safrano::Deprecation.output = false # do not output deprecation messages
13
+ #
14
+ # Safrano::Deprecation.prefix = "SAFRANO DEPRECATION WARNING: " # prefix deprecation messages with a given string (default)
15
+ # Safrano::Deprecation.prefix = false # do not prefix deprecation messages
16
+ #
17
+ # Safrano::Deprecation.backtrace_filter = false # don't include backtraces
18
+ # Safrano::Deprecation.backtrace_filter = true # include full backtraces
19
+ # Safrano::Deprecation.backtrace_filter = 2 # include 2 backtrace lines (default)
20
+ # Safrano::Deprecation.backtrace_filter = 1 # include 1 backtrace line
21
+ # Safrano::Deprecation.backtrace_filter = lambda{|line, line_no| line_no < 3 || line =~ /my_app/} # select backtrace lines to output
22
+ module Deprecation
23
+ @backtrace_filter = false
24
+ @output = $stderr
25
+ @prefix = 'SAFRANO DEPRECATION WARNING: '
26
+
27
+ class << self
28
+ # How to filter backtraces. +false+ does not include backtraces, +true+ includes
29
+ # full backtraces, an Integer includes that number of backtrace lines, and
30
+ # a proc is called with the backtrace line and line number to select the backtrace
31
+ # lines to include. The default is no backtrace .
32
+ attr_accessor :backtrace_filter
33
+
34
+ # Where deprecation messages should be output, must respond to puts. $stderr by default.
35
+ attr_accessor :output
36
+
37
+ # Where deprecation messages should be prefixed with ("SEQUEL DEPRECATION WARNING: " by default).
38
+ attr_accessor :prefix
39
+ end
40
+
41
+ # Print the message and possibly backtrace to the output.
42
+ def self.deprecate(method, instead = nil)
43
+ return unless output
44
+
45
+ message = instead ? "#{method} is deprecated and will be removed in Safrano 0.6. #{instead}." : method
46
+ message = "#{prefix}#{message}" if prefix
47
+ output.puts(message)
48
+ case b = backtrace_filter
49
+ when Integer
50
+ caller.each do |c|
51
+ b -= 1
52
+ output.puts(c)
53
+ break if b <= 0
54
+ end
55
+ when true
56
+ caller.each { |c| output.puts(c) }
57
+ when Proc
58
+ caller.each_with_index { |line, line_no| output.puts(line) if b.call(line, line_no) }
59
+ end
60
+ nil
61
+ end
62
+
63
+ # If using ruby 2.3+, use Module#deprecate_constant to deprecate the constant,
64
+ # otherwise do nothing as the ruby implementation does not support constant deprecation.
65
+ def self.deprecate_constant(mod, constant)
66
+ # :nocov:
67
+ return unless RUBY_VERSION > '2.3'
68
+
69
+ # :nocov:
70
+ mod.deprecate_constant(constant)
71
+ end
72
+ end
73
+ end
@@ -1,21 +1,25 @@
1
- CRLF = "\r\n".freeze
2
- LF = "\n".freeze
1
+ # frozen_string_literal: true
2
+
3
+ CRLF = "\r\n"
4
+ LF = "\n"
3
5
 
4
6
  require 'securerandom'
5
7
  require 'webrick/httpstatus'
6
8
 
7
9
  # Simple multipart support for OData $batch purpose
8
10
  module MIME
11
+ CTT_TYPE_LC = 'content-type'
12
+ TEXT_PLAIN = 'text/plain'
13
+
9
14
  # a mime object has a header(with content-type etc) and a content(aka body)
10
15
  class Media
11
16
  # Parser for MIME::Media
12
17
  class Parser
13
18
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
14
19
 
15
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
16
-
17
20
  attr_accessor :lines
18
21
  attr_accessor :target
22
+
19
23
  def initialize
20
24
  @state = :h
21
25
  @lines = []
@@ -54,9 +58,8 @@ module MIME
54
58
  if (hmd = HMD_RGX.match(line))
55
59
  @target_hd[hmd[1].downcase] = hmd[2].strip
56
60
 
57
- # elsif CRLF_LINE_RGX =~ line
58
61
  elsif CRLF == line
59
- @target_ct = @target_hd['content-type'] || 'text/plain'
62
+ @target_ct = @target_hd[CTT_TYPE_LC] || TEXT_PLAIN
60
63
  @state = new_content
61
64
 
62
65
  end
@@ -100,24 +103,22 @@ module MIME
100
103
  end
101
104
 
102
105
  def hook_multipart(content_type, boundary)
103
- @target_hd['content-type'] = content_type
104
- @target_ct = @target_hd['content-type']
106
+ @target_hd[CTT_TYPE_LC] = content_type
107
+ @target_ct = @target_hd[CTT_TYPE_LC]
105
108
  @target = multipart_content(boundary)
106
109
  @target.hd = @target_hd
107
110
  @target.ct = @target_ct
108
111
  @state = :bmp
109
112
  end
110
- MPS = 'multipart/'.freeze
111
- MP_RGX1 = %r{^(digest|mixed);\s*boundary=\"(.*)\"}.freeze
113
+ MPS = 'multipart/'
114
+ MP_RGX1 = %r{^(digest|mixed);\s*boundary="(.*)"}.freeze
112
115
  MP_RGX2 = %r{^(digest|mixed);\s*boundary=(.*)}.freeze
113
116
  # APP_HTTP_RGX = %r{^application/http}.freeze
114
- APP_HTTP = 'application/http'.freeze
117
+ APP_HTTP = 'application/http'
115
118
  def new_content
116
119
  @target =
117
- if @target_ct.start_with?(MPS) and
118
- (md = ((MP_RGX1.match(@target_ct[10..-1])) ||
119
- (MP_RGX2.match(@target_ct[10..-1])))
120
- )
120
+ if @target_ct.start_with?(MPS) &&
121
+ (md = (MP_RGX1.match(@target_ct[10..-1]) || MP_RGX2.match(@target_ct[10..-1])))
121
122
  multipart_content(md[2].strip)
122
123
  elsif @target_ct.start_with?(APP_HTTP)
123
124
  MIME::Content::Application::Http.new
@@ -198,7 +199,6 @@ module MIME
198
199
  # Parser for Text::Plain
199
200
  class Parser
200
201
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
201
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
202
202
  def initialize(target)
203
203
  @state = :h
204
204
  @lines = []
@@ -212,7 +212,6 @@ module MIME
212
212
  def parse_head(line)
213
213
  if (hmd = HMD_RGX.match(line))
214
214
  @target.hd[hmd[1].downcase] = hmd[2].strip
215
- # elsif CRLF_LINE_RGX =~ line
216
215
  elsif CRLF == line
217
216
  @state = :b
218
217
  else
@@ -241,10 +240,11 @@ module MIME
241
240
 
242
241
  def initialize
243
242
  @hd = {}
244
- @content = ''
243
+ # we need it unfrozen --> +
244
+ @content = +''
245
245
  # set default values. Can be overwritten by parser
246
- @hd['content-type'] = 'text/plain'
247
- @ct = 'text/plain'
246
+ @hd[CTT_TYPE_LC] = TEXT_PLAIN
247
+ @ct = TEXT_PLAIN
248
248
  @parser = Parser.new(self)
249
249
  end
250
250
 
@@ -309,8 +309,10 @@ module MIME
309
309
  # to remove it from the end of the last body line
310
310
  return unless @body_lines
311
311
 
312
- # @body_lines.last.sub!(CRLF_ENDING_RGX, '')
313
- @body_lines.last.chomp!(CRLF)
312
+ # the last line ends up frozen --> chomp! fails
313
+ # @body_lines.last.chomp!(CRLF)
314
+ last_line = @body_lines.pop.chomp(CRLF)
315
+ @body_lines.push last_line
314
316
  @parts << @body_lines
315
317
  end
316
318
 
@@ -364,7 +366,7 @@ module MIME
364
366
  end
365
367
 
366
368
  def set_multipart_header
367
- @hd['content-type'] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
369
+ @hd[CTT_TYPE_LC] = "#{Safrano::MP_MIXED}; boundary=#{@boundary}"
368
370
  end
369
371
 
370
372
  def get_http_resp(batcha)
@@ -382,9 +384,7 @@ module MIME
382
384
  # of the changes
383
385
  batcha.db.transaction do
384
386
  begin
385
- @response.content = @content.map { |part|
386
- part.get_response(batcha)
387
- }
387
+ @response.content = @content.map { |part| part.get_response(batcha) }
388
388
  rescue Sequel::Rollback => e
389
389
  # one of the changes of the changeset has failed
390
390
  # --> provide a dummy empty response for the change-parts
@@ -405,18 +405,18 @@ module MIME
405
405
  @response.content = [{ 'odata.error' =>
406
406
  { 'message' =>
407
407
  'Bad Request: Failed changeset ' } }.to_json]
408
- @response.hd = OData::CT_JSON
408
+ @response.hd = Safrano::CT_JSON
409
409
  @response
410
410
  end
411
411
 
412
412
  def unparse(bodyonly = false)
413
- b = ''
413
+ b = +String.new
414
414
  unless bodyonly
415
- # b << OData::CONTENT_TYPE << ': ' << @hd[OData::CTT_TYPE_LC] << CRLF
416
- b << "#{OData::CONTENT_TYPE}: #{@hd[OData::CTT_TYPE_LC]}#{CRLF}"
415
+ b << "#{Safrano::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
417
416
  end
418
- b << "#{CRLF}--#{@boundary}#{CRLF}"
419
- b << @content.map(&:unparse).join("#{CRLF}--#{@boundary}#{CRLF}")
417
+
418
+ b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
419
+ b << @content.map(&:unparse).join(crbdcr)
420
420
  b << "#{CRLF}--#{@boundary}--#{CRLF}"
421
421
  b
422
422
  end
@@ -436,15 +436,14 @@ module MIME
436
436
 
437
437
  def initialize
438
438
  @hd = {}
439
- @content = ''
439
+ @content = +String.new
440
440
  end
441
441
 
442
442
  def unparse
443
- b = "#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
443
+ b = +"#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
444
444
  @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
445
- # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
446
445
  b << CRLF
447
- b << @content if @content != ''
446
+ b << @content unless content.empty?
448
447
  b
449
448
  end
450
449
  end
@@ -470,10 +469,9 @@ module MIME
470
469
  end
471
470
 
472
471
  def unparse
473
- b = String.new(APPLICATION_HTTP_11)
472
+ b = +String.new(APPLICATION_HTTP_11)
474
473
  b << "#{@status} #{StatusMessage[@status]} #{CRLF}"
475
474
  @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
476
- # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
477
475
  b << CRLF
478
476
  b << @content.join if @content
479
477
  b
@@ -482,15 +480,14 @@ module MIME
482
480
 
483
481
  # For application/http . Content is either a Request or a Response
484
482
  class Http < Media
485
- HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH|DELETE'.freeze
483
+ HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH|DELETE'
486
484
  HTTP_R_RGX = %r{^(#{HTTP_MTHS_RGX})\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
487
- HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
485
+ HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-/,\s]+;?\S*)\s*$}.freeze
488
486
  HTTP_RESP_RGX = %r{^HTTP/1\.1\s(\d+)\s}.freeze
489
487
 
490
488
  # Parser for Http Media
491
489
  class Parser
492
490
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
493
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
494
491
 
495
492
  def initialize(target)
496
493
  @state = :http
@@ -523,7 +520,6 @@ module MIME
523
520
  if (hmd = HMD_RGX.match(line))
524
521
  @target.content.hd[hmd[1].downcase] = hmd[2].strip
525
522
  elsif CRLF == line
526
- # elsif CRLF_LINE_RGX =~ line
527
523
  @state = :b
528
524
  else
529
525
  @body_lines << line
@@ -575,7 +571,7 @@ module MIME
575
571
  end
576
572
 
577
573
  def unparse
578
- b = "Content-Type: #{@ct}#{CRLF}"
574
+ b = +"Content-Type: #{@ct}#{CRLF}"
579
575
  b << "Content-Transfer-Encoding: binary#{CRLF}#{CRLF}"
580
576
  b << @content.unparse
581
577
  b
@@ -1,98 +1,72 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
- require_relative '../odata/walker.rb'
5
- require_relative 'request.rb'
6
- require_relative 'response.rb'
4
+ require_relative '../odata/walker'
5
+ require_relative 'request'
6
+ require_relative 'response'
7
7
 
8
- module OData
8
+ module Safrano
9
9
  # handle GET PUT etc
10
10
  module MethodHandlers
11
11
  def odata_options
12
- # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
13
-
14
- x = if @walker.status == :end
15
- headers.delete('Content-Type')
16
- @response.headers.delete('Content-Type')
17
- [200, {}, '']
18
- else
19
- odata_error
20
- end
21
- @response.headers['Content-Type'] = ''
22
- x
23
- end
24
-
25
- def odata_error
26
- return @walker.error.odata_get(@request) unless @walker.error.nil?
27
-
28
- # this is too critical; raise a real Exception
29
- raise 'Walker construction failed with a unknown Error '
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
30
20
  end
31
21
 
32
22
  def odata_delete
33
- if @walker.status == :end
34
- @walker.end_context.odata_delete(@request)
35
- else
36
- odata_error
37
- end
23
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
24
+ .if_valid { |context| context.odata_delete(@request) }
38
25
  end
39
26
 
40
27
  def odata_put
41
- if @walker.status == :end
42
- @walker.end_context.odata_put(@request)
43
- else
44
- odata_error
45
- end
28
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
29
+ .if_valid { |context| context.odata_put(@request) }
46
30
  end
47
31
 
48
32
  def odata_patch
49
- if @walker.status == :end
50
- @walker.end_context.odata_patch(@request)
51
- else
52
- odata_error
53
- end
33
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
34
+ .if_valid { |context| context.odata_patch(@request) }
54
35
  end
55
36
 
56
37
  def odata_get
57
- if @walker.status == :end
58
- @walker.end_context.odata_get(@request)
59
- else
60
- odata_error
61
- end
38
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
39
+ .if_valid { |context| context.odata_get(@request) }
62
40
  end
63
41
 
64
42
  def odata_post
65
- if @walker.status == :end
66
- @walker.end_context.odata_post(@request)
67
- else
68
- odata_error
69
- end
43
+ @walker.finalize.tap_error { |err| return err.odata_get(@request) }
44
+ .if_valid { |context| context.odata_post(@request) }
70
45
  end
71
46
 
72
47
  def odata_head
73
- [200, {}, ['']]
48
+ [200, EMPTY_HASH, [EMPTY_STRING]]
74
49
  end
75
50
  end
76
51
 
77
52
  # the main Rack server app. Source: the Rack docu/examples and partly
78
53
  # inspired from Sinatra
79
54
  class ServerApp
80
- METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE')
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'
81
60
  include MethodHandlers
82
- def before
83
- headers 'Cache-Control' => 'no-cache'
84
- headers 'Expires' => '-1'
85
- headers 'Pragma' => 'no-cache'
86
61
 
62
+ def before
87
63
  @request.service_base = self.class.get_service_base
88
64
 
89
- neg_error = @request.negotiate_service_version
90
-
91
- raise RuntimeError if neg_error
92
-
93
- return false unless @request.service
94
-
95
- headers 'DataServiceVersion' => @request.service.data_service_version
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
96
70
  end
97
71
 
98
72
  # dispatch for all methods requiring parsing of the path
@@ -117,9 +91,14 @@ module OData
117
91
  end
118
92
  end
119
93
 
94
+ def dispatch_error(err)
95
+ @response.status, rsph, @response.body = err.odata_get(@request)
96
+ headers rsph
97
+ end
98
+
120
99
  def dispatch
121
100
  req_ret = if @request.request_method !~ METHODS_REGEXP
122
- [404, {}, ['Did you get lost?']]
101
+ [404, EMPTY_HASH, ['Did you get lost?']]
123
102
  elsif @request.request_method == 'HEAD'
124
103
  odata_head
125
104
  else
@@ -130,13 +109,23 @@ module OData
130
109
  end
131
110
 
132
111
  def call(env)
133
- @request = OData::Request.new(env)
134
- @response = OData::Response.new
112
+ # for thread safety
113
+ dup._call(env)
114
+ end
135
115
 
136
- before
116
+ def _call(env)
117
+ begin
118
+ @request = Safrano::Request.new(env)
119
+ @response = Safrano::Response.new
137
120
 
138
- dispatch
121
+ before.tap_error { |err| dispatch_error(err) }
122
+ .tap_valid { |res| dispatch }
139
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
140
129
  @response.finish
141
130
  end
142
131
 
@@ -165,10 +154,22 @@ module OData
165
154
  end
166
155
 
167
156
  def self.publish_service(&block)
168
- sbase = OData::ServiceBase.new
157
+ sbase = Safrano::ServiceBase.new
169
158
  sbase.instance_eval(&block) if block_given?
170
159
  sbase.finalize_publishing
171
160
  set_servicebase(sbase)
172
161
  end
173
162
  end
174
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