safrano 0.4.5 → 0.5.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/collection.rb +9 -7
- data/lib/odata/collection_filter.rb +2 -2
- data/lib/odata/collection_media.rb +41 -27
- data/lib/odata/complex_type.rb +175 -33
- data/lib/odata/edm/primitive_types.rb +8 -2
- data/lib/odata/entity.rb +36 -9
- data/lib/odata/error.rb +19 -0
- data/lib/odata/function_import.rb +24 -25
- data/lib/odata/model_ext.rb +19 -8
- data/lib/odata/transition.rb +22 -2
- data/lib/odata/walker.rb +20 -2
- data/lib/safrano/contract.rb +2 -4
- data/lib/safrano/multipart.rb +2 -2
- data/lib/safrano/request.rb +1 -0
- data/lib/safrano/service.rb +26 -5
- data/lib/safrano/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64699787371f5e9f696461faac80382cf2d53fe78d387742d8650e5a4b2c54df
|
4
|
+
data.tar.gz: 530e7619b4b27a1a50a63f832d4f3a8894850c6cea36e638769cdffbc6b7d942
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5d118f15a3a3ba78f5385867c82370e36612585b7f83329343babb2e3a8a7220d1059ad766fbe7c9e1f7f74a57945c086e60624d0301259c1427f0053b59052
|
7
|
+
data.tar.gz: 52b283be3e8902226f14864a74eae30bc62c65908be748ea37c5a150f574f3130c8957f26251e1958e298eba221eeada9f0155fcc73ed2591034e97466f1e629
|
data/lib/odata/collection.rb
CHANGED
@@ -60,10 +60,13 @@ module Safrano
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def initialize_dataset(dtset = nil)
|
63
|
-
@cx = dtset || @modelk
|
63
|
+
@cx = @cx || dtset || @modelk
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize_uparms
|
64
67
|
@uparms = UrlParameters4Coll.new(@cx, @params)
|
65
68
|
end
|
66
|
-
|
69
|
+
|
67
70
|
def odata_get_apply_params
|
68
71
|
@uparms.apply_to_dataset(@cx).map_result! do |dataset|
|
69
72
|
@cx = dataset
|
@@ -131,9 +134,9 @@ module Safrano
|
|
131
134
|
|
132
135
|
# on model class level we return the collection
|
133
136
|
def odata_get(req)
|
134
|
-
@params = req.params
|
135
|
-
initialize_dataset
|
136
|
-
|
137
|
+
@params = @params || req.params
|
138
|
+
initialize_dataset
|
139
|
+
initialize_uparms
|
137
140
|
@uparms.check_all.if_valid { |_ret|
|
138
141
|
odata_get_apply_params.if_valid { |_ret|
|
139
142
|
odata_get_output(req)
|
@@ -170,8 +173,7 @@ module Safrano
|
|
170
173
|
end
|
171
174
|
|
172
175
|
def initialize_dataset(dtset = nil)
|
173
|
-
@cx = dtset || navigated_dataset
|
174
|
-
@uparms = UrlParameters4Coll.new(@cx, @params)
|
176
|
+
@cx = @cx || dtset || navigated_dataset
|
175
177
|
end
|
176
178
|
# redefinitions of the main methods for a navigated collection
|
177
179
|
# (eg. all Books of Author[2] is Author[2].Books.all )
|
@@ -45,12 +45,12 @@ module Safrano
|
|
45
45
|
# the join-helper is shared by the order-by object and was potentially already
|
46
46
|
# partly built on order-by object creation.
|
47
47
|
def finalize(jh)
|
48
|
-
@filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
|
48
|
+
@filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
|
49
49
|
end
|
50
50
|
|
51
51
|
def apply_to_dataset(dtcx)
|
52
52
|
# normally finalize is called before, and thus @filtexpr is set
|
53
|
-
@filtexpr.map_result! { |f| dtcx.where(f) }
|
53
|
+
@filtexpr.map_result! { |f| dtcx.where(f) }
|
54
54
|
end
|
55
55
|
|
56
56
|
# Note: this is really only *parse* error, ie the error encounterd while
|
@@ -8,6 +8,11 @@ module Safrano
|
|
8
8
|
module Media
|
9
9
|
# base class for Media Handler
|
10
10
|
class Handler
|
11
|
+
def check_before_create(data:,
|
12
|
+
entity:,
|
13
|
+
filename:)
|
14
|
+
Contract::OK
|
15
|
+
end
|
11
16
|
end
|
12
17
|
|
13
18
|
# Simple static File/Directory based media store handler
|
@@ -54,13 +59,13 @@ module Safrano
|
|
54
59
|
end
|
55
60
|
|
56
61
|
# relative to @root
|
57
|
-
# eg Photo/1/
|
62
|
+
# eg Photo/1/1
|
58
63
|
def filename(entity)
|
59
64
|
Dir.chdir(abs_path(entity)) do
|
60
65
|
# simple design: one file per directory, and the directory
|
61
66
|
# contains the media entity-id --> implicit link between the media
|
62
67
|
# entity
|
63
|
-
File.join(media_path(entity), Dir.glob('*').
|
68
|
+
File.join(media_path(entity), Dir.glob('*').max)
|
64
69
|
end
|
65
70
|
end
|
66
71
|
|
@@ -147,15 +152,14 @@ module Safrano
|
|
147
152
|
|
148
153
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
149
154
|
# tree-structure
|
150
|
-
# media_path_ids = 1 --> 1
|
151
|
-
# media_path_ids = 15 --> 1/5
|
152
|
-
# media_path_ids = 555 --> 5/5/5
|
153
|
-
# media_path_ids = 5,5,5 --> 5/00/5/00/5
|
154
|
-
# media_path_ids = 5,00,5 --> 5/00/0/0/00/5
|
155
|
-
# media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
|
155
|
+
# media_path_ids = 1 --> 1/v
|
156
|
+
# media_path_ids = 15 --> 1/5/v
|
157
|
+
# media_path_ids = 555 --> 5/5/5/v
|
158
|
+
# media_path_ids = 5,5,5 --> 5/00/5/00/5/v
|
159
|
+
# media_path_ids = 5,00,5 --> 5/00/0/0/00/5/v
|
160
|
+
# media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5/v
|
156
161
|
def media_directory(entity)
|
157
162
|
StaticTree.path_builder(entity.media_path_ids)
|
158
|
-
# entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
|
159
163
|
end
|
160
164
|
|
161
165
|
def in_media_directory(entity)
|
@@ -267,29 +271,39 @@ module Safrano
|
|
267
271
|
missing: :skip)
|
268
272
|
end
|
269
273
|
|
270
|
-
#
|
271
|
-
if
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
274
|
+
# call before_create_entity media hook
|
275
|
+
new_entity.before_create_media_entity(data: data, mimetype: mimetype) if new_entity.respond_to? :before_create_media_entity
|
276
|
+
|
277
|
+
media_handler.check_before_create(data: data,
|
278
|
+
entity: new_entity,
|
279
|
+
filename: filename).if_valid { |_ret|
|
280
|
+
# to_one rels are create with FK data set on the parent entity
|
281
|
+
if parent
|
282
|
+
odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
283
|
+
else
|
284
|
+
# in-changeset requests get their own transaction
|
285
|
+
new_entity.save(transaction: !req.in_changeset)
|
286
|
+
end
|
287
|
+
|
288
|
+
req.register_content_id_ref(new_entity)
|
289
|
+
new_entity.copy_request_infos(req)
|
277
290
|
|
278
|
-
|
279
|
-
|
291
|
+
# call before_create_media hook
|
292
|
+
new_entity.before_create_media if new_entity.respond_to? :before_create_media
|
280
293
|
|
281
|
-
|
282
|
-
|
294
|
+
media_handler.save_file(data: data,
|
295
|
+
entity: new_entity,
|
296
|
+
filename: filename)
|
283
297
|
|
284
|
-
|
285
|
-
|
286
|
-
filename: filename)
|
298
|
+
# call after_create_media hook
|
299
|
+
new_entity.after_create_media if new_entity.respond_to? :after_create_media
|
287
300
|
|
288
|
-
|
289
|
-
|
301
|
+
# json is default content type so we dont need to specify it here again
|
302
|
+
# Contract.valid([201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)])
|
303
|
+
# TODO quirks array mode !
|
304
|
+
Contract.valid([201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)])
|
305
|
+
}.tap_error { |e| return e.odata_get(req) }.result
|
290
306
|
|
291
|
-
# json is default content type so we dont need to specify it here again
|
292
|
-
[201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
|
293
307
|
else # TODO: other formats
|
294
308
|
415
|
295
309
|
end
|
data/lib/odata/complex_type.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Safrano
|
4
4
|
module FunctionImport
|
5
|
+
EMPTY_HASH = {}.freeze
|
5
6
|
class ResultDefinition
|
6
7
|
D = 'd'
|
7
8
|
DJ_OPEN = '{"d":'
|
@@ -11,67 +12,174 @@ module Safrano
|
|
11
12
|
VALUEK = 'value'
|
12
13
|
RESULTSK = 'results'
|
13
14
|
COLLECTION = 'Collection'
|
14
|
-
|
15
|
-
def
|
16
|
-
|
15
|
+
|
16
|
+
def allowed_transitions
|
17
|
+
[Safrano::TransitionEnd]
|
17
18
|
end
|
18
|
-
|
19
|
-
def
|
20
|
-
|
19
|
+
|
20
|
+
def transition_end(_match_result)
|
21
|
+
Safrano::Transition::RESULT_END
|
21
22
|
end
|
22
|
-
|
23
|
-
|
23
|
+
|
24
|
+
# we will have this on class and instance level for making things simpler first
|
25
|
+
def self.klassmod
|
26
|
+
@klassmod
|
27
|
+
end
|
28
|
+
|
29
|
+
# return a subclass of ResultAsComplexType
|
30
|
+
def self.asComplexType(klassmod)
|
31
|
+
Class.new(ResultAsComplexType) do
|
32
|
+
@klassmod = klassmod
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# return a subclass of ResultAsComplexType
|
37
|
+
def self.asComplexTypeColl(klassmod)
|
38
|
+
Class.new(ResultAsComplexTypeColl) do
|
39
|
+
@klassmod = klassmod
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.asPrimitiveType(klassmod)
|
44
|
+
Class.new(ResultAsPrimitiveType) do
|
45
|
+
@klassmod = klassmod
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.asPrimitiveTypeColl(klassmod)
|
50
|
+
Class.new(ResultAsPrimitiveTypeColl) do
|
51
|
+
@klassmod = klassmod
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.asEntity(klassmod)
|
56
|
+
Class.new(ResultAsEntity) do
|
57
|
+
@klassmod = klassmod
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.asEntityColl(klassmod)
|
62
|
+
Class.new(ResultAsEntityColl) do
|
63
|
+
@klassmod = klassmod
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(value)
|
68
|
+
@value = value
|
69
|
+
end
|
70
|
+
|
71
|
+
def odata_get(req)
|
72
|
+
[200, EMPTY_HASH, [to_odata_json(req)]]
|
73
|
+
end
|
74
|
+
def self.type_metadata
|
24
75
|
@klassmod.type_name
|
25
76
|
end
|
77
|
+
def type_metadata
|
78
|
+
self.class.type_metadata
|
79
|
+
end
|
80
|
+
|
81
|
+
# needed for ComplexType result
|
82
|
+
def to_odata_json(_req)
|
83
|
+
"#{DJ_OPEN}#{@value.odata_h.to_json}#{DJ_CLOSE}"
|
84
|
+
end
|
85
|
+
|
86
|
+
# wrapper
|
87
|
+
# for OData Entity and Collections, return them directly
|
88
|
+
# for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
|
89
|
+
def self.do_execute_func_result(result, _req, apply_query_params=false)
|
90
|
+
self.new(result)
|
91
|
+
end
|
92
|
+
|
26
93
|
end
|
94
|
+
|
27
95
|
class ResultAsComplexType < ResultDefinition
|
96
|
+
def self.type_metadata
|
97
|
+
@klassmod.type_name
|
98
|
+
end
|
28
99
|
end
|
100
|
+
|
29
101
|
class ResultAsComplexTypeColl < ResultDefinition
|
30
|
-
def type_metadata
|
102
|
+
def self.type_metadata
|
31
103
|
"Collection(#{@klassmod.type_name})"
|
32
104
|
end
|
33
105
|
|
34
|
-
def to_odata_json(
|
35
|
-
"#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
106
|
+
def to_odata_json(req)
|
107
|
+
# "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
108
|
+
template = self.class.klassmod.output_template
|
109
|
+
# TODO: Error handling if database contains binary BLOB data that cant be
|
110
|
+
# interpreted as UTF-8 then JSON will fail here
|
111
|
+
|
112
|
+
innerh = req.service.get_coll_odata_h(array: @value,
|
113
|
+
template: template)
|
114
|
+
|
115
|
+
innerj = innerh.to_json
|
116
|
+
|
117
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
36
118
|
end
|
37
119
|
end
|
120
|
+
|
38
121
|
class ResultAsEntity < ResultDefinition
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
to_odata_json(request: req)
|
43
|
-
end
|
122
|
+
|
123
|
+
def self.type_metadata
|
124
|
+
@klassmod.type_name
|
44
125
|
end
|
126
|
+
|
127
|
+
|
128
|
+
# wrapper
|
129
|
+
# for OData Entity return them directly
|
130
|
+
def self.do_execute_func_result(result, _req, apply_query_params=false)
|
131
|
+
# note: Sequel entities instances seem to be thread safe, so we can
|
132
|
+
# safely add request-dependant data (eg. req.params) there
|
133
|
+
apply_query_params ? result : result.inactive_query_params
|
134
|
+
end
|
135
|
+
|
45
136
|
end
|
137
|
+
|
46
138
|
class ResultAsEntityColl < ResultDefinition
|
47
|
-
|
139
|
+
|
140
|
+
def self.type_metadata
|
48
141
|
"Collection(#{@klassmod.type_name})"
|
49
142
|
end
|
50
|
-
|
51
|
-
|
143
|
+
|
144
|
+
# wrapper
|
145
|
+
# for OData Entity Collection return them directly
|
146
|
+
def self.do_execute_func_result(result, req, apply_query_params=false)
|
52
147
|
coll = Safrano::OData::Collection.new(@klassmod)
|
148
|
+
# instance_exec has other instance variables; @values would be nil in the block below
|
149
|
+
# need to pass a local copy
|
150
|
+
dtset = result
|
53
151
|
coll.instance_exec do
|
54
|
-
|
55
|
-
|
152
|
+
|
153
|
+
@params = apply_query_params ? req.params : EMPTY_HASH
|
154
|
+
initialize_dataset(dtset)
|
155
|
+
initialize_uparms
|
56
156
|
end
|
57
|
-
coll
|
157
|
+
coll
|
58
158
|
end
|
159
|
+
|
59
160
|
end
|
161
|
+
|
60
162
|
class ResultAsPrimitiveType < ResultDefinition
|
61
|
-
def
|
163
|
+
def self.type_metadata
|
164
|
+
@klassmod.type_name
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_odata_json(_req)
|
62
168
|
{ D => { METAK => { TYPEK => type_metadata },
|
63
|
-
VALUEK =>
|
169
|
+
VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
|
64
170
|
end
|
65
171
|
end
|
172
|
+
|
66
173
|
class ResultAsPrimitiveTypeColl < ResultDefinition
|
67
|
-
def type_metadata
|
174
|
+
def self.type_metadata
|
68
175
|
"Collection(#{@klassmod.type_name})"
|
69
176
|
end
|
70
177
|
|
71
|
-
def to_odata_json(
|
72
|
-
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
-
RESULTSK =>
|
178
|
+
def to_odata_json(_req)
|
179
|
+
{ D => { METAK => { TYPEK => self.class.type_metadata },
|
180
|
+
RESULTSK => self.class.klassmod.odata_collection(@value) } }.to_json
|
74
181
|
end
|
182
|
+
|
75
183
|
end
|
76
184
|
end
|
77
185
|
|
@@ -80,7 +188,8 @@ module Safrano
|
|
80
188
|
# with added OData functionality
|
81
189
|
class ComplexType
|
82
190
|
attr_reader :values
|
83
|
-
|
191
|
+
EMPTYH = {}.freeze
|
192
|
+
|
84
193
|
@namespace = nil
|
85
194
|
def self.namespace
|
86
195
|
@namespace
|
@@ -89,9 +198,41 @@ module Safrano
|
|
89
198
|
def self.props
|
90
199
|
@props
|
91
200
|
end
|
92
|
-
|
201
|
+
|
202
|
+
def type_name
|
203
|
+
self.class.type_name
|
204
|
+
end
|
205
|
+
|
206
|
+
def metadata_h
|
207
|
+
{ type: type_name }
|
208
|
+
end
|
209
|
+
|
210
|
+
def casted_values
|
211
|
+
# MVP... TODO: handle time mappings like in Entity models
|
212
|
+
values
|
213
|
+
end
|
214
|
+
|
215
|
+
# needed for nested json output
|
216
|
+
# this is a simpler version of model_ext#output_template
|
217
|
+
def self.default_template
|
218
|
+
template = {}
|
219
|
+
expand_e = {}
|
220
|
+
|
221
|
+
template[:all_values] = EMPTYH
|
222
|
+
@props.each { |prop, kl|
|
223
|
+
if kl.respond_to? :default_template
|
224
|
+
expand_e[prop] = kl.default_template
|
225
|
+
end
|
226
|
+
}
|
227
|
+
template[:expand_e] = expand_e
|
228
|
+
template
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.output_template
|
232
|
+
default_template
|
233
|
+
end
|
93
234
|
def self.type_name
|
94
|
-
"#{@namespace}.#{self.to_s}"
|
235
|
+
@namespace ? "#{@namespace}.#{self.to_s}" : self.to_s
|
95
236
|
end
|
96
237
|
|
97
238
|
def initialize
|
@@ -102,6 +243,7 @@ module Safrano
|
|
102
243
|
|
103
244
|
def odata_h
|
104
245
|
ret = { METAK => { TYPEK => self.class.type_name } }
|
246
|
+
|
105
247
|
@values.each { |k, v|
|
106
248
|
ret[k] = if v.respond_to? :odata_h
|
107
249
|
v.odata_h
|
@@ -113,11 +255,11 @@ module Safrano
|
|
113
255
|
end
|
114
256
|
|
115
257
|
def self.return_as_collection_descriptor
|
116
|
-
FunctionImport::
|
258
|
+
FunctionImport::ResultDefinition.asComplexTypeColl(self)
|
117
259
|
end
|
118
260
|
|
119
261
|
def self.return_as_instance_descriptor
|
120
|
-
FunctionImport::
|
262
|
+
FunctionImport::ResultDefinition.asComplexType(self)
|
121
263
|
end
|
122
264
|
|
123
265
|
# add metadata xml to the passed REXML schema object
|
@@ -20,13 +20,15 @@ module Safrano
|
|
20
20
|
# cf. Sequel Database column_schema_default_to_ruby_value
|
21
21
|
# schema_column_type
|
22
22
|
# https://www.odata.org/documentation/odata-version-2-0/overview/
|
23
|
-
def self.default_edm_type(ruby_type:)
|
23
|
+
def self.default_edm_type(ruby_type:, db_type: )
|
24
24
|
case ruby_type
|
25
25
|
when :integer
|
26
26
|
'Edm.Int32'
|
27
27
|
when :string
|
28
28
|
'Edm.String'
|
29
|
-
when :date
|
29
|
+
when :date
|
30
|
+
'Edm.DateTime'
|
31
|
+
when :datetime
|
30
32
|
'Edm.DateTime'
|
31
33
|
when :time
|
32
34
|
'Edm.Time'
|
@@ -38,6 +40,10 @@ module Safrano
|
|
38
40
|
'Edm.Decimal'
|
39
41
|
when :blob
|
40
42
|
'Edm.Binary'
|
43
|
+
else # try with db_type:
|
44
|
+
if ( db_type =~ /\ANUMERIC/ )
|
45
|
+
'Edm.Decimal'
|
46
|
+
end
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
data/lib/odata/entity.rb
CHANGED
@@ -112,10 +112,11 @@ module Safrano
|
|
112
112
|
selvals
|
113
113
|
end
|
114
114
|
|
115
|
-
# post
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
# some clients wrongly expect post payload with the new entity in an array
|
116
|
+
# TODO quirks array mode !
|
117
|
+
def to_odata_array_json(request:)
|
118
|
+
innerj = request.service.get_coll_odata_h(array: [self],
|
119
|
+
template: self.class.default_template).to_json
|
119
120
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
120
121
|
end
|
121
122
|
|
@@ -124,7 +125,7 @@ module Safrano
|
|
124
125
|
end
|
125
126
|
|
126
127
|
def copy_request_infos(req)
|
127
|
-
@params = req.params
|
128
|
+
@params = @inactive_query_params ? EMPTY_HASH : req.params
|
128
129
|
@do_links = req.walker.do_links
|
129
130
|
@uparms = UrlParameters4Single.new(self, @params)
|
130
131
|
end
|
@@ -150,7 +151,11 @@ module Safrano
|
|
150
151
|
@uparms.check_all.tap_valid { return odata_get_output(req) }
|
151
152
|
.tap_error { |e| return e.odata_get(req) }
|
152
153
|
end
|
153
|
-
|
154
|
+
def inactive_query_params
|
155
|
+
@inactive_query_params = true
|
156
|
+
self # chaining
|
157
|
+
end
|
158
|
+
|
154
159
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
155
160
|
Safrano.remove_nav_relation(assoc, parent)
|
156
161
|
entity.destroy(transaction: false)
|
@@ -263,6 +268,7 @@ module Safrano
|
|
263
268
|
ret
|
264
269
|
end
|
265
270
|
end
|
271
|
+
|
266
272
|
# end of module SafranoEntity
|
267
273
|
module Entity
|
268
274
|
include EntityBase
|
@@ -305,7 +311,7 @@ module Safrano
|
|
305
311
|
end
|
306
312
|
|
307
313
|
module MappingBeforeOutput
|
308
|
-
# needed for proper datetime output
|
314
|
+
# needed for proper datetime or Decimal output
|
309
315
|
def casted_values(cols = nil)
|
310
316
|
vals = case cols
|
311
317
|
when nil
|
@@ -316,6 +322,10 @@ module Safrano
|
|
316
322
|
else
|
317
323
|
selected_values_for_odata(cols)
|
318
324
|
end
|
325
|
+
# TODO better design (perf/ do more during startup and less during request runtime )
|
326
|
+
# TODO replace the quick and dirty BigDecimal hack with something better
|
327
|
+
self.class.decimal_cols.each { |dc| vals[dc] = BigDecimal(vals[dc].to_s).to_s('F') if vals.key?(dc) }
|
328
|
+
|
319
329
|
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
320
330
|
vals
|
321
331
|
end
|
@@ -438,5 +448,22 @@ module Safrano
|
|
438
448
|
pk_hash.values
|
439
449
|
end
|
440
450
|
end
|
441
|
-
|
442
|
-
|
451
|
+
|
452
|
+
module EntityCreateStandardOutput
|
453
|
+
# Json formatter for a create entity POST call / Standard version; return as json object
|
454
|
+
def to_odata_create_json(request:)
|
455
|
+
# TODO Perf: reduce method call overhead
|
456
|
+
# we added this redirection for readability and flexibility
|
457
|
+
to_odata_json(request: request)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
module EntityCreateArrayOutput
|
462
|
+
# Json formatter for a create entity POST call Array version
|
463
|
+
def to_odata_create_json(request:)
|
464
|
+
# TODO Perf: reduce method call overhead
|
465
|
+
# we added this redirection for readability and flexibility
|
466
|
+
to_odata_array_json(request: request)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end # end of Module OData
|
data/lib/odata/error.rb
CHANGED
@@ -114,6 +114,18 @@ module Safrano
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
+
# http Unprocessable Entity for example when trying to
|
118
|
+
# upload duplicated media ressource
|
119
|
+
class UnprocessableEntityError
|
120
|
+
extend ErrorClass
|
121
|
+
include ErrorInstance
|
122
|
+
HTTP_CODE = 422
|
123
|
+
@msg = 'Unprocessable Entity'
|
124
|
+
def initialize(reason)
|
125
|
+
@msg = reason
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
117
129
|
# http Bad Req.
|
118
130
|
class BadRequestError
|
119
131
|
extend ErrorClass
|
@@ -125,6 +137,13 @@ module Safrano
|
|
125
137
|
end
|
126
138
|
end
|
127
139
|
|
140
|
+
# for upload empty media
|
141
|
+
class BadRequestEmptyMediaUpload < BadRequestError
|
142
|
+
include ErrorInstance
|
143
|
+
def initialize(path)
|
144
|
+
@msg = "Bad Request: empty media file #{path}"
|
145
|
+
end
|
146
|
+
end
|
128
147
|
# Generic failed changeset
|
129
148
|
class BadRequestFailedChangeSet < BadRequestError
|
130
149
|
@msg = 'Bad Request: Failed changeset '
|
@@ -21,7 +21,7 @@ module Safrano
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def allowed_transitions
|
24
|
-
[Safrano::
|
24
|
+
[Safrano::TransitionExecuteFunc]
|
25
25
|
end
|
26
26
|
|
27
27
|
def input(**parmtypes)
|
@@ -42,7 +42,13 @@ module Safrano
|
|
42
42
|
end
|
43
43
|
self
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
|
+
def auto_query_parameters
|
47
|
+
@auto_query_params = true
|
48
|
+
self # chaining
|
49
|
+
end
|
50
|
+
alias auto_query_params auto_query_parameters
|
51
|
+
|
46
52
|
def return(klassmod, &proc)
|
47
53
|
raise('Please provide a code block') unless block_given?
|
48
54
|
|
@@ -51,7 +57,7 @@ module Safrano
|
|
51
57
|
else
|
52
58
|
# if it's neither a ComplexType nor a Model-Entity
|
53
59
|
# --> assume it is a Primitive
|
54
|
-
|
60
|
+
ResultDefinition.asPrimitiveType(klassmod)
|
55
61
|
end
|
56
62
|
@proc = proc
|
57
63
|
self
|
@@ -65,7 +71,8 @@ module Safrano
|
|
65
71
|
else
|
66
72
|
# if it's neither a ComplexType nor a Modle-Entity
|
67
73
|
# --> assume it is a Primitive
|
68
|
-
ResultAsPrimitiveTypeColl.new(klassmod)
|
74
|
+
# ResultAsPrimitiveTypeColl.new(klassmod)
|
75
|
+
ResultDefinition.asPrimitiveTypeColl(klassmod)
|
69
76
|
end
|
70
77
|
@proc = proc
|
71
78
|
self
|
@@ -135,33 +142,25 @@ module Safrano
|
|
135
142
|
end if @input
|
136
143
|
funky
|
137
144
|
end
|
138
|
-
|
139
|
-
def
|
145
|
+
|
146
|
+
def with_transition_validated(req)
|
140
147
|
# initialize_params
|
148
|
+
@params = req.params
|
141
149
|
return yield unless (@error = check_url_func_params)
|
142
150
|
|
143
|
-
@error
|
144
|
-
end
|
145
|
-
|
146
|
-
def to_odata_json(req)
|
147
|
-
result = @proc.call(**@funcparams)
|
148
|
-
@returning.to_odata_json(result, req)
|
151
|
+
[nil, :error, @error] if @error
|
149
152
|
end
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
@params = req.params
|
157
|
-
|
158
|
-
with_validated_get(req) do
|
159
|
-
odata_get_output(req)
|
153
|
+
|
154
|
+
|
155
|
+
def do_execute_func(req)
|
156
|
+
with_transition_validated(req) do
|
157
|
+
result = @proc.call(**@funcparams)
|
158
|
+
[@returning.do_execute_func_result(result, req, @auto_query_params), :run]
|
160
159
|
end
|
161
160
|
end
|
162
|
-
|
163
|
-
def
|
164
|
-
|
161
|
+
|
162
|
+
def transition_execute_func(_match_result)
|
163
|
+
[self, :run_with_execute_func]
|
165
164
|
end
|
166
165
|
end
|
167
166
|
end
|
data/lib/odata/model_ext.rb
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
|
7
7
|
require 'json'
|
8
8
|
require 'rexml/document'
|
9
|
+
require 'bigdecimal'
|
9
10
|
require_relative '../safrano/core'
|
10
11
|
require_relative 'error'
|
11
12
|
require_relative 'collection_filter'
|
@@ -32,6 +33,7 @@ module Safrano
|
|
32
33
|
attr_reader :uri
|
33
34
|
attr_reader :odata_upk_parts
|
34
35
|
attr_reader :time_cols
|
36
|
+
attr_reader :decimal_cols
|
35
37
|
attr_reader :namespace
|
36
38
|
|
37
39
|
# initialising block of code to be executed at end of
|
@@ -73,7 +75,7 @@ module Safrano
|
|
73
75
|
@uparms = nil
|
74
76
|
@params = nil
|
75
77
|
@cx = nil
|
76
|
-
@@time_cols = nil
|
78
|
+
# @@time_cols = nil
|
77
79
|
end
|
78
80
|
|
79
81
|
def build_uri(uribase)
|
@@ -81,11 +83,11 @@ module Safrano
|
|
81
83
|
end
|
82
84
|
|
83
85
|
def return_as_collection_descriptor
|
84
|
-
Safrano::FunctionImport::
|
86
|
+
Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
|
85
87
|
end
|
86
88
|
|
87
89
|
def return_as_instance_descriptor
|
88
|
-
Safrano::FunctionImport::
|
90
|
+
Safrano::FunctionImport::ResultDefinition.asEntity(self)
|
89
91
|
end
|
90
92
|
|
91
93
|
def execute_deferred_iblock
|
@@ -94,8 +96,10 @@ module Safrano
|
|
94
96
|
|
95
97
|
# Factory json-> Model Object instance
|
96
98
|
def new_from_hson_h(hash)
|
97
|
-
enty = new
|
98
|
-
enty.set_fields(hash, data_fields, missing: :skip)
|
99
|
+
#enty = new
|
100
|
+
#enty.set_fields(hash, data_fields, missing: :skip)
|
101
|
+
enty = create(hash)
|
102
|
+
#enty.set(hash)
|
99
103
|
enty
|
100
104
|
end
|
101
105
|
|
@@ -356,11 +360,16 @@ module Safrano
|
|
356
360
|
# Time columns
|
357
361
|
@time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
|
358
362
|
|
363
|
+
|
364
|
+
|
359
365
|
# add edm_types into schema
|
360
366
|
db_schema.each do |_col, props|
|
361
|
-
props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type]
|
367
|
+
props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type],
|
368
|
+
db_type: props[:db_type])
|
362
369
|
end
|
363
|
-
|
370
|
+
# Edm.Decimal cols
|
371
|
+
@decimal_cols = db_schema.select { |_c, v| v[:odata_edm_type] == 'Edm.Decimal' }.map { |c, _v| c }
|
372
|
+
|
364
373
|
# and finally build the path lists and allowed tr's
|
365
374
|
build_attribute_path_list
|
366
375
|
build_expand_path_list
|
@@ -627,7 +636,9 @@ module Safrano
|
|
627
636
|
req.register_content_id_ref(new_entity)
|
628
637
|
new_entity.copy_request_infos(req)
|
629
638
|
# json is default content type so we dont need to specify it here again
|
630
|
-
|
639
|
+
# TODO quirks array mode !
|
640
|
+
# [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
|
641
|
+
[201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)]
|
631
642
|
else # TODO: other formats
|
632
643
|
415
|
633
644
|
end
|
data/lib/odata/transition.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative 'error'
|
|
6
6
|
module Safrano
|
7
7
|
# represents a state transition when navigating/parsing the url path
|
8
8
|
# from left to right
|
9
|
-
class Transition
|
9
|
+
class Transition
|
10
10
|
attr_accessor :trans
|
11
11
|
attr_accessor :match_result
|
12
12
|
attr_accessor :rgx
|
@@ -52,8 +52,28 @@ module Safrano
|
|
52
52
|
ctx.method(@trans).call(@match_result)
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
|
+
#Transition that does not move/change the input
|
57
|
+
class InplaceTransition < Transition
|
58
|
+
def initialize(trans: )
|
59
|
+
@trans = trans
|
60
|
+
end
|
61
|
+
def do_match(str)
|
62
|
+
@str = str
|
63
|
+
end
|
64
|
+
def path_remain
|
65
|
+
@str
|
66
|
+
end
|
67
|
+
def path_done
|
68
|
+
EMPTYSTR
|
69
|
+
end
|
70
|
+
def do_transition(ctx)
|
71
|
+
ctx.method(@trans).call(@str)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
56
75
|
TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
|
76
|
+
TransitionExecuteFunc = InplaceTransition.new(trans: 'transition_execute_func')
|
57
77
|
TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
|
58
78
|
trans: 'transition_metadata')
|
59
79
|
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
data/lib/odata/walker.rb
CHANGED
@@ -29,18 +29,24 @@ module Safrano
|
|
29
29
|
|
30
30
|
# are $links requested ?
|
31
31
|
attr_reader :do_links
|
32
|
+
|
33
|
+
attr_reader :request
|
32
34
|
|
33
35
|
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
34
36
|
EMPTYSTR = ''
|
35
37
|
SLASH = '/'
|
36
38
|
|
37
|
-
def initialize(service, path, content_id_refs = nil)
|
39
|
+
def initialize(service, path, request, content_id_refs = nil )
|
38
40
|
raise NIL_SERVICE_FATAL unless service
|
39
41
|
|
40
42
|
path = URI.decode_www_form_component(path)
|
41
43
|
@context = service
|
42
44
|
@content_id_refs = content_id_refs
|
43
|
-
|
45
|
+
|
46
|
+
# needed because for function import we need access to the url parameters (req.params)
|
47
|
+
# who contains the functions params
|
48
|
+
@request = request
|
49
|
+
|
44
50
|
@contexts = [@context]
|
45
51
|
|
46
52
|
@path_start = @path_remain = if service
|
@@ -109,6 +115,16 @@ module Safrano
|
|
109
115
|
end
|
110
116
|
end
|
111
117
|
|
118
|
+
# execute function import with request parameters
|
119
|
+
# input: @context containt the function to exectute,
|
120
|
+
# @request.params should normally contain the params
|
121
|
+
# result: validate the params for the given function, execute the function and
|
122
|
+
# return it's result back into @context,
|
123
|
+
# and finaly set status :end (or error if anyting went wrong )
|
124
|
+
def do_run_with_execute_func
|
125
|
+
@context, @status, @error = @context.do_execute_func(@request)
|
126
|
+
end
|
127
|
+
|
112
128
|
# little hacks... depending on returned state, set some attributes
|
113
129
|
def state_mappings
|
114
130
|
case @status
|
@@ -137,6 +153,8 @@ module Safrano
|
|
137
153
|
# entity reference here and place it in @context
|
138
154
|
when :run_with_content_id
|
139
155
|
do_run_with_content_id
|
156
|
+
when :run_with_execute_func
|
157
|
+
do_run_with_execute_func
|
140
158
|
end
|
141
159
|
|
142
160
|
@contexts << @context
|
data/lib/safrano/contract.rb
CHANGED
@@ -62,6 +62,8 @@ module Safrano
|
|
62
62
|
# All tap_valid* handlers are executed
|
63
63
|
# tap_error* handlers are not executed
|
64
64
|
class Valid
|
65
|
+
attr_reader :result
|
66
|
+
|
65
67
|
def initialize(result)
|
66
68
|
@result = result
|
67
69
|
end
|
@@ -100,10 +102,6 @@ module Safrano
|
|
100
102
|
def error
|
101
103
|
nil
|
102
104
|
end
|
103
|
-
|
104
|
-
def result
|
105
|
-
@result
|
106
|
-
end
|
107
105
|
end # class Valid
|
108
106
|
|
109
107
|
def self.valid(result)
|
data/lib/safrano/multipart.rb
CHANGED
@@ -4,7 +4,7 @@ CRLF = "\r\n"
|
|
4
4
|
LF = "\n"
|
5
5
|
|
6
6
|
require 'securerandom'
|
7
|
-
require '
|
7
|
+
require 'rack/utils'
|
8
8
|
|
9
9
|
# Simple multipart support for OData $batch purpose
|
10
10
|
module MIME
|
@@ -457,7 +457,7 @@ module MIME
|
|
457
457
|
"Content-Transfer-Encoding: binary#{CRLF}",
|
458
458
|
'HTTP/1.1 '].join(CRLF).freeze
|
459
459
|
|
460
|
-
StatusMessage = ::
|
460
|
+
StatusMessage = ::Rack::Utils::HTTP_STATUS_CODES.freeze
|
461
461
|
|
462
462
|
def initialize
|
463
463
|
@hd = {}
|
data/lib/safrano/request.rb
CHANGED
data/lib/safrano/service.rb
CHANGED
@@ -112,7 +112,7 @@ module Safrano
|
|
112
112
|
include Safrano
|
113
113
|
include ExpandHandler
|
114
114
|
|
115
|
-
XML_PREAMBLE = %
|
115
|
+
XML_PREAMBLE = %(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
|
116
116
|
|
117
117
|
# This is just a hash of entity Set Names to the corresponding Class
|
118
118
|
# Example
|
@@ -137,6 +137,7 @@ module Safrano
|
|
137
137
|
attr_accessor :relman
|
138
138
|
attr_accessor :complex_types
|
139
139
|
attr_accessor :function_imports
|
140
|
+
attr_accessor :function_import_keys
|
140
141
|
|
141
142
|
# Instance attributes for specialized Version specific Instances
|
142
143
|
attr_accessor :v1
|
@@ -155,6 +156,7 @@ module Safrano
|
|
155
156
|
@relman = Safrano::RelationManager.new
|
156
157
|
@complex_types = Set.new
|
157
158
|
@function_imports = {}
|
159
|
+
@function_import_keys = []
|
158
160
|
@cmap = {}
|
159
161
|
instance_eval(&block) if block_given?
|
160
162
|
end
|
@@ -204,6 +206,12 @@ module Safrano
|
|
204
206
|
(@v2.xserver_url = @xserver_url) if @v2
|
205
207
|
end
|
206
208
|
|
209
|
+
# keep the bug active for now, but allow to activate the fix,
|
210
|
+
# later we will change the default to be fixed
|
211
|
+
def bugfix_create_response(bool = false)
|
212
|
+
@bugfix_create_response = bool
|
213
|
+
end
|
214
|
+
|
207
215
|
# end public API
|
208
216
|
|
209
217
|
def set_uribase
|
@@ -233,6 +241,7 @@ module Safrano
|
|
233
241
|
other.batch_handler = @batch_handler
|
234
242
|
other.complex_types = @complex_types
|
235
243
|
other.function_imports = @function_imports
|
244
|
+
other.function_import_keys = @function_import_keys
|
236
245
|
other
|
237
246
|
end
|
238
247
|
|
@@ -316,6 +325,8 @@ module Safrano
|
|
316
325
|
def function_import(name)
|
317
326
|
funcimp = Safrano::FunctionImport(name)
|
318
327
|
@function_imports[name] = funcimp
|
328
|
+
@function_import_keys << name
|
329
|
+
set_funcimports_sorted
|
319
330
|
funcimp
|
320
331
|
end
|
321
332
|
|
@@ -332,7 +343,11 @@ module Safrano
|
|
332
343
|
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
|
333
344
|
@collections
|
334
345
|
end
|
335
|
-
|
346
|
+
|
347
|
+
# need to be sorted by size too
|
348
|
+
def set_funcimports_sorted
|
349
|
+
@function_import_keys.sort_by!{|k| k.size}.reverse!
|
350
|
+
end
|
336
351
|
# to be called at end of publishing block to ensure we get the right names
|
337
352
|
# and additionally build the list of valid attribute path's used
|
338
353
|
# for validation of $orderby or $filter params
|
@@ -360,7 +375,11 @@ module Safrano
|
|
360
375
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
361
376
|
@collections.each do |klass|
|
362
377
|
klass.build_uri(@uribase)
|
363
|
-
|
378
|
+
# TODO perf
|
379
|
+
klass.include( (klass.time_cols.empty? && klass.decimal_cols.empty?) ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
380
|
+
|
381
|
+
# Output create (POST) as single entity (Standard) or as array (non-standard buggy)
|
382
|
+
klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)
|
364
383
|
end
|
365
384
|
|
366
385
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
@@ -392,7 +411,7 @@ module Safrano
|
|
392
411
|
end
|
393
412
|
|
394
413
|
def base_url_func_regexp
|
395
|
-
@
|
414
|
+
@function_import_keys.join('|')
|
396
415
|
end
|
397
416
|
|
398
417
|
def service
|
@@ -486,7 +505,9 @@ module Safrano
|
|
486
505
|
doc.add_element('edmx:Edmx', 'Version' => '1.0')
|
487
506
|
doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
|
488
507
|
serv = doc.root.add_element('edmx:DataServices',
|
489
|
-
|
508
|
+
# TODO: export the real version (result from version negotions)
|
509
|
+
# but currently we support only v1 and v2, and most users will use v2
|
510
|
+
'm:DataServiceVersion' => '2.0')
|
490
511
|
# 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
|
491
512
|
# DataServiceVersion: This attribute MUST be in the data service
|
492
513
|
# metadata namespace
|
data/lib/safrano/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.51'
|
111
|
-
description: Safrano is an OData server library based on Ruby
|
111
|
+
description: Safrano is an OData server library based on Ruby Sequel and Rack.
|
112
112
|
email: dev@aithscel.eu
|
113
113
|
executables: []
|
114
114
|
extensions: []
|
@@ -189,8 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
189
|
- !ruby/object:Gem::Version
|
190
190
|
version: '0'
|
191
191
|
requirements: []
|
192
|
-
rubygems_version: 3.2.
|
192
|
+
rubygems_version: 3.2.5
|
193
193
|
signing_key:
|
194
194
|
specification_version: 4
|
195
|
-
summary: Safrano is
|
195
|
+
summary: Safrano is an OData server library based on Ruby Sequel and Rack
|
196
196
|
test_files: []
|