safrano 0.3.4 → 0.4.4
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 +17 -15
- data/lib/odata/collection.rb +141 -500
- data/lib/odata/collection_filter.rb +44 -37
- data/lib/odata/collection_media.rb +193 -43
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +39 -12
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +201 -176
- data/lib/odata/error.rb +186 -33
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +69 -0
- data/lib/odata/filter/error.rb +55 -6
- data/lib/odata/filter/parse.rb +38 -36
- data/lib/odata/filter/sequel.rb +121 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +15 -11
- data/lib/odata/filter/tree.rb +110 -60
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +50 -32
- data/lib/odata/relations.rb +7 -7
- data/lib/odata/select.rb +54 -0
- data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +19 -11
- data/lib/safrano.rb +18 -28
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +43 -0
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
- data/lib/safrano/rack_app.rb +175 -0
- data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
- data/lib/{request.rb → safrano/request.rb} +102 -50
- data/lib/{response.rb → safrano/response.rb} +5 -4
- data/lib/safrano/sequel_join_by_paths.rb +5 -0
- data/lib/{service.rb → safrano/service.rb} +257 -188
- data/lib/safrano/version.rb +5 -0
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +53 -17
- data/lib/rack_app.rb +0 -174
- data/lib/sequel_join_by_paths.rb +0 -5
- data/lib/version.rb +0 -4
@@ -1,61 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'odata/error.rb'
|
2
4
|
|
3
5
|
require_relative 'filter/parse.rb'
|
4
6
|
require_relative 'filter/sequel.rb'
|
5
7
|
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
# filter base class and subclass in our OData namespace
|
9
|
+
module Safrano
|
10
|
+
class FilterBase
|
11
|
+
# re-useable empty filtering (idempotent)
|
12
|
+
EmptyFilter = new.freeze
|
13
|
+
|
14
|
+
def self.factory(filterstr)
|
15
|
+
filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply_to_dataset(dtcx)
|
19
|
+
Contract.valid(dtcx)
|
17
20
|
end
|
18
|
-
yield self
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
# finalize
|
23
|
+
def finalize(_jh) Contract::OK end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
true
|
23
27
|
end
|
24
|
-
end
|
25
28
|
|
26
|
-
|
27
|
-
|
28
|
-
repl = {}
|
29
|
-
tmpstr = gsub(MASK_RGX) do |_m|
|
30
|
-
cnt += 1
|
31
|
-
repl["$#{cnt}"] = Regexp.last_match(1)
|
32
|
-
"'$#{cnt}'"
|
29
|
+
def parse_error?
|
30
|
+
false
|
33
31
|
end
|
34
|
-
yield tmpstr
|
35
32
|
end
|
36
|
-
end
|
37
33
|
|
38
|
-
# filter base class and subclass in our OData namespace
|
39
|
-
module OData
|
40
34
|
# should handle everything by parsing
|
41
|
-
class FilterByParse
|
42
|
-
|
35
|
+
class FilterByParse < FilterBase
|
36
|
+
attr_reader :filterstr
|
37
|
+
|
38
|
+
def initialize(filterstr)
|
43
39
|
@filterstr = filterstr.dup
|
44
|
-
@ast =
|
45
|
-
@jh = jh
|
40
|
+
@ast = Safrano::Filter::Parser.new(@filterstr).build
|
46
41
|
end
|
47
42
|
|
48
|
-
|
49
|
-
|
50
|
-
|
43
|
+
# this build's up the Sequel Filter Expression, and as a side effect,
|
44
|
+
# it also finalizes the join helper that we need for the start dataset join
|
45
|
+
# the join-helper is shared by the order-by object and was potentially already
|
46
|
+
# partly built on order-by object creation.
|
47
|
+
def finalize(jh)
|
48
|
+
@filtexpr = @ast.if_valid do |ast| ast.sequel_expr(jh) end
|
51
49
|
end
|
52
50
|
|
53
|
-
def
|
54
|
-
@
|
51
|
+
def apply_to_dataset(dtcx)
|
52
|
+
# normally finalize is called before, and thus @filtexpr is set
|
53
|
+
@filtexpr.map_result! do |f| dtcx.where(f) end
|
55
54
|
end
|
56
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
|
57
60
|
def parse_error?
|
58
|
-
@ast.
|
61
|
+
@ast.error
|
62
|
+
end
|
63
|
+
|
64
|
+
def empty?
|
65
|
+
false
|
59
66
|
end
|
60
67
|
end
|
61
68
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack'
|
4
|
+
require 'fileutils'
|
2
5
|
require_relative './navigation_attribute.rb'
|
3
6
|
|
4
|
-
module
|
7
|
+
module Safrano
|
5
8
|
module Media
|
6
9
|
# base class for Media Handler
|
7
10
|
class Handler
|
@@ -9,61 +12,173 @@ module OData
|
|
9
12
|
|
10
13
|
# Simple static File/Directory based media store handler
|
11
14
|
# similar to Rack::Static
|
15
|
+
# with a flat directory structure
|
12
16
|
class Static < Handler
|
13
|
-
def initialize(root: nil)
|
17
|
+
def initialize(root: nil, mediaklass:)
|
14
18
|
@root = File.absolute_path(root || Dir.pwd)
|
15
19
|
@file_server = ::Rack::File.new(@root)
|
20
|
+
@media_class = mediaklass
|
21
|
+
@media_dir_name = mediaklass.to_s
|
22
|
+
register
|
23
|
+
end
|
24
|
+
|
25
|
+
def register
|
26
|
+
@abs_klass_dir = File.absolute_path(@media_dir_name, @root)
|
27
|
+
FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
|
16
28
|
end
|
17
29
|
|
18
30
|
# minimal working implementation...
|
19
31
|
# Note: @file_server works relative to @root directory
|
20
32
|
def odata_get(request:, entity:)
|
21
33
|
media_env = request.env.dup
|
22
|
-
|
34
|
+
media_env['PATH_INFO'] = filename(entity)
|
35
|
+
fsret = @file_server.call(media_env)
|
36
|
+
if fsret.first == 200
|
37
|
+
# provide own content type as we keep it in the media entity
|
38
|
+
fsret[1]['Content-Type'] = entity.content_type
|
39
|
+
end
|
40
|
+
fsret
|
41
|
+
end
|
42
|
+
|
43
|
+
# this is relative to @root
|
44
|
+
# eg. Photo/1
|
45
|
+
def media_path(entity)
|
46
|
+
File.join(@media_dir_name, media_directory(entity))
|
47
|
+
end
|
48
|
+
|
49
|
+
# relative to @root
|
50
|
+
# eg Photo/1/pommes-topaz.jpg
|
51
|
+
def filename(entity)
|
52
|
+
Dir.chdir(abs_path(entity)) do
|
23
53
|
# simple design: one file per directory, and the directory
|
24
54
|
# contains the media entity-id --> implicit link between the media
|
25
55
|
# entity
|
26
|
-
|
27
|
-
|
28
|
-
File.join(path(entity), filename)
|
56
|
+
File.join(media_path(entity), Dir.glob('*').sort.first)
|
29
57
|
end
|
30
|
-
media_env['PATH_INFO'] = relpath
|
31
|
-
@file_server.call(media_env)
|
32
58
|
end
|
33
59
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
File.absolute_path(entity.klass_dir, @root)
|
60
|
+
# /@root/Photo/1
|
61
|
+
def abs_path(entity)
|
62
|
+
File.absolute_path(media_path(entity), @root)
|
38
63
|
end
|
39
64
|
|
40
|
-
|
41
|
-
|
65
|
+
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
66
|
+
# simplest implementation is media_directory = entity.media_path_id
|
67
|
+
# --> we get a 1 level depth flat directory structure
|
68
|
+
def media_directory(entity)
|
69
|
+
entity.media_path_id
|
42
70
|
end
|
43
71
|
|
44
|
-
|
45
|
-
|
46
|
-
|
72
|
+
def in_media_directory(entity)
|
73
|
+
mpi = media_directory(entity)
|
74
|
+
Dir.mkdir mpi unless Dir.exist?(mpi)
|
75
|
+
Dir.chdir mpi do
|
76
|
+
yield
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def odata_delete(entity:)
|
81
|
+
Dir.chdir(@abs_klass_dir) do
|
82
|
+
in_media_directory(entity) do
|
83
|
+
Dir.glob('*').each { |oldf| File.delete(oldf) }
|
84
|
+
end
|
85
|
+
end
|
47
86
|
end
|
48
87
|
|
49
88
|
# Here as well, MVP implementation
|
50
89
|
def save_file(data:, filename:, entity:)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
Dir.chdir mpi do
|
90
|
+
Dir.chdir(@abs_klass_dir) do
|
91
|
+
in_media_directory(entity) do
|
92
|
+
filename = '1'
|
55
93
|
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
56
94
|
end
|
57
95
|
end
|
58
96
|
end
|
59
97
|
|
98
|
+
# needed for having a changing media ressource "source" metadata
|
99
|
+
# after each upload, so that clients get informed about new versions
|
100
|
+
# of the same media ressource
|
101
|
+
def ressource_version(entity)
|
102
|
+
Dir.chdir(@abs_klass_dir) do
|
103
|
+
in_media_directory(entity) do
|
104
|
+
Dir.glob('*').sort.last
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
60
109
|
# Here as well, MVP implementation
|
61
110
|
def replace_file(data:, filename:, entity:)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
111
|
+
Dir.chdir(@abs_klass_dir) do
|
112
|
+
in_media_directory(entity) do
|
113
|
+
version = nil
|
114
|
+
Dir.glob('*').sort.each do |oldf|
|
115
|
+
version = oldf
|
116
|
+
File.delete(oldf)
|
117
|
+
end
|
118
|
+
filename = (version.to_i + 1).to_s
|
119
|
+
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
# Simple static File/Directory based media store handler
|
125
|
+
# similar to Rack::Static
|
126
|
+
# with directory Tree structure
|
127
|
+
|
128
|
+
class StaticTree < Static
|
129
|
+
SEP = '/00/'.freeze
|
130
|
+
VERS = '/v'.freeze
|
131
|
+
|
132
|
+
def self.path_builder(ids)
|
133
|
+
ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
|
134
|
+
end
|
135
|
+
|
136
|
+
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
137
|
+
# tree-structure
|
138
|
+
# media_path_ids = 1 --> 1
|
139
|
+
# media_path_ids = 15 --> 1/5
|
140
|
+
# media_path_ids = 555 --> 5/5/5
|
141
|
+
# media_path_ids = 5,5,5 --> 5/00/5/00/5
|
142
|
+
# media_path_ids = 5,00,5 --> 5/00/0/0/00/5
|
143
|
+
# media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
|
144
|
+
def media_directory(entity)
|
145
|
+
StaticTree.path_builder(entity.media_path_ids)
|
146
|
+
# entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
|
147
|
+
end
|
148
|
+
|
149
|
+
def in_media_directory(entity)
|
150
|
+
mpi = media_directory(entity)
|
151
|
+
FileUtils.makedirs mpi unless Dir.exist?(mpi)
|
152
|
+
Dir.chdir(mpi) { yield }
|
153
|
+
end
|
154
|
+
|
155
|
+
def odata_delete(entity:)
|
156
|
+
Dir.chdir(@abs_klass_dir) do
|
157
|
+
in_media_directory(entity) do
|
158
|
+
Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Here as well, MVP implementation
|
164
|
+
# def replace_file(data:, filename:, entity:)
|
165
|
+
# Dir.chdir(abs_klass_dir(entity)) do
|
166
|
+
# in_media_directory(entity) do
|
167
|
+
# Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
168
|
+
# File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
# Here as well, MVP implementation
|
173
|
+
def replace_file(data:, filename:, entity:)
|
174
|
+
Dir.chdir(@abs_klass_dir) do
|
175
|
+
in_media_directory(entity) do
|
176
|
+
version = nil
|
177
|
+
Dir.glob('*').sort.each do |oldf|
|
178
|
+
version = oldf
|
179
|
+
File.delete(oldf)
|
180
|
+
end
|
181
|
+
filename = (version.to_i + 1).to_s
|
67
182
|
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
68
183
|
end
|
69
184
|
end
|
@@ -74,33 +189,36 @@ module OData
|
|
74
189
|
# special handling for media entity
|
75
190
|
module EntityClassMedia
|
76
191
|
attr_reader :media_handler
|
192
|
+
attr_reader :slug_field
|
77
193
|
|
78
194
|
# API method for defining the media handler
|
79
195
|
# eg.
|
80
196
|
# publish_media_model photos do
|
81
|
-
# use
|
197
|
+
# use Safrano::Media::Static, :root => '/media_root'
|
82
198
|
# end
|
83
199
|
|
84
200
|
def set_default_media_handler
|
85
|
-
@media_handler =
|
201
|
+
@media_handler = Safrano::Media::Static.new(mediaklass: self)
|
202
|
+
end
|
203
|
+
|
204
|
+
def use(klass, args)
|
205
|
+
args[:mediaklass] = self
|
206
|
+
@media_handler = klass.new(**args)
|
86
207
|
end
|
87
208
|
|
88
|
-
|
89
|
-
|
209
|
+
# API method for setting the model field mapped to SLUG on upload
|
210
|
+
def slug(inp)
|
211
|
+
@slug_field = inp
|
90
212
|
end
|
91
213
|
|
92
214
|
def api_check_media_fields
|
93
|
-
unless
|
94
|
-
raise OData::API::MediaModelError, self
|
95
|
-
end
|
96
|
-
# unless self.db_schema.has_key?(:media_src)
|
97
|
-
# raise OData::API::MediaModelError, self
|
98
|
-
# end
|
215
|
+
raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
|
99
216
|
end
|
100
217
|
|
218
|
+
# END API methods
|
219
|
+
|
101
220
|
def new_media_entity(mimetype:)
|
102
|
-
nh = {}
|
103
|
-
nh['content_type'] = mimetype
|
221
|
+
nh = { 'content_type' => mimetype }
|
104
222
|
new_from_hson_h(nh)
|
105
223
|
end
|
106
224
|
|
@@ -113,11 +231,11 @@ module OData
|
|
113
231
|
# NOTE: we will implement this first in a MVP way. There will be plenty of
|
114
232
|
# potential future enhancements (performance, scallability, flexibility... with added complexity)
|
115
233
|
# 4. create relation to parent if needed
|
116
|
-
def odata_create_entity_and_relation(req, assoc, parent)
|
234
|
+
def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
|
117
235
|
req.with_media_data do |data, mimetype, filename|
|
118
236
|
## future enhancement: validate allowed mimetypes ?
|
119
237
|
# if (invalid = invalid_media_mimetype(mimetype))
|
120
|
-
# ::
|
238
|
+
# ::Safrano::Request::ON_CGST_ERROR.call(req)
|
121
239
|
# return [422, {}, ['Invalid mime type: ', invalid.to_s]]
|
122
240
|
# end
|
123
241
|
|
@@ -125,6 +243,13 @@ module OData
|
|
125
243
|
|
126
244
|
new_entity = new_media_entity(mimetype: mimetype)
|
127
245
|
|
246
|
+
if slug_field
|
247
|
+
|
248
|
+
new_entity.set_fields({ slug_field => filename },
|
249
|
+
data_fields,
|
250
|
+
missing: :skip)
|
251
|
+
end
|
252
|
+
|
128
253
|
# to_one rels are create with FK data set on the parent entity
|
129
254
|
if parent
|
130
255
|
odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
@@ -136,9 +261,11 @@ module OData
|
|
136
261
|
req.register_content_id_ref(new_entity)
|
137
262
|
new_entity.copy_request_infos(req)
|
138
263
|
|
139
|
-
media_handler.save_file(data: data,
|
140
|
-
|
141
|
-
|
264
|
+
media_handler.save_file(data: data,
|
265
|
+
entity: new_entity,
|
266
|
+
filename: filename)
|
267
|
+
# json is default content type so we dont need to specify it here again
|
268
|
+
[201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
|
142
269
|
else # TODO: other formats
|
143
270
|
415
|
144
271
|
end
|
@@ -146,3 +273,26 @@ module OData
|
|
146
273
|
end
|
147
274
|
end
|
148
275
|
end
|
276
|
+
|
277
|
+
# deprecated
|
278
|
+
# REMOVE 0.6
|
279
|
+
module OData
|
280
|
+
module Media
|
281
|
+
class Static < ::Safrano::Media::Static
|
282
|
+
def initialize(root: nil, mediaklass:)
|
283
|
+
::Safrano::Deprecation.deprecate('OData::Media::Static',
|
284
|
+
'Use Safrano::Media::Static instead')
|
285
|
+
|
286
|
+
super
|
287
|
+
end
|
288
|
+
end
|
289
|
+
class StaticTree < ::Safrano::Media::StaticTree
|
290
|
+
def initialize(root: nil, mediaklass:)
|
291
|
+
::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
|
292
|
+
'Use Safrano::Media::StaticTree instead')
|
293
|
+
|
294
|
+
super
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -1,19 +1,36 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
module OrderWithRuby
|
5
|
-
# this module requires the @fn attribute to exist where it is used
|
6
|
-
def fn=(fnam)
|
7
|
-
@fn = fnam
|
8
|
-
@fn_tab = fnam.split('/').map(&:to_sym)
|
9
|
-
end
|
10
|
-
end
|
3
|
+
require 'odata/error.rb'
|
11
4
|
|
12
5
|
# all ordering related classes in our OData module
|
13
|
-
module
|
6
|
+
module Safrano
|
14
7
|
# base class for ordering
|
15
|
-
class
|
8
|
+
class OrderBase
|
9
|
+
# re-useable empty ordering (idempotent)
|
10
|
+
EmptyOrder = new.freeze
|
11
|
+
|
12
|
+
# input : the OData order string
|
13
|
+
# returns a Order object that should have a apply_to(cx) method
|
14
|
+
def self.factory(orderstr, jh)
|
15
|
+
orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_error?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_to_dataset(dtcx)
|
27
|
+
dtcx
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Order < OrderBase
|
16
32
|
attr_reader :oarg
|
33
|
+
|
17
34
|
def initialize(ostr, jh)
|
18
35
|
ostr.strip!
|
19
36
|
@orderp = ostr
|
@@ -21,23 +38,14 @@ module OData
|
|
21
38
|
build_oarg if @orderp
|
22
39
|
end
|
23
40
|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
# input : the filter string
|
29
|
-
# returns a filter object that should have a apply_to(cx) method
|
30
|
-
def self.new_by_parse(orderstr, jh)
|
31
|
-
Order.new_full_match_complexpr(orderstr, jh)
|
32
|
-
end
|
33
|
-
|
34
|
-
# handle with Sequel
|
35
|
-
def self.new_full_match_complexpr(orderstr, jh)
|
36
|
-
ComplexOrder.new(orderstr, jh)
|
41
|
+
def empty?
|
42
|
+
false
|
37
43
|
end
|
38
44
|
|
39
45
|
def apply_to_dataset(dtcx)
|
40
|
-
|
46
|
+
# Warning, we need order_append, simply order(oarg) overwrites
|
47
|
+
# previous one !
|
48
|
+
dtcx.order_append(@oarg)
|
41
49
|
end
|
42
50
|
|
43
51
|
def build_oarg
|
@@ -60,26 +68,31 @@ module OData
|
|
60
68
|
end
|
61
69
|
|
62
70
|
# complex ordering logic
|
63
|
-
class
|
71
|
+
class MultiOrder < Order
|
64
72
|
def initialize(orderstr, jh)
|
65
73
|
super
|
66
74
|
@olist = []
|
67
75
|
@jh = jh
|
68
|
-
|
69
|
-
|
70
|
-
@olist = orderstr.split(',').map do |ostr|
|
71
|
-
oo = Order.new(ostr, @jh)
|
72
|
-
oo.oarg
|
73
|
-
end
|
76
|
+
@orderstr = orderstr.dup
|
77
|
+
@olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
|
74
78
|
end
|
75
79
|
|
76
80
|
def apply_to_dataset(dtcx)
|
77
|
-
@olist.each { |
|
78
|
-
# Warning, we need order_append, simply order(oarg) overwrites
|
79
|
-
# previous one !
|
80
|
-
dtcx = dtcx.order_append(oarg)
|
81
|
-
}
|
81
|
+
@olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
|
82
82
|
dtcx
|
83
83
|
end
|
84
|
+
|
85
|
+
def parse_error?
|
86
|
+
@orderstr.split(',').each do |pord|
|
87
|
+
pord.strip!
|
88
|
+
qualfn, dir = pord.split(/\s/)
|
89
|
+
qualfn.strip!
|
90
|
+
dir.strip! if dir
|
91
|
+
return true unless @jh.start_model.attrib_path_valid? qualfn
|
92
|
+
return true unless [nil, 'asc', 'desc'].include? dir
|
93
|
+
end
|
94
|
+
|
95
|
+
false
|
96
|
+
end
|
84
97
|
end
|
85
98
|
end
|