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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d1ee63cc31ce50cff784481ce75f5f5581c2b9ff406a18efb516908bbf7c65d
4
- data.tar.gz: 5770021472ccce8efb445114d4b4834b4fb0ff31b58f6444654cfb16b5aeb860
3
+ metadata.gz: 56284ecd8544233a820d8163c324a4b86df3f87909e529ac993407dad3358132
4
+ data.tar.gz: 3097012a5e22ccd3cc2b61e8ecc22427c35e8db12a02dfd3c9a819cfc13abd53
5
5
  SHA512:
6
- metadata.gz: b1be48927ce7f3b5350ea903fa6ebb514cd6c09a1eafb064b7760c56bb396f02e59cab6e2a2e091ab8c526c77d45e9ff18838728d34d7500b29f611ba98e858b
7
- data.tar.gz: 5877c1e41a0bbbcb76d948914a703a24ea8cdf52215815b7c4051c3cfb3807463adfa0144ccce4861dfd988abf8776a002de6353d6343205f3767af02b9ca090
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) > 0 ? "+#{min_off}" : min_off.to_s
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
@@ -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? || (gmt_offset == 0)
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) > 0 ? "+#{min_off}" : min_off.to_s
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
@@ -194,9 +194,8 @@ module Safrano
194
194
  @child_dataset_method.call
195
195
  end
196
196
 
197
- def each
198
- y = @child_method.call
199
- y.each { |enty| yield enty }
197
+ def each(&block)
198
+ @child_method.call.each(&block)
200
199
  end
201
200
 
202
201
  def to_a
@@ -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 = (::Rack.release[0..2] == '2.0') ? ::Rack::File : ::Rack::Files
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 + 'tmp'
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 + '1'
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(with_directory = false).max.to_s
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
@@ -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, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME] + env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
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, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME], env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
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
 
@@ -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, _apply_query_params = false)
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 = false)
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 = false)
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 |k, v|
245
- ret[k] = if v.respond_to? :odata_h
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
- case itype.downcase
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 (DB_TYPE_GUID_RGX.match(props[:db_type]))
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] = case props[: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(v)
157
- return Contract::NOK unless v == 'null'
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(v)
168
- # TODO this should use base64
169
- Contract.valid(v.dup.force_encoding('BINARY'))
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 { |v| odata_value(v) }
178
+ array.map { |val| odata_value(val) }
184
179
  end
185
180
 
186
- def self.convert_from_urlparam(v)
187
- return Contract::NOK unless %w[true false].include?(v)
181
+ def self.convert_from_urlparam(val)
182
+ return Contract::NOK unless %w[true false].include?(val)
188
183
 
189
- Contract.valid(v == 'true')
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(v)
199
- return Contract::NOK unless (bytev = v.to_i) < 256
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 { |v| odata_value(v) }
207
+ array.map { |val| odata_value(val) }
213
208
  end
214
209
 
215
- def self.convert_from_urlparam(v)
216
- Contract.valid(DateTime.parse(v))
210
+ def self.convert_from_urlparam(val)
211
+ Contract.valid(DateTime.parse(val))
217
212
  rescue StandardError
218
- convertion_error(v)
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(v)
226
- Contract.valid(v)
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(v)
234
- return Contract::NOK unless (ret = number_or_nil(v))
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(v)
244
- return Contract::NOK unless (ret = number_or_nil(v))
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(v)
254
- Contract.valid(v.to_f)
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(v)
264
- Contract::NOK unless m = Filter::Parser::Token::GUIDRGX.match(v)
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 m[1])
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
- if result.respond_to?(:error) && (err = result.error)
54
- yield err
55
- end
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
@@ -14,9 +14,6 @@ module Safrano
14
14
  expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr, model)
15
15
  end
16
16
 
17
- # output template
18
- attr_reader :template
19
-
20
17
  def apply_to_dataset(dtcx)
21
18
  Contract.valid(dtcx)
22
19
  end
@@ -196,7 +196,7 @@ module Safrano
196
196
  unless (@error = check_url_func_params)
197
197
  begin
198
198
  return yield
199
- rescue LocalJumpError => e
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::with_error(result) do |error|
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, @auto_query_params), :run]
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
 
@@ -143,39 +143,38 @@ module Safrano
143
143
  end
144
144
 
145
145
  MAX_DEPTH = 6
146
- def attribute_path_list(depth = 0)
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 depth > MAX_DEPTH
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(depth).map { |kc| "#{a}/#{kc}" })
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(depth).map { |kc| "#{a}/#{kc}" })
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(depth = 0)
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 depth > MAX_DEPTH
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(depth).map { |kc| "#{a}/#{kc}" })
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(depth).map { |kc| "#{a}/#{kc}" })
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] = ->(x) {
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
- @pk_castfunc = @casted_cols[primary_key]
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 harmonize this with primitive_types.rb convert_from_url
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 = ->(str) {
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
- if mpk && mval
681
- mdch[mpk] = if (pk_cast = @pk_cast_from_string[mpk])
682
- pk_cast.call(mval)
683
- else
684
- mval # no cast needed, eg for string
685
- end
686
- scan_rgx_parts.delete(mpk)
687
- else
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
- # this does not work if there are multiple valid transitions
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
- # this is a very fragile and obscure but required hack (wanted: a
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
- @tr_next = if valid_tr
91
- if valid_tr.size == 1
92
- valid_tr.first
93
- elsif valid_tr.size > 1
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
@@ -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
- elsif @target.parser.last_part(line)
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
- @lines = 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
- @lines = inpstr.read.lines(@sep)
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.unparse(true)]
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(bodyonly = false)
412
+ def unparse
414
413
  b = +String.new
415
- b << "#{Safrano::CONTENT_TYPE}: #{@hd[CTT_TYPE_LC]}#{CRLF}" unless bodyonly
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}"
@@ -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
@@ -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 = false)
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 { |func| func.check_definition }
453
+ function_imports.each_value(&:check_definition)
454
454
  end
455
455
 
456
456
  def execute_deferred_iblocks
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Safrano
4
- VERSION = '0.6.8'
4
+ VERSION = '0.7.1'
5
5
  end
@@ -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
- # TODO
126
- # when # :many_to_many :: A join table is used that has a foreign key that points
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
- @result.flatten.inject(start_dataset) do |dt, jo|
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.6.8
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-05-13 00:00:00.000000000 Z
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://gitlab.com/dm0da/safrano
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
- wiki_uri: https://gitlab.com/dm0da/safrano/wikis/home
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: