safrano 0.4.1 → 0.4.2
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/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
|
|