safrano 0.0.10 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ae6e14f540ba9628bfc170798e0323f25c0fea026f1ce887ae9f3ca1a11d9ea
4
- data.tar.gz: 302b802de3bed4f604ff0c9bd1591521b45ce40deef7773d2df8631f9c6b577f
3
+ metadata.gz: 34ab64bf4fd2bbde8453e754091a3bd5618f41e6d51d9d15c9c678efcb38d544
4
+ data.tar.gz: d0ee4590389199a0e3530aee4a7aa244dde9355de8506a1091c5e0215ad40d78
5
5
  SHA512:
6
- metadata.gz: c316569a4275ef505467d6c4f954da9d9c357998ca5584aa58a50311e4d81ea3bf8f4fa34b1137640aa00c591cd6aaedb38f65824b0c7d0f2ba650a14a9882a7
7
- data.tar.gz: bbdd2ea715ebfe121dd0f82c10b6c5dc788202b5a8dafc1c81c14b5506377fa91d51379443c9a8769a54efacaf12cb8edbd9cb3a71bfcc0a0df959aec79eef37
6
+ metadata.gz: b9531e9de93d7d117ea1b58c1a90967706b102078513752f0bfef2447dc9c5988438d7ee1e9053827da0e58ac2aad07a6450a7c27b1fa5833ba329fe6c2bd4a8
7
+ data.tar.gz: 9e102e3c98ce0d47c0dd1dda33c8b2b76e0b7a3b9503d3af86c2ae08404773000b87442711ca230875d715148f90d870e9e63b9aae5c866677d4b0e8ca687ddd
data/lib/multipart.rb CHANGED
@@ -1,182 +1,518 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ CRLF = "\r\n".freeze
4
+ LF = "\n".freeze
5
+
6
+ require 'securerandom'
7
+ require 'webrick/httpstatus'
8
+
3
9
  # Simple multipart support for OData $batch purpose
10
+ module MIME
11
+ # a mime object has a header(with content-type etc) and a content(aka body)
12
+ class Media
13
+ # Parser for MIME::Media
14
+ class Parser
15
+ attr_accessor :lines
16
+ attr_accessor :target
17
+ def initialize
18
+ @state = :h
19
+ @lines = []
20
+ @target_hd = {}
21
+ @target_ct = nil
22
+ @sep = CRLF
23
+ end
4
24
 
5
- module Multi
6
- RETNL = "\r\n".freeze
7
- HTTP_VERB_URI_REGEXP = /^(POST|GET|PUT|PATCH)\s+(\S*)\s?(\S*)$/.freeze
8
- HEADER_REGEXP = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
9
- module Parts
10
- class Base
11
- attr_accessor :parts
12
- attr_reader :body
13
- attr_reader :headers
14
-
15
- def initialize(boundary)
16
- @boundary = boundary
17
- @headers = {}
18
- @body = ''
19
- @parts = []
25
+ def addline(line)
26
+ @lines << line
20
27
  end
21
28
 
22
- def finalize
23
- @parts.each do |part|
24
- @body << "--#{@boundary}#{RETNL}"
25
- @body << part.message_s
26
- @body << RETNL
29
+ def parse(level: 0)
30
+ @level = level
31
+ return unless @lines
32
+
33
+ @lines.each do |line|
34
+ case @state
35
+ when :h
36
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
37
+ @target_hd[hmd[1].downcase] = hmd[2].strip
38
+
39
+ elsif /^#{CRLF}$/ =~ line
40
+ @target_ct = @target_hd['content-type'] || 'text/plain'
41
+ @state = new_content
42
+
43
+ end
44
+
45
+ when :b
46
+ @target.parser.addline(line)
47
+
48
+ when :bmp
49
+
50
+ @state = :first_part if @target.parser.first_part(line)
51
+
52
+ when :first_part
53
+ if @target.parser.next_part(line)
54
+ @state = :next_part
55
+ # this is for when there is only one part
56
+ # (first part is the last one)
57
+ elsif @target.parser.last_part(line)
58
+ @state = :end
59
+ else
60
+ @target.parser.addline(line)
61
+ end
62
+
63
+ when :next_part
64
+
65
+ if @target.parser.next_part(line)
66
+
67
+ elsif @target.parser.last_part(line)
68
+ @state = :end
69
+ else
70
+ @target.parser.addline(line)
71
+ end
72
+ end
27
73
  end
28
- @body << "--#{@boundary}--#{RETNL}"
29
- @headers['Content-Length'] = @body.bytesize.to_s
30
- @body
74
+ # Warning: recursive here
75
+ @target.parser.parse(level: level)
76
+ @target.parser = nil
77
+ @lines = nil
78
+ @target
31
79
  end
32
- end
33
- class Mixed < Base
34
- def initialize(boundary)
35
- super(boundary)
36
- @headers['Content-Type'] = "multipart/mixed;boundary=#{@boundary}"
80
+
81
+ def multipart_content(boundary)
82
+ MIME::Content::Multipart::Base.new(boundary)
37
83
  end
38
84
 
39
- def shadow_copy
40
- self.class.new(@boundary)
85
+ def hook_multipart(content_type, boundary)
86
+ @target_hd['content-type'] = content_type
87
+ @target_ct = @target_hd['content-type']
88
+ @target = multipart_content(boundary)
89
+ @target.hd = @target_hd
90
+ @target.ct = @target_ct
91
+ @state = :bmp
92
+ end
93
+ MP_RGX1 = %r{^multipart/(digest|mixed);\s*boundary=\"(.*)\"}.freeze
94
+ MP_RGX2 = %r{^multipart/(digest|mixed);\s*boundary=(.*)}.freeze
95
+ def new_content
96
+ @target =
97
+ if (md = MP_RGX1.match(@target_ct)) ||
98
+ (md = MP_RGX2.match(@target_ct))
99
+ multipart_content(md[2].strip)
100
+ elsif %r{^application/http} =~ @target_ct
101
+ MIME::Content::Application::Http.new
102
+ else
103
+ MIME::Content::Text::Plain.new
104
+ end
105
+ @target.hd.merge! @target_hd
106
+ @target.ct = @target_ct
107
+ @target.level = @level
108
+ @state = @target.is_a?(MIME::Content::Multipart::Base) ? :bmp : :b
41
109
  end
42
110
 
43
- def self.parse(inp, boundary)
44
- @mmboundary = "--#{boundary}"
45
- ret = Multi::Parts::Mixed.new(boundary)
46
- inp.split(@mmboundary).each do |p|
47
- ret.parts << parse_part(p) unless (p.strip == '--') || p.empty?
111
+ def parse_lines(inp, level: 0)
112
+ @lines = inp
113
+ parse(level: level)
114
+ end
115
+
116
+ def parse_str(inpstr, level: 0)
117
+ # we need to keep the line separators --> use io.readlines
118
+ if inpstr.respond_to?(:readlines)
119
+ @lines = inpstr.readlines(@sep)
120
+ else
121
+ # rack input wrapper only has gets but not readlines
122
+ sepsave = $INPUT_RECORD_SEPARATOR
123
+ $INPUT_RECORD_SEPARATOR = @sep
124
+ while (line = inpstr.gets)
125
+ @lines << line
126
+ end
127
+ $INPUT_RECORD_SEPARATOR = sepsave
128
+
129
+ end
130
+ # tmp hack for test-tools that convert CRLF in payload to LF :-(
131
+ if @lines.size == 1
132
+ @sep = LF
133
+ @lines = @lines.first.split(LF).map { |xline| "#{xline}#{CRLF}" }
48
134
  end
49
- ret
135
+ parse(level: level)
50
136
  end
51
137
 
52
- # Ultrasimplified parser...
53
- def self.parse_part(inp)
54
- state = :start
55
- ct_headers = {}
56
- headers = {}
57
- body_str = ''
58
- http_method = nil
59
- uri = nil
60
- inp.each_line do |l|
61
- next if l.strip.empty?
62
-
63
- case state
64
- when :start, :ohd
65
- md = l.strip.match(HEADER_REGEXP)
66
-
67
- if md
68
- state = :ohd
69
- ct_headers[md[1]] = md[2]
70
- else
71
- md = l.strip.match(HTTP_VERB_URI_REGEXP)
72
- if md
73
- state = :hd
74
- http_method = md[1]
75
- uri = md[2]
138
+ def parse_string(inpstr, level: 0)
139
+ @lines = inpstr.split(@sep)
140
+ if @lines.size == 1
141
+ @sep = LF
142
+ @lines = @lines.first.split(LF)
143
+ end
144
+ # split is not keeping the separator, we re-add it
145
+ @lines = @lines.map { |line| "#{line}#{CRLF}" }
146
+ parse(level: level)
147
+ end
148
+ end
149
+
150
+ attr_accessor :content
151
+ attr_accessor :hd
152
+ attr_accessor :ct
153
+ attr_accessor :level
154
+ attr_reader :response
155
+
156
+ def initialize
157
+ @state = :h
158
+ @hd = {}
159
+ @ct = nil
160
+ @parser = Parser.new(self)
161
+ end
162
+ end
163
+
164
+ # Subclasses for Mime media content
165
+ module Content
166
+ # due to the recursive nature of multipart media, a media-content
167
+ # should be seen as a full-fledged media(header+content) as well
168
+ class Media < ::MIME::Media
169
+ attr_accessor :parser
170
+ end
171
+
172
+ # text mime types
173
+ module Text
174
+ # Well, it says plain text
175
+ class Plain < Media
176
+ # Parser for Text::Plain
177
+ class Parser
178
+ def initialize(target)
179
+ @state = :h
180
+ @lines = []
181
+ @target = target
182
+ end
183
+
184
+ def addline(line)
185
+ @lines << line
186
+ end
187
+
188
+ def parse(level: 0)
189
+ return unless @lines
190
+
191
+ @level = level
192
+ @lines.each do |line|
193
+ case @state
194
+ when :h
195
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
196
+ @target.hd[hmd[1].downcase] = hmd[2].strip
197
+ elsif /^#{CRLF}$/ =~ line
198
+ @state = :b
199
+ else
200
+ @target.content << line
201
+ @state = :b
202
+ end
203
+ when :b
204
+ @target.content << line
76
205
  end
77
206
  end
78
- when :hd
79
- md = l.strip.match(HEADER_REGEXP)
80
- if md
81
- state = :hd
82
- headers[md[1]] = md[2]
83
- else
84
- body_str << l
85
- state = :bd
86
- end
87
-
88
- when :bd
89
- body_str << l
207
+ @lines = nil
208
+ @target.level = level
209
+ @target
90
210
  end
91
211
  end
92
- if ct_headers['Content-Type'] == 'application/http'
93
212
 
94
- Multi::Part::HttpReq.new(headers,
95
- http_method,
96
- uri,
97
- body_str)
213
+ def initialize
214
+ @hd = {}
215
+ @content = ''
216
+ # set default values. Can be overwritten by parser
217
+ @hd['content-type'] = 'text/plain'
218
+ @ct = 'text/plain'
219
+ @parser = Parser.new(self)
220
+ end
98
221
 
99
- else
100
- # TODO: should probably be a BadRequestError ?
101
- raise RuntimeError
222
+ def unparse
223
+ @content
224
+ end
225
+
226
+ def ==(other)
227
+ (@hd == other.hd) && (@content == other.content)
102
228
  end
103
229
  end
104
230
  end
105
- end
106
231
 
107
- module Part
108
- class Base
109
- attr_accessor :content_type
110
- attr_accessor :content_transfer_enc
111
- def initialize
112
- @content = nil
113
- end
232
+ # Multipart media
233
+ module Multipart
234
+ # base class for multipart mixed, related, digest etc
235
+ class Base < Media
236
+ # Parser for Multipart Base class
237
+ class Parser
238
+ def initialize(target)
239
+ @body_lines = []
240
+ @target = target
241
+ @parts = []
242
+ @bound_rgx = /^--#{@target.boundary}\s*$/
243
+ @lastb_rgx = /^--#{@target.boundary}--\s*$/
244
+ end
245
+
246
+ def parse(level: 0)
247
+ @target.content = @parts.map do |mlines|
248
+ MIME::Media::Parser.new.parse_lines(mlines, level: level + 1)
249
+ end
250
+ @body_lines = @bound_rgx = @lastb_rgx = nil
251
+ @parts = nil
252
+ @target.level = level
253
+ @target
254
+ end
255
+
256
+ def first_part(line)
257
+ return unless @bound_rgx =~ line
258
+
259
+ @body_lines = []
260
+ end
261
+
262
+ def next_part(line)
263
+ return unless @bound_rgx =~ line
264
+
265
+ collect_part
266
+ @body_lines = []
267
+ end
268
+
269
+ def last_part(line)
270
+ return unless @lastb_rgx =~ line
271
+
272
+ collect_part
273
+ end
274
+
275
+ def collect_part
276
+ # according to the multipart RFC spec, the preceeding CRLF
277
+ # belongs to the boundary
278
+ # but because we are a CRLF line-based parser we need
279
+ # to remove it from the end of the last body line
280
+ return unless @body_lines
114
281
 
115
- attr_writer :content
282
+ @body_lines.last.sub!(/#{CRLF}$/, '')
283
+ @parts << @body_lines
284
+ end
285
+
286
+ def addline(line)
287
+ @body_lines << line
288
+ end
289
+ end
116
290
 
117
- def message_s
118
- result = ''
119
- result << "Content-Type: #{@content_type}#{RETNL}"
120
- result << "Content-Transfer-Encoding: #{@content_transfer_enc}#{RETNL}"
121
- result << RETNL
122
- result << content.to_s
123
- result
291
+ attr_reader :boundary
292
+
293
+ def initialize(boundary)
294
+ @boundary = boundary
295
+ @hd = {}
296
+ @parser = Parser.new(self)
297
+ end
298
+
299
+ def ==(other)
300
+ (@boundary == other.boundary) && (@content == other.content)
301
+ end
302
+
303
+ def set_multipart_header
304
+ @hd['content-type'] = "multipart/mixed; boundary=#{@boundary}"
305
+ end
306
+
307
+ def get_http_resp(batcha)
308
+ get_response(batcha)
309
+ [@response.hd, @response.unparse(true)]
310
+ end
311
+
312
+ def get_response(batcha)
313
+ @response = self.class.new(::SecureRandom.uuid)
314
+ @response.set_multipart_header
315
+ if @level == 1 # changeset need their own global transaction
316
+ # the change requests that are part of if have @level==2
317
+ # and will be flagged with in_changeset=true
318
+ # and this will finally be used to skip the transaction
319
+ # of the changes
320
+ batcha.db.transaction do
321
+ begin
322
+ @response.content = @content.map { |part| part.get_response(batcha) }
323
+ rescue Sequel::Rollback => e
324
+ # one of the changes of the changeset has failed
325
+ # --> provide a dummy empty response for the change-parts
326
+ # then transmit the Rollback to Sequel
327
+ @response.content = @content.map { |_part|
328
+ MIME::Content::Application::HttpResp.new
329
+ }
330
+ raise
331
+ end
332
+ end
333
+ else
334
+ @response.content = @content.map { |part| part.get_response(batcha) }
335
+ end
336
+ @response
337
+ end
338
+
339
+ def unparse(bodyonly = false)
340
+ b = ''
341
+ unless bodyonly
342
+ b << 'Content-Type' << ': ' << @hd['content-type'] << CRLF
343
+ end
344
+ b << "#{CRLF}--#{@boundary}#{CRLF}"
345
+ b << @content.map(&:unparse).join("#{CRLF}--#{@boundary}#{CRLF}")
346
+ b << "#{CRLF}--#{@boundary}--#{CRLF}"
347
+ b
348
+ end
124
349
  end
125
350
  end
126
351
 
127
- class HttpReq < Base
128
- def initialize(headers, method, url, body)
129
- super()
130
- @content_type = 'application/http'
131
- @content_transfer_enc = 'binary'
132
- @headers = headers
133
- @http_method = method
134
- @url = url
135
- @body = body
136
- end
352
+ # for application(http etc.) mime types
353
+ module Application
354
+ # Http Request
355
+ class HttpReq < Media
356
+ attr_accessor :http_method
357
+ attr_accessor :uri
137
358
 
138
- def finalize
139
- @content = "#{@http_method} #{@url} HTTP/1.1#{RETNL}"
140
- @headers.each do |k, v|
141
- @content << "#{k}: #{v}#{RETNL}"
359
+ def initialize
360
+ @hd = {}
361
+ @content = ''
142
362
  end
143
- if @body != ''
144
- @content << RETNL
145
- @content << @body
363
+
364
+ def unparse
365
+ b = "#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
366
+ @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
367
+ b << CRLF
368
+ b << @content if @content != ''
369
+ b
146
370
  end
147
- @content
148
371
  end
149
372
 
150
- def content
151
- @content = (@content || finalize)
152
- end
373
+ # Http Response
374
+ class HttpResp < Media
375
+ attr_accessor :status
376
+ attr_accessor :content
153
377
 
154
- # shamelessely copied from Rack::TEST:Session
155
- def headers_for_env
156
- converted_headers = {}
378
+ APPLICATION_HTTP_11 = ['Content-Type: application/http',
379
+ "Content-Transfer-Encoding: binary#{CRLF}",
380
+ 'HTTP/1.1 '].join(CRLF).freeze
157
381
 
158
- @headers.each do |name, value|
159
- env_key = name.upcase.tr('-', '_')
160
- env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
161
- converted_headers[env_key] = value
382
+ StatusMessage = ::WEBrick::HTTPStatus::StatusMessage.freeze
383
+
384
+ def initialize
385
+ @hd = {}
386
+ @content = []
162
387
  end
163
388
 
164
- converted_headers
165
- end
389
+ def ==(other)
390
+ (@hd == other.hd) && (@content == other.content)
391
+ end
166
392
 
167
- def batch_env
168
- @env = ::Rack::MockRequest.env_for(@url,
169
- method: @http_method,
170
- input: @body_str)
171
- @env.merge! headers_for_env
172
- @env
393
+ def unparse
394
+ b = String.new(APPLICATION_HTTP_11)
395
+ b << "#{@status} #{StatusMessage[@status]} #{CRLF}"
396
+ @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
397
+ b << CRLF
398
+ b << @content.join if @content
399
+ b
400
+ end
173
401
  end
174
- end
175
402
 
176
- class HttpGet < HttpReq
177
- def initialize(url, app_headers)
178
- super(app_headers, 'GET', url, '')
403
+ # For application/http . Content is either a Request or a Response
404
+ class Http < Media
405
+ HTTP_R_RGX = %r{^(POST|GET|PUT|MERGE|PATCH)\s+(\S*)\s?(HTTP/1\.1)\s*$}.freeze
406
+ HEADER_RGX = %r{^([a-zA-Z\-]+):\s*([0-9a-zA-Z\-\/,\s]+;?\S*)\s*$}.freeze
407
+ HTTP_RESP_RGX = %r{^HTTP/1\.1\s(\d+)\s}.freeze
408
+
409
+ # Parser for Http Media
410
+ class Parser
411
+ def initialize(target)
412
+ @state = :http
413
+ @lines = []
414
+ @body_lines = []
415
+ @target = target
416
+ end
417
+
418
+ def addline(line)
419
+ @lines << line
420
+ end
421
+
422
+ def parse(level: 0)
423
+ return unless @lines
424
+
425
+ @lines.each do |line|
426
+ case @state
427
+ when :http
428
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
429
+ @target.hd[hmd[1].downcase] = hmd[2].strip
430
+ elsif (mdht = HTTP_R_RGX.match(line))
431
+ @state = :hd
432
+ @target.content = MIME::Content::Application::HttpReq.new
433
+ @target.content.http_method = mdht[1]
434
+ @target.content.uri = mdht[2]
435
+ # HTTP 1.1 status line --> HttpResp.new
436
+ elsif (mdht = HTTP_RESP_RGX.match(line))
437
+ @state = :hd
438
+ @target.content = MIME::Content::Application::HttpResp.new
439
+ @target.content.status = mdht[1]
440
+ end
441
+ when :hd
442
+ if (hmd = /^([\w-]+)\s*:\s*(.*)/.match(line))
443
+ @target.content.hd[hmd[1].downcase] = hmd[2].strip
444
+ elsif /^#{CRLF}$/ =~ line
445
+ @state = :b
446
+ else
447
+ @body_lines << line
448
+ @state = :b
449
+ end
450
+ when :b
451
+ @body_lines << line
452
+ end
453
+ end
454
+
455
+ @target.content.content = @body_lines.join
456
+ @target.content.level = level
457
+ @target.level = level
458
+ @lines = nil
459
+ @body_lines = nil
460
+ @target
461
+ end
462
+ end
463
+
464
+ def initialize
465
+ @hd = {}
466
+ @ct = 'application/http'
467
+ @parser = Parser.new(self)
468
+ end
469
+
470
+ def ==(other)
471
+ @content = other.content
472
+ end
473
+
474
+ def get_response(batchapp)
475
+ # self.content should be the request
476
+ rack_resp = batchapp.batch_call(@content)
477
+ @response = MIME::Content::Application::HttpResp.new
478
+ @response.status = rack_resp[0]
479
+ @response.hd = rack_resp[1]
480
+ @response.content = rack_resp[2]
481
+ @response
482
+ end
483
+
484
+ def unparse
485
+ b = "Content-Type: #{@ct}#{CRLF}"
486
+ b << "Content-Transfer-Encoding: binary#{CRLF}#{CRLF}"
487
+ b << @content.unparse
488
+ b
489
+ end
179
490
  end
180
491
  end
181
492
  end
182
493
  end
494
+
495
+ # @mimep = MIME::Media::Parser.new
496
+
497
+ # @inpstr = File.open('../test/multipart/odata_changeset_1_body.txt','r')
498
+ # @boundary = 'batch_48f8-3aea-2f04'
499
+
500
+ # @inpstr = File.open('../test/multipart/odata_changeset_body.txt','r')
501
+ # @boundary = 'batch_36522ad7-fc75-4b56-8c71-56071383e77b'
502
+
503
+ # @mime = @mimep.hook_multipart('multipart/mixed', @boundary)
504
+ # @mime = @mimep.parse_str(@inpstr)
505
+
506
+ # require 'pry'
507
+ # binding.pry
508
+
509
+ # @inpstr.close
510
+
511
+ # inp = File.open(ARGV[0], 'r') do |f|
512
+ # f.readlines(CRLF)
513
+ # end
514
+ # x = MIME::Media::Parser.new
515
+ # y = x.parsein(inp)
516
+ # pp y
517
+ # puts '-----------------------------------------------'
518
+ # puts y.unparse
data/lib/odata/batch.rb CHANGED
@@ -2,33 +2,19 @@
2
2
 
3
3
  require 'rack_app.rb'
4
4
  require 'safrano_core.rb'
5
- require 'webrick/httpstatus'
6
5
 
7
6
  module OData
8
- RETNEW = "\r\n".freeze
7
+ # Support for OData multipart $batch Requests
9
8
  class Request
10
9
  def create_batch_app
11
10
  Batch::MyOApp.new(self)
12
11
  end
13
12
 
14
- def parse_multipart_mixed
13
+ def parse_multipart
14
+ @mimep = MIME::Media::Parser.new
15
15
  @boundary = media_type_params['boundary']
16
- Multi::Parts::Mixed.parse(body.read, @boundary)
17
- end
18
- end
19
-
20
- class Response
21
- APPLICATION_HTTP_11 = ['Content-Type: application/http',
22
- "Content-Transfer-Encoding: binary#{RETNEW}",
23
- 'HTTP/1.1 '].join(RETNEW).freeze
24
- StatusMessage = WEBrick::HTTPStatus::StatusMessage.freeze
25
- def message_s
26
- b = String.new(APPLICATION_HTTP_11)
27
- b << "#{@status} #{StatusMessage[@status]} #{RETNEW}"
28
- headers.each { |k, v| b << k.to_s << ':' << v.to_s << RETNEW }
29
- b << RETNEW
30
- b << @body[0].to_s
31
- b << RETNEW
16
+ @mimep.hook_multipart(media_type, @boundary)
17
+ @mimep.parse_str(body)
32
18
  end
33
19
  end
34
20
 
@@ -37,9 +23,11 @@ module OData
37
23
  class MyOApp < OData::ServerApp
38
24
  attr_reader :full_req
39
25
  attr_reader :response
26
+ attr_reader :db
40
27
 
41
28
  def initialize(full_req)
42
29
  @full_req = full_req
30
+ @db = full_req.service.collections.first.db
43
31
  end
44
32
 
45
33
  # redefined for $batch
@@ -50,16 +38,40 @@ module OData
50
38
  end
51
39
 
52
40
  def batch_call(part_req)
53
- env = part_req.batch_env
41
+ env = batch_env(part_req)
54
42
  env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
55
43
 
56
44
  @request = OData::Request.new(env)
57
45
  @response = OData::Response.new
58
46
 
47
+ @request.in_changeset = true if part_req.level == 2
48
+
59
49
  before
60
50
  dispatch
51
+
61
52
  @response.finish
62
53
  end
54
+
55
+ # shamelessely copied from Rack::TEST:Session
56
+ def headers_for_env(headers)
57
+ converted_headers = {}
58
+
59
+ headers.each do |name, value|
60
+ env_key = name.upcase.tr('-', '_')
61
+ env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
62
+ converted_headers[env_key] = value
63
+ end
64
+
65
+ converted_headers
66
+ end
67
+
68
+ def batch_env(mime_req)
69
+ @env = ::Rack::MockRequest.env_for(mime_req.uri,
70
+ method: mime_req.http_method,
71
+ input: mime_req.content)
72
+ @env.merge! headers_for_env(mime_req.hd)
73
+ @env
74
+ end
63
75
  end
64
76
 
65
77
  # Huile d'olive extra
@@ -91,9 +103,7 @@ module OData
91
103
  attr_accessor :parts
92
104
  attr_accessor :request
93
105
 
94
- def initialize
95
- @parts = []
96
- end
106
+ def initialize; end
97
107
 
98
108
  # here we are in the Batch handler object, and this POST should
99
109
  # normally handle a $batch request
@@ -101,15 +111,14 @@ module OData
101
111
  @request = req
102
112
 
103
113
  if @request.media_type == 'multipart/mixed'
114
+
104
115
  batcha = @request.create_batch_app
105
- @mult_request = @request.parse_multipart_mixed
106
- @mult_response = @mult_request.shadow_copy
107
-
108
- @mult_request.parts.each_with_object(@mult_response) do |req_part, mresp|
109
- batcha.batch_call(req_part)
110
- mresp.parts << batcha.response
111
- end
112
- [202, @mult_response.headers, @mult_response.finalize]
116
+ @mult_request = @request.parse_multipart
117
+ @mult_response = OData::Response.new
118
+
119
+ resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
120
+
121
+ [202, resp_hdrs, @mult_response.body[0]]
113
122
  else
114
123
  [415, {}, 'Unsupported Media Type']
115
124
  end
@@ -82,7 +82,7 @@ module OData
82
82
  end
83
83
 
84
84
  # Factory json-> Model Object instance
85
- def new_from_hson_h(hash)
85
+ def new_from_hson_h(hash, in_changeset: false)
86
86
  enty = new
87
87
  hash.delete('__metadata')
88
88
  # DONE: move this somewhere else where it's evaluated only once at setup
@@ -90,7 +90,8 @@ module OData
90
90
  # cattr[:primary_key] ? nil : col
91
91
  # end.select { |col| col }
92
92
  enty.set_fields(hash, @data_fields, missing: :skip)
93
- enty.save
93
+ # in-changeset requests get their own transaction
94
+ enty.save(transaction: !in_changeset)
94
95
  enty
95
96
  end
96
97
 
@@ -206,7 +207,7 @@ module OData
206
207
  def check_u_p_orderby
207
208
  # TODO: this should be moved into OData::Order somehow,
208
209
  # at least partly
209
- return unless (@params['$orderby'])
210
+ return unless @params['$orderby']
210
211
 
211
212
  pordlist = @params['$orderby'].dup
212
213
  pordlist.split(',').each do |pord|
@@ -278,7 +279,7 @@ module OData
278
279
  else
279
280
  [200, { 'Content-Type' => 'application/json;charset=utf-8' },
280
281
  to_odata_json(service: req.service)]
281
- end
282
+ end
282
283
  else # TODO: other formats
283
284
  406
284
285
  end
@@ -304,12 +305,22 @@ module OData
304
305
  def odata_post(req)
305
306
  # TODO: check Request body format...
306
307
  # TODO: this is for v2 only...
307
- data = JSON.parse(req.body.read)
308
- if req.accept?('application/json')
309
- [201, { 'Content-Type' => 'application/json;charset=utf-8' },
310
- new_from_hson_h(data).to_odata_post_json(service: req.service)]
311
- else # TODO: other formats
312
- 415
308
+ on_error = (proc { raise Sequel::Rollback } if req.in_changeset)
309
+ req.with_parsed_data(on_error: on_error) do |data|
310
+ data.delete('__metadata')
311
+ # validate payload column names
312
+ if (invalid = invalid_hash_data?(data))
313
+ on_error.call if on_error
314
+ return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
315
+ end
316
+
317
+ if req.accept?('application/json')
318
+ [201, { 'Content-Type' => 'application/json;charset=utf-8' },
319
+ new_from_hson_h(data, in_changeset: req.in_changeset)
320
+ .to_odata_post_json(service: req.service)]
321
+ else # TODO: other formats
322
+ 415
323
+ end
313
324
  end
314
325
  end
315
326
 
@@ -377,6 +388,10 @@ module OData
377
388
  end.select { |col| col }
378
389
  end
379
390
 
391
+ def invalid_hash_data?(data)
392
+ data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
393
+ end
394
+
380
395
  # A regexp matching all allowed attributes of the Entity
381
396
  # (eg ID|name|size etc... )
382
397
  def attribute_url_regexp
@@ -396,9 +411,7 @@ module OData
396
411
  end
397
412
 
398
413
  def transition_id(match_result)
399
- # binding.pry
400
414
  if (id = match_result[1])
401
- # puts "in transition_id, found #{y}"
402
415
  if (y = find(id))
403
416
  [y, :run]
404
417
  else
@@ -4,9 +4,7 @@ require 'odata/error.rb'
4
4
 
5
5
  # a few helper method
6
6
  class String
7
- # MASK_RGX = /'([^']*)'/.freeze
8
7
  MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
9
- # QUOTED_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
10
8
  UNMASK_RGX = /'(%?)(\$\d+)(%?)'/.freeze
11
9
  def with_mask_quoted_substrings!
12
10
  cnt = 0
@@ -261,10 +259,10 @@ module OData
261
259
  def get_assoc
262
260
  @assoc, @fn = @fn.split('/') if @fn.include?('/')
263
261
  assoc1, @fn1 = @fn1.split('/') if @fn1.include?('/')
264
- if assoc1 != @assoc
265
- # TODO... handle this
266
- raise OData::ServerError
267
- end
262
+ return unless assoc1 != @assoc
263
+
264
+ # TODO... handle this
265
+ raise OData::ServerError
268
266
  end
269
267
 
270
268
  def get_qualified_fn1(dtcx)
data/lib/odata/entity.rb CHANGED
@@ -151,7 +151,12 @@ module OData
151
151
  if req.accept?('application/json')
152
152
  data.delete('__metadata')
153
153
 
154
- update_fields(data, self.class.data_fields, missing: :skip)
154
+ if req.in_changeset
155
+ set_fields(data, self.class.data_fields, missing: :skip)
156
+ save(transaction: false)
157
+ else
158
+ update_fields(data, self.class.data_fields, missing: :skip)
159
+ end
155
160
 
156
161
  [202, to_odata_post_json(service: req.service)]
157
162
  else # TODO: other formats
@@ -160,17 +165,45 @@ module OData
160
165
  end
161
166
 
162
167
  def odata_patch(req)
163
- data = JSON.parse(req.body.read)
164
- @uribase = req.uribase
165
-
166
- if req.accept?('application/json')
168
+ on_error = (proc { raise Sequel::Rollback } if req.in_changeset)
169
+ req.with_parsed_data(on_error: on_error) do |data|
167
170
  data.delete('__metadata')
168
- update_fields(data, self.class.data_fields, missing: :skip)
171
+
172
+ # validate payload column names
173
+ if (invalid = self.class.invalid_hash_data?(data))
174
+ on_error.call if on_error
175
+ return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
176
+ end
177
+ # TODO: check values/types
178
+
179
+ my_data_fields = self.class.data_fields
180
+ @uribase = req.uribase
181
+ # if req.accept?('application/json')
182
+
183
+ if req.in_changeset
184
+ set_fields(data, my_data_fields, missing: :skip)
185
+ save(transaction: false)
186
+ else
187
+ update_fields(data, my_data_fields, missing: :skip)
188
+ end
169
189
  # patch should return 204 + no content
170
190
  [204, {}, []]
171
- else # TODO: other formats
172
- 415
173
191
  end
192
+
193
+ # if ( req.content_type == 'application/json' )
194
+ ## Parse json payload
195
+ # begin
196
+ # data = JSON.parse(req.body.read)
197
+ # rescue JSON::ParserError => e
198
+ # return [400, {}, ['JSON Parser Error while parsing payload : ',
199
+ # e.message]]
200
+
201
+ # end
202
+
203
+ # else # TODO: other formats
204
+
205
+ # [415, {}, []]
206
+ # end
174
207
  end
175
208
 
176
209
  # redefinitions of the main methods for a navigated collection
data/lib/rack_app.rb CHANGED
@@ -77,7 +77,7 @@ module OData
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|PUT|DELETE')
80
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE')
81
81
  include MethodHandlers
82
82
  def before
83
83
  headers 'Cache-Control' => 'no-cache'
@@ -108,7 +108,7 @@ module OData
108
108
  odata_delete
109
109
  when 'OPTIONS'
110
110
  odata_options
111
- when 'PATCH', 'PUT'
111
+ when 'PATCH', 'PUT', 'MERGE'
112
112
  odata_patch
113
113
  else
114
114
  raise Error
data/lib/request.rb CHANGED
@@ -13,6 +13,8 @@ module OData
13
13
  HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
14
14
  HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
15
15
 
16
+ attr_accessor :in_changeset
17
+
16
18
  # borowed from Sinatra
17
19
  class AcceptEntry
18
20
  attr_accessor :params
@@ -115,6 +117,25 @@ module OData
115
117
  end
116
118
  end
117
119
 
120
+ def with_parsed_data(on_error: nil)
121
+ if content_type == 'application/json'
122
+ # Parse json payload
123
+ begin
124
+ data = JSON.parse(body.read)
125
+ rescue JSON::ParserError => e
126
+ on_error.call if on_error
127
+ return [400, {}, ['JSON Parser Error while parsing payload : ',
128
+ e.message]]
129
+ end
130
+
131
+ yield data
132
+
133
+ else # TODO: other formats
134
+
135
+ [415, {}, []]
136
+ end
137
+ end
138
+
118
139
  def negotiate_service_version
119
140
  maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
120
141
  OData::ServiceBase.parse_data_service_version(rqv)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safrano
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - D.M.
@@ -120,7 +120,11 @@ files:
120
120
  homepage: https://gitlab.com/dm0da/safrano
121
121
  licenses:
122
122
  - MIT
123
- metadata: {}
123
+ metadata:
124
+ bug_tracker_uri: https://gitlab.com/dm0da/safrano/issues
125
+ changelog_uri: https://gitlab.com/dm0da/safrano/blob/master/CHANGELOG
126
+ source_code_uri: https://gitlab.com/dm0da/safrano/tree/master
127
+ wiki_uri: https://gitlab.com/dm0da/safrano/wikis/home
124
128
  post_install_message:
125
129
  rdoc_options: []
126
130
  require_paths:
@@ -139,5 +143,5 @@ requirements: []
139
143
  rubygems_version: 3.0.3
140
144
  signing_key:
141
145
  specification_version: 4
142
- summary: Safrano is a ruby (Sequel + Rack) OData provider
146
+ summary: Safrano is a Ruby OData provider based on Sequel and Rack
143
147
  test_files: []