safrano 0.3.3 → 0.4.3

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -85,7 +85,7 @@ module OData
85
85
  end
86
86
  end
87
87
 
88
- def get_metadata_xml_attribs(from, to, assoc_type, xnamespace)
88
+ def get_metadata_xml_attribs(from, to, assoc_type, xnamespace, attrname)
89
89
  rel = get([from, to])
90
90
  # use Sequel reflection to get multiplicity (will be used later
91
91
  # in 2. Associations below)
@@ -107,7 +107,7 @@ module OData
107
107
  # <NavigationProperty Name="Supplier"
108
108
  # Relationship="ODataDemo.Product_Supplier_Supplier_Products"
109
109
  # FromRole="Product_Supplier" ToRole="Supplier_Products"/>
110
- { 'Name' => to, 'Relationship' => "#{xnamespace}.#{rel.name}",
110
+ { 'Name' => attrname, 'Relationship' => "#{xnamespace}.#{rel.name}",
111
111
  'FromRole' => from, 'ToRole' => to }
112
112
  end
113
113
  end
@@ -0,0 +1,42 @@
1
+ require 'odata/error.rb'
2
+
3
+ # all dataset selecting related classes in our OData module
4
+ # ie do eager loading
5
+ module OData
6
+ # base class for selecting. We have to distinguish between
7
+ # fields of the current entity, and the navigation properties
8
+ # we can have one special case
9
+ # empty, ie no $select specified --> return all fields and all nav props
10
+ # ==> SelectAll
11
+
12
+ class SelectBase
13
+ ALL = new # re-useable selecting-all handler
14
+
15
+ def self.factory(selectstr)
16
+ case selectstr&.strip
17
+ when nil, '', '*'
18
+ ALL
19
+ else
20
+ Select.new(selectstr)
21
+ end
22
+ end
23
+
24
+ def all_props?
25
+ false
26
+ end
27
+
28
+ def ALL.all_props?
29
+ true
30
+ end
31
+ end
32
+
33
+ # single select
34
+ class Select < SelectBase
35
+ COMASPLIT = /\s*,\s*/.freeze
36
+ attr_reader :props
37
+ def initialize(selstr)
38
+ @selectp = selstr.strip
39
+ @props = @selectp.split(COMASPLIT)
40
+ end
41
+ end
42
+ end
@@ -3,57 +3,72 @@ require 'odata/error.rb'
3
3
  # url parameters processing . Mostly delegates to specialised classes
4
4
  # (filter, order...) to convert into Sequel exprs.
5
5
  module OData
6
- class UrlParameters
6
+ class UrlParametersBase
7
+ attr_reader :expand
8
+ attr_reader :select
9
+
10
+ def check_expand
11
+ return BadRequestExpandParseError if @expand.parse_error?
12
+ end
13
+ end
14
+
15
+ # url parameters for a single entity expand/select
16
+ class UrlParameters4Single < UrlParametersBase
17
+ def initialize(params)
18
+ @params = params
19
+ @expand = ExpandBase.factory(@params['$expand'])
20
+ @select = SelectBase.factory(@params['$select'])
21
+ end
22
+ end
23
+
24
+ # url parameters for a collection expand/select + filter/order
25
+ class UrlParameters4Coll < UrlParametersBase
7
26
  attr_reader :filt
8
27
  attr_reader :ordby
9
- def initialize(jh, params)
10
- @jh = jh
28
+
29
+ def initialize(model, params)
30
+ # join helper is only needed for odering or filtering
31
+ @jh = model.join_by_paths_helper if params['$orderby'] || params['$filter']
11
32
  @params = params
33
+ @ordby = OrderBase.factory(@params['$orderby'], @jh)
34
+ @filt = FilterBase.factory(@params['$filter'])
35
+ @expand = ExpandBase.factory(@params['$expand'])
36
+ @select = SelectBase.factory(@params['$select'])
12
37
  end
13
38
 
14
39
  def check_filter
15
- return unless @params['$filter']
16
-
17
- @filt = FilterByParse.new(@params['$filter'], @jh)
18
40
  return BadRequestFilterParseError if @filt.parse_error?
19
-
20
- # nil is the expected return for no errors
21
- nil
22
41
  end
23
42
 
24
43
  def check_order
25
- return unless @params['$orderby']
44
+ return BadRequestOrderParseError if @ordby.parse_error?
45
+ end
26
46
 
27
- pordlist = @params['$orderby'].dup
28
- pordlist.split(',').each do |pord|
29
- pord.strip!
30
- qualfn, dir = pord.split(/\s/)
31
- qualfn.strip!
32
- dir.strip! if dir
33
- return BadRequestError unless @jh.start_model.attrib_path_valid? qualfn
34
- return BadRequestError unless [nil, 'asc', 'desc'].include? dir
35
- end
47
+ def apply_to_dataset(dtcx)
48
+ dtcx = apply_expand_to_dataset(dtcx)
49
+ apply_filter_order_to_dataset(dtcx)
50
+ end
36
51
 
37
- @ordby = Order.new_by_parse(@params['$orderby'], @jh)
52
+ def apply_expand_to_dataset(dtcx)
53
+ return dtcx if @expand.empty?
38
54
 
39
- # nil is the expected return for no errors
40
- nil
55
+ @expand.apply_to_dataset(dtcx)
41
56
  end
42
57
 
43
- def apply_to_dataset(dtcx)
44
- return dtcx if (@filt.nil? && @ordby.nil?)
45
-
46
- if @filt.nil?
47
- dtcx = @jh.dataset.select_all(@jh.start_model.table_name)
48
-
49
- @ordby.apply_to_dataset(dtcx)
50
- elsif @ordby.nil?
51
- @filt.apply_to_dataset(dtcx)
52
- else
53
- filtexpr = @filt.sequel_expr
54
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
55
- @ordby.apply_to_dataset(dtcx)
56
- end
58
+ # Warning, the @ordby and @filt objects are coupled by way of the join helper
59
+ def apply_filter_order_to_dataset(dtcx)
60
+ return dtcx if @filt.empty? && @ordby.empty?
61
+
62
+ # filter object and join-helper need to be finalized after filter has been parsed and checked
63
+ @filt.finalize(@jh)
64
+
65
+ # start with the join
66
+ dtcx = @jh.dataset(dtcx)
67
+
68
+ dtcx = @filt.apply_to_dataset(dtcx)
69
+ dtcx = @ordby.apply_to_dataset(dtcx)
70
+
71
+ dtcx.select_all(@jh.start_model.table_name)
57
72
  end
58
73
  end
59
74
  end
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'json'
4
2
  require 'rexml/document'
5
3
  require 'safrano.rb'
@@ -29,9 +27,11 @@ module OData
29
27
 
30
28
  # are $links requested ?
31
29
  attr_reader :do_links
32
-
30
+ NIL_SERVICE_FATAL = 'Walker is called with a nil service'.freeze
31
+ EMPTYSTR = ''.freeze
32
+ SLASH = '/'.freeze
33
33
  def initialize(service, path, content_id_refs = nil)
34
- raise 'Walker is called with a nil service' unless service
34
+ raise NIL_SERVICE_FATAL unless service
35
35
 
36
36
  path = URI.decode_www_form_component(path)
37
37
  @context = service
@@ -53,12 +53,12 @@ module OData
53
53
  end
54
54
 
55
55
  def unprefixed(prefix, path)
56
- if (prefix == '') || (prefix == '/')
56
+ if (prefix == EMPTYSTR) || (prefix == SLASH)
57
57
  path
58
58
  else
59
59
  # path.sub!(/\A#{prefix}/, '')
60
60
  # TODO check
61
- path.sub(/\A#{prefix}/, '')
61
+ path.sub(/\A#{prefix}/, EMPTYSTR)
62
62
  end
63
63
  end
64
64
 
@@ -1,19 +1,18 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'json'
4
2
  require 'rexml/document'
5
- require_relative './multipart.rb'
6
- require 'safrano_core.rb'
7
- require 'odata/entity.rb'
8
- require 'odata/attribute.rb'
9
- require 'odata/navigation_attribute.rb'
10
- require 'odata/collection.rb'
11
- require 'service.rb'
12
- require 'odata/walker.rb'
3
+ require_relative 'safrano/multipart.rb'
4
+ require_relative 'safrano/core.rb'
5
+ require_relative 'odata/entity.rb'
6
+ require_relative 'odata/attribute.rb'
7
+ require_relative 'odata/navigation_attribute.rb'
8
+ require_relative 'odata/collection.rb'
9
+ require_relative 'safrano/service.rb'
10
+ require_relative 'odata/walker.rb'
13
11
  require 'sequel'
14
- require_relative './sequel_join_by_paths.rb'
15
- require 'rack_app'
16
- require 'odata_rack_builder'
12
+ require_relative 'safrano/sequel_join_by_paths.rb'
13
+ require_relative 'safrano/rack_app'
14
+ require_relative 'safrano/odata_rack_builder'
15
+ require_relative 'safrano/version'
17
16
 
18
17
  # picked from activsupport; needed for ruby < 2.5
19
18
  # Destructively converts all keys using the +block+ operations.
@@ -30,3 +29,14 @@ class Hash
30
29
  transform_keys! { |key| key.to_sym rescue key }
31
30
  end
32
31
  end
32
+
33
+ # needed for ruby < 2.5
34
+ class Dir
35
+ def self.each_child(dir)
36
+ Dir.foreach(dir) do |x|
37
+ next if (x == '.') || (x == '..')
38
+
39
+ yield x
40
+ end
41
+ end unless respond_to? :each_child
42
+ end
@@ -1,7 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  # our main namespace
4
2
  module OData
3
+ # frozen empty Array/Hash to reduce unncecessary object creation
4
+ EMPTY_ARRAY = [].freeze
5
+ EMPTY_HASH = {}.freeze
6
+ EMPTY_HASH_IN_ARY = [EMPTY_HASH].freeze
7
+ EMPTY_STRING = ''.freeze
8
+ ARY_204_EMPTY_HASH_ARY = [204, EMPTY_HASH, EMPTY_ARRAY].freeze
9
+ SPACE = ' '.freeze
10
+ COMMA = ','.freeze
11
+
5
12
  # some prominent constants... probably already defined elsewhere eg in Rack
6
13
  # but lets KISS
7
14
  CONTENT_TYPE = 'Content-Type'.freeze
@@ -32,14 +39,15 @@ module OData
32
39
  # database-specific types.
33
40
  DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
34
41
 
42
+ # TODO... complete; used in $metadata
35
43
  def self.get_edm_type(db_type:)
36
- case db_type
44
+ case db_type.upcase
37
45
  when 'INTEGER'
38
46
  'Edm.Int32'
39
47
  when 'TEXT', 'STRING'
40
48
  'Edm.String'
41
49
  else
42
- 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type
50
+ 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type.upcase
43
51
  end
44
52
  end
45
53
  end
@@ -6,14 +6,15 @@ require 'webrick/httpstatus'
6
6
 
7
7
  # Simple multipart support for OData $batch purpose
8
8
  module MIME
9
+ CTT_TYPE_LC = 'content-type'.freeze
10
+ TEXT_PLAIN = 'text/plain'.freeze
11
+
9
12
  # a mime object has a header(with content-type etc) and a content(aka body)
10
13
  class Media
11
14
  # Parser for MIME::Media
12
15
  class Parser
13
16
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
14
17
 
15
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
16
-
17
18
  attr_accessor :lines
18
19
  attr_accessor :target
19
20
  def initialize
@@ -54,9 +55,8 @@ module MIME
54
55
  if (hmd = HMD_RGX.match(line))
55
56
  @target_hd[hmd[1].downcase] = hmd[2].strip
56
57
 
57
- # elsif CRLF_LINE_RGX =~ line
58
58
  elsif CRLF == line
59
- @target_ct = @target_hd['content-type'] || 'text/plain'
59
+ @target_ct = @target_hd[CTT_TYPE_LC] || TEXT_PLAIN
60
60
  @state = new_content
61
61
 
62
62
  end
@@ -100,8 +100,8 @@ module MIME
100
100
  end
101
101
 
102
102
  def hook_multipart(content_type, boundary)
103
- @target_hd['content-type'] = content_type
104
- @target_ct = @target_hd['content-type']
103
+ @target_hd[CTT_TYPE_LC] = content_type
104
+ @target_ct = @target_hd[CTT_TYPE_LC]
105
105
  @target = multipart_content(boundary)
106
106
  @target.hd = @target_hd
107
107
  @target.ct = @target_ct
@@ -114,10 +114,8 @@ module MIME
114
114
  APP_HTTP = 'application/http'.freeze
115
115
  def new_content
116
116
  @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
- )
117
+ if @target_ct.start_with?(MPS) &&
118
+ (md = (MP_RGX1.match(@target_ct[10..-1]) || MP_RGX2.match(@target_ct[10..-1])))
121
119
  multipart_content(md[2].strip)
122
120
  elsif @target_ct.start_with?(APP_HTTP)
123
121
  MIME::Content::Application::Http.new
@@ -198,7 +196,6 @@ module MIME
198
196
  # Parser for Text::Plain
199
197
  class Parser
200
198
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
201
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
202
199
  def initialize(target)
203
200
  @state = :h
204
201
  @lines = []
@@ -212,7 +209,6 @@ module MIME
212
209
  def parse_head(line)
213
210
  if (hmd = HMD_RGX.match(line))
214
211
  @target.hd[hmd[1].downcase] = hmd[2].strip
215
- # elsif CRLF_LINE_RGX =~ line
216
212
  elsif CRLF == line
217
213
  @state = :b
218
214
  else
@@ -243,8 +239,8 @@ module MIME
243
239
  @hd = {}
244
240
  @content = ''
245
241
  # set default values. Can be overwritten by parser
246
- @hd['content-type'] = 'text/plain'
247
- @ct = 'text/plain'
242
+ @hd[CTT_TYPE_LC] = TEXT_PLAIN
243
+ @ct = TEXT_PLAIN
248
244
  @parser = Parser.new(self)
249
245
  end
250
246
 
@@ -364,7 +360,7 @@ module MIME
364
360
  end
365
361
 
366
362
  def set_multipart_header
367
- @hd['content-type'] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
363
+ @hd[CTT_TYPE_LC] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
368
364
  end
369
365
 
370
366
  def get_http_resp(batcha)
@@ -382,9 +378,7 @@ module MIME
382
378
  # of the changes
383
379
  batcha.db.transaction do
384
380
  begin
385
- @response.content = @content.map { |part|
386
- part.get_response(batcha)
387
- }
381
+ @response.content = @content.map { |part| part.get_response(batcha) }
388
382
  rescue Sequel::Rollback => e
389
383
  # one of the changes of the changeset has failed
390
384
  # --> provide a dummy empty response for the change-parts
@@ -413,10 +407,11 @@ module MIME
413
407
  b = ''
414
408
  unless bodyonly
415
409
  # b << OData::CONTENT_TYPE << ': ' << @hd[OData::CTT_TYPE_LC] << CRLF
416
- b << "#{OData::CONTENT_TYPE}: #{@hd[OData::CTT_TYPE_LC]}#{CRLF}"
410
+ b << "#{OData::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
417
411
  end
418
- b << "#{CRLF}--#{@boundary}#{CRLF}"
419
- b << @content.map(&:unparse).join("#{CRLF}--#{@boundary}#{CRLF}")
412
+
413
+ b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
414
+ b << @content.map(&:unparse).join(crbdcr)
420
415
  b << "#{CRLF}--#{@boundary}--#{CRLF}"
421
416
  b
422
417
  end
@@ -442,7 +437,6 @@ module MIME
442
437
  def unparse
443
438
  b = "#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
444
439
  @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
445
- # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
446
440
  b << CRLF
447
441
  b << @content if @content != ''
448
442
  b
@@ -473,7 +467,6 @@ module MIME
473
467
  b = String.new(APPLICATION_HTTP_11)
474
468
  b << "#{@status} #{StatusMessage[@status]} #{CRLF}"
475
469
  @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
476
- # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
477
470
  b << CRLF
478
471
  b << @content.join if @content
479
472
  b
@@ -482,7 +475,7 @@ module MIME
482
475
 
483
476
  # For application/http . Content is either a Request or a Response
484
477
  class Http < Media
485
- HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH'.freeze
478
+ HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH|DELETE'.freeze
486
479
  HTTP_R_RGX = %r{^(#{HTTP_MTHS_RGX})\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
487
480
  HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
488
481
  HTTP_RESP_RGX = %r{^HTTP/1\.1\s(\d+)\s}.freeze
@@ -490,7 +483,6 @@ module MIME
490
483
  # Parser for Http Media
491
484
  class Parser
492
485
  HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
493
- CRLF_LINE_RGX = /^#{CRLF}$/.freeze
494
486
 
495
487
  def initialize(target)
496
488
  @state = :http
@@ -523,7 +515,6 @@ module MIME
523
515
  if (hmd = HMD_RGX.match(line))
524
516
  @target.content.hd[hmd[1].downcase] = hmd[2].strip
525
517
  elsif CRLF == line
526
- # elsif CRLF_LINE_RGX =~ line
527
518
  @state = :b
528
519
  else
529
520
  @body_lines << line
@@ -10,7 +10,6 @@ module Rack
10
10
  super(default_app) {}
11
11
  use ::Rack::Cors
12
12
  instance_eval(&block) if block_given?
13
- use ::Rack::Lint
14
13
  use ::Rack::ContentLength
15
14
  end
16
15
  end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rack'
4
- require_relative 'odata/walker.rb'
4
+ require_relative '../odata/walker.rb'
5
5
  require_relative 'request.rb'
6
6
  require_relative 'response.rb'
7
7
 
@@ -14,7 +14,7 @@ module OData
14
14
  x = if @walker.status == :end
15
15
  headers.delete('Content-Type')
16
16
  @response.headers.delete('Content-Type')
17
- [200, {}, '']
17
+ [200, EMPTY_HASH, '']
18
18
  else
19
19
  odata_error
20
20
  end
@@ -70,20 +70,20 @@ module OData
70
70
  end
71
71
 
72
72
  def odata_head
73
- [200, {}, ['']]
73
+ [200, EMPTY_HASH, [EMPTY_STRING]]
74
74
  end
75
75
  end
76
76
 
77
77
  # the main Rack server app. Source: the Rack docu/examples and partly
78
78
  # inspired from Sinatra
79
79
  class ServerApp
80
- METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE')
80
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
81
+ NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
82
+ 'Expires' => '-1',
83
+ 'Pragma' => 'no-cache' }.freeze
84
+ DATASERVICEVERSION = 'DataServiceVersion'.freeze
81
85
  include MethodHandlers
82
86
  def before
83
- headers 'Cache-Control' => 'no-cache'
84
- headers 'Expires' => '-1'
85
- headers 'Pragma' => 'no-cache'
86
-
87
87
  @request.service_base = self.class.get_service_base
88
88
 
89
89
  neg_error = @request.negotiate_service_version
@@ -92,7 +92,9 @@ module OData
92
92
 
93
93
  return false unless @request.service
94
94
 
95
- headers 'DataServiceVersion' => @request.service.data_service_version
95
+ myhdrs = NOCACHE_HDRS.dup
96
+ myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
97
+ headers myhdrs
96
98
  end
97
99
 
98
100
  # dispatch for all methods requiring parsing of the path
@@ -119,7 +121,7 @@ module OData
119
121
 
120
122
  def dispatch
121
123
  req_ret = if @request.request_method !~ METHODS_REGEXP
122
- [404, {}, ['Did you get lost?']]
124
+ [404, EMPTY_HASH, ['Did you get lost?']]
123
125
  elsif @request.request_method == 'HEAD'
124
126
  odata_head
125
127
  else