safrano 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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