safrano 0.7.1 → 0.8.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: 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: []