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/odata/entity.rb
CHANGED
@@ -11,7 +11,7 @@ module OData
|
|
11
11
|
attr_reader :uribase
|
12
12
|
|
13
13
|
include EntityBase::NavigationInfo
|
14
|
-
|
14
|
+
|
15
15
|
# methods related to transitions to next state (cf. walker)
|
16
16
|
module Transitions
|
17
17
|
def allowed_transitions
|
@@ -79,7 +79,6 @@ module OData
|
|
79
79
|
self.class.nav_entity_attribs&.each_key do |na_str|
|
80
80
|
@nav_values[na_str.to_sym] = send(na_str)
|
81
81
|
end
|
82
|
-
|
83
82
|
@nav_values
|
84
83
|
end
|
85
84
|
|
@@ -95,31 +94,47 @@ module OData
|
|
95
94
|
"#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
|
96
95
|
end
|
97
96
|
D = 'd'.freeze
|
98
|
-
|
99
|
-
|
97
|
+
DJ_OPEN = '{"d":'.freeze
|
98
|
+
DJ_CLOSE = '}'.freeze
|
99
|
+
|
100
100
|
# Json formatter for a single entity (probably OData V1/V2 like)
|
101
101
|
def to_odata_json(service:)
|
102
|
+
template = self.class.output_template(@uparms)
|
102
103
|
innerj = service.get_entity_odata_h(entity: self,
|
103
|
-
|
104
|
+
template: template,
|
104
105
|
uribase: @uribase).to_json
|
105
|
-
"#{
|
106
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
106
107
|
end
|
107
108
|
|
108
109
|
# Json formatter for a single entity reached by navigation $links
|
109
110
|
def to_odata_onelink_json(service:)
|
110
111
|
innerj = service.get_entity_odata_link_h(entity: self,
|
111
112
|
uribase: @uribase).to_json
|
112
|
-
"#{
|
113
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
113
114
|
end
|
114
115
|
|
116
|
+
def selected_values_for_odata(cols)
|
117
|
+
allvals = values_for_odata
|
118
|
+
selvals = {}
|
119
|
+
cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
|
120
|
+
selvals
|
121
|
+
end
|
115
122
|
|
116
123
|
# needed for proper datetime output
|
117
|
-
# TODO design/performance
|
118
|
-
def casted_values
|
124
|
+
# TODO: design/performance
|
125
|
+
def casted_values(cols = nil)
|
126
|
+
vals = case cols
|
127
|
+
when nil
|
128
|
+
values_for_odata
|
129
|
+
else
|
130
|
+
selected_values_for_odata(cols)
|
131
|
+
end
|
132
|
+
|
119
133
|
# WARNING; this code is duplicated in attribute.rb
|
120
134
|
# (and the inverted transformation is in test/client.rb)
|
121
135
|
# will require a more systematic solution some day
|
122
|
-
|
136
|
+
|
137
|
+
vals.transform_values! do |v|
|
123
138
|
case v
|
124
139
|
when Time
|
125
140
|
# try to get back the database time zone and value
|
@@ -127,14 +142,15 @@ module OData
|
|
127
142
|
else
|
128
143
|
v
|
129
144
|
end
|
130
|
-
|
145
|
+
end
|
131
146
|
end
|
132
147
|
|
133
148
|
# post paylod expects the new entity in an array
|
134
149
|
def to_odata_post_json(service:)
|
135
150
|
innerj = service.get_coll_odata_h(array: [self],
|
151
|
+
template: self.class.default_template,
|
136
152
|
uribase: @uribase).to_json
|
137
|
-
"#{
|
153
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
138
154
|
end
|
139
155
|
|
140
156
|
def type_name
|
@@ -145,12 +161,12 @@ module OData
|
|
145
161
|
@params = req.params
|
146
162
|
@uribase = req.uribase
|
147
163
|
@do_links = req.walker.do_links
|
164
|
+
@uparms = UrlParameters4Single.new(@params)
|
148
165
|
end
|
149
166
|
|
150
167
|
# Finally Process REST verbs...
|
151
168
|
def odata_get(req)
|
152
169
|
copy_request_infos(req)
|
153
|
-
|
154
170
|
if req.walker.media_value
|
155
171
|
odata_media_value_get(req)
|
156
172
|
elsif req.accept?(APPJSON)
|
@@ -163,12 +179,12 @@ module OData
|
|
163
179
|
415
|
164
180
|
end
|
165
181
|
end
|
166
|
-
|
182
|
+
|
167
183
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
168
|
-
OData.remove_nav_relation(
|
184
|
+
OData.remove_nav_relation(assoc, parent)
|
169
185
|
entity.destroy(transaction: false)
|
170
186
|
end
|
171
|
-
|
187
|
+
|
172
188
|
def odata_delete_relation_and_entity(req, assoc, parent)
|
173
189
|
if parent
|
174
190
|
if req.in_changeset
|
@@ -182,52 +198,48 @@ module OData
|
|
182
198
|
else
|
183
199
|
destroy(transaction: false)
|
184
200
|
end
|
185
|
-
|
186
|
-
|
201
|
+
rescue StandardError => e
|
202
|
+
raise SequelAdapterError.new(e)
|
187
203
|
end
|
188
|
-
|
204
|
+
|
189
205
|
# TODO: differentiate between POST/PUT/PATCH/MERGE
|
190
206
|
def odata_post(req)
|
191
207
|
if req.walker.media_value
|
192
208
|
odata_media_value_put(req)
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
update_fields(data, self.class.data_fields, missing: :skip)
|
202
|
-
end
|
203
|
-
|
204
|
-
[202, {}, to_odata_post_json(service: req.service)]
|
205
|
-
else # TODO: other formats
|
206
|
-
415
|
209
|
+
elsif req.accept?(APPJSON)
|
210
|
+
data.delete('__metadata')
|
211
|
+
|
212
|
+
if req.in_changeset
|
213
|
+
set_fields(data, self.class.data_fields, missing: :skip)
|
214
|
+
save(transaction: false)
|
215
|
+
else
|
216
|
+
update_fields(data, self.class.data_fields, missing: :skip)
|
207
217
|
end
|
218
|
+
|
219
|
+
[202, EMPTY_HASH, to_odata_post_json(service: req.service)]
|
220
|
+
else # TODO: other formats
|
221
|
+
415
|
208
222
|
end
|
209
223
|
end
|
210
224
|
|
211
225
|
def odata_put(req)
|
212
226
|
if req.walker.media_value
|
213
227
|
odata_media_value_put(req)
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
data.delete('__metadata')
|
219
|
-
|
220
|
-
if req.in_changeset
|
221
|
-
set_fields(data, self.class.data_fields, missing: :skip)
|
222
|
-
save(transaction: false)
|
223
|
-
else
|
224
|
-
update_fields(data, self.class.data_fields, missing: :skip)
|
225
|
-
end
|
228
|
+
elsif req.accept?(APPJSON)
|
229
|
+
data = JSON.parse(req.body.read)
|
230
|
+
@uribase = req.uribase
|
231
|
+
data.delete('__metadata')
|
226
232
|
|
227
|
-
|
228
|
-
|
229
|
-
|
233
|
+
if req.in_changeset
|
234
|
+
set_fields(data, self.class.data_fields, missing: :skip)
|
235
|
+
save(transaction: false)
|
236
|
+
else
|
237
|
+
update_fields(data, self.class.data_fields, missing: :skip)
|
230
238
|
end
|
239
|
+
|
240
|
+
ARY_204_EMPTY_HASH_ARY
|
241
|
+
else # TODO: other formats
|
242
|
+
415
|
231
243
|
end
|
232
244
|
end
|
233
245
|
|
@@ -238,7 +250,7 @@ module OData
|
|
238
250
|
# validate payload column names
|
239
251
|
if (invalid = self.class.invalid_hash_data?(data))
|
240
252
|
::OData::Request::ON_CGST_ERROR.call(req)
|
241
|
-
return [422,
|
253
|
+
return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
|
242
254
|
end
|
243
255
|
# TODO: check values/types
|
244
256
|
|
@@ -253,7 +265,7 @@ module OData
|
|
253
265
|
update_fields(data, my_data_fields, missing: :skip)
|
254
266
|
end
|
255
267
|
# patch should return 204 + no content
|
256
|
-
|
268
|
+
ARY_204_EMPTY_HASH_ARY
|
257
269
|
end
|
258
270
|
end
|
259
271
|
|
@@ -281,7 +293,7 @@ module OData
|
|
281
293
|
y.each { |enty| yield enty }
|
282
294
|
end
|
283
295
|
|
284
|
-
# TODO design... this is not DRY
|
296
|
+
# TODO: design... this is not DRY
|
285
297
|
def slug_field
|
286
298
|
superclass.slug_field
|
287
299
|
end
|
@@ -294,6 +306,10 @@ module OData
|
|
294
306
|
superclass.media_handler
|
295
307
|
end
|
296
308
|
|
309
|
+
def default_template
|
310
|
+
superclass.default_template
|
311
|
+
end
|
312
|
+
|
297
313
|
def to_a
|
298
314
|
y = @child_method.call
|
299
315
|
y.to_a
|
@@ -321,7 +337,7 @@ module OData
|
|
321
337
|
extend NavigationRedefinitions
|
322
338
|
end
|
323
339
|
end
|
324
|
-
|
340
|
+
|
325
341
|
# GetRelatedEntity that returns an single related Entity
|
326
342
|
# (...to_one relationship )
|
327
343
|
def get_related_entity(childattrib)
|
@@ -335,9 +351,9 @@ module OData
|
|
335
351
|
# allows to receive a POST operation that would actually create the nav attribute entity
|
336
352
|
|
337
353
|
ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
|
338
|
-
|
354
|
+
|
339
355
|
ret.set_relation_info(self, childattrib)
|
340
|
-
|
356
|
+
|
341
357
|
ret
|
342
358
|
end
|
343
359
|
end
|
@@ -359,7 +375,7 @@ module OData
|
|
359
375
|
|
360
376
|
def odata_delete(req)
|
361
377
|
if req.accept?(APPJSON)
|
362
|
-
# delete
|
378
|
+
# delete
|
363
379
|
begin
|
364
380
|
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
365
381
|
[200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
@@ -372,13 +388,13 @@ module OData
|
|
372
388
|
end
|
373
389
|
|
374
390
|
# in case of a non media entity, we have to return an error on $value request
|
375
|
-
def odata_media_value_get(
|
376
|
-
|
391
|
+
def odata_media_value_get(_req)
|
392
|
+
BadRequestNonMediaValue.odata_get
|
377
393
|
end
|
378
394
|
|
379
395
|
# in case of a non media entity, we have to return an error on $value PUT
|
380
|
-
def odata_media_value_put(
|
381
|
-
|
396
|
+
def odata_media_value_put(_req)
|
397
|
+
BadRequestNonMediaValue.odata_get
|
382
398
|
end
|
383
399
|
end
|
384
400
|
|
@@ -396,11 +412,11 @@ module OData
|
|
396
412
|
version = self.class.media_handler.ressource_version(self)
|
397
413
|
"#{uri(uribase)}/$value?version=#{version}"
|
398
414
|
end
|
399
|
-
|
415
|
+
|
400
416
|
def edit_media(uribase)
|
401
417
|
"#{uri(uribase)}/$value"
|
402
418
|
end
|
403
|
-
|
419
|
+
|
404
420
|
# directory where to put/find the media files for this entity-type
|
405
421
|
def klass_dir
|
406
422
|
type_name
|
@@ -422,9 +438,8 @@ module OData
|
|
422
438
|
# delete the MR
|
423
439
|
# delegate to the media handler on collection(ie class) level
|
424
440
|
# TODO error handling
|
425
|
-
|
426
|
-
|
427
|
-
self.class.media_handler.odata_delete(request: req, entity: self)
|
441
|
+
|
442
|
+
self.class.media_handler.odata_delete(entity: self)
|
428
443
|
# delete the relation(s) to parent(s) (if any) and then entity
|
429
444
|
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
430
445
|
# result
|
@@ -444,7 +459,7 @@ module OData
|
|
444
459
|
def odata_media_value_put(req)
|
445
460
|
model = self.class
|
446
461
|
req.with_media_data do |data, mimetype, filename|
|
447
|
-
emdata = { :
|
462
|
+
emdata = { content_type: mimetype }
|
448
463
|
if req.in_changeset
|
449
464
|
set_fields(emdata, model.data_fields, missing: :skip)
|
450
465
|
save(transaction: false)
|
@@ -454,7 +469,7 @@ module OData
|
|
454
469
|
model.media_handler.replace_file(data: data,
|
455
470
|
entity: self,
|
456
471
|
filename: filename)
|
457
|
-
|
472
|
+
ARY_204_EMPTY_HASH_ARY
|
458
473
|
end
|
459
474
|
end
|
460
475
|
end
|
@@ -469,6 +484,7 @@ module OData
|
|
469
484
|
def media_path_id
|
470
485
|
pk.to_s
|
471
486
|
end
|
487
|
+
|
472
488
|
def media_path_ids
|
473
489
|
[pk]
|
474
490
|
end
|
@@ -479,15 +495,15 @@ module OData
|
|
479
495
|
include Entity
|
480
496
|
def pk_uri
|
481
497
|
# pk_hash is provided by Sequel
|
482
|
-
|
498
|
+
pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
|
483
499
|
end
|
484
500
|
|
485
501
|
def media_path_id
|
486
|
-
|
502
|
+
pk_hash.values.join(SPACE)
|
487
503
|
end
|
488
|
-
|
504
|
+
|
489
505
|
def media_path_ids
|
490
|
-
|
506
|
+
pk_hash.values
|
491
507
|
end
|
492
508
|
end
|
493
509
|
end
|
data/lib/odata/error.rb
CHANGED
@@ -40,7 +40,7 @@ module OData
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
# base module for HTTP errors, when used as an Error instance
|
45
45
|
module ErrorInstance
|
46
46
|
def odata_get(req)
|
@@ -52,7 +52,7 @@ module OData
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
# http Bad Req.
|
57
57
|
class BadRequestError
|
58
58
|
extend ErrorClass
|
@@ -76,12 +76,25 @@ module OData
|
|
76
76
|
@msg = err.inner.message
|
77
77
|
end
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
# for Syntax error in Filtering
|
81
81
|
class BadRequestFilterParseError < BadRequestError
|
82
82
|
HTTP_CODE = 400
|
83
|
-
@msg = 'Bad Request: Syntax error in
|
83
|
+
@msg = 'Bad Request: Syntax error in $filter'
|
84
|
+
end
|
85
|
+
|
86
|
+
# for Syntax error in $expand param
|
87
|
+
class BadRequestExpandParseError < BadRequestError
|
88
|
+
HTTP_CODE = 400
|
89
|
+
@msg = 'Bad Request: Syntax error in $expand'
|
84
90
|
end
|
91
|
+
|
92
|
+
# for Syntax error in $orderby param
|
93
|
+
class BadRequestOrderParseError < BadRequestError
|
94
|
+
HTTP_CODE = 400
|
95
|
+
@msg = 'Bad Request: Syntax error in $orderby'
|
96
|
+
end
|
97
|
+
|
85
98
|
# for $inlinecount error
|
86
99
|
class BadRequestInlineCountParamError < BadRequestError
|
87
100
|
HTTP_CODE = 400
|
data/lib/odata/expand.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'odata/error.rb'
|
2
|
+
|
3
|
+
# all dataset expanding related classes in our OData module
|
4
|
+
# ie do eager loading
|
5
|
+
module OData
|
6
|
+
# base class for expanding
|
7
|
+
class ExpandBase
|
8
|
+
EmptyExpand = new # re-useable empty expanding (idempotent)
|
9
|
+
EMPTYH = {}.freeze
|
10
|
+
|
11
|
+
def self.factory(expandstr)
|
12
|
+
expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
|
13
|
+
end
|
14
|
+
|
15
|
+
# output template
|
16
|
+
attr_reader :template
|
17
|
+
|
18
|
+
def apply_to_dataset(dtcx)
|
19
|
+
dtcx
|
20
|
+
end
|
21
|
+
|
22
|
+
def empty?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_error?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def template
|
31
|
+
EMPTYH
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# single expand
|
36
|
+
class Expand < ExpandBase
|
37
|
+
# sequel eager arg.
|
38
|
+
attr_reader :arg
|
39
|
+
attr_reader :template
|
40
|
+
|
41
|
+
# used for Sequel eager argument
|
42
|
+
# Recursive array to deep hash
|
43
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
44
|
+
# [1] --> 1
|
45
|
+
DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
|
46
|
+
|
47
|
+
# used for building output template
|
48
|
+
# Recursive array to deep hash
|
49
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
50
|
+
# [1] --> { 1 => {} }
|
51
|
+
DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
|
52
|
+
|
53
|
+
NODESEP = '/'.freeze
|
54
|
+
|
55
|
+
def initialize(exstr)
|
56
|
+
exstr.strip!
|
57
|
+
@expandp = exstr
|
58
|
+
@nodes = @expandp.split(NODESEP)
|
59
|
+
build_arg
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_to_dataset(dtcx)
|
63
|
+
dtcx
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_arg
|
67
|
+
# 'a/b/c/d' ==> {a: {b:{c: :d}}}
|
68
|
+
# 'xy' ==> :xy
|
69
|
+
@arg = DEEPH_0.call(@nodes.map(&:to_sym))
|
70
|
+
@template = DEEPH_1.call(@nodes)
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_error?
|
74
|
+
# todo
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def empty?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Multi expanding logic
|
84
|
+
class MultiExpand < ExpandBase
|
85
|
+
COMASPLIT = /\s*,\s*/.freeze
|
86
|
+
attr_reader :template
|
87
|
+
|
88
|
+
def initialize(expandstr)
|
89
|
+
expandstr.strip!
|
90
|
+
@expandp = expandstr
|
91
|
+
@exlist = []
|
92
|
+
|
93
|
+
@exlist = expandstr.split(COMASPLIT).map { |exstr| Expand.new(exstr) }
|
94
|
+
build_template
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_to_dataset(dtcx)
|
98
|
+
# use eager loading for each used association
|
99
|
+
@exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
|
100
|
+
dtcx
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_template
|
104
|
+
# 'a/b/c/d,xy' ==> [ {'a' =>{ 'b' => {'c' => {'d' => {} } }}},
|
105
|
+
# { 'xy' => {} }]
|
106
|
+
#
|
107
|
+
@template = @exlist.map(&:template)
|
108
|
+
|
109
|
+
# { 'a' => { 'b' => {'c' => 'd' }},
|
110
|
+
# 'xy' => {} }
|
111
|
+
@template = @template.inject({}) { |mrg, elmt| mrg.merge elmt }
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_error?
|
115
|
+
# todo
|
116
|
+
false
|
117
|
+
end
|
118
|
+
|
119
|
+
def empty?
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|