safrano 0.3.2 → 0.3.3
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 +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
|