safrano 0.4.1 → 0.4.2
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/batch.rb +6 -6
- data/lib/odata/collection.rb +134 -74
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +53 -54
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +34 -34
- data/lib/odata/entity.rb +86 -70
- data/lib/odata/error.rb +17 -4
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/parse.rb +4 -12
- data/lib/odata/filter/sequel.rb +11 -13
- data/lib/odata/filter/tree.rb +11 -15
- data/lib/odata/navigation_attribute.rb +36 -40
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/safrano.rb +5 -5
- data/lib/safrano/core.rb +10 -1
- data/lib/safrano/multipart.rb +16 -16
- data/lib/safrano/rack_app.rb +3 -3
- data/lib/safrano/request.rb +6 -6
- data/lib/safrano/response.rb +1 -1
- data/lib/safrano/service.rb +64 -119
- data/lib/safrano/version.rb +1 -1
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +5 -3
@@ -37,25 +37,56 @@ end
|
|
37
37
|
|
38
38
|
# filter base class and subclass in our OData namespace
|
39
39
|
module OData
|
40
|
+
class FilterBase
|
41
|
+
# re-useable empty filtering (idempotent)
|
42
|
+
EmptyFilter = new
|
43
|
+
|
44
|
+
def self.factory(filterstr)
|
45
|
+
filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_to_dataset(dtcx)
|
49
|
+
dtcx
|
50
|
+
end
|
51
|
+
|
52
|
+
# finalize
|
53
|
+
def finalize(jh) end
|
54
|
+
|
55
|
+
def empty?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_error?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
40
63
|
# should handle everything by parsing
|
41
|
-
class FilterByParse
|
42
|
-
|
64
|
+
class FilterByParse < FilterBase
|
65
|
+
attr_reader :filterstr
|
66
|
+
def initialize(filterstr)
|
43
67
|
@filterstr = filterstr.dup
|
44
68
|
@ast = OData::Filter::Parser.new(@filterstr).build
|
45
|
-
@jh = jh
|
46
69
|
end
|
47
70
|
|
48
|
-
|
49
|
-
|
50
|
-
|
71
|
+
# this build's up the Sequel Filter Expression, and as a side effect,
|
72
|
+
# it also finalizes the join helper that we need for the start dataset join
|
73
|
+
# the join-helper is shared by the order-by object and was potentially already
|
74
|
+
# partly built on order-by object creation.
|
75
|
+
def finalize(jh)
|
76
|
+
@filtexpr = @ast.sequel_expr(jh)
|
51
77
|
end
|
52
78
|
|
53
|
-
def
|
54
|
-
@
|
79
|
+
def apply_to_dataset(dtcx)
|
80
|
+
# normally finalize is called before, and thus @filtexpr is set
|
81
|
+
dtcx.where(@filtexpr)
|
55
82
|
end
|
56
83
|
|
57
84
|
def parse_error?
|
58
|
-
@ast.
|
85
|
+
@ast.is_a? StandardError
|
86
|
+
end
|
87
|
+
|
88
|
+
def empty?
|
89
|
+
false
|
59
90
|
end
|
60
91
|
end
|
61
92
|
end
|
@@ -12,18 +12,17 @@ module OData
|
|
12
12
|
# similar to Rack::Static
|
13
13
|
# with a flat directory structure
|
14
14
|
class Static < Handler
|
15
|
-
|
16
15
|
def initialize(root: nil)
|
17
16
|
@root = File.absolute_path(root || Dir.pwd)
|
18
17
|
@file_server = ::Rack::File.new(@root)
|
19
18
|
end
|
20
19
|
|
21
|
-
# TODO testcase and better abs_klass_dir design
|
20
|
+
# TODO: testcase and better abs_klass_dir design
|
22
21
|
def register(klass)
|
23
22
|
abs_klass_dir = File.absolute_path(klass.type_name, @root)
|
24
|
-
FileUtils.makedirs abs_klass_dir unless Dir.
|
23
|
+
FileUtils.makedirs abs_klass_dir unless Dir.exist?(abs_klass_dir)
|
25
24
|
end
|
26
|
-
|
25
|
+
|
27
26
|
# minimal working implementation...
|
28
27
|
# Note: @file_server works relative to @root directory
|
29
28
|
def odata_get(request:, entity:)
|
@@ -37,7 +36,7 @@ module OData
|
|
37
36
|
fsret
|
38
37
|
end
|
39
38
|
|
40
|
-
# TODO perf
|
39
|
+
# TODO: [perf] this can be precalculated and cached on MediaModelKlass level
|
41
40
|
# and passed as argument to save_file
|
42
41
|
# eg. /@root/Photo
|
43
42
|
def abs_klass_dir(entity)
|
@@ -49,7 +48,7 @@ module OData
|
|
49
48
|
def media_path(entity)
|
50
49
|
File.join(entity.klass_dir, media_directory(entity))
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
# relative to @root
|
54
53
|
# eg Photo/1/pommes-topaz.jpg
|
55
54
|
def filename(entity)
|
@@ -60,7 +59,7 @@ module OData
|
|
60
59
|
File.join(media_path(entity), Dir.glob('*').first)
|
61
60
|
end
|
62
61
|
end
|
63
|
-
|
62
|
+
|
64
63
|
# /@root/Photo/1
|
65
64
|
def abs_path(entity)
|
66
65
|
File.absolute_path(media_path(entity), @root)
|
@@ -75,18 +74,18 @@ module OData
|
|
75
74
|
|
76
75
|
def in_media_directory(entity)
|
77
76
|
mpi = media_directory(entity)
|
78
|
-
Dir.mkdir mpi unless Dir.
|
77
|
+
Dir.mkdir mpi unless Dir.exist?(mpi)
|
79
78
|
Dir.chdir mpi do
|
80
79
|
yield
|
81
80
|
end
|
82
81
|
end
|
83
82
|
|
84
|
-
def odata_delete(
|
83
|
+
def odata_delete(entity:)
|
85
84
|
Dir.chdir(abs_klass_dir(entity)) do
|
86
85
|
in_media_directory(entity) do
|
87
86
|
Dir.glob('*').each { |oldf| File.delete(oldf) }
|
88
87
|
end
|
89
|
-
end
|
88
|
+
end
|
90
89
|
end
|
91
90
|
|
92
91
|
# Here as well, MVP implementation
|
@@ -105,17 +104,20 @@ module OData
|
|
105
104
|
def ressource_version(entity)
|
106
105
|
Dir.chdir(abs_klass_dir(entity)) do
|
107
106
|
in_media_directory(entity) do
|
108
|
-
Dir.glob('*').last
|
107
|
+
Dir.glob('*').last
|
109
108
|
end
|
110
|
-
end
|
109
|
+
end
|
111
110
|
end
|
112
|
-
|
111
|
+
|
113
112
|
# Here as well, MVP implementation
|
114
113
|
def replace_file(data:, filename:, entity:)
|
115
114
|
Dir.chdir(abs_klass_dir(entity)) do
|
116
115
|
in_media_directory(entity) do
|
117
116
|
version = nil
|
118
|
-
Dir.glob('*').each
|
117
|
+
Dir.glob('*').each do |oldf|
|
118
|
+
version = oldf
|
119
|
+
File.delete(oldf)
|
120
|
+
end
|
119
121
|
filename = (version.to_i + 1).to_s
|
120
122
|
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
121
123
|
end
|
@@ -125,15 +127,15 @@ module OData
|
|
125
127
|
# Simple static File/Directory based media store handler
|
126
128
|
# similar to Rack::Static
|
127
129
|
# with directory Tree structure
|
128
|
-
|
130
|
+
|
129
131
|
class StaticTree < Static
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
def
|
134
|
-
ids.map{|id| id.to_s.chars.join('/')}.join(SEP)
|
132
|
+
SEP = '/00/'.freeze
|
133
|
+
VERS = '/v'.freeze
|
134
|
+
|
135
|
+
def self.path_builder(ids)
|
136
|
+
ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
|
135
137
|
end
|
136
|
-
|
138
|
+
|
137
139
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
138
140
|
# tree-structure
|
139
141
|
# media_path_ids = 1 --> 1
|
@@ -144,55 +146,54 @@ module OData
|
|
144
146
|
# media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
|
145
147
|
def media_directory(entity)
|
146
148
|
StaticTree.path_builder(entity.media_path_ids)
|
147
|
-
# entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
|
149
|
+
# entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
|
148
150
|
end
|
149
151
|
|
150
152
|
def in_media_directory(entity)
|
151
153
|
mpi = media_directory(entity)
|
152
|
-
FileUtils.makedirs mpi unless Dir.
|
153
|
-
Dir.chdir
|
154
|
-
yield
|
155
|
-
end
|
154
|
+
FileUtils.makedirs mpi unless Dir.exist?(mpi)
|
155
|
+
Dir.chdir(mpi) { yield }
|
156
156
|
end
|
157
157
|
|
158
|
-
def odata_delete(
|
158
|
+
def odata_delete(entity:)
|
159
159
|
Dir.chdir(abs_klass_dir(entity)) do
|
160
160
|
in_media_directory(entity) do
|
161
161
|
Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
162
162
|
end
|
163
|
-
end
|
163
|
+
end
|
164
164
|
end
|
165
|
-
|
165
|
+
|
166
|
+
# Here as well, MVP implementation
|
167
|
+
# def replace_file(data:, filename:, entity:)
|
168
|
+
# Dir.chdir(abs_klass_dir(entity)) do
|
169
|
+
# in_media_directory(entity) do
|
170
|
+
# Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
171
|
+
# File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
# end
|
166
175
|
# Here as well, MVP implementation
|
167
|
-
# def replace_file(data:, filename:, entity:)
|
168
|
-
# Dir.chdir(abs_klass_dir(entity)) do
|
169
|
-
# in_media_directory(entity) do
|
170
|
-
# Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
171
|
-
# File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
172
|
-
# end
|
173
|
-
# end
|
174
|
-
# end
|
175
|
-
# Here as well, MVP implementation
|
176
176
|
def replace_file(data:, filename:, entity:)
|
177
177
|
Dir.chdir(abs_klass_dir(entity)) do
|
178
178
|
in_media_directory(entity) do
|
179
179
|
version = nil
|
180
|
-
Dir.glob('*').each
|
180
|
+
Dir.glob('*').each do |oldf|
|
181
|
+
version = oldf
|
182
|
+
File.delete(oldf)
|
183
|
+
end
|
181
184
|
filename = (version.to_i + 1).to_s
|
182
185
|
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
183
186
|
end
|
184
187
|
end
|
185
188
|
end
|
186
|
-
|
187
189
|
end
|
188
|
-
|
189
190
|
end
|
190
191
|
|
191
192
|
# special handling for media entity
|
192
193
|
module EntityClassMedia
|
193
194
|
attr_reader :media_handler
|
194
195
|
attr_reader :slug_field
|
195
|
-
|
196
|
+
|
196
197
|
# API method for defining the media handler
|
197
198
|
# eg.
|
198
199
|
# publish_media_model photos do
|
@@ -209,23 +210,21 @@ module OData
|
|
209
210
|
|
210
211
|
# API method for setting the model field mapped to SLUG on upload
|
211
212
|
def slug(inp)
|
212
|
-
@slug_field = inp
|
213
|
+
@slug_field = inp
|
213
214
|
end
|
214
|
-
|
215
|
+
|
215
216
|
def api_check_media_fields
|
216
|
-
unless
|
217
|
-
|
218
|
-
end
|
217
|
+
raise(OData::API::MediaModelError, self) unless db_schema.key?(:content_type)
|
218
|
+
|
219
219
|
# unless self.db_schema.has_key?(:media_src)
|
220
220
|
# raise OData::API::MediaModelError, self
|
221
221
|
# end
|
222
222
|
end
|
223
223
|
|
224
|
-
# END API methods
|
224
|
+
# END API methods
|
225
225
|
|
226
226
|
def new_media_entity(mimetype:)
|
227
|
-
nh = {}
|
228
|
-
nh['content_type'] = mimetype
|
227
|
+
nh = { 'content_type' => mimetype }
|
229
228
|
new_from_hson_h(nh)
|
230
229
|
end
|
231
230
|
|
@@ -252,11 +251,11 @@ module OData
|
|
252
251
|
|
253
252
|
if slug_field
|
254
253
|
|
255
|
-
new_entity.set_fields({ slug_field =>
|
256
|
-
|
257
|
-
|
254
|
+
new_entity.set_fields({ slug_field => filename },
|
255
|
+
data_fields,
|
256
|
+
missing: :skip)
|
258
257
|
end
|
259
|
-
|
258
|
+
|
260
259
|
# to_one rels are create with FK data set on the parent entity
|
261
260
|
if parent
|
262
261
|
odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
@@ -1,18 +1,32 @@
|
|
1
1
|
require 'odata/error.rb'
|
2
2
|
|
3
|
-
# Ordering with ruby expression
|
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
|
11
|
-
|
12
3
|
# all ordering related classes in our OData module
|
13
4
|
module OData
|
14
5
|
# base class for ordering
|
15
|
-
class
|
6
|
+
class OrderBase
|
7
|
+
# re-useable empty ordering (idempotent)
|
8
|
+
EmptyOrder = new
|
9
|
+
|
10
|
+
# input : the OData order string
|
11
|
+
# returns a Order object that should have a apply_to(cx) method
|
12
|
+
def self.factory(orderstr, jh)
|
13
|
+
orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
|
14
|
+
end
|
15
|
+
|
16
|
+
def empty?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_error?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def apply_to_dataset(dtcx)
|
25
|
+
dtcx
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Order < OrderBase
|
16
30
|
attr_reader :oarg
|
17
31
|
def initialize(ostr, jh)
|
18
32
|
ostr.strip!
|
@@ -21,23 +35,14 @@ module OData
|
|
21
35
|
build_oarg if @orderp
|
22
36
|
end
|
23
37
|
|
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)
|
38
|
+
def empty?
|
39
|
+
false
|
37
40
|
end
|
38
41
|
|
39
42
|
def apply_to_dataset(dtcx)
|
40
|
-
|
43
|
+
# Warning, we need order_append, simply order(oarg) overwrites
|
44
|
+
# previous one !
|
45
|
+
dtcx.order_append(@oarg)
|
41
46
|
end
|
42
47
|
|
43
48
|
def build_oarg
|
@@ -60,26 +65,31 @@ module OData
|
|
60
65
|
end
|
61
66
|
|
62
67
|
# complex ordering logic
|
63
|
-
class
|
68
|
+
class MultiOrder < Order
|
64
69
|
def initialize(orderstr, jh)
|
65
70
|
super
|
66
71
|
@olist = []
|
67
72
|
@jh = jh
|
68
|
-
|
69
|
-
|
70
|
-
@olist = orderstr.split(',').map do |ostr|
|
71
|
-
oo = Order.new(ostr, @jh)
|
72
|
-
oo.oarg
|
73
|
-
end
|
73
|
+
@orderstr = orderstr.dup
|
74
|
+
@olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
|
74
75
|
end
|
75
76
|
|
76
77
|
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
|
-
}
|
78
|
+
@olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
|
82
79
|
dtcx
|
83
80
|
end
|
81
|
+
|
82
|
+
def parse_error?
|
83
|
+
@orderstr.split(',').each do |pord|
|
84
|
+
pord.strip!
|
85
|
+
qualfn, dir = pord.split(/\s/)
|
86
|
+
qualfn.strip!
|
87
|
+
dir.strip! if dir
|
88
|
+
return true unless @jh.start_model.attrib_path_valid? qualfn
|
89
|
+
return true unless [nil, 'asc', 'desc'].include? dir
|
90
|
+
end
|
91
|
+
|
92
|
+
false
|
93
|
+
end
|
84
94
|
end
|
85
95
|
end
|
data/lib/odata/common_logger.rb
CHANGED
@@ -5,41 +5,41 @@ module Rack
|
|
5
5
|
super
|
6
6
|
end
|
7
7
|
|
8
|
-
# Handle https://github.com/rack/rack/pull/1526
|
8
|
+
# Handle https://github.com/rack/rack/pull/1526
|
9
9
|
# new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
|
10
|
-
|
11
|
-
MSG_FUNC = if
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
10
|
+
|
11
|
+
MSG_FUNC = if FORMAT.count('%') == 10
|
12
|
+
lambda { |env, length, status, began_at|
|
13
|
+
FORMAT % [
|
14
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
15
|
+
env['REMOTE_USER'] || '-',
|
16
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
17
|
+
env[REQUEST_METHOD],
|
18
|
+
env[SCRIPT_NAME] + env[PATH_INFO],
|
19
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
20
|
+
env[SERVER_PROTOCOL],
|
21
|
+
status.to_s[0..3],
|
22
|
+
length,
|
23
|
+
Utils.clock_time - began_at
|
24
|
+
]
|
25
|
+
}
|
26
|
+
elsif FORMAT.count('%') == 11
|
27
|
+
lambda { |env, length, status, began_at|
|
28
|
+
FORMAT % [
|
29
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
30
|
+
env['REMOTE_USER'] || '-',
|
31
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
32
|
+
env[REQUEST_METHOD],
|
33
|
+
env[SCRIPT_NAME],
|
34
|
+
env[PATH_INFO],
|
35
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
36
|
+
env[SERVER_PROTOCOL],
|
37
|
+
status.to_s[0..3],
|
38
|
+
length,
|
39
|
+
Utils.clock_time - began_at
|
40
|
+
]
|
41
|
+
}
|
42
|
+
end
|
43
43
|
|
44
44
|
def batch_log(env, status, header, began_at)
|
45
45
|
length = extract_content_length(header)
|