safrano 0.4.0 → 0.4.5

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 +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