safrano 0.4.2 → 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 +8 -7
- data/lib/odata/collection.rb +86 -34
- data/lib/odata/entity.rb +65 -69
- data/lib/odata/error.rb +18 -0
- data/lib/odata/filter/base.rb +66 -0
- data/lib/odata/filter/error.rb +27 -0
- data/lib/odata/filter/parse.rb +2 -0
- data/lib/odata/filter/sequel.rb +32 -17
- data/lib/odata/filter/sequel_function_adapter.rb +147 -0
- data/lib/odata/filter/token.rb +5 -1
- data/lib/odata/filter/tree.rb +34 -14
- data/lib/odata/navigation_attribute.rb +4 -2
- data/lib/odata/walker.rb +6 -4
- data/lib/safrano/core.rb +0 -2
- data/lib/safrano/multipart.rb +0 -9
- data/lib/safrano/odata_rack_builder.rb +0 -1
- data/lib/safrano/rack_app.rb +8 -6
- data/lib/safrano/request.rb +0 -7
- data/lib/safrano/service.rb +111 -47
- data/lib/safrano/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 497482614bfc6a87f8a42b9378a4fc8099ddb5b4f180d747b3717a6952f027b0
|
4
|
+
data.tar.gz: cd5236302671643fd9fbd9bcd200034f305ce7069e5d56ab2db2225dcfb4ce87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4415ca4dc24362f3c179f0cc40f1d73fac6fb7207c0571ef63f1af318fc345e0be8043d8aa1b5ab48e0ead1f5c904dcab07b84061b3ca47f6b7d3c38696d4ae8
|
7
|
+
data.tar.gz: 6646d12aadb3827b43c013a3da6c6223d072e5676f8676eac0db59ad8e3ac80d1a64ceb0f5e87d52bb75e5b556eecda701e2d473c4bdd3b650cb680b6ce19b96
|
data/lib/odata/attribute.rb
CHANGED
@@ -13,15 +13,14 @@ module OData
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def value
|
16
|
-
# WARNING
|
17
|
-
# (and the inverted transformation is in test/client.rb)
|
18
|
-
# will require a more systematic solution some day
|
19
|
-
# WARNING 2... this require more work to handle the timezones topci
|
16
|
+
# WARNING ... this require more work to handle the timezones topci
|
20
17
|
# currently it is just set to make some minimal testcase work
|
21
18
|
case (v = @entity.values[@name.to_sym])
|
22
19
|
when Time
|
23
20
|
# try to get back the database time zone and value
|
24
|
-
(v + v.gmt_offset).utc.to_datetime
|
21
|
+
# (v + v.gmt_offset).utc.to_datetime
|
22
|
+
v.iso8601
|
23
|
+
|
25
24
|
else
|
26
25
|
v
|
27
26
|
end
|
@@ -57,9 +56,11 @@ module OData
|
|
57
56
|
[self, :end_with_value]
|
58
57
|
end
|
59
58
|
|
59
|
+
ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
|
60
|
+
Safrano::TransitionValue].freeze
|
61
|
+
|
60
62
|
def allowed_transitions
|
61
|
-
|
62
|
-
Safrano::TransitionValue]
|
63
|
+
ALLOWED_TRANSITIONS
|
63
64
|
end
|
64
65
|
end
|
65
66
|
include Transitions
|
data/lib/odata/collection.rb
CHANGED
@@ -55,6 +55,8 @@ module OData
|
|
55
55
|
attr_reader :data_fields
|
56
56
|
attr_reader :inlinecount
|
57
57
|
attr_reader :default_template
|
58
|
+
attr_reader :uri
|
59
|
+
attr_reader :time_cols
|
58
60
|
|
59
61
|
# Sequel associations pointing to this model. Sequel provides association
|
60
62
|
# reflection information on the "from" side. But in some cases
|
@@ -90,7 +92,11 @@ module OData
|
|
90
92
|
# the class-name is not the OData Type. In these subclass we redefine "type_name"
|
91
93
|
# thus when we need the Odata type name, we shall use this method instead of just the collection class name
|
92
94
|
def type_name
|
93
|
-
|
95
|
+
@type_name
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_type_name
|
99
|
+
@type_name = to_s
|
94
100
|
end
|
95
101
|
|
96
102
|
# convention: default for entity_set_name is the type name
|
@@ -102,9 +108,15 @@ module OData
|
|
102
108
|
# TODO: automatically reset all attributes?
|
103
109
|
@deferred_iblock = nil
|
104
110
|
@entity_set_name = nil
|
111
|
+
@uri = nil
|
105
112
|
@uparms = nil
|
106
113
|
@params = nil
|
107
114
|
@cx = nil
|
115
|
+
@@time_cols = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def build_uri(uribase)
|
119
|
+
@uri = "#{uribase}/#{entity_set_name}"
|
108
120
|
end
|
109
121
|
|
110
122
|
def execute_deferred_iblock
|
@@ -159,15 +171,7 @@ module OData
|
|
159
171
|
end
|
160
172
|
|
161
173
|
def odata_get_apply_params
|
162
|
-
|
163
|
-
@cx = @uparms.apply_to_dataset(@cx)
|
164
|
-
rescue OData::Filter::Parser::ErrorWrongColumnName
|
165
|
-
@error = BadRequestFilterParseError
|
166
|
-
return
|
167
|
-
rescue OData::Filter::Parser::ErrorFunctionArgumentType
|
168
|
-
@error = BadRequestFilterParseError
|
169
|
-
return
|
170
|
-
end
|
174
|
+
@cx = @uparms.apply_to_dataset(@cx)
|
171
175
|
odata_get_inlinecount_w_sequel
|
172
176
|
|
173
177
|
@cx = @cx.offset(@params['$skip']) if @params['$skip']
|
@@ -217,17 +221,19 @@ module OData
|
|
217
221
|
@attribute_path_list = attribute_path_list
|
218
222
|
end
|
219
223
|
|
220
|
-
|
224
|
+
MAX_DEPTH = 6
|
225
|
+
def attribute_path_list(depth = 0)
|
226
|
+
ret = @columns.map(&:to_s)
|
221
227
|
# break circles
|
222
|
-
return
|
228
|
+
return ret if depth > MAX_DEPTH
|
229
|
+
|
230
|
+
depth += 1
|
223
231
|
|
224
|
-
ret = @columns.map(&:to_s)
|
225
|
-
nodes.add entity_set_name
|
226
232
|
@nav_entity_attribs&.each do |a, k|
|
227
|
-
ret.concat(k.attribute_path_list(
|
233
|
+
ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
|
228
234
|
end
|
229
235
|
@nav_collection_attribs&.each do |a, k|
|
230
|
-
ret.concat(k.attribute_path_list(
|
236
|
+
ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
|
231
237
|
end
|
232
238
|
ret
|
233
239
|
end
|
@@ -267,15 +273,31 @@ module OData
|
|
267
273
|
end
|
268
274
|
end
|
269
275
|
|
276
|
+
# validation/error handling methods.
|
277
|
+
# normal processing is done in the passed proc
|
278
|
+
|
279
|
+
def with_validated_get(req)
|
280
|
+
begin
|
281
|
+
initialize_dataset
|
282
|
+
return yield unless (@error = check_url_params)
|
283
|
+
rescue OData::Filter::Parser::ErrorWrongColumnName
|
284
|
+
@error = BadRequestFilterParseError
|
285
|
+
rescue OData::Filter::Parser::ErrorFunctionArgumentType
|
286
|
+
@error = BadRequestFilterParseError
|
287
|
+
rescue OData::Filter::FunctionNotImplemented => e
|
288
|
+
@error = e.odata_error
|
289
|
+
rescue OData::Filter::Parser::ErrorInvalidFunction => e
|
290
|
+
@error = e.odata_error
|
291
|
+
end
|
292
|
+
|
293
|
+
@error.odata_get(req) if @error
|
294
|
+
end
|
295
|
+
|
270
296
|
# on model class level we return the collection
|
271
297
|
def odata_get(req)
|
272
298
|
@params = req.params
|
273
|
-
@uribase = req.uribase
|
274
|
-
initialize_dataset
|
275
299
|
|
276
|
-
|
277
|
-
@error.odata_get(req)
|
278
|
-
else
|
300
|
+
with_validated_get(req) do
|
279
301
|
odata_get_apply_params
|
280
302
|
odata_get_output(req)
|
281
303
|
end
|
@@ -340,8 +362,7 @@ module OData
|
|
340
362
|
DJ_OPEN = '{"d":'.freeze
|
341
363
|
DJ_CLOSE = '}'.freeze
|
342
364
|
def to_odata_links_json(service:)
|
343
|
-
innerj = service.get_coll_odata_links_h(array:
|
344
|
-
uribase: @uribase,
|
365
|
+
innerj = service.get_coll_odata_links_h(array: @cx.all,
|
345
366
|
icount: @inlinecount).to_json
|
346
367
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
347
368
|
end
|
@@ -417,17 +438,12 @@ module OData
|
|
417
438
|
|
418
439
|
def to_odata_json(service:)
|
419
440
|
template = output_template(@uparms)
|
420
|
-
innerj = service.get_coll_odata_h(array:
|
441
|
+
innerj = service.get_coll_odata_h(array: @cx.all,
|
421
442
|
template: template,
|
422
|
-
uribase: @uribase,
|
423
443
|
icount: @inlinecount).to_json
|
424
444
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
425
445
|
end
|
426
446
|
|
427
|
-
def get_a
|
428
|
-
@cx.all
|
429
|
-
end
|
430
|
-
|
431
447
|
# this functionally similar to the Sequel Rels (many_to_one etc)
|
432
448
|
# We need to base this on the Sequel rels, or extend them
|
433
449
|
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
|
@@ -476,14 +492,22 @@ module OData
|
|
476
492
|
# alias_method :add_nav_prop_single, :addNavEntityAttrib
|
477
493
|
|
478
494
|
def finalize_publishing
|
495
|
+
build_type_name
|
496
|
+
|
479
497
|
# finalize media handler
|
480
498
|
@media_handler.register(self) if @media_handler
|
481
499
|
|
482
500
|
# build default output template structure
|
483
501
|
@default_template = build_default_template
|
484
502
|
|
485
|
-
#
|
503
|
+
# Time columns
|
504
|
+
@time_cols = db_schema.select { |c, v| v[:type] == :datetime }.map { |c, v| c }
|
505
|
+
|
506
|
+
# and finally build the path list and allowed tr's
|
486
507
|
build_attribute_path_list
|
508
|
+
|
509
|
+
build_allowed_transitions
|
510
|
+
build_entity_allowed_transitions
|
487
511
|
end
|
488
512
|
|
489
513
|
def prepare_pk
|
@@ -574,10 +598,38 @@ module OData
|
|
574
598
|
end
|
575
599
|
|
576
600
|
def allowed_transitions
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
601
|
+
@allowed_transitions
|
602
|
+
end
|
603
|
+
|
604
|
+
def entity_allowed_transitions
|
605
|
+
@entity_allowed_transitions
|
606
|
+
end
|
607
|
+
|
608
|
+
def build_allowed_transitions
|
609
|
+
@allowed_transitions = [Safrano::TransitionEnd,
|
610
|
+
Safrano::TransitionCount,
|
611
|
+
Safrano::Transition.new(entity_id_url_regexp,
|
612
|
+
trans: 'transition_id')].freeze
|
613
|
+
end
|
614
|
+
|
615
|
+
def build_entity_allowed_transitions
|
616
|
+
@entity_allowed_transitions = [
|
617
|
+
Safrano::TransitionEnd,
|
618
|
+
Safrano::TransitionCount,
|
619
|
+
Safrano::TransitionLinks,
|
620
|
+
Safrano::TransitionValue,
|
621
|
+
Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
|
622
|
+
]
|
623
|
+
if (ncurgx = @nav_collection_url_regexp)
|
624
|
+
@entity_allowed_transitions <<
|
625
|
+
Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
|
626
|
+
end
|
627
|
+
if (neurgx = @nav_entity_url_regexp)
|
628
|
+
@entity_allowed_transitions <<
|
629
|
+
Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
|
630
|
+
end
|
631
|
+
@entity_allowed_transitions.freeze
|
632
|
+
@entity_allowed_transitions
|
581
633
|
end
|
582
634
|
end
|
583
635
|
include Transitions
|
data/lib/odata/entity.rb
CHANGED
@@ -8,33 +8,13 @@ module OData
|
|
8
8
|
# this will be mixed in the Model classes (subclasses of Sequel Model)
|
9
9
|
module EntityBase
|
10
10
|
attr_reader :params
|
11
|
-
attr_reader :uribase
|
12
11
|
|
13
12
|
include EntityBase::NavigationInfo
|
14
13
|
|
15
14
|
# methods related to transitions to next state (cf. walker)
|
16
15
|
module Transitions
|
17
16
|
def allowed_transitions
|
18
|
-
|
19
|
-
Safrano::TransitionEnd,
|
20
|
-
Safrano::TransitionCount,
|
21
|
-
Safrano::TransitionLinks,
|
22
|
-
Safrano::TransitionValue,
|
23
|
-
Safrano::Transition.new(self.class.transition_attribute_regexp,
|
24
|
-
trans: 'transition_attribute')
|
25
|
-
]
|
26
|
-
if (ncurgx = self.class.nav_collection_url_regexp)
|
27
|
-
alltr <<
|
28
|
-
Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
|
29
|
-
trans: 'transition_nav_collection')
|
30
|
-
|
31
|
-
end
|
32
|
-
if (neurgx = self.class.nav_entity_url_regexp)
|
33
|
-
alltr <<
|
34
|
-
Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
|
35
|
-
trans: 'transition_nav_entity')
|
36
|
-
end
|
37
|
-
alltr
|
17
|
+
self.class.entity_allowed_transitions
|
38
18
|
end
|
39
19
|
|
40
20
|
def transition_end(_match_result)
|
@@ -90,9 +70,11 @@ module OData
|
|
90
70
|
@nav_coll
|
91
71
|
end
|
92
72
|
|
93
|
-
def uri
|
94
|
-
"
|
73
|
+
def uri
|
74
|
+
@odata_pk ||= "(#{pk_uri})"
|
75
|
+
"#{self.class.uri}#{@odata_pk}"
|
95
76
|
end
|
77
|
+
|
96
78
|
D = 'd'.freeze
|
97
79
|
DJ_OPEN = '{"d":'.freeze
|
98
80
|
DJ_CLOSE = '}'.freeze
|
@@ -101,15 +83,13 @@ module OData
|
|
101
83
|
def to_odata_json(service:)
|
102
84
|
template = self.class.output_template(@uparms)
|
103
85
|
innerj = service.get_entity_odata_h(entity: self,
|
104
|
-
template: template
|
105
|
-
uribase: @uribase).to_json
|
86
|
+
template: template).to_json
|
106
87
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
107
88
|
end
|
108
89
|
|
109
90
|
# Json formatter for a single entity reached by navigation $links
|
110
91
|
def to_odata_onelink_json(service:)
|
111
|
-
innerj = service.get_entity_odata_link_h(entity: self
|
112
|
-
uribase: @uribase).to_json
|
92
|
+
innerj = service.get_entity_odata_link_h(entity: self).to_json
|
113
93
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
114
94
|
end
|
115
95
|
|
@@ -120,36 +100,10 @@ module OData
|
|
120
100
|
selvals
|
121
101
|
end
|
122
102
|
|
123
|
-
# needed for proper datetime output
|
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
|
-
|
133
|
-
# WARNING; this code is duplicated in attribute.rb
|
134
|
-
# (and the inverted transformation is in test/client.rb)
|
135
|
-
# will require a more systematic solution some day
|
136
|
-
|
137
|
-
vals.transform_values! do |v|
|
138
|
-
case v
|
139
|
-
when Time
|
140
|
-
# try to get back the database time zone and value
|
141
|
-
(v + v.gmt_offset).utc.to_datetime
|
142
|
-
else
|
143
|
-
v
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
103
|
# post paylod expects the new entity in an array
|
149
104
|
def to_odata_post_json(service:)
|
150
105
|
innerj = service.get_coll_odata_h(array: [self],
|
151
|
-
template: self.class.default_template
|
152
|
-
uribase: @uribase).to_json
|
106
|
+
template: self.class.default_template).to_json
|
153
107
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
154
108
|
end
|
155
109
|
|
@@ -159,7 +113,6 @@ module OData
|
|
159
113
|
|
160
114
|
def copy_request_infos(req)
|
161
115
|
@params = req.params
|
162
|
-
@uribase = req.uribase
|
163
116
|
@do_links = req.walker.do_links
|
164
117
|
@uparms = UrlParameters4Single.new(@params)
|
165
118
|
end
|
@@ -227,7 +180,6 @@ module OData
|
|
227
180
|
odata_media_value_put(req)
|
228
181
|
elsif req.accept?(APPJSON)
|
229
182
|
data = JSON.parse(req.body.read)
|
230
|
-
@uribase = req.uribase
|
231
183
|
data.delete('__metadata')
|
232
184
|
|
233
185
|
if req.in_changeset
|
@@ -255,8 +207,6 @@ module OData
|
|
255
207
|
# TODO: check values/types
|
256
208
|
|
257
209
|
my_data_fields = self.class.data_fields
|
258
|
-
@uribase = req.uribase
|
259
|
-
# if req.accept?('application/json')
|
260
210
|
|
261
211
|
if req.in_changeset
|
262
212
|
set_fields(data, my_data_fields, missing: :skip)
|
@@ -302,14 +252,30 @@ module OData
|
|
302
252
|
superclass.type_name
|
303
253
|
end
|
304
254
|
|
255
|
+
def time_cols
|
256
|
+
superclass.time_cols
|
257
|
+
end
|
258
|
+
|
305
259
|
def media_handler
|
306
260
|
superclass.media_handler
|
307
261
|
end
|
308
262
|
|
263
|
+
def uri
|
264
|
+
superclass.uri
|
265
|
+
end
|
266
|
+
|
309
267
|
def default_template
|
310
268
|
superclass.default_template
|
311
269
|
end
|
312
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
|
+
|
313
279
|
def to_a
|
314
280
|
y = @child_method.call
|
315
281
|
y.to_a
|
@@ -364,13 +330,13 @@ module OData
|
|
364
330
|
|
365
331
|
module NonMediaEntity
|
366
332
|
# non media entity metadata for json h
|
367
|
-
def metadata_h
|
368
|
-
{ uri: uri
|
333
|
+
def metadata_h
|
334
|
+
{ uri: uri,
|
369
335
|
type: type_name }
|
370
336
|
end
|
371
337
|
|
372
338
|
def values_for_odata
|
373
|
-
values
|
339
|
+
values
|
374
340
|
end
|
375
341
|
|
376
342
|
def odata_delete(req)
|
@@ -398,23 +364,53 @@ module OData
|
|
398
364
|
end
|
399
365
|
end
|
400
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
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
401
397
|
module MediaEntity
|
402
398
|
# media entity metadata for json h
|
403
|
-
def metadata_h
|
404
|
-
{ uri: uri
|
399
|
+
def metadata_h
|
400
|
+
{ uri: uri,
|
405
401
|
type: type_name,
|
406
|
-
media_src: media_src
|
407
|
-
edit_media: edit_media
|
402
|
+
media_src: media_src,
|
403
|
+
edit_media: edit_media,
|
408
404
|
content_type: @values[:content_type] }
|
409
405
|
end
|
410
406
|
|
411
|
-
def media_src
|
407
|
+
def media_src
|
412
408
|
version = self.class.media_handler.ressource_version(self)
|
413
|
-
"#{uri
|
409
|
+
"#{uri}/$value?version=#{version}"
|
414
410
|
end
|
415
411
|
|
416
|
-
def edit_media
|
417
|
-
"#{uri
|
412
|
+
def edit_media
|
413
|
+
"#{uri}/$value"
|
418
414
|
end
|
419
415
|
|
420
416
|
# directory where to put/find the media files for this entity-type
|