safrano 0.3.2 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'
@@ -21,9 +19,12 @@ module OData
21
19
  # is $count requested?
22
20
  attr_accessor :do_count
23
21
 
24
- # is $value requested?
22
+ # is $value (of attribute) requested?
25
23
  attr_reader :raw_value
26
24
 
25
+ # is $value (of media entity) requested?
26
+ attr_reader :media_value
27
+
27
28
  # are $links requested ?
28
29
  attr_reader :do_links
29
30
 
@@ -35,9 +36,11 @@ module OData
35
36
  @content_id_refs = content_id_refs
36
37
 
37
38
  @contexts = [@context]
39
+
38
40
  @path_start = @path_remain = if service
39
41
  unprefixed(service.xpath_prefix, path)
40
42
  else # This is for batch function
43
+
41
44
  path
42
45
  end
43
46
  @path_done = ''
@@ -51,7 +54,9 @@ module OData
51
54
  if (prefix == '') || (prefix == '/')
52
55
  path
53
56
  else
54
- path.sub!(/\A#{prefix}/, '')
57
+ # path.sub!(/\A#{prefix}/, '')
58
+ # TODO check
59
+ path.sub(/\A#{prefix}/, '')
55
60
  end
56
61
  end
57
62
 
@@ -107,6 +112,9 @@ module OData
107
112
  when :end_with_value
108
113
  @raw_value = true
109
114
  @status = :end
115
+ when :end_with_media_value
116
+ @media_value = true
117
+ @status = :end
110
118
  when :run_with_links
111
119
  @do_links = true
112
120
  @status = :run
@@ -1,18 +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/collection.rb'
10
- require 'service.rb'
11
- 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'
12
11
  require 'sequel'
13
- require_relative './sequel_join_by_paths.rb'
14
- require 'rack_app'
15
- 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'
16
16
 
17
17
  # picked from activsupport; needed for ruby < 2.5
18
18
  # Destructively converts all keys using the +block+ operations.
@@ -29,3 +29,14 @@ class Hash
29
29
  transform_keys! { |key| key.to_sym rescue key }
30
30
  end
31
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
@@ -2,13 +2,23 @@
2
2
 
3
3
  # our main namespace
4
4
  module OData
5
+ # frozen empty Array/Hash to reduce unncecessary object creation
6
+ EMPTY_ARRAY = [].freeze
7
+ EMPTY_HASH = {}.freeze
8
+ EMPTY_HASH_IN_ARY = [EMPTY_HASH].freeze
9
+ EMPTY_STRING = ''.freeze
10
+ ARY_204_EMPTY_HASH_ARY = [204, EMPTY_HASH, EMPTY_ARRAY].freeze
11
+ SPACE = ' '.freeze
12
+ COMMA = ','.freeze
13
+
5
14
  # some prominent constants... probably already defined elsewhere eg in Rack
6
15
  # but lets KISS
7
16
  CONTENT_TYPE = 'Content-Type'.freeze
17
+ CTT_TYPE_LC = 'content-type'.freeze
8
18
  TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'.freeze
9
19
  APPJSON = 'application/json'.freeze
10
20
  APPXML = 'application/xml'.freeze
11
-
21
+ MP_MIXED = 'multipart/mixed'.freeze
12
22
  APPXML_UTF8 = 'application/xml;charset=utf-8'.freeze
13
23
  APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'.freeze
14
24
  APPJSON_UTF8 = 'application/json;charset=utf-8'.freeze
@@ -31,14 +41,15 @@ module OData
31
41
  # database-specific types.
32
42
  DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
33
43
 
44
+ # TODO... complete; used in $metadata
34
45
  def self.get_edm_type(db_type:)
35
- case db_type
46
+ case db_type.upcase
36
47
  when 'INTEGER'
37
48
  'Edm.Int32'
38
49
  when 'TEXT', 'STRING'
39
50
  'Edm.String'
40
51
  else
41
- 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type
52
+ 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type.upcase
42
53
  end
43
54
  end
44
55
  end
@@ -6,10 +6,17 @@ 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
16
+ HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
17
+
18
+ CRLF_LINE_RGX = /^#{CRLF}$/.freeze
19
+
13
20
  attr_accessor :lines
14
21
  attr_accessor :target
15
22
  def initialize
@@ -47,11 +54,12 @@ module MIME
47
54
  end
48
55
 
49
56
  def parse_head(line)
50
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
57
+ if (hmd = HMD_RGX.match(line))
51
58
  @target_hd[hmd[1].downcase] = hmd[2].strip
52
59
 
53
- elsif /^#{CRLF}$/ =~ line
54
- @target_ct = @target_hd['content-type'] || 'text/plain'
60
+ # elsif CRLF_LINE_RGX =~ line
61
+ elsif CRLF == line
62
+ @target_ct = @target_hd[CTT_TYPE_LC] || TEXT_PLAIN
55
63
  @state = new_content
56
64
 
57
65
  end
@@ -95,21 +103,24 @@ module MIME
95
103
  end
96
104
 
97
105
  def hook_multipart(content_type, boundary)
98
- @target_hd['content-type'] = content_type
99
- @target_ct = @target_hd['content-type']
106
+ @target_hd[CTT_TYPE_LC] = content_type
107
+ @target_ct = @target_hd[CTT_TYPE_LC]
100
108
  @target = multipart_content(boundary)
101
109
  @target.hd = @target_hd
102
110
  @target.ct = @target_ct
103
111
  @state = :bmp
104
112
  end
105
- MP_RGX1 = %r{^multipart/(digest|mixed);\s*boundary=\"(.*)\"}.freeze
106
- MP_RGX2 = %r{^multipart/(digest|mixed);\s*boundary=(.*)}.freeze
113
+ MPS = 'multipart/'.freeze
114
+ MP_RGX1 = %r{^(digest|mixed);\s*boundary=\"(.*)\"}.freeze
115
+ MP_RGX2 = %r{^(digest|mixed);\s*boundary=(.*)}.freeze
116
+ # APP_HTTP_RGX = %r{^application/http}.freeze
117
+ APP_HTTP = 'application/http'.freeze
107
118
  def new_content
108
119
  @target =
109
- if (md = MP_RGX1.match(@target_ct)) ||
110
- (md = MP_RGX2.match(@target_ct))
120
+ if @target_ct.start_with?(MPS) &&
121
+ (md = (MP_RGX1.match(@target_ct[10..-1]) || MP_RGX2.match(@target_ct[10..-1])))
111
122
  multipart_content(md[2].strip)
112
- elsif %r{^application/http} =~ @target_ct
123
+ elsif @target_ct.start_with?(APP_HTTP)
113
124
  MIME::Content::Application::Http.new
114
125
  else
115
126
  MIME::Content::Text::Plain.new
@@ -187,6 +198,8 @@ module MIME
187
198
  class Plain < Media
188
199
  # Parser for Text::Plain
189
200
  class Parser
201
+ HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
202
+ CRLF_LINE_RGX = /^#{CRLF}$/.freeze
190
203
  def initialize(target)
191
204
  @state = :h
192
205
  @lines = []
@@ -198,9 +211,10 @@ module MIME
198
211
  end
199
212
 
200
213
  def parse_head(line)
201
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
214
+ if (hmd = HMD_RGX.match(line))
202
215
  @target.hd[hmd[1].downcase] = hmd[2].strip
203
- elsif /^#{CRLF}$/ =~ line
216
+ # elsif CRLF_LINE_RGX =~ line
217
+ elsif CRLF == line
204
218
  @state = :b
205
219
  else
206
220
  @target.content << line
@@ -230,8 +244,8 @@ module MIME
230
244
  @hd = {}
231
245
  @content = ''
232
246
  # set default values. Can be overwritten by parser
233
- @hd['content-type'] = 'text/plain'
234
- @ct = 'text/plain'
247
+ @hd[CTT_TYPE_LC] = TEXT_PLAIN
248
+ @ct = TEXT_PLAIN
235
249
  @parser = Parser.new(self)
236
250
  end
237
251
 
@@ -251,6 +265,7 @@ module MIME
251
265
  class Base < Media
252
266
  # Parser for Multipart Base class
253
267
  class Parser
268
+ CRLF_ENDING_RGX = /#{CRLF}$/.freeze
254
269
  def initialize(target)
255
270
  @body_lines = []
256
271
  @target = target
@@ -295,7 +310,8 @@ module MIME
295
310
  # to remove it from the end of the last body line
296
311
  return unless @body_lines
297
312
 
298
- @body_lines.last.sub!(/#{CRLF}$/, '')
313
+ # @body_lines.last.sub!(CRLF_ENDING_RGX, '')
314
+ @body_lines.last.chomp!(CRLF)
299
315
  @parts << @body_lines
300
316
  end
301
317
 
@@ -349,7 +365,7 @@ module MIME
349
365
  end
350
366
 
351
367
  def set_multipart_header
352
- @hd['content-type'] = "multipart/mixed; boundary=#{@boundary}"
368
+ @hd[CTT_TYPE_LC] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
353
369
  end
354
370
 
355
371
  def get_http_resp(batcha)
@@ -367,9 +383,7 @@ module MIME
367
383
  # of the changes
368
384
  batcha.db.transaction do
369
385
  begin
370
- @response.content = @content.map { |part|
371
- part.get_response(batcha)
372
- }
386
+ @response.content = @content.map { |part| part.get_response(batcha) }
373
387
  rescue Sequel::Rollback => e
374
388
  # one of the changes of the changeset has failed
375
389
  # --> provide a dummy empty response for the change-parts
@@ -390,17 +404,19 @@ module MIME
390
404
  @response.content = [{ 'odata.error' =>
391
405
  { 'message' =>
392
406
  'Bad Request: Failed changeset ' } }.to_json]
393
- @response.hd = { 'Content-Type' => 'application/json;charset=utf-8' }
407
+ @response.hd = OData::CT_JSON
394
408
  @response
395
409
  end
396
410
 
397
411
  def unparse(bodyonly = false)
398
412
  b = ''
399
413
  unless bodyonly
400
- b << 'Content-Type' << ': ' << @hd['content-type'] << CRLF
414
+ # b << OData::CONTENT_TYPE << ': ' << @hd[OData::CTT_TYPE_LC] << CRLF
415
+ b << "#{OData::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
401
416
  end
402
- b << "#{CRLF}--#{@boundary}#{CRLF}"
403
- b << @content.map(&:unparse).join("#{CRLF}--#{@boundary}#{CRLF}")
417
+
418
+ b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
419
+ b << @content.map(&:unparse).join(crbdcr)
404
420
  b << "#{CRLF}--#{@boundary}--#{CRLF}"
405
421
  b
406
422
  end
@@ -425,7 +441,8 @@ module MIME
425
441
 
426
442
  def unparse
427
443
  b = "#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
428
- @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
444
+ @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
445
+ # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
429
446
  b << CRLF
430
447
  b << @content if @content != ''
431
448
  b
@@ -455,7 +472,8 @@ module MIME
455
472
  def unparse
456
473
  b = String.new(APPLICATION_HTTP_11)
457
474
  b << "#{@status} #{StatusMessage[@status]} #{CRLF}"
458
- @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
475
+ @hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
476
+ # @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
459
477
  b << CRLF
460
478
  b << @content.join if @content
461
479
  b
@@ -464,13 +482,16 @@ module MIME
464
482
 
465
483
  # For application/http . Content is either a Request or a Response
466
484
  class Http < Media
467
- HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH'.freeze
485
+ HTTP_MTHS_RGX = 'POST|GET|PUT|MERGE|PATCH|DELETE'.freeze
468
486
  HTTP_R_RGX = %r{^(#{HTTP_MTHS_RGX})\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
469
487
  HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
470
488
  HTTP_RESP_RGX = %r{^HTTP/1\.1\s(\d+)\s}.freeze
471
489
 
472
490
  # Parser for Http Media
473
491
  class Parser
492
+ HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
493
+ CRLF_LINE_RGX = /^#{CRLF}$/.freeze
494
+
474
495
  def initialize(target)
475
496
  @state = :http
476
497
  @lines = []
@@ -483,7 +504,7 @@ module MIME
483
504
  end
484
505
 
485
506
  def parse_http(line)
486
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
507
+ if (hmd = HMD_RGX.match(line))
487
508
  @target.hd[hmd[1].downcase] = hmd[2].strip
488
509
  elsif (mdht = HTTP_R_RGX.match(line))
489
510
  @state = :hd
@@ -499,9 +520,10 @@ module MIME
499
520
  end
500
521
 
501
522
  def parse_head(line)
502
- if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
523
+ if (hmd = HMD_RGX.match(line))
503
524
  @target.content.hd[hmd[1].downcase] = hmd[2].strip
504
- elsif /^#{CRLF}$/ =~ line
525
+ elsif CRLF == line
526
+ # elsif CRLF_LINE_RGX =~ line
505
527
  @state = :b
506
528
  else
507
529
  @body_lines << line