safrano 0.4.2 → 0.5.0
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/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +140 -591
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +123 -172
- data/lib/odata/error.rb +183 -32
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +41 -25
- data/lib/odata/filter/sequel.rb +133 -62
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +106 -52
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +13 -26
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +23 -107
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -33
- data/lib/safrano/rack_app.rb +66 -65
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +96 -45
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +240 -130
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +32 -11
@@ -1,56 +1,26 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative 'filter/sequel.rb'
|
3
|
+
require 'odata/error'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
9
|
-
UNMASK_RGX = /'(%?)(\$\d+)(%?)'/.freeze
|
10
|
-
def with_mask_quoted_substrings!
|
11
|
-
cnt = 0
|
12
|
-
repl = {}
|
13
|
-
gsub!(MASK_RGX) do |_m|
|
14
|
-
cnt += 1
|
15
|
-
repl["$#{cnt}"] = Regexp.last_match(1)
|
16
|
-
"'$#{cnt}'"
|
17
|
-
end
|
18
|
-
yield self
|
19
|
-
|
20
|
-
gsub!(UNMASK_RGX) do |_m|
|
21
|
-
k = Regexp.last_match(2).to_s
|
22
|
-
"'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def with_mask_quoted_substrings
|
27
|
-
cnt = 0
|
28
|
-
repl = {}
|
29
|
-
tmpstr = gsub(MASK_RGX) do |_m|
|
30
|
-
cnt += 1
|
31
|
-
repl["$#{cnt}"] = Regexp.last_match(1)
|
32
|
-
"'$#{cnt}'"
|
33
|
-
end
|
34
|
-
yield tmpstr
|
35
|
-
end
|
36
|
-
end
|
5
|
+
require_relative 'filter/parse'
|
6
|
+
require_relative 'filter/sequel'
|
37
7
|
|
38
8
|
# filter base class and subclass in our OData namespace
|
39
|
-
module
|
9
|
+
module Safrano
|
40
10
|
class FilterBase
|
41
11
|
# re-useable empty filtering (idempotent)
|
42
|
-
EmptyFilter = new
|
12
|
+
EmptyFilter = new.freeze
|
43
13
|
|
44
14
|
def self.factory(filterstr)
|
45
15
|
filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
|
46
16
|
end
|
47
17
|
|
48
18
|
def apply_to_dataset(dtcx)
|
49
|
-
dtcx
|
19
|
+
Contract.valid(dtcx)
|
50
20
|
end
|
51
21
|
|
52
22
|
# finalize
|
53
|
-
def finalize(
|
23
|
+
def finalize(_jh) Contract::OK end
|
54
24
|
|
55
25
|
def empty?
|
56
26
|
true
|
@@ -60,12 +30,14 @@ module OData
|
|
60
30
|
false
|
61
31
|
end
|
62
32
|
end
|
33
|
+
|
63
34
|
# should handle everything by parsing
|
64
35
|
class FilterByParse < FilterBase
|
65
36
|
attr_reader :filterstr
|
37
|
+
|
66
38
|
def initialize(filterstr)
|
67
39
|
@filterstr = filterstr.dup
|
68
|
-
@ast =
|
40
|
+
@ast = Safrano::Filter::Parser.new(@filterstr).build
|
69
41
|
end
|
70
42
|
|
71
43
|
# this build's up the Sequel Filter Expression, and as a side effect,
|
@@ -73,16 +45,20 @@ module OData
|
|
73
45
|
# the join-helper is shared by the order-by object and was potentially already
|
74
46
|
# partly built on order-by object creation.
|
75
47
|
def finalize(jh)
|
76
|
-
@filtexpr = @ast.sequel_expr(jh)
|
48
|
+
@filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
|
77
49
|
end
|
78
50
|
|
79
51
|
def apply_to_dataset(dtcx)
|
80
52
|
# normally finalize is called before, and thus @filtexpr is set
|
81
|
-
dtcx.where(
|
53
|
+
@filtexpr.map_result! { |f| dtcx.where(f) }
|
82
54
|
end
|
83
55
|
|
56
|
+
# Note: this is really only *parse* error, ie the error encounterd while
|
57
|
+
# trying to build the AST
|
58
|
+
# Later when evaluating the AST, there can be other errors, they shall
|
59
|
+
# be tracked with @error
|
84
60
|
def parse_error?
|
85
|
-
@ast.
|
61
|
+
@ast.error
|
86
62
|
end
|
87
63
|
|
88
64
|
def empty?
|
@@ -1,26 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack'
|
2
4
|
require 'fileutils'
|
3
|
-
require_relative './navigation_attribute
|
5
|
+
require_relative './navigation_attribute'
|
4
6
|
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
module Media
|
7
9
|
# base class for Media Handler
|
8
10
|
class Handler
|
11
|
+
def check_before_create(data:,
|
12
|
+
entity:,
|
13
|
+
filename:)
|
14
|
+
Contract::OK
|
15
|
+
end
|
9
16
|
end
|
10
17
|
|
11
18
|
# Simple static File/Directory based media store handler
|
12
19
|
# similar to Rack::Static
|
13
20
|
# with a flat directory structure
|
14
21
|
class Static < Handler
|
15
|
-
def initialize(root: nil)
|
22
|
+
def initialize(root: nil, mediaklass:)
|
16
23
|
@root = File.absolute_path(root || Dir.pwd)
|
17
24
|
@file_server = ::Rack::File.new(@root)
|
25
|
+
@media_class = mediaklass
|
26
|
+
@media_dir_name = mediaklass.to_s
|
27
|
+
register
|
28
|
+
end
|
29
|
+
|
30
|
+
def register
|
31
|
+
@abs_klass_dir = File.absolute_path(@media_dir_name, @root)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_abs_class_dir
|
35
|
+
FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
|
18
36
|
end
|
19
37
|
|
20
|
-
|
21
|
-
|
22
|
-
abs_klass_dir = File.absolute_path(klass.type_name, @root)
|
23
|
-
FileUtils.makedirs abs_klass_dir unless Dir.exist?(abs_klass_dir)
|
38
|
+
def finalize
|
39
|
+
create_abs_class_dir
|
24
40
|
end
|
25
41
|
|
26
42
|
# minimal working implementation...
|
@@ -36,27 +52,20 @@ module OData
|
|
36
52
|
fsret
|
37
53
|
end
|
38
54
|
|
39
|
-
# TODO: [perf] this can be precalculated and cached on MediaModelKlass level
|
40
|
-
# and passed as argument to save_file
|
41
|
-
# eg. /@root/Photo
|
42
|
-
def abs_klass_dir(entity)
|
43
|
-
File.absolute_path(entity.klass_dir, @root)
|
44
|
-
end
|
45
|
-
|
46
55
|
# this is relative to @root
|
47
56
|
# eg. Photo/1
|
48
57
|
def media_path(entity)
|
49
|
-
File.join(
|
58
|
+
File.join(@media_dir_name, media_directory(entity))
|
50
59
|
end
|
51
60
|
|
52
61
|
# relative to @root
|
53
|
-
# eg Photo/1/
|
62
|
+
# eg Photo/1/1
|
54
63
|
def filename(entity)
|
55
64
|
Dir.chdir(abs_path(entity)) do
|
56
65
|
# simple design: one file per directory, and the directory
|
57
66
|
# contains the media entity-id --> implicit link between the media
|
58
67
|
# entity
|
59
|
-
File.join(media_path(entity), Dir.glob('*').
|
68
|
+
File.join(media_path(entity), Dir.glob('*').max)
|
60
69
|
end
|
61
70
|
end
|
62
71
|
|
@@ -65,6 +74,11 @@ module OData
|
|
65
74
|
File.absolute_path(media_path(entity), @root)
|
66
75
|
end
|
67
76
|
|
77
|
+
# absolute filename
|
78
|
+
def abs_filename(entity)
|
79
|
+
File.absolute_path(filename(entity), @root)
|
80
|
+
end
|
81
|
+
|
68
82
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
69
83
|
# simplest implementation is media_directory = entity.media_path_id
|
70
84
|
# --> we get a 1 level depth flat directory structure
|
@@ -81,7 +95,7 @@ module OData
|
|
81
95
|
end
|
82
96
|
|
83
97
|
def odata_delete(entity:)
|
84
|
-
Dir.chdir(abs_klass_dir
|
98
|
+
Dir.chdir(@abs_klass_dir) do
|
85
99
|
in_media_directory(entity) do
|
86
100
|
Dir.glob('*').each { |oldf| File.delete(oldf) }
|
87
101
|
end
|
@@ -90,7 +104,7 @@ module OData
|
|
90
104
|
|
91
105
|
# Here as well, MVP implementation
|
92
106
|
def save_file(data:, filename:, entity:)
|
93
|
-
Dir.chdir(abs_klass_dir
|
107
|
+
Dir.chdir(@abs_klass_dir) do
|
94
108
|
in_media_directory(entity) do
|
95
109
|
filename = '1'
|
96
110
|
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
@@ -102,19 +116,19 @@ module OData
|
|
102
116
|
# after each upload, so that clients get informed about new versions
|
103
117
|
# of the same media ressource
|
104
118
|
def ressource_version(entity)
|
105
|
-
Dir.chdir(abs_klass_dir
|
119
|
+
Dir.chdir(@abs_klass_dir) do
|
106
120
|
in_media_directory(entity) do
|
107
|
-
Dir.glob('*').
|
121
|
+
Dir.glob('*').max
|
108
122
|
end
|
109
123
|
end
|
110
124
|
end
|
111
125
|
|
112
126
|
# Here as well, MVP implementation
|
113
127
|
def replace_file(data:, filename:, entity:)
|
114
|
-
Dir.chdir(abs_klass_dir
|
128
|
+
Dir.chdir(@abs_klass_dir) do
|
115
129
|
in_media_directory(entity) do
|
116
130
|
version = nil
|
117
|
-
Dir.glob('*').each do |oldf|
|
131
|
+
Dir.glob('*').sort.each do |oldf|
|
118
132
|
version = oldf
|
119
133
|
File.delete(oldf)
|
120
134
|
end
|
@@ -138,15 +152,14 @@ module OData
|
|
138
152
|
|
139
153
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
140
154
|
# tree-structure
|
141
|
-
# media_path_ids = 1 --> 1
|
142
|
-
# media_path_ids = 15 --> 1/5
|
143
|
-
# media_path_ids = 555 --> 5/5/5
|
144
|
-
# media_path_ids = 5,5,5 --> 5/00/5/00/5
|
145
|
-
# media_path_ids = 5,00,5 --> 5/00/0/0/00/5
|
146
|
-
# 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
|
147
161
|
def media_directory(entity)
|
148
162
|
StaticTree.path_builder(entity.media_path_ids)
|
149
|
-
# entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
|
150
163
|
end
|
151
164
|
|
152
165
|
def in_media_directory(entity)
|
@@ -156,9 +169,9 @@ module OData
|
|
156
169
|
end
|
157
170
|
|
158
171
|
def odata_delete(entity:)
|
159
|
-
Dir.chdir(abs_klass_dir
|
172
|
+
Dir.chdir(@abs_klass_dir) do
|
160
173
|
in_media_directory(entity) do
|
161
|
-
Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
174
|
+
Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
162
175
|
end
|
163
176
|
end
|
164
177
|
end
|
@@ -174,10 +187,10 @@ module OData
|
|
174
187
|
# end
|
175
188
|
# Here as well, MVP implementation
|
176
189
|
def replace_file(data:, filename:, entity:)
|
177
|
-
Dir.chdir(abs_klass_dir
|
190
|
+
Dir.chdir(@abs_klass_dir) do
|
178
191
|
in_media_directory(entity) do
|
179
192
|
version = nil
|
180
|
-
Dir.glob('*').each do |oldf|
|
193
|
+
Dir.glob('*').sort.each do |oldf|
|
181
194
|
version = oldf
|
182
195
|
File.delete(oldf)
|
183
196
|
end
|
@@ -197,15 +210,21 @@ module OData
|
|
197
210
|
# API method for defining the media handler
|
198
211
|
# eg.
|
199
212
|
# publish_media_model photos do
|
200
|
-
# use
|
213
|
+
# use Safrano::Media::Static, :root => '/media_root'
|
201
214
|
# end
|
202
215
|
|
203
216
|
def set_default_media_handler
|
204
|
-
@media_handler =
|
217
|
+
@media_handler = Safrano::Media::Static.new(mediaklass: self)
|
218
|
+
end
|
219
|
+
|
220
|
+
def finalize_media
|
221
|
+
@media_handler.finalize
|
205
222
|
end
|
206
223
|
|
207
224
|
def use(klass, args)
|
225
|
+
args[:mediaklass] = self
|
208
226
|
@media_handler = klass.new(**args)
|
227
|
+
@media_handler.create_abs_class_dir
|
209
228
|
end
|
210
229
|
|
211
230
|
# API method for setting the model field mapped to SLUG on upload
|
@@ -214,11 +233,7 @@ module OData
|
|
214
233
|
end
|
215
234
|
|
216
235
|
def api_check_media_fields
|
217
|
-
raise(
|
218
|
-
|
219
|
-
# unless self.db_schema.has_key?(:media_src)
|
220
|
-
# raise OData::API::MediaModelError, self
|
221
|
-
# end
|
236
|
+
raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
|
222
237
|
end
|
223
238
|
|
224
239
|
# END API methods
|
@@ -237,11 +252,11 @@ module OData
|
|
237
252
|
# NOTE: we will implement this first in a MVP way. There will be plenty of
|
238
253
|
# potential future enhancements (performance, scallability, flexibility... with added complexity)
|
239
254
|
# 4. create relation to parent if needed
|
240
|
-
def odata_create_entity_and_relation(req, assoc, parent)
|
255
|
+
def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
|
241
256
|
req.with_media_data do |data, mimetype, filename|
|
242
257
|
## future enhancement: validate allowed mimetypes ?
|
243
258
|
# if (invalid = invalid_media_mimetype(mimetype))
|
244
|
-
# ::
|
259
|
+
# ::Safrano::Request::ON_CGST_ERROR.call(req)
|
245
260
|
# return [422, {}, ['Invalid mime type: ', invalid.to_s]]
|
246
261
|
# end
|
247
262
|
|
@@ -256,20 +271,39 @@ module OData
|
|
256
271
|
missing: :skip)
|
257
272
|
end
|
258
273
|
|
259
|
-
#
|
260
|
-
if
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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)
|
290
|
+
|
291
|
+
# call before_create_media hook
|
292
|
+
new_entity.before_create_media if new_entity.respond_to? :before_create_media
|
293
|
+
|
294
|
+
media_handler.save_file(data: data,
|
295
|
+
entity: new_entity,
|
296
|
+
filename: filename)
|
266
297
|
|
267
|
-
|
268
|
-
|
298
|
+
# call after_create_media hook
|
299
|
+
new_entity.after_create_media if new_entity.respond_to? :after_create_media
|
269
300
|
|
270
|
-
|
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
|
271
306
|
|
272
|
-
[201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
|
273
307
|
else # TODO: other formats
|
274
308
|
415
|
275
309
|
end
|
@@ -277,3 +311,26 @@ module OData
|
|
277
311
|
end
|
278
312
|
end
|
279
313
|
end
|
314
|
+
|
315
|
+
# deprecated
|
316
|
+
# REMOVE 0.6
|
317
|
+
module OData
|
318
|
+
module Media
|
319
|
+
class Static < ::Safrano::Media::Static
|
320
|
+
def initialize(root: nil, mediaklass:)
|
321
|
+
::Safrano::Deprecation.deprecate('OData::Media::Static',
|
322
|
+
'Use Safrano::Media::Static instead')
|
323
|
+
|
324
|
+
super
|
325
|
+
end
|
326
|
+
end
|
327
|
+
class StaticTree < ::Safrano::Media::StaticTree
|
328
|
+
def initialize(root: nil, mediaklass:)
|
329
|
+
::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
|
330
|
+
'Use Safrano::Media::StaticTree instead')
|
331
|
+
|
332
|
+
super
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'odata/error.rb'
|
2
4
|
|
3
5
|
# all ordering related classes in our OData module
|
4
|
-
module
|
6
|
+
module Safrano
|
5
7
|
# base class for ordering
|
6
8
|
class OrderBase
|
7
9
|
# re-useable empty ordering (idempotent)
|
8
|
-
EmptyOrder = new
|
10
|
+
EmptyOrder = new.freeze
|
9
11
|
|
10
12
|
# input : the OData order string
|
11
13
|
# returns a Order object that should have a apply_to(cx) method
|
@@ -28,6 +30,7 @@ module OData
|
|
28
30
|
|
29
31
|
class Order < OrderBase
|
30
32
|
attr_reader :oarg
|
33
|
+
|
31
34
|
def initialize(ostr, jh)
|
32
35
|
ostr.strip!
|
33
36
|
@orderp = ostr
|
data/lib/odata/common_logger.rb
CHANGED
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
module FunctionImport
|
5
|
+
class ResultDefinition
|
6
|
+
D = 'd'
|
7
|
+
DJ_OPEN = '{"d":'
|
8
|
+
DJ_CLOSE = '}'
|
9
|
+
METAK = '__metadata'
|
10
|
+
TYPEK = 'type'
|
11
|
+
VALUEK = 'value'
|
12
|
+
RESULTSK = 'results'
|
13
|
+
COLLECTION = 'Collection'
|
14
|
+
|
15
|
+
def initialize(klassmod)
|
16
|
+
@klassmod = klassmod
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_odata_json(result, _req)
|
20
|
+
"#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def type_metadata
|
24
|
+
@klassmod.type_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class ResultAsComplexType < ResultDefinition
|
28
|
+
end
|
29
|
+
class ResultAsComplexTypeColl < ResultDefinition
|
30
|
+
def type_metadata
|
31
|
+
"Collection(#{@klassmod.type_name})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_odata_json(coll, _req)
|
35
|
+
"#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
class ResultAsEntity < ResultDefinition
|
39
|
+
def to_odata_json(result_entity, req)
|
40
|
+
result_entity.instance_exec do
|
41
|
+
copy_request_infos(req)
|
42
|
+
to_odata_json(request: req)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
class ResultAsEntityColl < ResultDefinition
|
47
|
+
def type_metadata
|
48
|
+
"Collection(#{@klassmod.type_name})"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_odata_json(result_dataset, req)
|
52
|
+
coll = Safrano::OData::Collection.new(@klassmod)
|
53
|
+
coll.instance_exec do
|
54
|
+
@params = req.params
|
55
|
+
initialize_dataset(result_dataset)
|
56
|
+
end
|
57
|
+
coll.to_odata_json(request: req)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
class ResultAsPrimitiveType < ResultDefinition
|
61
|
+
def to_odata_json(result, _req)
|
62
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
63
|
+
VALUEK => @klassmod.odata_value(result) } }.to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
class ResultAsPrimitiveTypeColl < ResultDefinition
|
67
|
+
def type_metadata
|
68
|
+
"Collection(#{@klassmod.type_name})"
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_odata_json(result, _req)
|
72
|
+
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
+
RESULTSK => @klassmod.odata_collection(result) } }.to_json
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# a generic Struct like ruby's standard Struct, but implemented with a
|
79
|
+
# @values Hash, similar to Sequel models and
|
80
|
+
# with added OData functionality
|
81
|
+
class ComplexType
|
82
|
+
attr_reader :values
|
83
|
+
|
84
|
+
@namespace = nil
|
85
|
+
def self.namespace
|
86
|
+
@namespace
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.props
|
90
|
+
@props
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.type_name
|
94
|
+
"#{@namespace}.#{self.to_s}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize
|
98
|
+
@values = {}
|
99
|
+
end
|
100
|
+
METAK = '__metadata'
|
101
|
+
TYPEK = 'type'
|
102
|
+
|
103
|
+
def odata_h
|
104
|
+
ret = { METAK => { TYPEK => self.class.type_name } }
|
105
|
+
@values.each { |k, v|
|
106
|
+
ret[k] = if v.respond_to? :odata_h
|
107
|
+
v.odata_h
|
108
|
+
else
|
109
|
+
v
|
110
|
+
end
|
111
|
+
}
|
112
|
+
ret
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.return_as_collection_descriptor
|
116
|
+
FunctionImport::ResultAsComplexTypeColl.new(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.return_as_instance_descriptor
|
120
|
+
FunctionImport::ResultAsComplexType.new(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
# add metadata xml to the passed REXML schema object
|
124
|
+
def self.add_metadata_rexml(schema)
|
125
|
+
ctty = schema.add_element('ComplexType', 'Name' => to_s)
|
126
|
+
|
127
|
+
# with their properties
|
128
|
+
@props.each do |prop, rbtype|
|
129
|
+
attrs = { 'Name' => prop.to_s,
|
130
|
+
'Type' => rbtype.type_name }
|
131
|
+
ctty.add_element('Property', attrs)
|
132
|
+
end
|
133
|
+
ctty
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def Safrano.ComplexType(**props)
|
138
|
+
Class.new(Safrano::ComplexType) do
|
139
|
+
@props = props
|
140
|
+
props.each { |a, klassmod|
|
141
|
+
asym = a.to_sym
|
142
|
+
define_method(asym) do @values[asym] end
|
143
|
+
define_method("#{a}=") do |val| @values[asym] = val end
|
144
|
+
}
|
145
|
+
define_method :initialize do |*p, **kwvals|
|
146
|
+
super()
|
147
|
+
p.zip(props.keys).each { |val, a| @values[a] = val } if p
|
148
|
+
kwvals.each { |a, val| @values[a] = val if props.key?(a) } if kwvals
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|