safrano 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/odata/batch.rb +6 -6
- data/lib/odata/collection.rb +134 -74
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +53 -54
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +34 -34
- data/lib/odata/entity.rb +86 -70
- data/lib/odata/error.rb +17 -4
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/parse.rb +4 -12
- data/lib/odata/filter/sequel.rb +11 -13
- data/lib/odata/filter/tree.rb +11 -15
- data/lib/odata/navigation_attribute.rb +36 -40
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/safrano.rb +5 -5
- data/lib/safrano/core.rb +10 -1
- data/lib/safrano/multipart.rb +16 -16
- data/lib/safrano/rack_app.rb +3 -3
- data/lib/safrano/request.rb +6 -6
- data/lib/safrano/response.rb +1 -1
- data/lib/safrano/service.rb +64 -119
- data/lib/safrano/version.rb +1 -1
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +5 -3
data/lib/safrano/core.rb
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
# our main namespace
|
4
4
|
module OData
|
5
|
+
# frozen empty Array/Hash to reduce unncecessary object creation
|
6
|
+
EMPTY_ARRAY = [].freeze
|
7
|
+
EMPTY_HASH = {}.freeze
|
8
|
+
EMPTY_HASH_IN_ARY = [EMPTY_HASH].freeze
|
9
|
+
EMPTY_STRING = ''.freeze
|
10
|
+
ARY_204_EMPTY_HASH_ARY = [204, EMPTY_HASH, EMPTY_ARRAY].freeze
|
11
|
+
SPACE = ' '.freeze
|
12
|
+
COMMA = ','.freeze
|
13
|
+
|
5
14
|
# some prominent constants... probably already defined elsewhere eg in Rack
|
6
15
|
# but lets KISS
|
7
16
|
CONTENT_TYPE = 'Content-Type'.freeze
|
@@ -32,7 +41,7 @@ module OData
|
|
32
41
|
# database-specific types.
|
33
42
|
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
34
43
|
|
35
|
-
# TODO... complete; used in $metadata
|
44
|
+
# TODO... complete; used in $metadata
|
36
45
|
def self.get_edm_type(db_type:)
|
37
46
|
case db_type.upcase
|
38
47
|
when 'INTEGER'
|
data/lib/safrano/multipart.rb
CHANGED
@@ -6,6 +6,9 @@ require 'webrick/httpstatus'
|
|
6
6
|
|
7
7
|
# Simple multipart support for OData $batch purpose
|
8
8
|
module MIME
|
9
|
+
CTT_TYPE_LC = 'content-type'.freeze
|
10
|
+
TEXT_PLAIN = 'text/plain'.freeze
|
11
|
+
|
9
12
|
# a mime object has a header(with content-type etc) and a content(aka body)
|
10
13
|
class Media
|
11
14
|
# Parser for MIME::Media
|
@@ -56,7 +59,7 @@ module MIME
|
|
56
59
|
|
57
60
|
# elsif CRLF_LINE_RGX =~ line
|
58
61
|
elsif CRLF == line
|
59
|
-
@target_ct = @target_hd[
|
62
|
+
@target_ct = @target_hd[CTT_TYPE_LC] || TEXT_PLAIN
|
60
63
|
@state = new_content
|
61
64
|
|
62
65
|
end
|
@@ -100,8 +103,8 @@ module MIME
|
|
100
103
|
end
|
101
104
|
|
102
105
|
def hook_multipart(content_type, boundary)
|
103
|
-
@target_hd[
|
104
|
-
@target_ct = @target_hd[
|
106
|
+
@target_hd[CTT_TYPE_LC] = content_type
|
107
|
+
@target_ct = @target_hd[CTT_TYPE_LC]
|
105
108
|
@target = multipart_content(boundary)
|
106
109
|
@target.hd = @target_hd
|
107
110
|
@target.ct = @target_ct
|
@@ -114,10 +117,8 @@ module MIME
|
|
114
117
|
APP_HTTP = 'application/http'.freeze
|
115
118
|
def new_content
|
116
119
|
@target =
|
117
|
-
if @target_ct.start_with?(MPS)
|
118
|
-
(md = (
|
119
|
-
(MP_RGX2.match(@target_ct[10..-1])))
|
120
|
-
)
|
120
|
+
if @target_ct.start_with?(MPS) &&
|
121
|
+
(md = (MP_RGX1.match(@target_ct[10..-1]) || MP_RGX2.match(@target_ct[10..-1])))
|
121
122
|
multipart_content(md[2].strip)
|
122
123
|
elsif @target_ct.start_with?(APP_HTTP)
|
123
124
|
MIME::Content::Application::Http.new
|
@@ -243,8 +244,8 @@ module MIME
|
|
243
244
|
@hd = {}
|
244
245
|
@content = ''
|
245
246
|
# set default values. Can be overwritten by parser
|
246
|
-
@hd[
|
247
|
-
@ct =
|
247
|
+
@hd[CTT_TYPE_LC] = TEXT_PLAIN
|
248
|
+
@ct = TEXT_PLAIN
|
248
249
|
@parser = Parser.new(self)
|
249
250
|
end
|
250
251
|
|
@@ -364,7 +365,7 @@ module MIME
|
|
364
365
|
end
|
365
366
|
|
366
367
|
def set_multipart_header
|
367
|
-
@hd[
|
368
|
+
@hd[CTT_TYPE_LC] = "#{OData::MP_MIXED}; boundary=#{@boundary}"
|
368
369
|
end
|
369
370
|
|
370
371
|
def get_http_resp(batcha)
|
@@ -382,9 +383,7 @@ module MIME
|
|
382
383
|
# of the changes
|
383
384
|
batcha.db.transaction do
|
384
385
|
begin
|
385
|
-
@response.content = @content.map { |part|
|
386
|
-
part.get_response(batcha)
|
387
|
-
}
|
386
|
+
@response.content = @content.map { |part| part.get_response(batcha) }
|
388
387
|
rescue Sequel::Rollback => e
|
389
388
|
# one of the changes of the changeset has failed
|
390
389
|
# --> provide a dummy empty response for the change-parts
|
@@ -413,10 +412,11 @@ module MIME
|
|
413
412
|
b = ''
|
414
413
|
unless bodyonly
|
415
414
|
# b << OData::CONTENT_TYPE << ': ' << @hd[OData::CTT_TYPE_LC] << CRLF
|
416
|
-
b << "#{OData::CONTENT_TYPE}: #{@hd[
|
415
|
+
b << "#{OData::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
|
417
416
|
end
|
418
|
-
|
419
|
-
b <<
|
417
|
+
|
418
|
+
b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
|
419
|
+
b << @content.map(&:unparse).join(crbdcr)
|
420
420
|
b << "#{CRLF}--#{@boundary}--#{CRLF}"
|
421
421
|
b
|
422
422
|
end
|
data/lib/safrano/rack_app.rb
CHANGED
@@ -14,7 +14,7 @@ module OData
|
|
14
14
|
x = if @walker.status == :end
|
15
15
|
headers.delete('Content-Type')
|
16
16
|
@response.headers.delete('Content-Type')
|
17
|
-
[200,
|
17
|
+
[200, EMPTY_HASH, '']
|
18
18
|
else
|
19
19
|
odata_error
|
20
20
|
end
|
@@ -70,7 +70,7 @@ module OData
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def odata_head
|
73
|
-
[200,
|
73
|
+
[200, EMPTY_HASH, [EMPTY_STRING]]
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
@@ -119,7 +119,7 @@ module OData
|
|
119
119
|
|
120
120
|
def dispatch
|
121
121
|
req_ret = if @request.request_method !~ METHODS_REGEXP
|
122
|
-
[404,
|
122
|
+
[404, EMPTY_HASH, ['Did you get lost?']]
|
123
123
|
elsif @request.request_method == 'HEAD'
|
124
124
|
odata_head
|
125
125
|
else
|
data/lib/safrano/request.rb
CHANGED
@@ -134,14 +134,14 @@ module OData
|
|
134
134
|
|
135
135
|
def with_media_data
|
136
136
|
if (filename = @env['HTTP_SLUG'])
|
137
|
-
|
138
|
-
yield @env['rack.input'],
|
137
|
+
|
138
|
+
yield @env['rack.input'],
|
139
139
|
content_type.split(';').first,
|
140
140
|
Rfc2047.decode(filename)
|
141
141
|
|
142
142
|
else
|
143
143
|
ON_CGST_ERROR.call(self)
|
144
|
-
|
144
|
+
[400, EMPTY_HASH, ['File upload error: Missing SLUG']]
|
145
145
|
end
|
146
146
|
end
|
147
147
|
|
@@ -152,15 +152,15 @@ module OData
|
|
152
152
|
data = JSON.parse(body.read)
|
153
153
|
rescue JSON::ParserError => e
|
154
154
|
ON_CGST_ERROR.call(self)
|
155
|
-
return [400,
|
156
|
-
|
155
|
+
return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
|
156
|
+
e.message]]
|
157
157
|
end
|
158
158
|
|
159
159
|
yield data
|
160
160
|
|
161
161
|
else # TODO: other formats
|
162
162
|
|
163
|
-
[415,
|
163
|
+
[415, EMPTY_HASH, EMPTY_ARRAY]
|
164
164
|
end
|
165
165
|
end
|
166
166
|
|
data/lib/safrano/response.rb
CHANGED
data/lib/safrano/service.rb
CHANGED
@@ -21,20 +21,22 @@ module OData
|
|
21
21
|
# TODO: check errorhandling
|
22
22
|
raise OData::ServerError if cur_exp.nil?
|
23
23
|
|
24
|
-
|
24
|
+
k_s = cur_exp
|
25
|
+
|
25
26
|
else
|
26
|
-
|
27
|
+
k_s = exp_one.strip
|
27
28
|
rest_exp = nil
|
28
29
|
end
|
29
|
-
|
30
|
+
k = k_s.to_sym
|
31
|
+
yield k, k_s, rest_exp
|
30
32
|
end
|
31
33
|
|
32
34
|
# default v2
|
33
35
|
# overriden in ServiceV1
|
34
|
-
def get_coll_odata_h(array:,
|
36
|
+
def get_coll_odata_h(array:, template:, uribase:, icount: nil)
|
35
37
|
res = array.map do |w|
|
36
38
|
get_entity_odata_h(entity: w,
|
37
|
-
|
39
|
+
template: template,
|
38
40
|
uribase: uribase)
|
39
41
|
end
|
40
42
|
if icount
|
@@ -44,107 +46,55 @@ module OData
|
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
47
|
-
# for expand 1..n nav attributes
|
48
|
-
# actually same as v1 get_coll_odata_h
|
49
|
-
# def get_expandcoll_odata_h(array:, expand: nil, uribase:, icount: nil)
|
50
|
-
# array.map do |w|
|
51
|
-
# get_entity_odata_h(entity: w,
|
52
|
-
# expand: expand,
|
53
|
-
# uribase: uribase)
|
54
|
-
# end
|
55
|
-
# end
|
56
|
-
|
57
|
-
# handle a single expand
|
58
|
-
def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
|
59
|
-
uribase:)
|
60
|
-
|
61
|
-
|
62
|
-
split_entity_expand_arg(exp_one) do |first, rest_exp|
|
63
|
-
if ( entity.nav_values.has_key?(first) )
|
64
|
-
if (enval = entity.nav_values[first])
|
65
|
-
nav_values_h[first.to_s] = get_entity_odata_h(entity: enval,
|
66
|
-
expand: rest_exp,
|
67
|
-
uribase: uribase)
|
68
|
-
else
|
69
|
-
# FK is NULL --> nav_value is nil --> return empty json
|
70
|
-
nav_values_h[first.to_s] = {}
|
71
|
-
end
|
72
|
-
elsif (encoll = entity.nav_coll[first])
|
73
|
-
# nav attributes that are a collection (x..n)
|
74
|
-
nav_coll_h[first.to_s] = get_coll_odata_h(array: encoll,
|
75
|
-
expand: rest_exp,
|
76
|
-
uribase: uribase)
|
77
|
-
# nav_coll_h[first.to_s] = get_expandcoll_odata_h(array: encoll,
|
78
|
-
# expand: rest_exp,
|
79
|
-
# uribase: uribase)
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def handle_entity_expand(entity:, expand:, nav_values_h:,
|
87
|
-
nav_coll_h:, uribase:)
|
88
|
-
expand.strip!
|
89
|
-
explist = expand.split(',')
|
90
|
-
# handle multiple expands
|
91
|
-
explist.each do |exp|
|
92
|
-
handle_entity_expand_one(entity: entity,
|
93
|
-
exp_one: exp,
|
94
|
-
nav_values_h: nav_values_h,
|
95
|
-
nav_coll_h: nav_coll_h,
|
96
|
-
uribase: uribase)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def handle_entity_deferred_attribs(entity:, nav_values_h:,
|
101
|
-
nav_coll_h:, uribase:)
|
102
|
-
entity.nav_values.each_key do |ksy|
|
103
|
-
ks = ksy.to_s
|
104
|
-
next if nav_values_h.key?(ks)
|
105
|
-
|
106
|
-
nav_values_h[ks] = get_deferred_odata_h(entity: entity,
|
107
|
-
attrib: ks, uribase: uribase)
|
108
|
-
end
|
109
|
-
entity.nav_coll.each_key do |ksy|
|
110
|
-
ks = ksy.to_s
|
111
|
-
next if nav_coll_h.key?(ks)
|
112
|
-
|
113
|
-
nav_coll_h[ks] = get_deferred_odata_h(entity: entity, attrib: ks,
|
114
|
-
uribase: uribase)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
49
|
# handle $links ... Note: $expand seems to be ignored when $links
|
119
50
|
# are requested
|
120
51
|
def get_entity_odata_link_h(entity:, uribase:)
|
121
52
|
{ uri: entity.uri(uribase) }
|
122
53
|
end
|
123
54
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
nav_values_h = {}
|
129
|
-
nav_coll_h = {}
|
55
|
+
EMPTYH = {}.freeze
|
56
|
+
def get_entity_odata_h(entity:, template:, uribase:)
|
57
|
+
# start with metadata
|
58
|
+
hres = { '__metadata' => entity.metadata_h(uribase: uribase) }
|
130
59
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
60
|
+
template.each do |elmt, arg|
|
61
|
+
case elmt
|
62
|
+
when :all_values
|
63
|
+
hres.merge! entity.casted_values
|
64
|
+
when :selected_vals
|
65
|
+
hres.merge! entity.casted_values(arg)
|
66
|
+
when :expand_e
|
138
67
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
nav_coll_h: nav_coll_h,
|
143
|
-
uribase: uribase)
|
144
|
-
# merge ...
|
145
|
-
hres.merge!(nav_values_h)
|
146
|
-
hres.merge!(nav_coll_h)
|
68
|
+
arg.each do |attr, templ|
|
69
|
+
enval = entity.send(attr)
|
70
|
+
hres[attr] = if enval
|
147
71
|
|
72
|
+
get_entity_odata_h(entity: enval,
|
73
|
+
template: templ,
|
74
|
+
uribase: uribase)
|
75
|
+
else
|
76
|
+
# FK is NULL --> nav_value is nil --> return empty json
|
77
|
+
EMPTYH
|
78
|
+
end
|
79
|
+
end
|
80
|
+
when :expand_c
|
81
|
+
arg.each do |attr, templ|
|
82
|
+
next unless (encoll = entity.send(attr))
|
83
|
+
|
84
|
+
# nav attributes that are a collection (x..n)
|
85
|
+
hres[attr] = get_coll_odata_h(array: encoll,
|
86
|
+
template: templ,
|
87
|
+
uribase: uribase)
|
88
|
+
# else error ?
|
89
|
+
end
|
90
|
+
when :deferr
|
91
|
+
arg.each do |attr|
|
92
|
+
hres[attr] = get_deferred_odata_h(entity: entity,
|
93
|
+
attrib: attr,
|
94
|
+
uribase: uribase)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
148
98
|
hres
|
149
99
|
end
|
150
100
|
end
|
@@ -219,7 +169,7 @@ module OData
|
|
219
169
|
|
220
170
|
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
221
171
|
TRAILING_SLASH = %r{/\z}.freeze
|
222
|
-
DEFAULT_PATH_PREFIX = '/'
|
172
|
+
DEFAULT_PATH_PREFIX = '/'.freeze
|
223
173
|
|
224
174
|
# input is the DataServiceVersion request header string, eg.
|
225
175
|
# '2.0;blabla' ---> Version -> 2
|
@@ -279,23 +229,20 @@ module OData
|
|
279
229
|
|
280
230
|
def register_model(modelklass, entity_set_name = nil, is_media = false)
|
281
231
|
# check that the provided klass is a Sequel Model
|
282
|
-
|
283
|
-
|
284
|
-
end
|
232
|
+
|
233
|
+
raise(OData::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
285
234
|
|
286
235
|
if modelklass.ancestors.include? OData::Entity
|
287
236
|
# modules were already added previously;
|
288
237
|
# cleanup state to avoid having data from previous calls
|
289
238
|
# mostly usefull for testing (eg API)
|
290
239
|
modelklass.reset
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
modelklass.include OData::EntitySinglePK
|
298
|
-
end
|
240
|
+
elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
|
241
|
+
modelklass.extend OData::EntityClassMultiPK
|
242
|
+
modelklass.include OData::EntityMultiPK
|
243
|
+
else
|
244
|
+
modelklass.extend OData::EntityClassSinglePK
|
245
|
+
modelklass.include OData::EntitySinglePK
|
299
246
|
end
|
300
247
|
# Media/Non-media
|
301
248
|
if is_media
|
@@ -345,9 +292,7 @@ module OData
|
|
345
292
|
# example: CrewMember must be matched before Crew otherwise we get error
|
346
293
|
def set_collections_sorted(coll_data)
|
347
294
|
@collections = coll_data
|
348
|
-
if @collections
|
349
|
-
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse!
|
350
|
-
end
|
295
|
+
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
|
351
296
|
@collections
|
352
297
|
end
|
353
298
|
|
@@ -369,9 +314,9 @@ module OData
|
|
369
314
|
path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
|
370
315
|
|
371
316
|
@collections.each(&:finalize_publishing)
|
372
|
-
|
373
|
-
#finalize the media handlers
|
374
|
-
@collections.each{|klass| }
|
317
|
+
|
318
|
+
# finalize the media handlers
|
319
|
+
@collections.each { |klass| }
|
375
320
|
end
|
376
321
|
|
377
322
|
def execute_deferred_iblocks
|
@@ -568,16 +513,16 @@ module OData
|
|
568
513
|
end
|
569
514
|
end
|
570
515
|
|
571
|
-
def get_coll_odata_h(array:,
|
516
|
+
def get_coll_odata_h(array:, template:, uribase:, icount: nil)
|
572
517
|
array.map do |w|
|
573
518
|
get_entity_odata_h(entity: w,
|
574
|
-
|
519
|
+
template: template,
|
575
520
|
uribase: uribase)
|
576
521
|
end
|
577
522
|
end
|
578
523
|
|
579
524
|
def get_emptycoll_odata_h
|
580
|
-
|
525
|
+
EMPTY_HASH_IN_ARY
|
581
526
|
end
|
582
527
|
end
|
583
528
|
|
@@ -599,7 +544,7 @@ module OData
|
|
599
544
|
end
|
600
545
|
|
601
546
|
def get_emptycoll_odata_h
|
602
|
-
{ 'results' =>
|
547
|
+
{ 'results' => EMPTY_HASH_IN_ARY }
|
603
548
|
end
|
604
549
|
end
|
605
550
|
|