safrano 0.3.3 → 0.4.3
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/attribute.rb +9 -8
- data/lib/odata/batch.rb +8 -8
- data/lib/odata/collection.rb +239 -92
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +159 -28
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +37 -12
- data/lib/odata/entity.rb +188 -99
- data/lib/odata/error.rb +60 -12
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/base.rb +66 -0
- data/lib/odata/filter/error.rb +33 -0
- data/lib/odata/filter/parse.rb +6 -12
- data/lib/odata/filter/sequel.rb +42 -29
- data/lib/odata/filter/sequel_function_adapter.rb +147 -0
- data/lib/odata/filter/token.rb +5 -1
- data/lib/odata/filter/tree.rb +45 -29
- data/lib/odata/navigation_attribute.rb +60 -27
- data/lib/odata/relations.rb +2 -2
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/odata/walker.rb +6 -6
- data/lib/safrano.rb +23 -13
- data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
- data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
- data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
- data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
- data/lib/{request.rb → safrano/request.rb} +8 -14
- data/lib/{response.rb → safrano/response.rb} +1 -2
- data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
- data/lib/{service.rb → safrano/service.rb} +162 -131
- data/lib/safrano/version.rb +3 -0
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +33 -16
- data/lib/version.rb +0 -4
data/lib/odata/entity.rb
CHANGED
@@ -2,36 +2,19 @@ require 'json'
|
|
2
2
|
require 'rexml/document'
|
3
3
|
require 'safrano.rb'
|
4
4
|
require 'odata/collection.rb' # required for self.class.entity_type_name ??
|
5
|
+
require_relative 'navigation_attribute'
|
5
6
|
|
6
7
|
module OData
|
7
8
|
# this will be mixed in the Model classes (subclasses of Sequel Model)
|
8
9
|
module EntityBase
|
9
10
|
attr_reader :params
|
10
|
-
|
11
|
+
|
12
|
+
include EntityBase::NavigationInfo
|
11
13
|
|
12
14
|
# methods related to transitions to next state (cf. walker)
|
13
15
|
module Transitions
|
14
16
|
def allowed_transitions
|
15
|
-
|
16
|
-
Safrano::TransitionEnd,
|
17
|
-
Safrano::TransitionCount,
|
18
|
-
Safrano::TransitionLinks,
|
19
|
-
Safrano::TransitionValue,
|
20
|
-
Safrano::Transition.new(self.class.transition_attribute_regexp,
|
21
|
-
trans: 'transition_attribute')
|
22
|
-
]
|
23
|
-
if (ncurgx = self.class.nav_collection_url_regexp)
|
24
|
-
alltr <<
|
25
|
-
Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
|
26
|
-
trans: 'transition_nav_collection')
|
27
|
-
|
28
|
-
end
|
29
|
-
if (neurgx = self.class.nav_entity_url_regexp)
|
30
|
-
alltr <<
|
31
|
-
Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
|
32
|
-
trans: 'transition_nav_entity')
|
33
|
-
end
|
34
|
-
alltr
|
17
|
+
self.class.entity_allowed_transitions
|
35
18
|
end
|
36
19
|
|
37
20
|
def transition_end(_match_result)
|
@@ -76,7 +59,6 @@ module OData
|
|
76
59
|
self.class.nav_entity_attribs&.each_key do |na_str|
|
77
60
|
@nav_values[na_str.to_sym] = send(na_str)
|
78
61
|
end
|
79
|
-
|
80
62
|
@nav_values
|
81
63
|
end
|
82
64
|
|
@@ -88,43 +70,41 @@ module OData
|
|
88
70
|
@nav_coll
|
89
71
|
end
|
90
72
|
|
91
|
-
def uri
|
92
|
-
"
|
73
|
+
def uri
|
74
|
+
@odata_pk ||= "(#{pk_uri})"
|
75
|
+
"#{self.class.uri}#{@odata_pk}"
|
93
76
|
end
|
77
|
+
|
94
78
|
D = 'd'.freeze
|
95
|
-
|
96
|
-
|
79
|
+
DJ_OPEN = '{"d":'.freeze
|
80
|
+
DJ_CLOSE = '}'.freeze
|
81
|
+
|
97
82
|
# Json formatter for a single entity (probably OData V1/V2 like)
|
98
83
|
def to_odata_json(service:)
|
84
|
+
template = self.class.output_template(@uparms)
|
99
85
|
innerj = service.get_entity_odata_h(entity: self,
|
100
|
-
|
101
|
-
|
102
|
-
uribase: @uribase).to_json
|
103
|
-
"#{DJopen}#{innerj}#{DJclose}"
|
86
|
+
template: template).to_json
|
87
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
104
88
|
end
|
105
89
|
|
106
|
-
#
|
107
|
-
|
108
|
-
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
else
|
118
|
-
v
|
119
|
-
end
|
120
|
-
}
|
90
|
+
# Json formatter for a single entity reached by navigation $links
|
91
|
+
def to_odata_onelink_json(service:)
|
92
|
+
innerj = service.get_entity_odata_link_h(entity: self).to_json
|
93
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def selected_values_for_odata(cols)
|
97
|
+
allvals = values_for_odata
|
98
|
+
selvals = {}
|
99
|
+
cols.map(&:to_sym).each { |k| selvals[k] = allvals[k] if allvals.key?(k) }
|
100
|
+
selvals
|
121
101
|
end
|
122
102
|
|
123
103
|
# post paylod expects the new entity in an array
|
124
104
|
def to_odata_post_json(service:)
|
125
105
|
innerj = service.get_coll_odata_h(array: [self],
|
126
|
-
|
127
|
-
"#{
|
106
|
+
template: self.class.default_template).to_json
|
107
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
128
108
|
end
|
129
109
|
|
130
110
|
def type_name
|
@@ -133,38 +113,53 @@ module OData
|
|
133
113
|
|
134
114
|
def copy_request_infos(req)
|
135
115
|
@params = req.params
|
136
|
-
@uribase = req.uribase
|
137
116
|
@do_links = req.walker.do_links
|
117
|
+
@uparms = UrlParameters4Single.new(@params)
|
138
118
|
end
|
139
119
|
|
140
120
|
# Finally Process REST verbs...
|
141
121
|
def odata_get(req)
|
142
122
|
copy_request_infos(req)
|
143
|
-
|
144
123
|
if req.walker.media_value
|
145
124
|
odata_media_value_get(req)
|
146
125
|
elsif req.accept?(APPJSON)
|
147
|
-
|
126
|
+
if req.walker.do_links
|
127
|
+
[200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
|
128
|
+
else
|
129
|
+
[200, CT_JSON, [to_odata_json(service: req.service)]]
|
130
|
+
end
|
148
131
|
else # TODO: other formats
|
149
132
|
415
|
150
133
|
end
|
151
134
|
end
|
152
135
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
136
|
+
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
137
|
+
OData.remove_nav_relation(assoc, parent)
|
138
|
+
entity.destroy(transaction: false)
|
139
|
+
end
|
140
|
+
|
141
|
+
def odata_delete_relation_and_entity(req, assoc, parent)
|
142
|
+
if parent
|
143
|
+
if req.in_changeset
|
144
|
+
# in-changeset requests get their own transaction
|
145
|
+
DELETE_REL_AND_ENTY.call(self, assoc, parent)
|
146
|
+
else
|
147
|
+
db.transaction do
|
148
|
+
DELETE_REL_AND_ENTY.call(self, assoc, parent)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
else
|
152
|
+
destroy(transaction: false)
|
159
153
|
end
|
154
|
+
rescue StandardError => e
|
155
|
+
raise SequelAdapterError.new(e)
|
160
156
|
end
|
161
157
|
|
162
158
|
# TODO: differentiate between POST/PUT/PATCH/MERGE
|
163
159
|
def odata_post(req)
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if req.accept?(APPJSON)
|
160
|
+
if req.walker.media_value
|
161
|
+
odata_media_value_put(req)
|
162
|
+
elsif req.accept?(APPJSON)
|
168
163
|
data.delete('__metadata')
|
169
164
|
|
170
165
|
if req.in_changeset
|
@@ -174,7 +169,7 @@ module OData
|
|
174
169
|
update_fields(data, self.class.data_fields, missing: :skip)
|
175
170
|
end
|
176
171
|
|
177
|
-
[202,
|
172
|
+
[202, EMPTY_HASH, to_odata_post_json(service: req.service)]
|
178
173
|
else # TODO: other formats
|
179
174
|
415
|
180
175
|
end
|
@@ -183,23 +178,20 @@ module OData
|
|
183
178
|
def odata_put(req)
|
184
179
|
if req.walker.media_value
|
185
180
|
odata_media_value_put(req)
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
@uribase = req.uribase
|
190
|
-
data.delete('__metadata')
|
191
|
-
|
192
|
-
if req.in_changeset
|
193
|
-
set_fields(data, self.class.data_fields, missing: :skip)
|
194
|
-
save(transaction: false)
|
195
|
-
else
|
196
|
-
update_fields(data, self.class.data_fields, missing: :skip)
|
197
|
-
end
|
181
|
+
elsif req.accept?(APPJSON)
|
182
|
+
data = JSON.parse(req.body.read)
|
183
|
+
data.delete('__metadata')
|
198
184
|
|
199
|
-
|
200
|
-
|
201
|
-
|
185
|
+
if req.in_changeset
|
186
|
+
set_fields(data, self.class.data_fields, missing: :skip)
|
187
|
+
save(transaction: false)
|
188
|
+
else
|
189
|
+
update_fields(data, self.class.data_fields, missing: :skip)
|
202
190
|
end
|
191
|
+
|
192
|
+
ARY_204_EMPTY_HASH_ARY
|
193
|
+
else # TODO: other formats
|
194
|
+
415
|
203
195
|
end
|
204
196
|
end
|
205
197
|
|
@@ -210,13 +202,11 @@ module OData
|
|
210
202
|
# validate payload column names
|
211
203
|
if (invalid = self.class.invalid_hash_data?(data))
|
212
204
|
::OData::Request::ON_CGST_ERROR.call(req)
|
213
|
-
return [422,
|
205
|
+
return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
|
214
206
|
end
|
215
207
|
# TODO: check values/types
|
216
208
|
|
217
209
|
my_data_fields = self.class.data_fields
|
218
|
-
@uribase = req.uribase
|
219
|
-
# if req.accept?('application/json')
|
220
210
|
|
221
211
|
if req.in_changeset
|
222
212
|
set_fields(data, my_data_fields, missing: :skip)
|
@@ -225,7 +215,7 @@ module OData
|
|
225
215
|
update_fields(data, my_data_fields, missing: :skip)
|
226
216
|
end
|
227
217
|
# patch should return 204 + no content
|
228
|
-
|
218
|
+
ARY_204_EMPTY_HASH_ARY
|
229
219
|
end
|
230
220
|
end
|
231
221
|
|
@@ -253,14 +243,39 @@ module OData
|
|
253
243
|
y.each { |enty| yield enty }
|
254
244
|
end
|
255
245
|
|
246
|
+
# TODO: design... this is not DRY
|
247
|
+
def slug_field
|
248
|
+
superclass.slug_field
|
249
|
+
end
|
250
|
+
|
256
251
|
def type_name
|
257
252
|
superclass.type_name
|
258
253
|
end
|
259
254
|
|
255
|
+
def time_cols
|
256
|
+
superclass.time_cols
|
257
|
+
end
|
258
|
+
|
260
259
|
def media_handler
|
261
260
|
superclass.media_handler
|
262
261
|
end
|
263
262
|
|
263
|
+
def uri
|
264
|
+
superclass.uri
|
265
|
+
end
|
266
|
+
|
267
|
+
def default_template
|
268
|
+
superclass.default_template
|
269
|
+
end
|
270
|
+
|
271
|
+
def allowed_transitions
|
272
|
+
superclass.allowed_transitions
|
273
|
+
end
|
274
|
+
|
275
|
+
def entity_allowed_transitions
|
276
|
+
superclass.entity_allowed_transitions
|
277
|
+
end
|
278
|
+
|
264
279
|
def to_a
|
265
280
|
y = @child_method.call
|
266
281
|
y.to_a
|
@@ -301,8 +316,9 @@ module OData
|
|
301
316
|
# then we return a Nil... wrapper object. This object then
|
302
317
|
# allows to receive a POST operation that would actually create the nav attribute entity
|
303
318
|
|
304
|
-
ret = method(childattrib.to_sym).call ||
|
305
|
-
|
319
|
+
ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
|
320
|
+
|
321
|
+
ret.set_relation_info(self, childattrib)
|
306
322
|
|
307
323
|
ret
|
308
324
|
end
|
@@ -314,38 +330,87 @@ module OData
|
|
314
330
|
|
315
331
|
module NonMediaEntity
|
316
332
|
# non media entity metadata for json h
|
317
|
-
def metadata_h
|
318
|
-
{ uri: uri
|
333
|
+
def metadata_h
|
334
|
+
{ uri: uri,
|
319
335
|
type: type_name }
|
320
336
|
end
|
321
337
|
|
322
338
|
def values_for_odata
|
323
|
-
values
|
339
|
+
values
|
340
|
+
end
|
341
|
+
|
342
|
+
def odata_delete(req)
|
343
|
+
if req.accept?(APPJSON)
|
344
|
+
# delete
|
345
|
+
begin
|
346
|
+
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
347
|
+
[200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
348
|
+
rescue SequelAdapterError => e
|
349
|
+
BadRequestSequelAdapterError.new(e).odata_get(req)
|
350
|
+
end
|
351
|
+
else # TODO: other formats
|
352
|
+
415
|
353
|
+
end
|
324
354
|
end
|
325
355
|
|
326
356
|
# in case of a non media entity, we have to return an error on $value request
|
327
|
-
def odata_media_value_get(
|
328
|
-
|
357
|
+
def odata_media_value_get(_req)
|
358
|
+
BadRequestNonMediaValue.odata_get
|
329
359
|
end
|
330
360
|
|
331
361
|
# in case of a non media entity, we have to return an error on $value PUT
|
332
|
-
def odata_media_value_put(
|
333
|
-
|
362
|
+
def odata_media_value_put(_req)
|
363
|
+
BadRequestNonMediaValue.odata_get
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
module MappingBeforeOutput
|
368
|
+
# needed for proper datetime output
|
369
|
+
def casted_values(cols = nil)
|
370
|
+
vals = case cols
|
371
|
+
when nil
|
372
|
+
# we need to dup the model values as we need to change it before passing to_json,
|
373
|
+
# but we dont want to interfere with Sequel's owned data
|
374
|
+
# (eg because then in worst case it could happen that we write back changed values to DB)
|
375
|
+
values_for_odata.dup
|
376
|
+
else
|
377
|
+
selected_values_for_odata(cols)
|
378
|
+
end
|
379
|
+
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
380
|
+
vals
|
381
|
+
end
|
382
|
+
end
|
383
|
+
module NoMappingBeforeOutput
|
384
|
+
# current model does not have eg. Time fields--> no special mapping, just to_json is fine
|
385
|
+
# --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
|
386
|
+
# need to change it, just output as is
|
387
|
+
def casted_values(cols = nil)
|
388
|
+
case cols
|
389
|
+
when nil
|
390
|
+
values_for_odata
|
391
|
+
else
|
392
|
+
selected_values_for_odata(cols)
|
393
|
+
end
|
334
394
|
end
|
335
395
|
end
|
336
396
|
|
337
397
|
module MediaEntity
|
338
398
|
# media entity metadata for json h
|
339
|
-
def metadata_h
|
340
|
-
{ uri: uri
|
399
|
+
def metadata_h
|
400
|
+
{ uri: uri,
|
341
401
|
type: type_name,
|
342
|
-
media_src: media_src
|
343
|
-
edit_media:
|
402
|
+
media_src: media_src,
|
403
|
+
edit_media: edit_media,
|
344
404
|
content_type: @values[:content_type] }
|
345
405
|
end
|
346
406
|
|
347
|
-
def media_src
|
348
|
-
|
407
|
+
def media_src
|
408
|
+
version = self.class.media_handler.ressource_version(self)
|
409
|
+
"#{uri}/$value?version=#{version}"
|
410
|
+
end
|
411
|
+
|
412
|
+
def edit_media
|
413
|
+
"#{uri}/$value"
|
349
414
|
end
|
350
415
|
|
351
416
|
# directory where to put/find the media files for this entity-type
|
@@ -364,6 +429,22 @@ module OData
|
|
364
429
|
ret
|
365
430
|
end
|
366
431
|
|
432
|
+
def odata_delete(req)
|
433
|
+
if req.accept?(APPJSON)
|
434
|
+
# delete the MR
|
435
|
+
# delegate to the media handler on collection(ie class) level
|
436
|
+
# TODO error handling
|
437
|
+
|
438
|
+
self.class.media_handler.odata_delete(entity: self)
|
439
|
+
# delete the relation(s) to parent(s) (if any) and then entity
|
440
|
+
odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
|
441
|
+
# result
|
442
|
+
[200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
|
443
|
+
else # TODO: other formats
|
444
|
+
415
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
367
448
|
# real implementation for returning $value for a media entity
|
368
449
|
def odata_media_value_get(req)
|
369
450
|
# delegate to the media handler on collection(ie class) level
|
@@ -374,7 +455,7 @@ module OData
|
|
374
455
|
def odata_media_value_put(req)
|
375
456
|
model = self.class
|
376
457
|
req.with_media_data do |data, mimetype, filename|
|
377
|
-
emdata = { :
|
458
|
+
emdata = { content_type: mimetype }
|
378
459
|
if req.in_changeset
|
379
460
|
set_fields(emdata, model.data_fields, missing: :skip)
|
380
461
|
save(transaction: false)
|
@@ -384,7 +465,7 @@ module OData
|
|
384
465
|
model.media_handler.replace_file(data: data,
|
385
466
|
entity: self,
|
386
467
|
filename: filename)
|
387
|
-
|
468
|
+
ARY_204_EMPTY_HASH_ARY
|
388
469
|
end
|
389
470
|
end
|
390
471
|
end
|
@@ -399,6 +480,10 @@ module OData
|
|
399
480
|
def media_path_id
|
400
481
|
pk.to_s
|
401
482
|
end
|
483
|
+
|
484
|
+
def media_path_ids
|
485
|
+
[pk]
|
486
|
+
end
|
402
487
|
end
|
403
488
|
|
404
489
|
# for multiple key
|
@@ -406,11 +491,15 @@ module OData
|
|
406
491
|
include Entity
|
407
492
|
def pk_uri
|
408
493
|
# pk_hash is provided by Sequel
|
409
|
-
|
494
|
+
pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
|
410
495
|
end
|
411
496
|
|
412
497
|
def media_path_id
|
413
|
-
|
498
|
+
pk_hash.values.join(SPACE)
|
499
|
+
end
|
500
|
+
|
501
|
+
def media_path_ids
|
502
|
+
pk_hash.values
|
414
503
|
end
|
415
504
|
end
|
416
505
|
end
|