safrano 0.0.10 → 0.1.0

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