safrano 0.6.8 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/core_ext/DateTime/format.rb +1 -1
- data/lib/core_ext/Time/format.rb +2 -2
- data/lib/odata/collection.rb +2 -3
- data/lib/odata/collection_media.rb +6 -7
- data/lib/odata/common_logger.rb +19 -2
- data/lib/odata/complex_type.rb +5 -9
- data/lib/odata/edm/primitive_types.rb +53 -64
- data/lib/odata/error.rb +3 -3
- data/lib/odata/expand.rb +0 -3
- data/lib/odata/function_import.rb +5 -3
- data/lib/odata/model_ext.rb +38 -31
- data/lib/odata/walker.rb +8 -15
- data/lib/safrano/multipart.rb +26 -17
- data/lib/safrano/request.rb +1 -8
- data/lib/safrano/service.rb +3 -3
- data/lib/safrano/version.rb +1 -1
- data/lib/sequel/plugins/join_by_paths.rb +48 -7
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56284ecd8544233a820d8163c324a4b86df3f87909e529ac993407dad3358132
|
4
|
+
data.tar.gz: 3097012a5e22ccd3cc2b61e8ecc22427c35e8db12a02dfd3c9a819cfc13abd53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23fffb3c40b6264ca8489ded795330c14410419db07fbad0f1fb9781a3ae31668264747df93df8e2d8df45dcdc546d4a6c61cf8d66cee54b7abb7b32e85bef2e
|
7
|
+
data.tar.gz: a4473504fe30c922e3f6803dc608beb5495174ca3f884f4da04de479769a602fdc94df5f45c9a8edad2996146028d1cddacd626d61310b776ced641d32abbbd2
|
@@ -49,7 +49,7 @@ module Safrano
|
|
49
49
|
else
|
50
50
|
# same as above with GMT offset in minutes
|
51
51
|
# DateTime offset is Rational ; fraction of hours per Day --> *24*60
|
52
|
-
min_off_s = (min_off = (offset * 60 * 24).to_i)
|
52
|
+
min_off_s = (min_off = (offset * 60 * 24).to_i).positive? ? "+#{min_off}" : min_off.to_s
|
53
53
|
"/Date(#{strftime('%Q')}#{min_off_s})/"
|
54
54
|
end
|
55
55
|
end
|
data/lib/core_ext/Time/format.rb
CHANGED
@@ -57,7 +57,7 @@ module Safrano
|
|
57
57
|
# the json raw data is like this : "HireDate": "\/Date(704678400000)\/"
|
58
58
|
# --> \/\/
|
59
59
|
def to_edm_json
|
60
|
-
if utc? ||
|
60
|
+
if utc? || gmt_offset.zero?
|
61
61
|
# no offset
|
62
62
|
# %s : seconds since unix epoch
|
63
63
|
# %L : milliseconds 000-999
|
@@ -66,7 +66,7 @@ module Safrano
|
|
66
66
|
else
|
67
67
|
# same as above with GMT offset in minutes
|
68
68
|
|
69
|
-
min_off_s = (min_off = gmt_offset / 60)
|
69
|
+
min_off_s = (min_off = gmt_offset / 60).positive? ? "+#{min_off}" : min_off.to_s
|
70
70
|
"/Date(#{strftime('%s%L')}#{min_off_s})/"
|
71
71
|
end
|
72
72
|
end
|
data/lib/odata/collection.rb
CHANGED
@@ -10,9 +10,7 @@ module Safrano
|
|
10
10
|
module Media
|
11
11
|
# base class for Media Handler
|
12
12
|
class Handler
|
13
|
-
def check_before_create(data:,
|
14
|
-
entity:,
|
15
|
-
filename:)
|
13
|
+
def check_before_create(data:, entity:, filename:)
|
16
14
|
Contract::OK
|
17
15
|
end
|
18
16
|
end
|
@@ -21,8 +19,9 @@ module Safrano
|
|
21
19
|
# with a flat directory structure
|
22
20
|
class Static < Handler
|
23
21
|
def initialize(root: nil, mediaklass:)
|
22
|
+
super()
|
24
23
|
@root = Pathname(File.absolute_path(root || Dir.pwd))
|
25
|
-
@files_class =
|
24
|
+
@files_class = ::Rack.release[0..2] == '2.0' ? ::Rack::File : ::Rack::Files
|
26
25
|
@media_class = mediaklass
|
27
26
|
@media_dir_name = Pathname(mediaklass.to_s)
|
28
27
|
@semaphore = Thread::Mutex.new
|
@@ -32,7 +31,7 @@ module Safrano
|
|
32
31
|
|
33
32
|
def register
|
34
33
|
@abs_klass_dir = @root + @media_dir_name
|
35
|
-
@abs_temp_dir = @abs_klass_dir
|
34
|
+
@abs_temp_dir = @abs_klass_dir.join('tmp')
|
36
35
|
end
|
37
36
|
|
38
37
|
def create_abs_class_dir
|
@@ -129,7 +128,7 @@ module Safrano
|
|
129
128
|
# Here as well, MVP implementation
|
130
129
|
def save_file(data:, filename:, entity:)
|
131
130
|
with_media_directory(entity) do |d|
|
132
|
-
filename = d
|
131
|
+
filename = d.join('1')
|
133
132
|
atomic_write(filename) { |f| IO.copy_stream(data, f) }
|
134
133
|
end
|
135
134
|
end
|
@@ -138,7 +137,7 @@ module Safrano
|
|
138
137
|
# after each upload, so that clients get informed about new versions
|
139
138
|
# of the same media ressource
|
140
139
|
def ressource_version(entity)
|
141
|
-
abs_media_directory(entity).children(
|
140
|
+
abs_media_directory(entity).children(false).max.to_s
|
142
141
|
end
|
143
142
|
|
144
143
|
# Note: add a new Version and remove the previous one
|
data/lib/odata/common_logger.rb
CHANGED
@@ -13,11 +13,28 @@ module Rack
|
|
13
13
|
MSG_FUNC = case FORMAT.count('%')
|
14
14
|
when 10
|
15
15
|
lambda { |env, length, status, began_at|
|
16
|
-
format(FORMAT,
|
16
|
+
format(FORMAT,
|
17
|
+
env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
|
18
|
+
env['REMOTE_USER'] || '-',
|
19
|
+
Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
|
20
|
+
env[REQUEST_METHOD],
|
21
|
+
env[SCRIPT_NAME] + env[PATH_INFO],
|
22
|
+
env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
|
23
|
+
env[SERVER_PROTOCOL],
|
24
|
+
status.to_s[0..3], length, Utils.clock_time - began_at)
|
17
25
|
}
|
18
26
|
when 11
|
19
27
|
lambda { |env, length, status, began_at|
|
20
|
-
format(FORMAT,
|
28
|
+
format(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], length, Utils.clock_time - began_at)
|
21
38
|
}
|
22
39
|
end
|
23
40
|
|
data/lib/odata/complex_type.rb
CHANGED
@@ -92,7 +92,7 @@ module Safrano
|
|
92
92
|
# wrapper
|
93
93
|
# for OData Entity and Collections, return them directly
|
94
94
|
# for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
|
95
|
-
def self.do_execute_func_result(result, _req,
|
95
|
+
def self.do_execute_func_result(result, _req, apply_query_params: false)
|
96
96
|
new(result)
|
97
97
|
end
|
98
98
|
end
|
@@ -129,7 +129,7 @@ module Safrano
|
|
129
129
|
|
130
130
|
# wrapper
|
131
131
|
# for OData Entity return them directly
|
132
|
-
def self.do_execute_func_result(result, _req, apply_query_params
|
132
|
+
def self.do_execute_func_result(result, _req, apply_query_params: false)
|
133
133
|
# note: Sequel entities instances seem to be thread safe, so we can
|
134
134
|
# safely add request-dependant data (eg. req.params) there
|
135
135
|
apply_query_params ? result : result.inactive_query_params
|
@@ -143,7 +143,7 @@ module Safrano
|
|
143
143
|
|
144
144
|
# wrapper
|
145
145
|
# for OData Entity Collection return them directly
|
146
|
-
def self.do_execute_func_result(result, req, apply_query_params
|
146
|
+
def self.do_execute_func_result(result, req, apply_query_params: false)
|
147
147
|
coll = Safrano::OData::Collection.new(@klassmod)
|
148
148
|
# instance_exec has other instance variables; @values would be nil in the block below
|
149
149
|
# need to pass a local copy
|
@@ -241,12 +241,8 @@ module Safrano
|
|
241
241
|
def odata_h
|
242
242
|
ret = { METAK => { TYPEK => self.class.type_name } }
|
243
243
|
|
244
|
-
@values.each do |
|
245
|
-
ret[
|
246
|
-
v.odata_h
|
247
|
-
else
|
248
|
-
v
|
249
|
-
end
|
244
|
+
@values.each do |key, val|
|
245
|
+
ret[key] = val.respond_to?(:odata_h) ? val.odata_h : val
|
250
246
|
end
|
251
247
|
ret
|
252
248
|
end
|
@@ -38,6 +38,28 @@ module Safrano
|
|
38
38
|
|
39
39
|
# type mappings are hard, especially between "Standards" like SQL and OData V2 (might be a bit better in V4 ?)
|
40
40
|
# this is all best effort/try to make it work logic
|
41
|
+
|
42
|
+
RUBY_TY_EDM_TY_MAP = { integer: 'Edm.Int32',
|
43
|
+
string: 'Edm.String',
|
44
|
+
date: 'Edm.DateTime',
|
45
|
+
datetime: 'Edm.DateTime',
|
46
|
+
time: 'Edm.Time',
|
47
|
+
boolean: 'Edm.Boolean',
|
48
|
+
float: 'Edm.Double',
|
49
|
+
decimal: 'Edm.Decimal',
|
50
|
+
blob: 'Edm.Binary' }.freeze
|
51
|
+
DB_TY_EDM_TY_MAP = { 'smallint' => 'Edm.Int16',
|
52
|
+
'int2' => 'Edm.Int16',
|
53
|
+
'smallserial' => 'Edm.Int16',
|
54
|
+
'int' => 'Edm.Int32',
|
55
|
+
'integer' => 'Edm.Int32',
|
56
|
+
'serial' => 'Edm.Int32',
|
57
|
+
'mediumint' => 'Edm.Int32',
|
58
|
+
'int4' => 'Edm.Int32',
|
59
|
+
'bigint' => 'Edm.Int64',
|
60
|
+
'bigserial' => 'Edm.Int64',
|
61
|
+
'int8' => 'Edm.Int64',
|
62
|
+
'tinyint' => 'Edm.Byte' }.freeze
|
41
63
|
def self.add_edm_types(metadata, props)
|
42
64
|
# try num/dec with db_type:
|
43
65
|
metadata[:edm_type] = if (md = DB_TYPE_NUMDEC_RGX.match(props[:db_type]))
|
@@ -80,51 +102,24 @@ module Safrano
|
|
80
102
|
|
81
103
|
# try int-like with db_type:
|
82
104
|
# smallint|int|integer|bigint|serial|bigserial
|
83
|
-
metadata[:edm_type] = if (md = DB_TYPE_INTLIKE_RGX.match(props[:db_type]))
|
84
105
|
|
106
|
+
metadata[:edm_type] = if (md = DB_TYPE_INTLIKE_RGX.match(props[:db_type]))
|
85
107
|
if (itype = md[1])
|
86
|
-
|
87
|
-
when 'smallint', 'int2', 'smallserial'
|
88
|
-
'Edm.Int16'
|
89
|
-
when 'int', 'integer', 'serial', 'mediumint', 'int4'
|
90
|
-
'Edm.Int32'
|
91
|
-
when 'bigint', 'bigserial', 'int8'
|
92
|
-
'Edm.Int64'
|
93
|
-
when 'tinyint'
|
94
|
-
'Edm.Byte'
|
95
|
-
end
|
108
|
+
DB_TY_EDM_TY_MAP[itype.downcase]
|
96
109
|
end
|
97
110
|
end
|
98
111
|
return if metadata[:edm_type]
|
99
112
|
|
100
113
|
# try Guid with db_type:
|
101
114
|
|
102
|
-
metadata[:edm_type] = if
|
115
|
+
metadata[:edm_type] = if DB_TYPE_GUID_RGX.match(props[:db_type])
|
103
116
|
'Edm.Guid'
|
104
117
|
end
|
118
|
+
|
105
119
|
return if metadata[:edm_type]
|
106
120
|
|
107
121
|
# try with Sequel(ruby) type
|
108
|
-
metadata[:edm_type] =
|
109
|
-
when :integer
|
110
|
-
'Edm.Int32'
|
111
|
-
when :string
|
112
|
-
'Edm.String'
|
113
|
-
when :date
|
114
|
-
'Edm.DateTime'
|
115
|
-
when :datetime
|
116
|
-
'Edm.DateTime'
|
117
|
-
when :time
|
118
|
-
'Edm.Time'
|
119
|
-
when :boolean
|
120
|
-
'Edm.Boolean'
|
121
|
-
when :float
|
122
|
-
'Edm.Double'
|
123
|
-
when :decimal
|
124
|
-
'Edm.Decimal'
|
125
|
-
when :blob
|
126
|
-
'Edm.Binary'
|
127
|
-
end
|
122
|
+
metadata[:edm_type] = RUBY_TY_EDM_TY_MAP[props[:type]]
|
128
123
|
end
|
129
124
|
|
130
125
|
# use Edm twice so that we can do include Safrano::Edm and then
|
@@ -153,8 +148,8 @@ module Safrano
|
|
153
148
|
nil
|
154
149
|
end
|
155
150
|
|
156
|
-
def self.convert_from_urlparam(
|
157
|
-
return Contract::NOK unless
|
151
|
+
def self.convert_from_urlparam(val)
|
152
|
+
return Contract::NOK unless val == 'null'
|
158
153
|
|
159
154
|
Contract.valid(nil)
|
160
155
|
end
|
@@ -164,9 +159,9 @@ module Safrano
|
|
164
159
|
class Binary < String
|
165
160
|
extend OutputClassMethods
|
166
161
|
|
167
|
-
def self.convert_from_urlparam(
|
168
|
-
# TODO this should use base64
|
169
|
-
Contract.valid(
|
162
|
+
def self.convert_from_urlparam(val)
|
163
|
+
# TODO: this should use base64
|
164
|
+
Contract.valid(val.dup.force_encoding('BINARY'))
|
170
165
|
end
|
171
166
|
end
|
172
167
|
|
@@ -180,13 +175,13 @@ module Safrano
|
|
180
175
|
end
|
181
176
|
|
182
177
|
def self.odata_collection(array)
|
183
|
-
array.map { |
|
178
|
+
array.map { |val| odata_value(val) }
|
184
179
|
end
|
185
180
|
|
186
|
-
def self.convert_from_urlparam(
|
187
|
-
return Contract::NOK unless %w[true false].include?(
|
181
|
+
def self.convert_from_urlparam(val)
|
182
|
+
return Contract::NOK unless %w[true false].include?(val)
|
188
183
|
|
189
|
-
Contract.valid(
|
184
|
+
Contract.valid(val == 'true')
|
190
185
|
end
|
191
186
|
end
|
192
187
|
|
@@ -195,8 +190,8 @@ module Safrano
|
|
195
190
|
class Byte < Integer
|
196
191
|
extend OutputClassMethods
|
197
192
|
|
198
|
-
def self.convert_from_urlparam(
|
199
|
-
return Contract::NOK unless (bytev =
|
193
|
+
def self.convert_from_urlparam(val)
|
194
|
+
return Contract::NOK unless (bytev = val.to_i) < 256
|
200
195
|
|
201
196
|
Contract.valid(bytev)
|
202
197
|
end
|
@@ -209,29 +204,29 @@ module Safrano
|
|
209
204
|
end
|
210
205
|
|
211
206
|
def self.odata_collection(array)
|
212
|
-
array.map { |
|
207
|
+
array.map { |val| odata_value(val) }
|
213
208
|
end
|
214
209
|
|
215
|
-
def self.convert_from_urlparam(
|
216
|
-
Contract.valid(DateTime.parse(
|
210
|
+
def self.convert_from_urlparam(val)
|
211
|
+
Contract.valid(DateTime.parse(val))
|
217
212
|
rescue StandardError
|
218
|
-
convertion_error(
|
213
|
+
convertion_error(val)
|
219
214
|
end
|
220
215
|
end
|
221
216
|
|
222
217
|
class String < ::String
|
223
218
|
extend OutputClassMethods
|
224
219
|
|
225
|
-
def self.convert_from_urlparam(
|
226
|
-
Contract.valid(
|
220
|
+
def self.convert_from_urlparam(val)
|
221
|
+
Contract.valid(val)
|
227
222
|
end
|
228
223
|
end
|
229
224
|
|
230
225
|
class Int32 < Integer
|
231
226
|
extend OutputClassMethods
|
232
227
|
|
233
|
-
def self.convert_from_urlparam(
|
234
|
-
return Contract::NOK unless (ret = number_or_nil(
|
228
|
+
def self.convert_from_urlparam(val)
|
229
|
+
return Contract::NOK unless (ret = number_or_nil(val))
|
235
230
|
|
236
231
|
Contract.valid(ret)
|
237
232
|
end
|
@@ -240,8 +235,8 @@ module Safrano
|
|
240
235
|
class Int64 < Integer
|
241
236
|
extend OutputClassMethods
|
242
237
|
|
243
|
-
def self.convert_from_urlparam(
|
244
|
-
return Contract::NOK unless (ret = number_or_nil(
|
238
|
+
def self.convert_from_urlparam(val)
|
239
|
+
return Contract::NOK unless (ret = number_or_nil(val))
|
245
240
|
|
246
241
|
Contract.valid(ret)
|
247
242
|
end
|
@@ -250,8 +245,8 @@ module Safrano
|
|
250
245
|
class Double < Float
|
251
246
|
extend OutputClassMethods
|
252
247
|
|
253
|
-
def self.convert_from_urlparam(
|
254
|
-
Contract.valid(
|
248
|
+
def self.convert_from_urlparam(val)
|
249
|
+
Contract.valid(val.to_f)
|
255
250
|
rescue StandardError
|
256
251
|
Contract::NOK
|
257
252
|
end
|
@@ -260,10 +255,10 @@ module Safrano
|
|
260
255
|
class Guid < UUIDTools::UUID
|
261
256
|
extend OutputClassMethods
|
262
257
|
|
263
|
-
def self.convert_from_urlparam(
|
264
|
-
Contract::NOK unless m = Filter::Parser::Token::GUIDRGX.match(
|
258
|
+
def self.convert_from_urlparam(val)
|
259
|
+
Contract::NOK unless (m = Filter::Parser::Token::GUIDRGX.match(val))
|
265
260
|
|
266
|
-
Contract.valid(UUIDTools::UUID.parse
|
261
|
+
Contract.valid(UUIDTools::UUID.parse(m[1]))
|
267
262
|
rescue StandardError
|
268
263
|
Contract::NOK
|
269
264
|
end
|
@@ -271,9 +266,3 @@ module Safrano
|
|
271
266
|
end
|
272
267
|
end
|
273
268
|
end
|
274
|
-
|
275
|
-
# include Safrano
|
276
|
-
|
277
|
-
# x = Edm::String.new('xxx')
|
278
|
-
|
279
|
-
# pp x
|
data/lib/odata/error.rb
CHANGED
@@ -50,9 +50,9 @@ module Safrano
|
|
50
50
|
|
51
51
|
# used in function import error handling. cf. func import / do_execute_func
|
52
52
|
def self.with_error(result)
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
return unless result.respond_to?(:error) && (err = result.error)
|
54
|
+
|
55
|
+
yield err
|
56
56
|
end
|
57
57
|
|
58
58
|
# base module for HTTP errors, when used as a Error Class
|
data/lib/odata/expand.rb
CHANGED
@@ -196,7 +196,7 @@ module Safrano
|
|
196
196
|
unless (@error = check_url_func_params)
|
197
197
|
begin
|
198
198
|
return yield
|
199
|
-
rescue LocalJumpError
|
199
|
+
rescue LocalJumpError
|
200
200
|
@error = Safrano::ServiceOperationReturnError.new
|
201
201
|
end
|
202
202
|
end
|
@@ -212,13 +212,15 @@ module Safrano
|
|
212
212
|
|
213
213
|
# Note: Sequel::Error exceptions are already
|
214
214
|
# handled on rack app level (cf. the call methode )
|
215
|
-
Safrano
|
215
|
+
Safrano.with_error(result) do |error|
|
216
216
|
@error = error
|
217
217
|
return [nil, :error, @error] # this is return from do_execute_func !
|
218
218
|
end
|
219
219
|
|
220
220
|
# non error case
|
221
|
-
[@returning.do_execute_func_result(result, req,
|
221
|
+
[@returning.do_execute_func_result(result, req,
|
222
|
+
apply_query_params: @auto_query_params),
|
223
|
+
:run]
|
222
224
|
end
|
223
225
|
end
|
224
226
|
|
data/lib/odata/model_ext.rb
CHANGED
@@ -143,39 +143,38 @@ module Safrano
|
|
143
143
|
end
|
144
144
|
|
145
145
|
MAX_DEPTH = 6
|
146
|
-
|
146
|
+
MAX_TYPE_REPETITION = 2
|
147
|
+
def attribute_path_list(depth_path = [])
|
147
148
|
ret = @columns_str.dup
|
148
149
|
# break circles
|
149
|
-
return ret if
|
150
|
-
|
151
|
-
depth += 1
|
150
|
+
return ret if depth_path.size > MAX_DEPTH
|
151
|
+
return ret if depth_path.count(self) > MAX_TYPE_REPETITION
|
152
152
|
|
153
153
|
@nav_entity_attribs&.each do |a, k|
|
154
|
-
ret.concat(k.attribute_path_list(
|
154
|
+
ret.concat(k.attribute_path_list(depth_path + [self]).map { |kc| "#{a}/#{kc}" })
|
155
155
|
end
|
156
156
|
|
157
157
|
@nav_collection_attribs&.each do |a, k|
|
158
|
-
ret.concat(k.attribute_path_list(
|
158
|
+
ret.concat(k.attribute_path_list(depth_path + [self]).map { |kc| "#{a}/#{kc}" })
|
159
159
|
end
|
160
160
|
ret
|
161
161
|
end
|
162
162
|
|
163
|
-
def expand_path_list(
|
163
|
+
def expand_path_list(depth_path = [])
|
164
164
|
ret = []
|
165
165
|
ret.concat(@nav_entity_attribs_keys) if @nav_entity_attribs
|
166
166
|
ret.concat(@nav_collection_attribs_keys) if @nav_collection_attribs
|
167
167
|
|
168
168
|
# break circles
|
169
|
-
return ret if
|
170
|
-
|
171
|
-
depth += 1
|
169
|
+
return ret if depth_path.size > MAX_DEPTH
|
170
|
+
return ret if depth_path.count(self) > MAX_TYPE_REPETITION
|
172
171
|
|
173
172
|
@nav_entity_attribs&.each do |a, k|
|
174
|
-
ret.concat(k.expand_path_list(
|
173
|
+
ret.concat(k.expand_path_list(depth_path + [self]).map { |kc| "#{a}/#{kc}" })
|
175
174
|
end
|
176
175
|
|
177
176
|
@nav_collection_attribs&.each do |a, k|
|
178
|
-
ret.concat(k.expand_path_list(
|
177
|
+
ret.concat(k.expand_path_list(depth_path + [self]).map { |kc| "#{a}/#{kc}" })
|
179
178
|
end
|
180
179
|
ret
|
181
180
|
end
|
@@ -387,7 +386,7 @@ module Safrano
|
|
387
386
|
end
|
388
387
|
|
389
388
|
# attribute specific type mapping
|
390
|
-
if colmap = @type_mappings[col]
|
389
|
+
if (colmap = @type_mappings[col])
|
391
390
|
metadata[:edm_type] = colmap.edm_type
|
392
391
|
if colmap.castfunc
|
393
392
|
@casted_cols[col] = colmap.castfunc
|
@@ -430,7 +429,7 @@ module Safrano
|
|
430
429
|
if metadata[:edm_type] == 'Edm.Guid'
|
431
430
|
|
432
431
|
if props[:type] == :blob # Edm.Guid but as 16 byte binary Blob on DB level, eg in Sqlite
|
433
|
-
@casted_cols[col] =
|
432
|
+
@casted_cols[col] = lambda { |x|
|
434
433
|
UUIDTools::UUID.parse_raw(x).to_s unless x.nil?
|
435
434
|
} # Base64
|
436
435
|
next
|
@@ -446,8 +445,20 @@ module Safrano
|
|
446
445
|
|
447
446
|
# check if key needs casting. Important for later entity-uri generation !
|
448
447
|
return unless primary_key.is_a? Symbol # single key field
|
449
|
-
|
450
|
-
|
448
|
+
|
449
|
+
# guid key as guid'xxx-yyy-zzz'
|
450
|
+
metadata = @cols_metadata[primary_key]
|
451
|
+
|
452
|
+
if metadata[:edm_type] == 'Edm.Guid'
|
453
|
+
props = db_schema[primary_key]
|
454
|
+
@pk_castfunc = if props[:type] == :blob # Edm.Guid but as 16 byte binary Blob on DB level, eg in Sqlite
|
455
|
+
lambda { |x| "guid'#{UUIDTools::UUID.parse_raw(x)}'" unless x.nil? }
|
456
|
+
else
|
457
|
+
lambda { |x| "guid'#{x}'" unless x.nil? }
|
458
|
+
end
|
459
|
+
else
|
460
|
+
@pk_castfunc = @casted_cols[primary_key]
|
461
|
+
end
|
451
462
|
end # build_casted_cols(service)
|
452
463
|
|
453
464
|
def finalize_publishing(service)
|
@@ -522,7 +533,7 @@ module Safrano
|
|
522
533
|
|
523
534
|
kvpredicate = case db_schema[primary_key][:type]
|
524
535
|
when :integer
|
525
|
-
# TODO
|
536
|
+
# TODO: Harmonize this with primitive_types.rb convert_from_url
|
526
537
|
@pk_cast_from_string = ->(str) { Integer(str) }
|
527
538
|
/(\d+)/.freeze
|
528
539
|
else
|
@@ -530,9 +541,7 @@ module Safrano
|
|
530
541
|
case metadata[:edm_type]
|
531
542
|
when 'Edm.Guid'
|
532
543
|
if db_schema[primary_key][:type] == :blob # Edm.Guid but as 16byte binary Blob on DB
|
533
|
-
@pk_cast_from_string =
|
534
|
-
# TODO harmonize this with primitive_types.rb convert_from_url
|
535
|
-
# Sequel::SQL::Blob.new([ str.gsub('-', '') ].pack('H*')) }
|
544
|
+
@pk_cast_from_string = lambda { |str|
|
536
545
|
Sequel::SQL::Blob.new(UUIDTools::UUID.parse(str).raw)
|
537
546
|
}
|
538
547
|
end
|
@@ -544,7 +553,7 @@ module Safrano
|
|
544
553
|
@iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
|
545
554
|
end
|
546
555
|
# @entity_id_url_regexp = /\A\(\s*#{kvpredicate}\s*\)(.*)/.freeze
|
547
|
-
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
556
|
+
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
548
557
|
end
|
549
558
|
|
550
559
|
def prepare_fields
|
@@ -677,16 +686,14 @@ module Safrano
|
|
677
686
|
mval = md[1]
|
678
687
|
end
|
679
688
|
end
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
return Contract::NOK
|
689
|
-
end
|
689
|
+
return Contract::NOK unless mpk && mval
|
690
|
+
|
691
|
+
mdch[mpk] = if (pk_cast = @pk_cast_from_string[mpk])
|
692
|
+
pk_cast.call(mval)
|
693
|
+
else
|
694
|
+
mval # no cast needed, eg for string
|
695
|
+
end
|
696
|
+
scan_rgx_parts.delete(mpk)
|
690
697
|
end
|
691
698
|
# normally arriving here we have mdch filled with key values pairs,
|
692
699
|
# but not in the model key ordering. lets just re-order the values
|
data/lib/odata/walker.rb
CHANGED
@@ -66,34 +66,27 @@ module Safrano
|
|
66
66
|
path
|
67
67
|
else
|
68
68
|
# path.sub!(/\A#{prefix}/, '')
|
69
|
-
# TODO check
|
69
|
+
# TODO: check
|
70
70
|
path.sub(/\A#{prefix}/, EMPTYSTR)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
74
|
def get_next_transition
|
75
|
-
#
|
75
|
+
# handle multiple valid transitions
|
76
76
|
# like when we have attributes that are substring of each other
|
77
77
|
# --> instead of using detect (ie take first transition)
|
78
78
|
# we need to use select and then find the longest match
|
79
|
-
# tr_next = @context.allowed_transitions.detect do |t|
|
80
|
-
# t.do_match(@path_remain)
|
81
|
-
# end
|
82
79
|
|
83
|
-
valid_tr = @context.allowed_transitions.select do |t|
|
80
|
+
valid_tr = @context.allowed_transitions.map(&:dup).select do |t|
|
84
81
|
t.do_match(@path_remain)
|
85
82
|
end
|
86
83
|
|
87
|
-
#
|
88
|
-
# better one) to make attributes that are substrings of each other
|
84
|
+
# HACK: (wanted: a better one) to make attributes that are substrings of each other
|
89
85
|
# work well
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
valid_tr.max_by { |t| t.match_result[1].size }
|
95
|
-
end
|
96
|
-
end
|
86
|
+
return unless valid_tr
|
87
|
+
return (@tr_next = nil) if valid_tr.empty?
|
88
|
+
|
89
|
+
@tr_next = valid_tr.size == 1 ? valid_tr.first : valid_tr.max_by { |t| t.match_result[1].size }
|
97
90
|
end
|
98
91
|
|
99
92
|
# perform a content-id ($batch changeset ref) transition
|
data/lib/safrano/multipart.rb
CHANGED
@@ -45,9 +45,9 @@ module MIME
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def parse_next_part(line)
|
48
|
-
if @target.parser.next_part(line)
|
48
|
+
return if @target.parser.next_part(line)
|
49
49
|
|
50
|
-
|
50
|
+
if @target.parser.last_part(line)
|
51
51
|
@state = :end
|
52
52
|
else
|
53
53
|
@target.parser.addline(line)
|
@@ -138,18 +138,17 @@ module MIME
|
|
138
138
|
|
139
139
|
def parse_str(inpstr, level: 0)
|
140
140
|
# we need to keep the line separators --> use io.readlines
|
141
|
-
if inpstr.respond_to?(:readlines)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
end
|
141
|
+
@lines = if inpstr.respond_to?(:readlines)
|
142
|
+
inpstr.readlines(@sep)
|
143
|
+
else
|
144
|
+
# rack input wrapper only has gets but not readlines
|
145
|
+
# BUT the rack SPEC says it only supports gets without argument!
|
146
|
+
# --> finally we end up using read and split into lines...
|
147
|
+
# normally should be ok for $batch POST payloads
|
148
|
+
|
149
|
+
# inpstr.read should be a String
|
150
|
+
inpstr.read.lines(@sep)
|
151
|
+
end
|
153
152
|
# tmp hack for test-tools that convert CRLF in payload to LF :-(
|
154
153
|
if @lines.size == 1
|
155
154
|
@sep = LF
|
@@ -371,7 +370,7 @@ module MIME
|
|
371
370
|
|
372
371
|
def get_mult_resp(full_req)
|
373
372
|
get_response(full_req)
|
374
|
-
[202, @response.hd, @response.
|
373
|
+
[202, @response.hd, @response.unparse_bodyonly]
|
375
374
|
end
|
376
375
|
|
377
376
|
def get_response(full_req)
|
@@ -410,10 +409,20 @@ module MIME
|
|
410
409
|
@response
|
411
410
|
end
|
412
411
|
|
413
|
-
def unparse
|
412
|
+
def unparse
|
414
413
|
b = +String.new
|
415
|
-
b << "#{Safrano::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
|
414
|
+
b << "#{Safrano::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}"
|
416
415
|
|
416
|
+
# warning: duplicated code with below
|
417
|
+
b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
|
418
|
+
b << @content.map(&:unparse).join(crbdcr)
|
419
|
+
b << "#{CRLF}--#{@boundary}--#{CRLF}"
|
420
|
+
b
|
421
|
+
end
|
422
|
+
|
423
|
+
def unparse_bodyonly
|
424
|
+
b = +String.new
|
425
|
+
# warning: duplicated code with above
|
417
426
|
b << crbdcr = "#{CRLF}--#{@boundary}#{CRLF}"
|
418
427
|
b << @content.map(&:unparse).join(crbdcr)
|
419
428
|
b << "#{CRLF}--#{@boundary}--#{CRLF}"
|
data/lib/safrano/request.rb
CHANGED
@@ -61,6 +61,7 @@ module Safrano
|
|
61
61
|
'Expires' => '-1',
|
62
62
|
'Pragma' => 'no-cache' }.freeze
|
63
63
|
DATASERVICEVERSION = 'DataServiceVersion'
|
64
|
+
|
64
65
|
# borowed from Sinatra
|
65
66
|
class AcceptEntry
|
66
67
|
attr_accessor :params
|
@@ -79,10 +80,6 @@ module Safrano
|
|
79
80
|
@q = @params.delete('q') { 1.0 }.to_f
|
80
81
|
end
|
81
82
|
|
82
|
-
def method_missing(*args, &block)
|
83
|
-
to_str.send(*args, &block)
|
84
|
-
end
|
85
|
-
|
86
83
|
def <=>(other)
|
87
84
|
other.priority <=> priority
|
88
85
|
end
|
@@ -96,10 +93,6 @@ module Safrano
|
|
96
93
|
super || to_str.respond_to?(*args)
|
97
94
|
end
|
98
95
|
|
99
|
-
def to_s(full = false)
|
100
|
-
full ? entry : to_str
|
101
|
-
end
|
102
|
-
|
103
96
|
def to_str
|
104
97
|
@type
|
105
98
|
end
|
data/lib/safrano/service.rb
CHANGED
@@ -257,7 +257,7 @@ module Safrano
|
|
257
257
|
# * Single/Multi PK
|
258
258
|
# * Media/Non-Media entity
|
259
259
|
# Putting this logic here in modules loaded once on start shall result in less runtime overhead
|
260
|
-
def register_model(modelklass, entity_set_name = nil, is_media
|
260
|
+
def register_model(modelklass, entity_set_name = nil, is_media: false)
|
261
261
|
# check that the provided klass is a Sequel Model
|
262
262
|
|
263
263
|
raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
@@ -314,7 +314,7 @@ module Safrano
|
|
314
314
|
end
|
315
315
|
|
316
316
|
def publish_media_model(modelklass, entity_set_name = nil, &block)
|
317
|
-
register_model(modelklass, entity_set_name, true)
|
317
|
+
register_model(modelklass, entity_set_name, is_media: true)
|
318
318
|
# we need to execute the passed block in a deferred step
|
319
319
|
# after all models have been registered (due to rel. dependancies)
|
320
320
|
# modelklass.instance_eval(&block) if block_given?
|
@@ -450,7 +450,7 @@ module Safrano
|
|
450
450
|
end
|
451
451
|
|
452
452
|
# check function import definition
|
453
|
-
function_imports.each_value
|
453
|
+
function_imports.each_value(&:check_definition)
|
454
454
|
end
|
455
455
|
|
456
456
|
def execute_deferred_iblocks
|
data/lib/safrano/version.rb
CHANGED
@@ -99,6 +99,8 @@ class JoinByPathsHelper < Set
|
|
99
99
|
@result << iset
|
100
100
|
end
|
101
101
|
|
102
|
+
many_to_many_count = 0 # counter for differing multiple
|
103
|
+
# many_to_many through alias
|
102
104
|
@result.map! do |jseg|
|
103
105
|
jseg.map do |seg|
|
104
106
|
leftm = seg.first.model_class
|
@@ -122,29 +124,65 @@ class JoinByPathsHelper < Set
|
|
122
124
|
lks = [leftm.primary_key].flatten
|
123
125
|
rks = [assoc[:key]].flatten
|
124
126
|
|
125
|
-
|
126
|
-
#
|
127
|
+
when :many_to_many, :one_through_one
|
128
|
+
# :many_to_many :: A join table is used that has a foreign key that points
|
127
129
|
# to this model's primary key and a foreign key that points to the
|
128
130
|
# associated model's primary key. Each current model object can be
|
129
131
|
# associated with many associated model objects, and each associated
|
130
132
|
# model object can be associated with many current model objects.
|
133
|
+
# TODO: testcase for :one_through_one
|
131
134
|
# when # :one_through_one :: Similar to many_to_many in terms of foreign keys, but only one object
|
132
135
|
# is associated to the current object through the association.
|
133
136
|
# Provides only getter methods, no setter or modification methods.
|
134
137
|
|
138
|
+
result_ = []
|
139
|
+
|
140
|
+
# in case of multiple many_to_many rels, we need differing through aliases
|
141
|
+
many_to_many_count = many_to_many_count + 1
|
142
|
+
through_alias = "t#{many_to_many_count}".to_sym
|
143
|
+
|
144
|
+
# For many_to_many first we add the join with assigment table
|
145
|
+
lks_ = [assoc[:left_primary_key]].flatten
|
146
|
+
rks_ = [assoc[:left_key]].flatten
|
147
|
+
|
148
|
+
lks_.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
|
149
|
+
rks_.map! { |k| Sequel[through_alias][k] }
|
150
|
+
|
151
|
+
result_ << {
|
152
|
+
type: assoc[:type],
|
153
|
+
left: leftm.table_name,
|
154
|
+
right: assoc[:join_table],
|
155
|
+
alias: through_alias,
|
156
|
+
cond: rks_.zip(lks_).to_h
|
157
|
+
}
|
158
|
+
|
159
|
+
# then we add the join with with target table (right)
|
160
|
+
lks = [assoc[:right_key]].flatten
|
161
|
+
rks = [assoc.right_primary_key].flatten
|
162
|
+
|
163
|
+
lks.map! { |k| Sequel[through_alias][k] }
|
164
|
+
rks.map! { |k| Sequel[seg.last.alias_sym][k] }
|
165
|
+
|
166
|
+
result_ << {
|
167
|
+
type: assoc[:type],
|
168
|
+
left: assoc[:join_table],
|
169
|
+
right: rightm.table_name,
|
170
|
+
alias: seg.last.alias_sym,
|
171
|
+
cond: rks.zip(lks).to_h
|
172
|
+
}
|
173
|
+
|
174
|
+
next result_
|
175
|
+
|
176
|
+
|
135
177
|
end
|
136
178
|
|
137
179
|
lks.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
|
138
|
-
|
139
180
|
rks.map! { |k| Sequel[seg.last.alias_sym][k] }
|
140
181
|
|
141
182
|
{ type: assoc[:type],
|
142
|
-
segment: seg,
|
143
183
|
left: leftm.table_name,
|
144
184
|
right: rightm.table_name,
|
145
185
|
alias: seg.last.alias_sym,
|
146
|
-
left_keys: lks,
|
147
|
-
right_keys: rks,
|
148
186
|
cond: rks.zip(lks).to_h }
|
149
187
|
end
|
150
188
|
end
|
@@ -159,11 +197,14 @@ class JoinByPathsHelper < Set
|
|
159
197
|
return start_dataset if empty?
|
160
198
|
|
161
199
|
build_unique_join_segments
|
162
|
-
|
200
|
+
need_distinct = false
|
201
|
+
ret = @result.flatten.inject(start_dataset) do |dt, jo|
|
202
|
+
need_distinct = true if jo[:type] == :many_to_many
|
163
203
|
dt.left_join(Sequel[jo[:right]].as(jo[:alias]),
|
164
204
|
jo[:cond],
|
165
205
|
implicit_qualifier: jo[:left])
|
166
206
|
end
|
207
|
+
need_distinct ? ret.distinct : ret
|
167
208
|
end
|
168
209
|
|
169
210
|
def join_by_paths_helper(*pathlist)
|
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.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -120,6 +120,20 @@ dependencies:
|
|
120
120
|
- - "~>"
|
121
121
|
- !ruby/object:Gem::Version
|
122
122
|
version: '2.2'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: rexml
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '3.0'
|
130
|
+
type: :runtime
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '3.0'
|
123
137
|
- !ruby/object:Gem::Dependency
|
124
138
|
name: rack-test
|
125
139
|
requirement: !ruby/object:Gem::Requirement
|
@@ -231,14 +245,14 @@ files:
|
|
231
245
|
- lib/safrano/type_mapping.rb
|
232
246
|
- lib/safrano/version.rb
|
233
247
|
- lib/sequel/plugins/join_by_paths.rb
|
234
|
-
homepage: https://
|
248
|
+
homepage: https://safrano.aithscel.eu
|
235
249
|
licenses:
|
236
250
|
- MIT
|
237
251
|
metadata:
|
238
252
|
bug_tracker_uri: https://gitlab.com/dm0da/safrano/issues
|
239
253
|
changelog_uri: https://gitlab.com/dm0da/safrano/blob/master/CHANGELOG
|
240
254
|
source_code_uri: https://gitlab.com/dm0da/safrano/tree/master
|
241
|
-
|
255
|
+
documentation_uri: https://gitlab.com/dm0da/safrano/-/blob/master/README.md
|
242
256
|
post_install_message:
|
243
257
|
rdoc_options: []
|
244
258
|
require_paths:
|