safrano 0.7.1 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56284ecd8544233a820d8163c324a4b86df3f87909e529ac993407dad3358132
4
- data.tar.gz: 3097012a5e22ccd3cc2b61e8ecc22427c35e8db12a02dfd3c9a819cfc13abd53
3
+ metadata.gz: 351f486db0cd9cfa46ac9770e739dce4fcf9b4fe4a564799febc2d23eea2f713
4
+ data.tar.gz: b214e28350f97dc2a442950803773accb428f8316de8482884c56c81b0aab754
5
5
  SHA512:
6
- metadata.gz: 23fffb3c40b6264ca8489ded795330c14410419db07fbad0f1fb9781a3ae31668264747df93df8e2d8df45dcdc546d4a6c61cf8d66cee54b7abb7b32e85bef2e
7
- data.tar.gz: a4473504fe30c922e3f6803dc608beb5495174ca3f884f4da04de479769a602fdc94df5f45c9a8edad2996146028d1cddacd626d61310b776ced641d32abbbd2
6
+ metadata.gz: 3e1a12730936b436c30c909970b8d31e90358b87f10242f76c7f8b238def70391fb20bc0568825495045a095db6d799b8da8c07c9f524914eecf0a072d083d40
7
+ data.tar.gz: 8a1fdd4ca3c8d4512bd6ef6890a653a1ad1e052daad13ee5355d51248c652a938cf75df16d42d97a42ee6149f111e69cce215102b69c414b58920cf4b62180a2
@@ -27,6 +27,7 @@ module Safrano
27
27
  end
28
28
  end
29
29
  end
30
+
30
31
  module CoreIncl
31
32
  module Date
32
33
  module Format
@@ -27,6 +27,7 @@ module Safrano
27
27
  end
28
28
  end
29
29
  end
30
+
30
31
  module CoreIncl
31
32
  module DateTime
32
33
  module Format
@@ -17,10 +17,10 @@ module Safrano
17
17
  def symbolize_keys!
18
18
  transform_keys! do |key|
19
19
  begin
20
- key.to_sym
20
+ key.to_sym
21
21
  rescue StandardError
22
22
  key
23
- end
23
+ end
24
24
  end
25
25
  end
26
26
  end
@@ -15,7 +15,7 @@ module Safrano
15
15
  return unless (md = instr.match(REGEX))
16
16
 
17
17
  sec, milli = md[1].to_i.divmod(1000)
18
- secm = milli.zero? ? sec : sec + Float(milli) / 1000
18
+ secm = milli.zero? ? sec : sec + (Float(milli) / 1000)
19
19
  if md[3].nil? # no offset
20
20
  # ::Time.at(sec, milli, :millisecond) # not supported in ruby 2.4
21
21
  ::Time.gm(1970, 1, 1) + secm
@@ -13,6 +13,7 @@ module Safrano
13
13
  def initialize(entity, name)
14
14
  @entity = entity
15
15
  @name = name
16
+ @allowed_transitions = ALLOWED_TRANSITIONS
16
17
  end
17
18
 
18
19
  def value
@@ -20,11 +21,7 @@ module Safrano
20
21
  # currently it is just set to make some minimal testcase work
21
22
  # See also model_ext.rb
22
23
  case (v = @entity.values[@name.to_sym])
23
- when Date
24
- v.to_edm_json
25
- when Time
26
- v.to_edm_json
27
- when DateTime
24
+ when Date, Time, DateTime
28
25
  v.to_edm_json
29
26
  else
30
27
  v
@@ -68,6 +65,8 @@ module Safrano
68
65
  def allowed_transitions
69
66
  Transitions::ALLOWED_TRANSITIONS
70
67
  end
68
+
69
+ include Safrano::Transitions::GetNextTrans::BySimpleDetect
71
70
  end
72
71
  include Transitions
73
72
  end
data/lib/odata/batch.rb CHANGED
@@ -103,7 +103,7 @@ module Safrano
103
103
 
104
104
  # $batch Handler
105
105
  class HandlerBase
106
- TREND = Safrano::Transition.new('', trans: 'transition_end')
106
+ TREND = Safrano::Transition.new('', trans: :transition_end)
107
107
  def allowed_transitions
108
108
  @allowed_transitions = [TREND]
109
109
  end
@@ -111,6 +111,8 @@ module Safrano
111
111
  def transition_end(_match_result)
112
112
  Safrano::Transition::RESULT_END
113
113
  end
114
+
115
+ include Safrano::Transitions::GetNextTrans::ForJustTransitionEnd
114
116
  end
115
117
 
116
118
  # $batch disabled Handler
@@ -20,6 +20,7 @@ module Safrano
20
20
 
21
21
  def initialize(modelk)
22
22
  @modelk = modelk
23
+ @allowed_transitions = @modelk.allowed_transitions
23
24
  end
24
25
 
25
26
  def allowed_transitions
@@ -52,6 +53,8 @@ module Safrano
52
53
  end
53
54
  end
54
55
 
56
+ include Safrano::Transitions::GetNextTrans::ByLongestMatch
57
+
55
58
  # pkid can be a single value for single-pk models, or an array.
56
59
  # type checking/convertion is done in check_odata_key_type
57
60
  def find_by_odata_key(pkid)
@@ -14,6 +14,7 @@ module Safrano
14
14
  Contract::OK
15
15
  end
16
16
  end
17
+
17
18
  # Simple static File/Directory based media store handler
18
19
  # similar to Rack::Static
19
20
  # with a flat directory structure
@@ -35,11 +36,11 @@ module Safrano
35
36
  end
36
37
 
37
38
  def create_abs_class_dir
38
- FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
39
+ FileUtils.makedirs @abs_klass_dir
39
40
  end
40
41
 
41
42
  def create_abs_temp_dir
42
- FileUtils.makedirs @abs_temp_dir unless Dir.exist?(@abs_temp_dir)
43
+ FileUtils.makedirs @abs_temp_dir
43
44
  end
44
45
 
45
46
  def finalize
@@ -114,7 +115,7 @@ module Safrano
114
115
  # and ensure the directory exists
115
116
  def with_media_directory(entity)
116
117
  mpi = abs_media_directory(entity)
117
- Dir.mkdir mpi unless Dir.exist?(mpi)
118
+ FileUtils.mkdir_p mpi
118
119
  yield Pathname(mpi)
119
120
  end
120
121
 
@@ -200,7 +201,7 @@ module Safrano
200
201
  def with_media_directory(entity)
201
202
  mpi = abs_media_directory(entity)
202
203
 
203
- FileUtils.makedirs mpi unless Dir.exist?(mpi)
204
+ FileUtils.makedirs mpi
204
205
  yield Pathname(mpi)
205
206
  end
206
207
 
@@ -21,6 +21,8 @@ module Safrano
21
21
  Safrano::Transition::RESULT_END
22
22
  end
23
23
 
24
+ include Safrano::Transitions::GetNextTrans::ForJustTransitionEnd
25
+
24
26
  # we will have this on class and instance level for making things simpler first
25
27
  class << self
26
28
  attr_reader :klassmod
@@ -202,7 +204,7 @@ module Safrano
202
204
  end
203
205
 
204
206
  def metadata_h
205
- { type: type_name }
207
+ { type: type_name }
206
208
  end
207
209
 
208
210
  def casted_values
data/lib/odata/entity.rb CHANGED
@@ -19,6 +19,8 @@ module Safrano
19
19
  self.class.entity_allowed_transitions
20
20
  end
21
21
 
22
+ include Safrano::Transitions::GetNextTrans::ByLongestMatchDyn
23
+
22
24
  def transition_end(_match_result)
23
25
  Safrano::Transition::RESULT_END
24
26
  end
@@ -184,16 +186,9 @@ module Safrano
184
186
  if req.walker.media_value
185
187
  odata_media_value_put(req)
186
188
  elsif req.accept?(APPJSON)
187
- data.delete('__metadata')
188
189
 
189
- if req.in_changeset
190
- set_fields(data, self.class.data_fields, missing: :skip)
191
- save(transaction: false)
192
- else
193
- update_fields(data, self.class.data_fields, missing: :skip)
194
- end
190
+ AlreadyExistsUnprocessableError.odata_get(req)
195
191
 
196
- [202, EMPTY_HASH, to_odata_post_json(service: req.service)]
197
192
  else # TODO: other formats
198
193
  415
199
194
  end
@@ -361,7 +356,7 @@ module Safrano
361
356
  # real implementation for replacing $value for a media entity
362
357
  def odata_media_value_put(req)
363
358
  model = self.class
364
- req.with_media_data do |data, mimetype, filename|
359
+ req.with_media_data do |data, mimetype, _filename|
365
360
  emdata = { content_type: mimetype }
366
361
  if req.in_changeset
367
362
  set_fields(emdata, model.data_fields, missing: :skip)
@@ -392,6 +387,7 @@ module Safrano
392
387
  [pk_uri]
393
388
  end
394
389
  end
390
+
395
391
  module PKUriWithoutFunc
396
392
  def pk_uri
397
393
  pk
@@ -405,6 +401,7 @@ module Safrano
405
401
  [pk]
406
402
  end
407
403
  end
404
+
408
405
  # for a single public key
409
406
  module EntitySinglePK
410
407
  include Entity
data/lib/odata/error.rb CHANGED
@@ -143,6 +143,10 @@ module Safrano
143
143
  end
144
144
  end
145
145
 
146
+ class AlreadyExistsUnprocessableError < UnprocessableEntityError
147
+ @msg = 'The ressource you are trying to create already exists'
148
+ end
149
+
146
150
  # http Bad Req.
147
151
  class BadRequestError
148
152
  extend ErrorClass
@@ -161,6 +165,7 @@ module Safrano
161
165
  @msg = "Bad Request: empty media file #{path}"
162
166
  end
163
167
  end
168
+
164
169
  # Generic failed changeset
165
170
  class BadRequestFailedChangeSet < BadRequestError
166
171
  @msg = 'Bad Request: Failed changeset '
@@ -170,6 +175,7 @@ module Safrano
170
175
  class BadRequestNonMediaValue < BadRequestError
171
176
  @msg = 'Bad Request: $value request for a non-media entity'
172
177
  end
178
+
173
179
  class BadRequestSequelAdapterError < BadRequestError
174
180
  include ErrorInstance
175
181
  def initialize(err)
@@ -197,6 +203,7 @@ module Safrano
197
203
  @msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
198
204
  end
199
205
  end
206
+
200
207
  # for invalid properti(es) in $select param
201
208
  class BadRequestSelectInvalidProps < BadRequestError
202
209
  include ErrorInstance
@@ -217,7 +224,7 @@ module Safrano
217
224
 
218
225
  # http not found
219
226
  class ErrorNotFound
220
- extend ErrorClass
227
+ extend ErrorClass
221
228
  HTTP_CODE = 404
222
229
  @msg = 'The requested ressource was not found'
223
230
  end
@@ -238,16 +245,17 @@ module Safrano
238
245
 
239
246
  # not implemented (Safrano specific)
240
247
  class NotImplementedError
241
- extend ErrorClass
248
+ extend ErrorClass
242
249
  HTTP_CODE = 501
243
250
  end
244
251
 
245
252
  # version not implemented (Safrano specific)
246
253
  class VersionNotImplementedError
247
- extend ErrorClass
254
+ extend ErrorClass
248
255
  HTTP_CODE = 501
249
256
  @msg = 'The requested OData version is not yet supported'
250
257
  end
258
+
251
259
  # batch not implemented (Safrano specific)
252
260
  class BatchNotImplementedError < RuntimeError
253
261
  @msg = 'Not implemented: OData batch'
@@ -266,22 +274,26 @@ module Safrano
266
274
  @msg = xmsg
267
275
  end
268
276
  end
277
+
269
278
  class FilterUnknownFunctionError < BadRequestError
270
279
  include ErrorInstance
271
280
  def initialize(badfuncname)
272
281
  @msg = "Bad request: unknown function #{badfuncname} in $filter"
273
282
  end
274
283
  end
284
+
275
285
  class FilterParseErrorWrongColumnName < BadRequestError
276
286
  extend ErrorClass
277
287
  @msg = 'Bad request: invalid property name in $filter'
278
288
  end
289
+
279
290
  class FilterParseWrappedError < BadRequestError
280
291
  include ErrorInstance
281
292
  def initialize(exception)
282
293
  @msg = exception.to_s
283
294
  end
284
295
  end
296
+
285
297
  class ServiceOperationParameterMissing < BadRequestError
286
298
  include ErrorInstance
287
299
  def initialize(missing:, sopname:)
@@ -77,6 +77,7 @@ module Safrano
77
77
  # DateTime Literals
78
78
  class DateTimeLit < Leave
79
79
  end
80
+
80
81
  class DateTimeOffsetLit < Leave
81
82
  end
82
83
 
@@ -35,6 +35,7 @@ module Safrano
35
35
  @cur_typ = cur.class
36
36
  end
37
37
  end
38
+
38
39
  # Invalid Tokens
39
40
  class ErrorInvalidToken < Error
40
41
  include ::Safrano::ErrorInstance
@@ -43,6 +44,7 @@ module Safrano
43
44
  @msg = "Bad Request: invalid token #{tok} in $filter"
44
45
  end
45
46
  end
47
+
46
48
  # Unmached closed
47
49
  class ErrorUnmatchedClose < Error
48
50
  include ::Safrano::ErrorInstance
@@ -73,6 +75,7 @@ module Safrano
73
75
  @msg = "Bad Request: wrong number of parameters for function #{cur.parent.value} in $filter"
74
76
  end
75
77
  end
78
+
76
79
  # Invalid separator in this context (missing parenthesis?)
77
80
  class ErrorInvalidSeparator < Error
78
81
  include ::Safrano::ErrorInstance
@@ -51,21 +51,17 @@ module Safrano
51
51
  when :substringof
52
52
 
53
53
  # there are multiple possible argument types (but all should return edm.string)
54
- if args[0].is_a?(QString)
55
- # substringof('Rhum', name) -->
56
- # name contains substr 'Rhum'
57
- Contract.collect_result!(args[1].leuqes(jh),
58
- args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
59
- Sequel.like(l1, l0)
60
- end
61
54
 
62
- # special non standard (ui5 client) case ?
63
- elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
55
+ if args[0].is_a?(QString) ||
56
+ # substringof('Rhum', name) -->
57
+ # name contains substr 'Rhum'
58
+
59
+ # special non standard (ui5 client) case ?
60
+ (args[0].is_a?(Literal) && args[1].is_a?(Literal))
64
61
  Contract.collect_result!(args[1].leuqes(jh),
65
62
  args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
66
63
  Sequel.like(l1, l0)
67
64
  end
68
-
69
65
  elsif args[1].is_a?(QString)
70
66
  substringof_sig2(jh) # adapter specific
71
67
  else
@@ -11,6 +11,7 @@ module Safrano
11
11
  Sequel.function(:datetime, @value)
12
12
  end
13
13
  end
14
+
14
15
  # non-sqlite adapter specific DateTime handler
15
16
  module DateTimeDefault
16
17
  def datetime(_val)
@@ -64,6 +64,7 @@ module Safrano
64
64
  Safrano::FilterFunctionNotImplementedError.new("$filter function 'ceiling' is not implemented in sqlite adapter")
65
65
  end
66
66
  end
67
+
67
68
  # re-useable module with math floor/ceil functions for those adapters having these SQL funcs
68
69
  module MathFloorCeilFuncTree
69
70
  def floor(lq)
@@ -355,14 +355,14 @@ module Safrano
355
355
 
356
356
  # Numbers (floating point, ints, dec)
357
357
  class FPNumber
358
- def initialize(val)
359
- # 1.53f --> value 1.53
360
- # 1.53d --> value 1.53
361
- # 1.53 --> value 1.53
362
- # Note: the tokenizer has already dropped the not usefull string parts
363
- # Note : we dont differentiate between Float and Double here
364
- super(val)
365
- end
358
+ # def initialize(val)
359
+ # 1.53f --> value 1.53
360
+ # 1.53d --> value 1.53
361
+ # 1.53 --> value 1.53
362
+ # Note: the tokenizer has already dropped the not usefull string parts
363
+ # Note : we dont differentiate between Float and Double here
364
+ # super(val)
365
+ # end
366
366
 
367
367
  def accept?(tok, typ)
368
368
  case typ
@@ -396,14 +396,13 @@ module Safrano
396
396
  end
397
397
 
398
398
  class DecimalLit
399
- def initialize(val)
400
- # 1.53m --> value 1.53
401
- # Warning, this assumes that the m|M part in the input is really not optional
402
- # Note: the tokenizer has already dropped the not usefull string parts
403
- # cf. DECIMALRGX in token.rb
404
-
405
- super(val)
406
- end
399
+ # def initialize(val)
400
+ # 1.53m --> value 1.53
401
+ # Warning, this assumes that the m|M part in the input is really not optional
402
+ # Note: the tokenizer has already dropped the not usefull string parts
403
+ # cf. DECIMALRGX in token.rb
404
+ # super(val)
405
+ # end
407
406
 
408
407
  def accept?(tok, typ)
409
408
  case typ
@@ -418,6 +417,7 @@ module Safrano
418
417
  :decimal
419
418
  end
420
419
  end
420
+
421
421
  # Literals are unquoted words without /
422
422
  class Literal
423
423
  def accept?(tok, typ)
@@ -465,11 +465,11 @@ module Safrano
465
465
 
466
466
  # DateTimeLit
467
467
  class DateTimeLit
468
- def initialize(val)
469
- # datetime'2000-12-12T12:00:53' --> value 2000-12-12T12:00:53
470
- # Note: the tokenizer has already dropped the not usefull string parts
471
- super(val)
472
- end
468
+ # def initialize(val)
469
+ # datetime'2000-12-12T12:00:53' --> value 2000-12-12T12:00:53
470
+ # Note: the tokenizer has already dropped the not usefull string parts
471
+ # super(val)
472
+ # end
473
473
 
474
474
  def accept?(tok, typ)
475
475
  case typ
@@ -484,13 +484,14 @@ module Safrano
484
484
  :datetime
485
485
  end
486
486
  end
487
+
487
488
  # DateTimeOffsetLit
488
489
  class DateTimeOffsetLit
489
- def initialize(val)
490
- # datetimeoffset'2000-12-12T12:00:53+02:00' --> value 2000-12-12T12:00:53+02:00
491
- # Note: the tokenizer has already dropped the not usefull string parts
492
- super(val)
493
- end
490
+ # def initialize(val)
491
+ # datetimeoffset'2000-12-12T12:00:53+02:00' --> value 2000-12-12T12:00:53+02:00
492
+ # Note: the tokenizer has already dropped the not usefull string parts
493
+ # super(val)
494
+ # end
494
495
 
495
496
  def accept?(tok, typ)
496
497
  case typ
@@ -505,6 +506,7 @@ module Safrano
505
506
  :datetimeoffset
506
507
  end
507
508
  end
509
+
508
510
  # Quoted Strings
509
511
  class QString
510
512
  DBL_QO = "''"
@@ -17,6 +17,7 @@ module Safrano
17
17
  super(msg)
18
18
  end
19
19
  end
20
+
20
21
  class ProcRedefinition < StandardError
21
22
  def initialize(fnam)
22
23
  msg = "Function import #{fnam}: Block/lambda Redefinition . Provide definition either as a return code block or with .definition(lambda) but not both"
@@ -39,6 +40,12 @@ module Safrano
39
40
  [Safrano::TransitionExecuteFunc]
40
41
  end
41
42
 
43
+ include Safrano::Transitions::GetNextTrans::ForJustTransitionEnd
44
+
45
+ def get_next_transresult(path_remain)
46
+ Safrano::TransitionExecuteFunc.result(path_remain)
47
+ end
48
+
42
49
  def input(**parmtypes)
43
50
  @input = {}
44
51
  parmtypes.each do |k, t|
@@ -445,17 +445,17 @@ module Safrano
445
445
 
446
446
  # check if key needs casting. Important for later entity-uri generation !
447
447
  return unless primary_key.is_a? Symbol # single key field
448
-
449
- # guid key as guid'xxx-yyy-zzz'
448
+
449
+ # guid key as guid'xxx-yyy-zzz'
450
450
  metadata = @cols_metadata[primary_key]
451
-
451
+
452
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
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
459
  else
460
460
  @pk_castfunc = @casted_cols[primary_key]
461
461
  end
@@ -607,12 +607,15 @@ module Safrano
607
607
  end
608
608
  end
609
609
  end
610
+
610
611
  # methods related to transitions to next state (cf. walker)
611
612
  module Transitions
612
613
  def allowed_transitions
613
614
  @allowed_transitions
614
615
  end
615
616
 
617
+ include Safrano::Transitions::GetNextTrans::BySimpleDetect
618
+
616
619
  def entity_allowed_transitions
617
620
  @entity_allowed_transitions
618
621
  end
@@ -621,7 +624,7 @@ module Safrano
621
624
  @allowed_transitions = [Safrano::TransitionEnd,
622
625
  Safrano::TransitionCount,
623
626
  Safrano::Transition.new(entity_id_url_regexp,
624
- trans: 'transition_id')].freeze
627
+ trans: :transition_id)].freeze
625
628
  end
626
629
 
627
630
  def build_entity_allowed_transitions
@@ -630,17 +633,17 @@ module Safrano
630
633
  Safrano::TransitionCount,
631
634
  Safrano::TransitionLinks,
632
635
  Safrano::TransitionValue,
633
- Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
636
+ Safrano::Transition.new(transition_attribute_regexp, trans: :transition_attribute)
634
637
  ]
635
638
  if (ncurgx = @nav_collection_url_regexp)
636
639
  @entity_allowed_transitions <<
637
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
640
+ Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: :transition_nav_collection)
638
641
  end
639
642
  if (neurgx = @nav_entity_url_regexp)
640
643
  @entity_allowed_transitions <<
641
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
644
+ Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: :transition_nav_entity)
642
645
  end
643
- @entity_allowed_transitions << Safrano::Transition.new(%r{\A/(\w+)(.*)\z}, trans: 'transition_invalid_attribute')
646
+ @entity_allowed_transitions << Safrano::Transition.new(%r{\A/(\w+)(.*)\z}, trans: :transition_invalid_attribute)
644
647
  @entity_allowed_transitions.freeze
645
648
  @entity_allowed_transitions
646
649
  end
@@ -78,6 +78,10 @@ module Safrano
78
78
  # Represents a named but nil-valued navigation-attribute of an Entity
79
79
  # (usually resulting from a NULL FK db value)
80
80
  class NilNavigationAttribute
81
+ def initialize
82
+ @allowed_transitions = ALLOWED_TRANSITIONS
83
+ end
84
+
81
85
  include Safrano::NavigationInfo
82
86
  def odata_get(req)
83
87
  if req.walker.media_value
@@ -131,6 +135,8 @@ module Safrano
131
135
  def allowed_transitions
132
136
  ALLOWED_TRANSITIONS
133
137
  end
138
+
139
+ include Safrano::Transitions::GetNextTrans::BySimpleDetect
134
140
  end
135
141
  include Transitions
136
142
  end
@@ -35,6 +35,7 @@ module Safrano
35
35
  end
36
36
  end
37
37
  end
38
+
38
39
  # This is used from the Test-suite code !
39
40
  # it does recursive / deep symbolize additionally to inbound casting
40
41
  module XJSON
@@ -4,16 +4,78 @@ require_relative 'error'
4
4
 
5
5
  # our main namespace
6
6
  module Safrano
7
+ module Transitions
8
+ module GetNextTrans
9
+ module BySimpleDetect
10
+ def get_next_transresult(path_remain)
11
+ # current url-parsing context
12
+ # has no ambiguous next match and we dont need to find longest match
13
+ # but it's sufficient to find the one matching (or nil)
14
+ tres_next = nil
15
+ @allowed_transitions.detect { |t| tres_next = t.result(path_remain) }
16
+ tres_next
17
+ end
18
+ end
19
+
20
+ module ForJustTransitionEnd
21
+ def get_next_transresult(path_remain)
22
+ Safrano::TransitionEnd.result(path_remain)
23
+ end
24
+ end
25
+
26
+ # Transitions::GetNextTrans::ByLongestMatch
27
+ module ByLongestMatch
28
+ def get_next_transresult(path_remain)
29
+ # current url-parsing context
30
+ # has ambiguous next match and we need to find longest match
31
+ # example: current context is "the top level service" and we have
32
+ # entity types Race and RaceType
33
+
34
+ match_len = -1
35
+ tres_next = nil
36
+
37
+ @allowed_transitions.each { |t|
38
+ if (res = t.longer_match(path_remain, match_len))
39
+ tres_next = res
40
+ match_len = tres_next.match_length
41
+ end
42
+ }
43
+ tres_next
44
+ end
45
+ end
46
+
47
+ # same as ByLongestMatch but use the getter method allowed_transitions instead of
48
+ # directly @allowed_transitions
49
+ module ByLongestMatchDyn
50
+ def get_next_transresult(path_remain)
51
+ # current url-parsing context
52
+ # has ambiguous next match and we need to find longest match
53
+ # example: current context is "the top level service" and we have
54
+ # entity types Race and RaceType
55
+
56
+ match_len = -1
57
+ tres_next = nil
58
+
59
+ allowed_transitions.each { |t|
60
+ if (res = t.longer_match(path_remain, match_len))
61
+ tres_next = res
62
+ match_len = tres_next.match_length
63
+ end
64
+ }
65
+ tres_next
66
+ end
67
+ end
68
+ end
69
+ end
70
+
7
71
  # represents a state transition when navigating/parsing the url path
8
72
  # from left to right
9
73
  class Transition
10
- attr_accessor :trans
11
- attr_accessor :match_result
12
- attr_accessor :rgx
13
- attr_reader :remain_idx
14
-
15
- EMPTYSTR = ''
16
- SLASH = '/'
74
+ # attr_accessor :trans
75
+ # attr_accessor :match_result
76
+ # attr_accessor :trans_result
77
+ # attr_accessor :rgx
78
+ # attr_reader :remain_idx
17
79
 
18
80
  RESULT_BAD_REQ_ERR = [nil, :error, ::Safrano::BadRequestError].freeze
19
81
  RESULT_NOT_FOUND_ERR = [nil, :error, ::Safrano::ErrorNotFound].freeze
@@ -25,31 +87,58 @@ module Safrano
25
87
  Regexp.new(arg)
26
88
  else
27
89
  arg
28
- end
90
+ end.freeze
91
+ @trans = trans.freeze
92
+ @remain_idx = remain_idx.freeze
93
+ end
94
+
95
+ def result(str)
96
+ return unless (mres = @rgx.match(str))
97
+
98
+ TransitionResult.new(trans: @trans, match_result: mres, remain_idx: @remain_idx)
99
+ end
100
+
101
+ # return match-result for str, if longer as min_match_length
102
+ def longer_match(str, min_match_length)
103
+ return unless (mres = @rgx.match(str))
104
+ return unless (mres.match_length(1) > min_match_length)
105
+
106
+ TransitionResult.new(trans: @trans, match_result: mres, remain_idx: @remain_idx)
107
+ end
108
+ end
109
+
110
+ class TransitionResult
111
+ attr_reader :match
112
+
113
+ EMPTYSTR = ''
114
+ SLASH = '/'
115
+
116
+ def initialize(trans:, match_result:, remain_idx:)
29
117
  @trans = trans
118
+ @match = match_result
30
119
  @remain_idx = remain_idx
31
120
  end
32
121
 
33
- def do_match(str)
34
- @match_result = @rgx.match(str)
122
+ def match_length
123
+ @match.match_length(1)
35
124
  end
36
125
 
37
126
  # remain_idx is the index of the last match-data. ususally its 2
38
127
  # but can be overidden
39
128
  def path_remain
40
- @match_result[@remain_idx] if @match_result && @match_result[@remain_idx]
129
+ @match[@remain_idx] if @match && @match[@remain_idx]
41
130
  end
42
131
 
43
132
  def path_done
44
- if @match_result
45
- @match_result[1] || EMPTYSTR
133
+ if @match
134
+ @match[1] || EMPTYSTR
46
135
  else
47
136
  EMPTYSTR
48
137
  end
49
138
  end
50
139
 
51
140
  def do_transition(ctx)
52
- ctx.method(@trans).call(@match_result)
141
+ ctx.__send__(@trans, @match)
53
142
  end
54
143
  end
55
144
 
@@ -59,8 +148,30 @@ module Safrano
59
148
  @trans = trans
60
149
  end
61
150
 
62
- def do_match(str)
151
+ def result(str)
152
+ @str = str
153
+ InplaceTransitionResult.new(trans: @trans, match_result: @str)
154
+ end
155
+
156
+ # return match-result for str, if longer as min_match_length
157
+ def longer_match(str, min_match_length)
63
158
  @str = str
159
+ return unless (@str.size > min_match_length)
160
+
161
+ InplaceTransitionResult.new(trans: @trans, match_result: @str)
162
+ end
163
+ end
164
+
165
+ # Transition that does not move/change the input
166
+ class InplaceTransitionResult < TransitionResult
167
+ def initialize(trans:, match_result:)
168
+ @trans = trans
169
+ @match = match_result
170
+ @str = match_result
171
+ end
172
+
173
+ def match_length
174
+ @str.size
64
175
  end
65
176
 
66
177
  def path_remain
@@ -70,26 +181,22 @@ module Safrano
70
181
  def path_done
71
182
  EMPTYSTR
72
183
  end
73
-
74
- def do_transition(ctx)
75
- ctx.method(@trans).call(@str)
76
- end
77
184
  end
78
185
 
79
- TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
80
- TransitionExecuteFunc = InplaceTransition.new(trans: 'transition_execute_func')
186
+ TransitionEnd = Transition.new('\A(\/?)\z', trans: :transition_end)
187
+ TransitionExecuteFunc = InplaceTransition.new(trans: :transition_execute_func)
81
188
  TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
82
- trans: 'transition_metadata')
189
+ trans: :transition_metadata)
83
190
  TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
84
- trans: 'transition_batch')
191
+ trans: :transition_batch)
85
192
  TransitionContentId = Transition.new('\A(\/\$(\d+))(.*)',
86
- trans: 'transition_content_id',
193
+ trans: :transition_content_id,
87
194
  remain_idx: 3)
88
195
  TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
89
- trans: 'transition_count')
196
+ trans: :transition_count)
90
197
  TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
91
- trans: 'transition_value')
198
+ trans: :transition_value)
92
199
  TransitionLinks = Transition.new('(\A\/\$links)(.*)\z',
93
- trans: 'transition_links')
200
+ trans: :transition_links)
94
201
  attr_accessor :allowed_transitions
95
202
  end
data/lib/odata/walker.rb CHANGED
@@ -65,30 +65,10 @@ module Safrano
65
65
  if (prefix == EMPTYSTR) || (prefix == SLASH)
66
66
  path
67
67
  else
68
- # path.sub!(/\A#{prefix}/, '')
69
- # TODO: check
70
68
  path.sub(/\A#{prefix}/, EMPTYSTR)
71
69
  end
72
70
  end
73
71
 
74
- def get_next_transition
75
- # handle multiple valid transitions
76
- # like when we have attributes that are substring of each other
77
- # --> instead of using detect (ie take first transition)
78
- # we need to use select and then find the longest match
79
-
80
- valid_tr = @context.allowed_transitions.map(&:dup).select do |t|
81
- t.do_match(@path_remain)
82
- end
83
-
84
- # HACK: (wanted: a better one) to make attributes that are substrings of each other
85
- # work well
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 }
90
- end
91
-
92
72
  # perform a content-id ($batch changeset ref) transition
93
73
  def do_run_with_content_id
94
74
  if @content_id_refs.is_a? Hash
@@ -137,7 +117,7 @@ module Safrano
137
117
  end
138
118
 
139
119
  def do_next_transition
140
- @context, @status, @error = @tr_next.do_transition(@context)
120
+ @context, @status, @error = @tres_next.do_transition(@context)
141
121
  # little hack's
142
122
  case @status
143
123
  # we dont have the content-id references data on service level
@@ -151,8 +131,8 @@ module Safrano
151
131
  end
152
132
 
153
133
  @contexts << @context
154
- @path_remain = @tr_next.path_remain
155
- @path_done << @tr_next.path_done
134
+ @path_remain = @tres_next.path_remain
135
+ @path_done << @tres_next.path_done
156
136
 
157
137
  # little hack's
158
138
  state_mappings
@@ -160,8 +140,8 @@ module Safrano
160
140
 
161
141
  def eo
162
142
  while @context
163
- get_next_transition
164
- if @tr_next
143
+ @tres_next = @context.get_next_transresult(@path_remain)
144
+ if @tres_next
165
145
  do_next_transition
166
146
  else
167
147
  @context = nil
@@ -8,7 +8,7 @@ require_relative 'response'
8
8
  module Safrano
9
9
  # Note there is a strong 1 to 1 relation between an app instance
10
10
  # and a published service. --> actually means also
11
- # we only support on service per App-class because publishing is
11
+ # we only support one service per App-class because publishing is
12
12
  # made on app class level
13
13
  class ServerApp
14
14
  def initialize
@@ -37,6 +37,11 @@ module Safrano
37
37
  @service_base
38
38
  end
39
39
 
40
+ # needed for safrano-rack_builder
41
+ def get_path_prefix
42
+ self.class.get_service_base.xpath_prefix
43
+ end
44
+
40
45
  def self.set_servicebase(sbase)
41
46
  @service_base = sbase
42
47
  @service_base.enable_v1_service
@@ -7,12 +7,73 @@ module Rack
7
7
  module Safrano
8
8
  # just a Wrapper to ensure (force?) that mandatory middlewares are acutally
9
9
  # used
10
+ LOCALHOST_ANY_PORT_RGX = /\A(?:https?:\/\/)?localhost(?::\d+)?\z/.freeze
11
+ CORS_RO_METHODS = %i[get head options].freeze
12
+ CORS_BATCH_METHODS = %i[post head options].freeze
13
+ CORS_RW_METHODS = %i[get post put patch delete head options].freeze
10
14
  class Builder < ::Rack::Builder
11
15
  def initialize(default_app = nil, &block)
12
16
  super(default_app) {}
13
- use ::Rack::Cors
17
+ @middlewares = []
18
+
14
19
  instance_eval(&block) if block_given?
15
- use ::Rack::ContentLength
20
+ prepend_rackcors_ifneeded
21
+ end
22
+
23
+ def use(middleware, *args, &block)
24
+ @middlewares << middleware
25
+ super(middleware, *args, &block)
26
+ end
27
+
28
+ def prepend_rackcors_ifneeded
29
+ return if stack_has_rackcors
30
+
31
+ # get the safrano app path prefix
32
+ # normally @run is a Safrano Server app
33
+ return unless @run.is_a? ::Safrano::ServerApp
34
+
35
+ service_path_prefix = @run.get_path_prefix.dup
36
+
37
+ # due to a bug in rack-cors
38
+ # we cant use the batch ressource path as
39
+ # a string but need to pass it as a regexp
40
+ # ( bug fixed in rack-cors git / new release? but still need to workaround it for
41
+ # current/old releases ),
42
+ batch_path_regexp = if service_path_prefix.empty?
43
+ # Ressource /$batch
44
+ /\A\/\$batch\z/
45
+ else
46
+ # Ressource like /foo/bar/baz/$batch
47
+ service_path_prefix.sub!(::Safrano::TRAILING_SLASH_RGX, '')
48
+ service_path_prefix.sub!(::Safrano::LEADING_SLASH_RGX, '')
49
+ # now is foo/bar/baz
50
+ path_prefix_rgx = Regexp.escape("/#{service_path_prefix}/$batch")
51
+ # now escaped path regexp /foo/bar/baz/$batch
52
+ # finaly just add start / end anchors
53
+ /\A#{path_prefix_rgx}\z/
54
+ end
55
+ # this will append rack-cors mw
56
+ # per default allow GET * from everywhere
57
+ # allow POST $batch from localhost only
58
+ use ::Rack::Cors do
59
+ allow do
60
+ origins LOCALHOST_ANY_PORT_RGX
61
+ resource batch_path_regexp, headers: :any, methods: CORS_BATCH_METHODS
62
+ resource '*', headers: :any, methods: CORS_RW_METHODS
63
+ end
64
+ allow do
65
+ origins '*'
66
+ resource '*', headers: :any, methods: CORS_RO_METHODS
67
+ end
68
+ end
69
+
70
+ # we need it in first place... move last element to beginin of mw stack
71
+ rackcors = @use.delete_at(-1)
72
+ @use.insert(0, rackcors)
73
+ end
74
+
75
+ def stack_has_rackcors
76
+ @middlewares.find { |mw| mw == Rack::Cors }
16
77
  end
17
78
  end
18
79
  end
@@ -7,14 +7,16 @@ module Safrano
7
7
  # handle GET PUT etc
8
8
  module MethodHandlers
9
9
  def odata_options
10
- @walker.finalize.tap_error { |err| return err.odata_get(self) }
11
- .if_valid do |_context|
12
- # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
13
- headers.delete('Content-Type')
14
- @response.headers.delete('Content-Type')
15
- @response.headers['Content-Type'] = ''
16
- [200, EMPTY_HASH, '']
17
- end
10
+ # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
11
+ headers.delete('Content-Type')
12
+ @response.headers.delete('Content-Type')
13
+ @response.headers['Content-Type'] = ''
14
+
15
+ # we only let rack-cors handle Cors OPTIONS .
16
+ # otherwise dont do it...
17
+ # see https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
18
+ # 501 not implemented
19
+ [501, EMPTY_HASH, '']
18
20
  end
19
21
 
20
22
  def odata_delete
@@ -97,6 +99,7 @@ module Safrano
97
99
  @type
98
100
  end
99
101
  end
102
+
100
103
  ## original coding:
101
104
  # # The set of media-types. Requests that do not indicate
102
105
  # # one of the media types presents in this list will not be eligible
@@ -107,7 +107,10 @@ module Safrano
107
107
  MIN_DATASERVICE_VERSION = '1'
108
108
  CV_MAX_DATASERVICE_VERSION = Contract.valid(MAX_DATASERVICE_VERSION).freeze
109
109
  CV_MIN_DATASERVICE_VERSION = Contract.valid(MIN_DATASERVICE_VERSION).freeze
110
+ TRAILING_SLASH_RGX = %r{/\z}.freeze
111
+ LEADING_SLASH_RGX = %r{\A/}.freeze
110
112
  include XMLNS
113
+ GENERIC_415_RESP = [415, {}, ['']].freeze
111
114
  # Base class for service. Subclass will be for V1, V2 etc...
112
115
  class ServiceBase
113
116
  include Safrano
@@ -166,7 +169,6 @@ module Safrano
166
169
  instance_eval(&block) if block_given?
167
170
  end
168
171
 
169
- TRAILING_SLASH = %r{/\z}.freeze
170
172
  DEFAULT_PATH_PREFIX = '/'
171
173
  DEFAULT_SERVER_URL = 'http://localhost:9494'
172
174
 
@@ -200,13 +202,14 @@ module Safrano
200
202
  end
201
203
 
202
204
  def path_prefix(path_pr)
203
- @xpath_prefix = path_pr.sub(TRAILING_SLASH, '')
205
+ @xpath_prefix = path_pr.sub(TRAILING_SLASH_RGX, '')
206
+ @xpath_prefix.freeze
204
207
  (@v1.xpath_prefix = @xpath_prefix) if @v1
205
208
  (@v2.xpath_prefix = @xpath_prefix) if @v2
206
209
  end
207
210
 
208
211
  def server_url(surl)
209
- @xserver_url = surl.sub(TRAILING_SLASH, '')
212
+ @xserver_url = surl.sub(TRAILING_SLASH_RGX, '')
210
213
  (@v1.xserver_url = @xserver_url) if @v1
211
214
  (@v2.xserver_url = @xserver_url) if @v2
212
215
  end
@@ -618,6 +621,8 @@ module Safrano
618
621
  Safrano::TransitionContentId
619
622
  ].freeze
620
623
 
624
+ include Safrano::Transitions::GetNextTrans::ByLongestMatch
625
+
621
626
  def build_allowed_transitions
622
627
  @allowed_transitions = if @function_imports.empty?
623
628
  (ALLOWED_TRANSITIONS_FIXED + [
@@ -677,7 +682,7 @@ module Safrano
677
682
  [200, CT_APPXML, [service_xml(req)]]
678
683
  else
679
684
  # this is returned by http://services.odata.org/V2/OData/Safrano.svc
680
- 415
685
+ GENERIC_415_RESP
681
686
  end
682
687
  end
683
688
  end
@@ -746,11 +751,13 @@ module Safrano
746
751
  Safrano::Transition::RESULT_END
747
752
  end
748
753
 
754
+ include Safrano::Transitions::GetNextTrans::ForJustTransitionEnd
755
+
749
756
  def odata_get(req)
750
757
  if req.accept?(APPXML)
751
758
  [200, CT_APPXML, [@service.metadata_xml(req)]]
752
759
  else
753
- 415
760
+ GENERIC_415_RESP
754
761
  end
755
762
  end
756
763
  end
@@ -12,6 +12,7 @@ module Safrano
12
12
  attr_reader :castfunc
13
13
  attr_reader :edm_type
14
14
  end
15
+
15
16
  # Model attribute (column) specific mapping
16
17
  class AttributeTypeMapping < TypeMapping
17
18
  attr_reader :attr_name
@@ -20,6 +21,7 @@ module Safrano
20
21
  @edm_type = builder.xedm_type
21
22
  @castfunc = builder.castfunc
22
23
  end
24
+
23
25
  # wrapper to handle API
24
26
  class Builder
25
27
  attr_reader :xedm_type
@@ -82,9 +84,8 @@ module Safrano
82
84
  end
83
85
 
84
86
  def match(curtyp)
85
- if @bui2 && (m = @bui2.match(curtyp))
86
- m
87
- elsif @bui1 && (m = @bui1.match(curtyp))
87
+ if (@bui2 && (m = @bui2.match(curtyp))) ||
88
+ (@bui1 && (m = @bui1.match(curtyp)))
88
89
  m
89
90
  elsif @rgx.match(curtyp)
90
91
  type_mapping
@@ -134,6 +135,7 @@ module Safrano
134
135
  RgxTypeMapping1Par.new(self)
135
136
  end
136
137
  end
138
+
137
139
  class Builder2Par < Builder
138
140
  def initialize(db_ty_rgx, proc)
139
141
  @db_types_rgx = db_ty_rgx
@@ -177,6 +179,7 @@ module Safrano
177
179
  @castfunc = builder.castfunc
178
180
  end
179
181
  end
182
+
180
183
  class RgxTypeMapping2Par < RgxTypeMapping
181
184
  def initialize(builder)
182
185
  @edm_type = builder.xedm_type
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Safrano
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.1'
5
5
  end
@@ -130,7 +130,7 @@ class JoinByPathsHelper < Set
130
130
  # associated model's primary key. Each current model object can be
131
131
  # associated with many associated model objects, and each associated
132
132
  # model object can be associated with many current model objects.
133
- # TODO: testcase for :one_through_one
133
+ # TODO: testcase for :one_through_one
134
134
  # when # :one_through_one :: Similar to many_to_many in terms of foreign keys, but only one object
135
135
  # is associated to the current object through the association.
136
136
  # Provides only getter methods, no setter or modification methods.
@@ -173,7 +173,6 @@ class JoinByPathsHelper < Set
173
173
 
174
174
  next result_
175
175
 
176
-
177
176
  end
178
177
 
179
178
  lks.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
@@ -229,6 +228,7 @@ module Sequel
229
228
  @alias_cnt = 0
230
229
  end
231
230
  end
231
+
232
232
  module ClassMethods
233
233
  attr_reader :aliases_sym
234
234
  attr_reader :alias_cnt
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.7.1
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - oz
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-22 00:00:00.000000000 Z
11
+ date: 2023-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -253,7 +253,7 @@ metadata:
253
253
  changelog_uri: https://gitlab.com/dm0da/safrano/blob/master/CHANGELOG
254
254
  source_code_uri: https://gitlab.com/dm0da/safrano/tree/master
255
255
  documentation_uri: https://gitlab.com/dm0da/safrano/-/blob/master/README.md
256
- post_install_message:
256
+ post_install_message:
257
257
  rdoc_options: []
258
258
  require_paths:
259
259
  - lib
@@ -268,8 +268,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
268
268
  - !ruby/object:Gem::Version
269
269
  version: '0'
270
270
  requirements: []
271
- rubygems_version: 3.2.5
272
- signing_key:
271
+ rubygems_version: 3.3.15
272
+ signing_key:
273
273
  specification_version: 4
274
274
  summary: Safrano is an OData server library
275
275
  test_files: []