safrano 0.6.8 → 0.7.1
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/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:
|