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 +4 -4
- data/lib/multipart.rb +472 -136
- data/lib/odata/batch.rb +40 -31
- data/lib/odata/collection.rb +25 -12
- data/lib/odata/collection_filter.rb +4 -6
- data/lib/odata/entity.rb +41 -8
- data/lib/rack_app.rb +2 -2
- data/lib/request.rb +21 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34ab64bf4fd2bbde8453e754091a3bd5618f41e6d51d9d15c9c678efcb38d544
|
4
|
+
data.tar.gz: d0ee4590389199a0e3530aee4a7aa244dde9355de8506a1091c5e0215ad40d78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
6
|
-
|
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
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
@
|
30
|
-
@
|
74
|
+
# Warning: recursive here
|
75
|
+
@target.parser.parse(level: level)
|
76
|
+
@target.parser = nil
|
77
|
+
@lines = nil
|
78
|
+
@target
|
31
79
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
40
|
-
|
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
|
44
|
-
@
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
135
|
+
parse(level: level)
|
50
136
|
end
|
51
137
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
@content << "#{k}: #{v}#{RETNL}"
|
359
|
+
def initialize
|
360
|
+
@hd = {}
|
361
|
+
@content = ''
|
142
362
|
end
|
143
|
-
|
144
|
-
|
145
|
-
@
|
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
|
-
|
151
|
-
|
152
|
-
|
373
|
+
# Http Response
|
374
|
+
class HttpResp < Media
|
375
|
+
attr_accessor :status
|
376
|
+
attr_accessor :content
|
153
377
|
|
154
|
-
|
155
|
-
|
156
|
-
|
378
|
+
APPLICATION_HTTP_11 = ['Content-Type: application/http',
|
379
|
+
"Content-Transfer-Encoding: binary#{CRLF}",
|
380
|
+
'HTTP/1.1 '].join(CRLF).freeze
|
157
381
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
382
|
+
StatusMessage = ::WEBrick::HTTPStatus::StatusMessage.freeze
|
383
|
+
|
384
|
+
def initialize
|
385
|
+
@hd = {}
|
386
|
+
@content = []
|
162
387
|
end
|
163
388
|
|
164
|
-
|
165
|
-
|
389
|
+
def ==(other)
|
390
|
+
(@hd == other.hd) && (@content == other.content)
|
391
|
+
end
|
166
392
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
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
|
13
|
+
def parse_multipart
|
14
|
+
@mimep = MIME::Media::Parser.new
|
15
15
|
@boundary = media_type_params['boundary']
|
16
|
-
|
17
|
-
|
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
|
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.
|
106
|
-
@mult_response =
|
107
|
-
|
108
|
-
@mult_request.
|
109
|
-
|
110
|
-
|
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
|
data/lib/odata/collection.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
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
|
-
|
164
|
-
|
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
|
-
|
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
|
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
|
146
|
+
summary: Safrano is a Ruby OData provider based on Sequel and Rack
|
143
147
|
test_files: []
|