safrano 0.1.0 → 0.2.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 +54 -8
- data/lib/odata/batch.rb +7 -3
- data/lib/odata/collection.rb +10 -6
- data/lib/odata/collection_order.rb +1 -1
- data/lib/odata/entity.rb +8 -3
- data/lib/odata/error.rb +6 -0
- data/lib/odata/walker.rb +30 -3
- data/lib/odata_rack_builder.rb +0 -2
- data/lib/rack_app.rb +4 -7
- data/lib/request.rb +20 -3
- data/lib/safrano_core.rb +2 -0
- data/lib/service.rb +7 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6894a03a0224ba8beedc0a05fba7700ad4e06b34d79858478c5e4fe998ed8eef
|
4
|
+
data.tar.gz: '059f1dc5d7d3c4537f279be2b170b5c67b2c61fdbe27199f511e8f1e894776ad'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d41f73aa8bdd9084eaa150ee06d6c9b05e91ff18dd24f25daae75f25572cf0500c0c5669ade95275b65891655fae4e0e0c53fbb2712e414fe8daf6df91efb8cb
|
7
|
+
data.tar.gz: 11e49d68d0fc005be501f069ab8fcbaad40050449435f4369938b10f1cb90c71cc86b361b4bbaa7541ac7e274ec34de1f6925a1a63d481bcd5bb5690bfb84649
|
data/lib/multipart.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
CRLF = "\r\n".freeze
|
4
2
|
LF = "\n".freeze
|
5
3
|
|
@@ -293,6 +291,7 @@ module MIME
|
|
293
291
|
def initialize(boundary)
|
294
292
|
@boundary = boundary
|
295
293
|
@hd = {}
|
294
|
+
@content_id_references = nil
|
296
295
|
@parser = Parser.new(self)
|
297
296
|
end
|
298
297
|
|
@@ -300,6 +299,37 @@ module MIME
|
|
300
299
|
(@boundary == other.boundary) && (@content == other.content)
|
301
300
|
end
|
302
301
|
|
302
|
+
def each_changeset
|
303
|
+
return unless @level.zero?
|
304
|
+
|
305
|
+
@content.each do |part|
|
306
|
+
yield part if part.is_a? MIME::Content::Multipart::Base
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def prepare_content_id_refs
|
311
|
+
if @level.zero?
|
312
|
+
each_changeset(&:prepare_content_id_refs)
|
313
|
+
elsif @level == 1
|
314
|
+
|
315
|
+
@content_id_references = {}
|
316
|
+
|
317
|
+
@content.each do |part|
|
318
|
+
next unless part.is_a? MIME::Content::Application::Http
|
319
|
+
|
320
|
+
case part.content.http_method
|
321
|
+
when 'POST', 'PUT'
|
322
|
+
if (ctid = part.hd['content-id'])
|
323
|
+
part.content.content_id = ctid
|
324
|
+
@content_id_references[ctid] = nil
|
325
|
+
end
|
326
|
+
end
|
327
|
+
part.content.content_id_references = @content_id_references
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
303
333
|
def set_multipart_header
|
304
334
|
@hd['content-type'] = "multipart/mixed; boundary=#{@boundary}"
|
305
335
|
end
|
@@ -313,29 +343,41 @@ module MIME
|
|
313
343
|
@response = self.class.new(::SecureRandom.uuid)
|
314
344
|
@response.set_multipart_header
|
315
345
|
if @level == 1 # changeset need their own global transaction
|
316
|
-
# the change requests that are part of
|
346
|
+
# the change requests that are part of it have @level==2
|
317
347
|
# and will be flagged with in_changeset=true
|
318
348
|
# and this will finally be used to skip the transaction
|
319
349
|
# of the changes
|
320
350
|
batcha.db.transaction do
|
321
351
|
begin
|
322
|
-
@response.content = @content.map { |part|
|
352
|
+
@response.content = @content.map { |part|
|
353
|
+
part.get_response(batcha)
|
354
|
+
}
|
323
355
|
rescue Sequel::Rollback => e
|
324
356
|
# one of the changes of the changeset has failed
|
325
357
|
# --> provide a dummy empty response for the change-parts
|
326
358
|
# then transmit the Rollback to Sequel
|
327
|
-
|
328
|
-
MIME::Content::Application::HttpResp.new
|
329
|
-
}
|
359
|
+
get_failed_changeset_response(e)
|
330
360
|
raise
|
331
361
|
end
|
332
362
|
end
|
333
363
|
else
|
334
|
-
@response.content = @content.map { |part|
|
364
|
+
@response.content = @content.map { |part|
|
365
|
+
part.get_response(batcha)
|
366
|
+
}
|
335
367
|
end
|
336
368
|
@response
|
337
369
|
end
|
338
370
|
|
371
|
+
def get_failed_changeset_response(_xeption)
|
372
|
+
@response = MIME::Content::Application::HttpResp.new
|
373
|
+
@response.status = '400'
|
374
|
+
@response.content = [{ 'odata.error' =>
|
375
|
+
{ 'message' =>
|
376
|
+
'Bad Request: Failed changeset ' } }.to_json]
|
377
|
+
@response.hd = { 'Content-Type' => 'application/json;charset=utf-8' }
|
378
|
+
@response
|
379
|
+
end
|
380
|
+
|
339
381
|
def unparse(bodyonly = false)
|
340
382
|
b = ''
|
341
383
|
unless bodyonly
|
@@ -356,6 +398,10 @@ module MIME
|
|
356
398
|
attr_accessor :http_method
|
357
399
|
attr_accessor :uri
|
358
400
|
|
401
|
+
# for changeset content-id refs
|
402
|
+
attr_accessor :content_id
|
403
|
+
attr_accessor :content_id_references
|
404
|
+
|
359
405
|
def initialize
|
360
406
|
@hd = {}
|
361
407
|
@content = ''
|
data/lib/odata/batch.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
require 'rack_app.rb'
|
4
2
|
require 'safrano_core.rb'
|
5
3
|
|
@@ -44,7 +42,11 @@ module OData
|
|
44
42
|
@request = OData::Request.new(env)
|
45
43
|
@response = OData::Response.new
|
46
44
|
|
47
|
-
|
45
|
+
if part_req.level == 2
|
46
|
+
@request.in_changeset = true
|
47
|
+
@request.content_id = part_req.content_id
|
48
|
+
@request.content_id_references = part_req.content_id_references
|
49
|
+
end
|
48
50
|
|
49
51
|
before
|
50
52
|
dispatch
|
@@ -114,6 +116,8 @@ module OData
|
|
114
116
|
|
115
117
|
batcha = @request.create_batch_app
|
116
118
|
@mult_request = @request.parse_multipart
|
119
|
+
|
120
|
+
@mult_request.prepare_content_id_refs
|
117
121
|
@mult_response = OData::Response.new
|
118
122
|
|
119
123
|
resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
|
data/lib/odata/collection.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
1
|
# Design: Collections are nothing more as Sequel based model classes that have
|
3
2
|
# somehow the character of an array (Enumerable)
|
4
3
|
# Thus Below we have called that "EntityClass". It's meant as "Collection"
|
@@ -179,7 +178,7 @@ module OData
|
|
179
178
|
return unless @params['$skip']
|
180
179
|
|
181
180
|
iskip = number_or_nil(@params['$skip'])
|
182
|
-
return BadRequestError if iskip.nil? ||
|
181
|
+
return BadRequestError if iskip.nil? || iskip.negative?
|
183
182
|
end
|
184
183
|
|
185
184
|
def check_u_p_inlinecount
|
@@ -303,9 +302,9 @@ module OData
|
|
303
302
|
end
|
304
303
|
|
305
304
|
def odata_post(req)
|
306
|
-
# TODO: check Request body format...
|
307
305
|
# TODO: this is for v2 only...
|
308
306
|
on_error = (proc { raise Sequel::Rollback } if req.in_changeset)
|
307
|
+
|
309
308
|
req.with_parsed_data(on_error: on_error) do |data|
|
310
309
|
data.delete('__metadata')
|
311
310
|
# validate payload column names
|
@@ -315,9 +314,14 @@ module OData
|
|
315
314
|
end
|
316
315
|
|
317
316
|
if req.accept?('application/json')
|
318
|
-
|
319
|
-
|
320
|
-
|
317
|
+
|
318
|
+
new_entity = new_from_hson_h(data, in_changeset: req.in_changeset)
|
319
|
+
req.register_content_id_ref(new_entity)
|
320
|
+
new_entity.copy_request_infos(req)
|
321
|
+
|
322
|
+
[201,
|
323
|
+
{ 'Content-Type' => 'application/json;charset=utf-8' },
|
324
|
+
new_entity.to_odata_post_json(service: req.service)]
|
321
325
|
else # TODO: other formats
|
322
326
|
415
|
323
327
|
end
|
data/lib/odata/entity.rb
CHANGED
@@ -101,7 +101,7 @@ module OData
|
|
101
101
|
def to_odata_json(service:)
|
102
102
|
{ 'd' => service.get_entity_odata_h(entity: self,
|
103
103
|
expand: @params['$expand'],
|
104
|
-
#
|
104
|
+
# links: @do_links,
|
105
105
|
uribase: @uribase) }.to_json
|
106
106
|
end
|
107
107
|
|
@@ -121,11 +121,16 @@ module OData
|
|
121
121
|
type: type_name }
|
122
122
|
end
|
123
123
|
|
124
|
-
|
125
|
-
def odata_get(req)
|
124
|
+
def copy_request_infos(req)
|
126
125
|
@params = req.params
|
127
126
|
@uribase = req.uribase
|
128
127
|
@do_links = req.walker.do_links
|
128
|
+
end
|
129
|
+
|
130
|
+
# Finally Process REST verbs...
|
131
|
+
def odata_get(req)
|
132
|
+
copy_request_infos(req)
|
133
|
+
|
129
134
|
if req.accept?('application/json')
|
130
135
|
[200, { 'Content-Type' => 'application/json;charset=utf-8' },
|
131
136
|
to_odata_json(service: req.service)]
|
data/lib/odata/error.rb
CHANGED
@@ -43,6 +43,12 @@ module OData
|
|
43
43
|
HTTP_CODE = 400
|
44
44
|
@msg = 'Bad Request Error'
|
45
45
|
end
|
46
|
+
# Generic failed changeset
|
47
|
+
class BadRequestFailedChangeSet < BadRequestError
|
48
|
+
HTTP_CODE = 400
|
49
|
+
@msg = 'Bad Request: Failed changeset '
|
50
|
+
end
|
51
|
+
|
46
52
|
# for Syntax error in Filtering
|
47
53
|
class BadRequestFilterParseError < BadRequestError
|
48
54
|
HTTP_CODE = 400
|
data/lib/odata/walker.rb
CHANGED
@@ -27,11 +27,13 @@ module OData
|
|
27
27
|
# are $links requested ?
|
28
28
|
attr_reader :do_links
|
29
29
|
|
30
|
-
def initialize(service, path)
|
30
|
+
def initialize(service, path, content_id_refs = nil)
|
31
31
|
raise 'Walker is called with a nil service' unless service
|
32
32
|
|
33
33
|
path = URI.decode_www_form_component(path)
|
34
34
|
@context = service
|
35
|
+
@content_id_refs = content_id_refs
|
36
|
+
|
35
37
|
@contexts = [@context]
|
36
38
|
@path_start = @path_remain = if service
|
37
39
|
unprefixed(service.xpath_prefix, path)
|
@@ -82,17 +84,42 @@ module OData
|
|
82
84
|
get_next_transition
|
83
85
|
if @tr_next
|
84
86
|
@context, @status, @error = @tr_next.do_transition(@context)
|
87
|
+
# little hack's
|
88
|
+
case @status
|
89
|
+
# we dont have the content-id references data on service level
|
90
|
+
# but we have it here, so in case of a $content-id transition
|
91
|
+
# the returned context is just the id, and we get the final result
|
92
|
+
# entity reference here and place it in @context
|
93
|
+
when :run_with_content_id
|
94
|
+
if @content_id_refs.is_a? Hash
|
95
|
+
if (@context = @content_id_refs[@context.to_s])
|
96
|
+
@status = :run
|
97
|
+
else
|
98
|
+
@context = nil
|
99
|
+
@status = :error
|
100
|
+
# TODO: more appropriate error handling
|
101
|
+
@error = OData::ErrorNotFound
|
102
|
+
end
|
103
|
+
else
|
104
|
+
@context = nil
|
105
|
+
@status = :error
|
106
|
+
# TODO: more appropriate error handling
|
107
|
+
@error = OData::ErrorNotFound
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
85
111
|
@contexts << @context
|
86
112
|
@path_remain = @tr_next.path_remain
|
87
113
|
@path_done << @tr_next.path_done
|
114
|
+
|
88
115
|
# little hack's
|
89
116
|
case @status
|
90
117
|
when :end_with_count
|
91
118
|
@do_count = true
|
92
|
-
@status
|
119
|
+
@status = :end
|
93
120
|
when :end_with_value
|
94
121
|
@raw_value = true
|
95
|
-
@status
|
122
|
+
@status = :end
|
96
123
|
when :run_with_links
|
97
124
|
@do_links = true
|
98
125
|
@status = :run
|
data/lib/odata_rack_builder.rb
CHANGED
data/lib/rack_app.rb
CHANGED
@@ -23,18 +23,15 @@ module OData
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def odata_error
|
26
|
-
|
26
|
+
return @walker.error.odata_get(@request) unless @walker.error.nil?
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
# this is too critical; raise a real Exception
|
29
|
+
# begin
|
30
|
+
raise 'Walker construction failed with a unknown Error '
|
31
31
|
# rescue StandardError
|
32
32
|
# binding.pry
|
33
33
|
# end
|
34
34
|
# [500, {}, 'Server Error']
|
35
|
-
else
|
36
|
-
@walker.error.odata_get(@request)
|
37
|
-
end
|
38
35
|
end
|
39
36
|
|
40
37
|
def odata_delete
|
data/lib/request.rb
CHANGED
@@ -13,8 +13,6 @@ 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
|
-
|
18
16
|
# borowed from Sinatra
|
19
17
|
class AcceptEntry
|
20
18
|
attr_accessor :params
|
@@ -73,13 +71,32 @@ module OData
|
|
73
71
|
def parseable_data?
|
74
72
|
false
|
75
73
|
end
|
74
|
+
|
76
75
|
# OData extension
|
77
76
|
attr_accessor :service_base
|
78
77
|
attr_accessor :service
|
79
78
|
attr_accessor :walker
|
80
79
|
|
80
|
+
# request is part of a $batch changeset
|
81
|
+
attr_accessor :in_changeset
|
82
|
+
|
83
|
+
# content_id of request in a $batch changeset
|
84
|
+
attr_accessor :content_id
|
85
|
+
|
86
|
+
# content-id references map
|
87
|
+
attr_accessor :content_id_references
|
88
|
+
|
89
|
+
# stores the newly created entity for the current content-id of
|
90
|
+
# the processed request
|
91
|
+
def register_content_id_ref(new_entity)
|
92
|
+
return unless @in_changeset
|
93
|
+
return unless @content_id
|
94
|
+
|
95
|
+
@content_id_references[@content_id] = new_entity
|
96
|
+
end
|
97
|
+
|
81
98
|
def create_odata_walker
|
82
|
-
@walker = Walker.new(@service, path_info)
|
99
|
+
@walker = Walker.new(@service, path_info, @content_id_references)
|
83
100
|
end
|
84
101
|
|
85
102
|
def accept
|
data/lib/safrano_core.rb
CHANGED
@@ -81,6 +81,8 @@ module Safrano
|
|
81
81
|
trans: 'transition_metadata')
|
82
82
|
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
83
83
|
trans: 'transition_batch')
|
84
|
+
TransitionContentId = Transition.new('\A(\/\$\d+)(.*)',
|
85
|
+
trans: 'transition_content_id')
|
84
86
|
TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
|
85
87
|
trans: 'transition_count')
|
86
88
|
TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
|
data/lib/service.rb
CHANGED
@@ -77,7 +77,7 @@ module OData
|
|
77
77
|
# handle $links ... Note: $expand seems to be ignored when $links
|
78
78
|
# are requested
|
79
79
|
def get_entity_odata_link_h(entity:, uribase:)
|
80
|
-
|
80
|
+
{ uri: entity.uri(uribase) }
|
81
81
|
end
|
82
82
|
|
83
83
|
def get_entity_odata_h(entity:, expand: nil, uribase:)
|
@@ -470,6 +470,7 @@ module OData
|
|
470
470
|
Safrano::TransitionEnd,
|
471
471
|
Safrano::TransitionMetadata,
|
472
472
|
Safrano::TransitionBatch,
|
473
|
+
Safrano::TransitionContentId,
|
473
474
|
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
474
475
|
trans: 'transition_collection')
|
475
476
|
]
|
@@ -483,6 +484,11 @@ module OData
|
|
483
484
|
[@batch_handler, :run]
|
484
485
|
end
|
485
486
|
|
487
|
+
def transition_content_id(match_result)
|
488
|
+
# TODO: a nicer way of extracting the content-id number?
|
489
|
+
[match_result[1].sub!(Regexp.new('\A\/\$'), ''), :run_with_content_id]
|
490
|
+
end
|
491
|
+
|
486
492
|
def transition_metadata(_match_result)
|
487
493
|
[@meta, :run]
|
488
494
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- D.M.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -140,7 +140,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
140
|
- !ruby/object:Gem::Version
|
141
141
|
version: '0'
|
142
142
|
requirements: []
|
143
|
-
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.7.6.2
|
144
145
|
signing_key:
|
145
146
|
specification_version: 4
|
146
147
|
summary: Safrano is a Ruby OData provider based on Sequel and Rack
|