safrano 0.3.2 → 0.3.3
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 +40 -18
- data/lib/odata/batch.rb +17 -3
- data/lib/odata/collection.rb +97 -46
- data/lib/odata/collection_media.rb +148 -0
- data/lib/odata/common_logger.rb +34 -0
- data/lib/odata/entity.rb +159 -38
- data/lib/odata/error.rb +16 -5
- data/lib/odata/navigation_attribute.rb +119 -0
- data/lib/odata/walker.rb +12 -2
- data/lib/odata_rack_builder.rb +1 -1
- data/lib/rack_app.rb +12 -7
- data/lib/request.rb +15 -3
- data/lib/safrano.rb +1 -0
- data/lib/safrano_core.rb +2 -1
- data/lib/service.rb +41 -12
- data/lib/version.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b25412d7e053724965ba296bdd43a6f0b3bbd8a62cee2f45a14edf18303ed6c1
|
4
|
+
data.tar.gz: 1e9ad5ce553d13199e4ce79cecf7209ce1bb2695f5c476d8f5e5e16bd57bd4f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad1361bb963f77870f784ed5322318b2567bac06e6ef915dcda62f57df767a0893a988672e4ace8dbd64cdda0cbff8d78de2aa712d4ebceb8821ed109018c909
|
7
|
+
data.tar.gz: d4f5f16fa548883e697479f313aa824c0798c38d71ead64172552ddfaec79fb53652063bf6dbfe36362cca5c30a80534665e8a12895fb2f343f1857a0dba8c1c
|
data/lib/multipart.rb
CHANGED
@@ -10,6 +10,10 @@ module MIME
|
|
10
10
|
class Media
|
11
11
|
# Parser for MIME::Media
|
12
12
|
class Parser
|
13
|
+
HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
|
14
|
+
|
15
|
+
CRLF_LINE_RGX = /^#{CRLF}$/.freeze
|
16
|
+
|
13
17
|
attr_accessor :lines
|
14
18
|
attr_accessor :target
|
15
19
|
def initialize
|
@@ -47,10 +51,11 @@ module MIME
|
|
47
51
|
end
|
48
52
|
|
49
53
|
def parse_head(line)
|
50
|
-
if (hmd =
|
54
|
+
if (hmd = HMD_RGX.match(line))
|
51
55
|
@target_hd[hmd[1].downcase] = hmd[2].strip
|
52
56
|
|
53
|
-
elsif
|
57
|
+
# elsif CRLF_LINE_RGX =~ line
|
58
|
+
elsif CRLF == line
|
54
59
|
@target_ct = @target_hd['content-type'] || 'text/plain'
|
55
60
|
@state = new_content
|
56
61
|
|
@@ -102,14 +107,19 @@ module MIME
|
|
102
107
|
@target.ct = @target_ct
|
103
108
|
@state = :bmp
|
104
109
|
end
|
105
|
-
|
106
|
-
|
110
|
+
MPS = 'multipart/'.freeze
|
111
|
+
MP_RGX1 = %r{^(digest|mixed);\s*boundary=\"(.*)\"}.freeze
|
112
|
+
MP_RGX2 = %r{^(digest|mixed);\s*boundary=(.*)}.freeze
|
113
|
+
# APP_HTTP_RGX = %r{^application/http}.freeze
|
114
|
+
APP_HTTP = 'application/http'.freeze
|
107
115
|
def new_content
|
108
116
|
@target =
|
109
|
-
if
|
110
|
-
(md =
|
117
|
+
if @target_ct.start_with?(MPS) and
|
118
|
+
(md = ((MP_RGX1.match(@target_ct[10..-1])) ||
|
119
|
+
(MP_RGX2.match(@target_ct[10..-1])))
|
120
|
+
)
|
111
121
|
multipart_content(md[2].strip)
|
112
|
-
elsif
|
122
|
+
elsif @target_ct.start_with?(APP_HTTP)
|
113
123
|
MIME::Content::Application::Http.new
|
114
124
|
else
|
115
125
|
MIME::Content::Text::Plain.new
|
@@ -187,6 +197,8 @@ module MIME
|
|
187
197
|
class Plain < Media
|
188
198
|
# Parser for Text::Plain
|
189
199
|
class Parser
|
200
|
+
HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
|
201
|
+
CRLF_LINE_RGX = /^#{CRLF}$/.freeze
|
190
202
|
def initialize(target)
|
191
203
|
@state = :h
|
192
204
|
@lines = []
|
@@ -198,9 +210,10 @@ module MIME
|
|
198
210
|
end
|
199
211
|
|
200
212
|
def parse_head(line)
|
201
|
-
if (hmd =
|
213
|
+
if (hmd = HMD_RGX.match(line))
|
202
214
|
@target.hd[hmd[1].downcase] = hmd[2].strip
|
203
|
-
elsif
|
215
|
+
# elsif CRLF_LINE_RGX =~ line
|
216
|
+
elsif CRLF == line
|
204
217
|
@state = :b
|
205
218
|
else
|
206
219
|
@target.content << line
|
@@ -251,6 +264,7 @@ module MIME
|
|
251
264
|
class Base < Media
|
252
265
|
# Parser for Multipart Base class
|
253
266
|
class Parser
|
267
|
+
CRLF_ENDING_RGX = /#{CRLF}$/.freeze
|
254
268
|
def initialize(target)
|
255
269
|
@body_lines = []
|
256
270
|
@target = target
|
@@ -295,7 +309,8 @@ module MIME
|
|
295
309
|
# to remove it from the end of the last body line
|
296
310
|
return unless @body_lines
|
297
311
|
|
298
|
-
@body_lines.last.sub!(
|
312
|
+
# @body_lines.last.sub!(CRLF_ENDING_RGX, '')
|
313
|
+
@body_lines.last.chomp!(CRLF)
|
299
314
|
@parts << @body_lines
|
300
315
|
end
|
301
316
|
|
@@ -349,7 +364,7 @@ module MIME
|
|
349
364
|
end
|
350
365
|
|
351
366
|
def set_multipart_header
|
352
|
-
@hd['content-type'] = "
|
367
|
+
@hd['content-type'] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
|
353
368
|
end
|
354
369
|
|
355
370
|
def get_http_resp(batcha)
|
@@ -390,14 +405,15 @@ module MIME
|
|
390
405
|
@response.content = [{ 'odata.error' =>
|
391
406
|
{ 'message' =>
|
392
407
|
'Bad Request: Failed changeset ' } }.to_json]
|
393
|
-
@response.hd =
|
408
|
+
@response.hd = OData::CT_JSON
|
394
409
|
@response
|
395
410
|
end
|
396
411
|
|
397
412
|
def unparse(bodyonly = false)
|
398
413
|
b = ''
|
399
414
|
unless bodyonly
|
400
|
-
b <<
|
415
|
+
# b << OData::CONTENT_TYPE << ': ' << @hd[OData::CTT_TYPE_LC] << CRLF
|
416
|
+
b << "#{OData::CONTENT_TYPE}: #{@hd[OData::CTT_TYPE_LC]}#{CRLF}"
|
401
417
|
end
|
402
418
|
b << "#{CRLF}--#{@boundary}#{CRLF}"
|
403
419
|
b << @content.map(&:unparse).join("#{CRLF}--#{@boundary}#{CRLF}")
|
@@ -425,7 +441,8 @@ module MIME
|
|
425
441
|
|
426
442
|
def unparse
|
427
443
|
b = "#{@http_method} #{@uri} HTTP/1.1#{CRLF}"
|
428
|
-
@hd.each { |k, v| b << k
|
444
|
+
@hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
|
445
|
+
# @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
|
429
446
|
b << CRLF
|
430
447
|
b << @content if @content != ''
|
431
448
|
b
|
@@ -455,7 +472,8 @@ module MIME
|
|
455
472
|
def unparse
|
456
473
|
b = String.new(APPLICATION_HTTP_11)
|
457
474
|
b << "#{@status} #{StatusMessage[@status]} #{CRLF}"
|
458
|
-
@hd.each { |k, v| b << k
|
475
|
+
@hd.each { |k, v| b << "#{k}: #{v}#{CRLF}" }
|
476
|
+
# @hd.each { |k, v| b << k.to_s << ': ' << v.to_s << CRLF }
|
459
477
|
b << CRLF
|
460
478
|
b << @content.join if @content
|
461
479
|
b
|
@@ -471,6 +489,9 @@ module MIME
|
|
471
489
|
|
472
490
|
# Parser for Http Media
|
473
491
|
class Parser
|
492
|
+
HMD_RGX = /^([\w-]+)\s*:\s*(.*)/.freeze
|
493
|
+
CRLF_LINE_RGX = /^#{CRLF}$/.freeze
|
494
|
+
|
474
495
|
def initialize(target)
|
475
496
|
@state = :http
|
476
497
|
@lines = []
|
@@ -483,7 +504,7 @@ module MIME
|
|
483
504
|
end
|
484
505
|
|
485
506
|
def parse_http(line)
|
486
|
-
if (hmd =
|
507
|
+
if (hmd = HMD_RGX.match(line))
|
487
508
|
@target.hd[hmd[1].downcase] = hmd[2].strip
|
488
509
|
elsif (mdht = HTTP_R_RGX.match(line))
|
489
510
|
@state = :hd
|
@@ -499,9 +520,10 @@ module MIME
|
|
499
520
|
end
|
500
521
|
|
501
522
|
def parse_head(line)
|
502
|
-
if (hmd =
|
523
|
+
if (hmd = HMD_RGX.match(line))
|
503
524
|
@target.content.hd[hmd[1].downcase] = hmd[2].strip
|
504
|
-
elsif
|
525
|
+
elsif CRLF == line
|
526
|
+
# elsif CRLF_LINE_RGX =~ line
|
505
527
|
@state = :b
|
506
528
|
else
|
507
529
|
@body_lines << line
|
data/lib/odata/batch.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'rack_app.rb'
|
2
2
|
require 'safrano_core.rb'
|
3
|
+
require 'rack/body_proxy'
|
4
|
+
require_relative './common_logger.rb'
|
3
5
|
|
4
6
|
module OData
|
5
7
|
# Support for OData multipart $batch Requests
|
@@ -38,7 +40,7 @@ module OData
|
|
38
40
|
def batch_call(part_req)
|
39
41
|
env = batch_env(part_req)
|
40
42
|
env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
|
41
|
-
|
43
|
+
began_at = Rack::Utils.clock_time
|
42
44
|
@request = OData::Request.new(env)
|
43
45
|
@response = OData::Response.new
|
44
46
|
|
@@ -51,7 +53,16 @@ module OData
|
|
51
53
|
before
|
52
54
|
dispatch
|
53
55
|
|
54
|
-
@response.finish
|
56
|
+
status, header, body = @response.finish
|
57
|
+
# Logging of sub-requests with ODataCommonLogger.
|
58
|
+
# A bit hacky but working
|
59
|
+
# TODO: test ?
|
60
|
+
if (logga = @full_req.env['safrano.logger_mw'])
|
61
|
+
logga.batch_log(env, status, header, began_at)
|
62
|
+
# TODO check why/if we need Rack::Utils::HeaderHash.new(header)
|
63
|
+
# and Rack::BodyProxy.new(body) ?
|
64
|
+
end
|
65
|
+
[status, header, body]
|
55
66
|
end
|
56
67
|
|
57
68
|
# shamelessely copied from Rack::TEST:Session
|
@@ -71,7 +82,10 @@ module OData
|
|
71
82
|
@env = ::Rack::MockRequest.env_for(mime_req.uri,
|
72
83
|
method: mime_req.http_method,
|
73
84
|
input: mime_req.content)
|
85
|
+
# Logging of sub-requests
|
86
|
+
@env[Rack::RACK_ERRORS] = @full_req.env[Rack::RACK_ERRORS]
|
74
87
|
@env.merge! headers_for_env(mime_req.hd)
|
88
|
+
|
75
89
|
@env
|
76
90
|
end
|
77
91
|
end
|
@@ -112,7 +126,7 @@ module OData
|
|
112
126
|
def odata_post(req)
|
113
127
|
@request = req
|
114
128
|
|
115
|
-
if @request.media_type ==
|
129
|
+
if @request.media_type == OData::MP_MIXED
|
116
130
|
|
117
131
|
batcha = @request.create_batch_app
|
118
132
|
@mult_request = @request.parse_multipart
|
data/lib/odata/collection.rb
CHANGED
@@ -9,6 +9,7 @@ require 'odata/error.rb'
|
|
9
9
|
require 'odata/collection_filter.rb'
|
10
10
|
require 'odata/collection_order.rb'
|
11
11
|
require 'odata/url_parameters.rb'
|
12
|
+
require 'odata/collection_media.rb'
|
12
13
|
|
13
14
|
# small helper method
|
14
15
|
# http://stackoverflow.com/
|
@@ -52,6 +53,9 @@ module OData
|
|
52
53
|
attr_reader :nav_entity_attribs
|
53
54
|
attr_reader :data_fields
|
54
55
|
attr_reader :inlinecount
|
56
|
+
# set to parent entity in case the collection is a nav.collection
|
57
|
+
# nil otherwise
|
58
|
+
attr_reader :nav_parent
|
55
59
|
|
56
60
|
attr_accessor :namespace
|
57
61
|
|
@@ -75,13 +79,27 @@ module OData
|
|
75
79
|
attr_accessor :deferred_iblock
|
76
80
|
|
77
81
|
# convention: entityType is the Ruby Model class --> name is just to_s
|
82
|
+
# Warning: for handling Navigation relations, we use anonymous collection classes
|
83
|
+
# dynamically subtyped from a Model class, and in such an anonymous class
|
84
|
+
# the class-name is not the OData Type. In these subclass we redefine "type_name"
|
85
|
+
# thus when we need the Odata type name, we shall use this method instead of just the collection class name
|
78
86
|
def type_name
|
79
87
|
to_s
|
80
88
|
end
|
81
89
|
|
82
|
-
# convention: default for entity_set_name is the
|
90
|
+
# convention: default for entity_set_name is the type name
|
83
91
|
def entity_set_name
|
84
|
-
@entity_set_name = (@entity_set_name ||
|
92
|
+
@entity_set_name = (@entity_set_name || type_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def reset
|
96
|
+
# TODO: automatically reset all attributes?
|
97
|
+
@deferred_iblock = nil
|
98
|
+
@entity_set_name = nil
|
99
|
+
@uparms = nil
|
100
|
+
@params = nil
|
101
|
+
@ax = nil
|
102
|
+
@cx = nil
|
85
103
|
end
|
86
104
|
|
87
105
|
def execute_deferred_iblock
|
@@ -89,19 +107,37 @@ module OData
|
|
89
107
|
end
|
90
108
|
|
91
109
|
# Factory json-> Model Object instance
|
92
|
-
def new_from_hson_h(hash
|
110
|
+
def new_from_hson_h(hash)
|
93
111
|
enty = new
|
94
|
-
hash.delete('__metadata')
|
95
|
-
# DONE: move this somewhere else where it's evaluated only once at setup
|
96
|
-
# data_fields = db_schema.map do |col, cattr|
|
97
|
-
# cattr[:primary_key] ? nil : col
|
98
|
-
# end.select { |col| col }
|
99
112
|
enty.set_fields(hash, @data_fields, missing: :skip)
|
100
|
-
# in-changeset requests get their own transaction
|
101
|
-
enty.save(transaction: !in_changeset)
|
102
113
|
enty
|
103
114
|
end
|
104
115
|
|
116
|
+
CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
|
117
|
+
# in-changeset requests get their own transaction
|
118
|
+
case assoc[:type]
|
119
|
+
when :one_to_many, :one_to_one
|
120
|
+
OData.create_nav_relation(new_entity, assoc, parent)
|
121
|
+
new_entity.save(transaction: false)
|
122
|
+
when :many_to_one
|
123
|
+
new_entity.save(transaction: false)
|
124
|
+
OData.create_nav_relation(new_entity, assoc, parent)
|
125
|
+
parent.save(transaction: false)
|
126
|
+
else
|
127
|
+
# not supported
|
128
|
+
end
|
129
|
+
end
|
130
|
+
def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
131
|
+
if req.in_changeset
|
132
|
+
# in-changeset requests get their own transaction
|
133
|
+
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
134
|
+
else
|
135
|
+
db.transaction do
|
136
|
+
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
105
141
|
def odata_get_inlinecount_w_sequel
|
106
142
|
return unless (icp = @params['$inlinecount'])
|
107
143
|
|
@@ -114,10 +150,6 @@ module OData
|
|
114
150
|
end
|
115
151
|
end
|
116
152
|
|
117
|
-
def navigated_coll
|
118
|
-
false
|
119
|
-
end
|
120
|
-
|
121
153
|
def attrib_path_valid?(path)
|
122
154
|
@attribute_path_list.include? path
|
123
155
|
end
|
@@ -209,7 +241,7 @@ module OData
|
|
209
241
|
def initialize_dataset
|
210
242
|
@cx = self
|
211
243
|
@ax = nil
|
212
|
-
@cx = navigated_dataset if @cx.
|
244
|
+
@cx = navigated_dataset if @cx.nav_parent
|
213
245
|
@model = if @cx.respond_to? :model
|
214
246
|
@cx.model
|
215
247
|
else
|
@@ -227,9 +259,9 @@ module OData
|
|
227
259
|
[200, CT_TEXT, @cx.count.to_s]
|
228
260
|
elsif req.accept?(APPJSON)
|
229
261
|
if req.walker.do_links
|
230
|
-
[200, CT_JSON, to_odata_links_json(service: req.service)]
|
262
|
+
[200, CT_JSON, [to_odata_links_json(service: req.service)]]
|
231
263
|
else
|
232
|
-
[200, CT_JSON, to_odata_json(service: req.service)]
|
264
|
+
[200, CT_JSON, [to_odata_json(service: req.service)]]
|
233
265
|
end
|
234
266
|
else # TODO: other formats
|
235
267
|
406
|
@@ -250,6 +282,10 @@ module OData
|
|
250
282
|
end
|
251
283
|
end
|
252
284
|
|
285
|
+
def odata_post(req)
|
286
|
+
odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
|
287
|
+
end
|
288
|
+
|
253
289
|
# add metadata xml to the passed REXML schema object
|
254
290
|
def add_metadata_rexml(schema)
|
255
291
|
enty = schema.add_element('EntityType', 'Name' => to_s)
|
@@ -289,48 +325,28 @@ module OData
|
|
289
325
|
end
|
290
326
|
end
|
291
327
|
|
328
|
+
D = 'd'.freeze
|
329
|
+
DJopen = '{"d":'.freeze
|
330
|
+
DJclose = '}'.freeze
|
292
331
|
def to_odata_links_json(service:)
|
293
|
-
|
332
|
+
innerj = service.get_coll_odata_links_h(array: get_a,
|
294
333
|
uribase: @uribase,
|
295
|
-
icount: @inlinecount)
|
334
|
+
icount: @inlinecount).to_json
|
335
|
+
"#{DJopen}#{innerj}#{DJclose}"
|
296
336
|
end
|
297
337
|
|
298
338
|
def to_odata_json(service:)
|
299
|
-
|
339
|
+
innerj = service.get_coll_odata_h(array: get_a,
|
300
340
|
expand: @params['$expand'],
|
301
341
|
uribase: @uribase,
|
302
|
-
icount: @inlinecount)
|
342
|
+
icount: @inlinecount).to_json
|
343
|
+
"#{DJopen}#{innerj}#{DJclose}"
|
303
344
|
end
|
304
345
|
|
305
346
|
def get_a
|
306
347
|
@ax.nil? ? @cx.to_a : @ax
|
307
348
|
end
|
308
349
|
|
309
|
-
def odata_post(req)
|
310
|
-
# TODO: this is for v2 only...
|
311
|
-
on_error = (proc { raise Sequel::Rollback } if req.in_changeset)
|
312
|
-
|
313
|
-
req.with_parsed_data(on_error: on_error) do |data|
|
314
|
-
data.delete('__metadata')
|
315
|
-
# validate payload column names
|
316
|
-
if (invalid = invalid_hash_data?(data))
|
317
|
-
on_error.call if on_error
|
318
|
-
return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
|
319
|
-
end
|
320
|
-
|
321
|
-
if req.accept?(APPJSON)
|
322
|
-
|
323
|
-
new_entity = new_from_hson_h(data, in_changeset: req.in_changeset)
|
324
|
-
req.register_content_id_ref(new_entity)
|
325
|
-
new_entity.copy_request_infos(req)
|
326
|
-
|
327
|
-
[201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
|
328
|
-
else # TODO: other formats
|
329
|
-
415
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
350
|
# this functionally similar to the Sequel Rels (many_to_one etc)
|
335
351
|
# We need to base this on the Sequel rels, or extend them
|
336
352
|
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
|
@@ -471,6 +487,7 @@ module OData
|
|
471
487
|
end
|
472
488
|
include Transitions
|
473
489
|
end
|
490
|
+
|
474
491
|
# special handling for composite key
|
475
492
|
module EntityClassMultiPK
|
476
493
|
include EntityClassBase
|
@@ -508,4 +525,38 @@ module OData
|
|
508
525
|
check_odata_val_type(id, db_schema[primary_key][:type])
|
509
526
|
end
|
510
527
|
end
|
528
|
+
|
529
|
+
# normal handling for non-media entity
|
530
|
+
module EntityClassNonMedia
|
531
|
+
# POST for non-media entity collection -->
|
532
|
+
# 1. Create and add entity from payload
|
533
|
+
# 2. Create relationship if needed
|
534
|
+
def odata_create_entity_and_relation(req, assoc, parent)
|
535
|
+
# TODO: this is for v2 only...
|
536
|
+
req.with_parsed_data do |data|
|
537
|
+
data.delete('__metadata')
|
538
|
+
# validate payload column names
|
539
|
+
if (invalid = invalid_hash_data?(data))
|
540
|
+
::OData::Request::ON_CGST_ERROR.call(req)
|
541
|
+
return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
|
542
|
+
end
|
543
|
+
|
544
|
+
if req.accept?(APPJSON)
|
545
|
+
new_entity = new_from_hson_h(data)
|
546
|
+
if parent
|
547
|
+
odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
548
|
+
else
|
549
|
+
# in-changeset requests get their own transaction
|
550
|
+
new_entity.save(transaction: !req.in_changeset)
|
551
|
+
end
|
552
|
+
req.register_content_id_ref(new_entity)
|
553
|
+
new_entity.copy_request_infos(req)
|
554
|
+
|
555
|
+
[201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
|
556
|
+
else # TODO: other formats
|
557
|
+
415
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
511
562
|
end
|