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/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
|