safrano 0.3.2 → 0.4.2

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.
@@ -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