safrano 0.4.3 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +8 -4
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +139 -642
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +196 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +78 -123
  23. data/lib/odata/error.rb +170 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +9 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +39 -25
  28. data/lib/odata/filter/sequel.rb +112 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +21 -18
  31. data/lib/odata/filter/tree.rb +78 -44
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +641 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +18 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +141 -0
  42. data/lib/safrano/core.rb +24 -106
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -24
  46. data/lib/safrano/rack_app.rb +62 -63
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +96 -38
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +156 -110
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +30 -11
@@ -0,0 +1,641 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Design: Collections are nothing more as Sequel based model classes that have
4
+ # somehow the character of an array (Enumerable)
5
+ # Thus Below we have called that "EntityClass". It's meant as "Collection"
6
+
7
+ require 'json'
8
+ require 'rexml/document'
9
+ require_relative '../safrano/core'
10
+ require_relative 'error'
11
+ require_relative 'collection_filter'
12
+ require_relative 'collection_order'
13
+ require_relative 'expand'
14
+ require_relative 'select'
15
+ require_relative 'url_parameters'
16
+ require_relative 'collection_media'
17
+ require_relative 'function_import'
18
+
19
+ module Safrano
20
+ # class methods. They Make heavy use of Sequel::Model functionality
21
+ # we will add this to our Model classes with "extend" --> self is the Class
22
+ module EntityClassBase
23
+ ONLY_INTEGER_RGX = /\A[+-]?\d+\z/.freeze
24
+
25
+ attr_reader :nav_collection_url_regexp
26
+ attr_reader :nav_entity_url_regexp
27
+ attr_reader :entity_id_url_regexp
28
+ attr_reader :nav_collection_attribs
29
+ attr_reader :nav_entity_attribs
30
+ attr_reader :data_fields
31
+ attr_reader :default_template
32
+ attr_reader :uri
33
+ attr_reader :odata_upk_parts
34
+ attr_reader :time_cols
35
+ attr_reader :namespace
36
+
37
+ # initialising block of code to be executed at end of
38
+ # ServerApp.publish_service after all model classes have been registered
39
+ # (without the associations/relationships)
40
+ # typically the block should contain the publication of the associations
41
+ attr_accessor :deferred_iblock
42
+
43
+ # convention: entityType is the namepsaced Ruby Model class --> name is just to_s
44
+ # Warning: for handling Navigation relations, we use anonymous collection classes
45
+ # dynamically subtyped from a Model class, and in such an anonymous class
46
+ # the class-name is not the OData Type. In these subclass we redefine "type_name"
47
+ # thus when we need the Odata type name, we shall use this method instead
48
+ # of just the collection class name
49
+ def type_name
50
+ @type_name
51
+ end
52
+
53
+ def default_entity_set_name
54
+ @default_entity_set_name
55
+ end
56
+
57
+ def build_type_name
58
+ @type_name = @namespace.to_s.empty? ? to_s : "#{@namespace}.#{self}"
59
+ @default_entity_set_name = to_s
60
+ end
61
+
62
+ # default for entity_set_name is @default_entity_set_name
63
+ def entity_set_name
64
+ @entity_set_name = (@entity_set_name || @default_entity_set_name)
65
+ end
66
+
67
+ def reset
68
+ # TODO: automatically reset all attributes?
69
+ @deferred_iblock = nil
70
+ @entity_set_name = nil
71
+ @uri = nil
72
+ @odata_upk_parts = nil
73
+ @uparms = nil
74
+ @params = nil
75
+ @cx = nil
76
+ @@time_cols = nil
77
+ end
78
+
79
+ def build_uri(uribase)
80
+ @uri = "#{uribase}/#{entity_set_name}"
81
+ end
82
+
83
+ def return_as_collection_descriptor
84
+ Safrano::FunctionImport::ResultAsEntityColl.new(self)
85
+ end
86
+
87
+ def return_as_instance_descriptor
88
+ Safrano::FunctionImport::ResultAsEntity.new(self)
89
+ end
90
+
91
+ def execute_deferred_iblock
92
+ instance_eval { @deferred_iblock.call } if @deferred_iblock
93
+ end
94
+
95
+ # Factory json-> Model Object instance
96
+ def new_from_hson_h(hash)
97
+ #enty = new
98
+ #enty.set_fields(hash, data_fields, missing: :skip)
99
+ enty = create(hash)
100
+ #enty.set(hash)
101
+ enty
102
+ end
103
+
104
+ def attrib_path_valid?(path)
105
+ @attribute_path_list.include? path
106
+ end
107
+
108
+ def expand_path_valid?(path)
109
+ @expand_path_list.include? path
110
+ end
111
+
112
+ def find_invalid_props(propsset)
113
+ (propsset - @all_props) unless propsset.subset?(@all_props)
114
+ end
115
+
116
+ def build_expand_path_list
117
+ @expand_path_list = expand_path_list
118
+ end
119
+
120
+ # list of table columns + all nav attribs --> all props
121
+ def build_all_props_list
122
+ @all_props = @columns_str.dup
123
+ (@all_props += @nav_entity_attribs_keys.map(&:to_s)) if @nav_entity_attribs
124
+ (@all_props += @nav_collection_attribs_keys.map(&:to_s)) if @nav_collection_attribs
125
+ @all_props = @all_props.to_set
126
+ end
127
+
128
+ def build_attribute_path_list
129
+ @attribute_path_list = attribute_path_list
130
+ end
131
+
132
+ MAX_DEPTH = 6
133
+ def attribute_path_list(depth = 0)
134
+ ret = @columns_str.dup
135
+ # break circles
136
+ return ret if depth > MAX_DEPTH
137
+
138
+ depth += 1
139
+
140
+ @nav_entity_attribs&.each do |a, k|
141
+ ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
142
+ end
143
+
144
+ @nav_collection_attribs&.each do |a, k|
145
+ ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
146
+ end
147
+ ret
148
+ end
149
+
150
+ def expand_path_list(depth = 0)
151
+ ret = []
152
+ ret.concat(@nav_entity_attribs_keys) if @nav_entity_attribs
153
+ ret.concat(@nav_collection_attribs_keys) if @nav_collection_attribs
154
+
155
+ # break circles
156
+ return ret if depth > MAX_DEPTH
157
+
158
+ depth += 1
159
+
160
+ @nav_entity_attribs&.each do |a, k|
161
+ ret.concat(k.expand_path_list(depth).map { |kc| "#{a}/#{kc}" })
162
+ end
163
+
164
+ @nav_collection_attribs&.each do |a, k|
165
+ ret.concat(k.expand_path_list(depth).map { |kc| "#{a}/#{kc}" })
166
+ end
167
+ ret
168
+ end
169
+
170
+ # add metadata xml to the passed REXML schema object
171
+ def add_metadata_rexml(schema)
172
+ enty = if @media_handler
173
+ schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true')
174
+ else
175
+ schema.add_element('EntityType', 'Name' => to_s)
176
+ end
177
+ # with their properties
178
+ db_schema.each do |pnam, prop|
179
+ if prop[:primary_key] == true
180
+ enty.add_element('Key').add_element('PropertyRef',
181
+ 'Name' => pnam.to_s)
182
+ end
183
+ attrs = { 'Name' => pnam.to_s,
184
+ # 'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
185
+ 'Type' => prop[:odata_edm_type] }
186
+ attrs['Nullable'] = 'false' if prop[:allow_null] == false
187
+ enty.add_element('Property', attrs)
188
+ end
189
+ enty
190
+ end
191
+
192
+ # metadata REXML data for a single Nav attribute
193
+ def metadata_nav_rexml_attribs(assoc, to_klass, relman)
194
+ from = to_s
195
+ to = to_klass.to_s
196
+ relman.get_metadata_xml_attribs(from,
197
+ to,
198
+ association_reflection(assoc.to_sym)[:type],
199
+ @namespace,
200
+ assoc)
201
+ end
202
+
203
+ # and their Nav attributes == Sequel Model association
204
+ def add_metadata_navs_rexml(schema_enty, relman)
205
+ @nav_entity_attribs&.each do |ne, klass|
206
+ nattr = metadata_nav_rexml_attribs(ne,
207
+ klass,
208
+ relman)
209
+ schema_enty.add_element('NavigationProperty', nattr)
210
+ end
211
+
212
+ @nav_collection_attribs&.each do |nc, klass|
213
+ nattr = metadata_nav_rexml_attribs(nc,
214
+ klass,
215
+ relman)
216
+ schema_enty.add_element('NavigationProperty', nattr)
217
+ end
218
+ end
219
+
220
+ # Recursive
221
+ # this method is performance critical. Called at least once for every request
222
+ def output_template(expand_list:,
223
+ select: Safrano::SelectBase::ALL)
224
+
225
+ return @default_template if expand_list.empty? && select.all_props?
226
+
227
+ template = {}
228
+ expand_e = {}
229
+ expand_c = {}
230
+ deferr = []
231
+
232
+ # 1. handle non-navigation properties, only consider $select
233
+ # 2. handle navigations properties, need to check $select and $expand
234
+ if select.all_props?
235
+
236
+ template[:all_values] = EMPTYH
237
+
238
+ # include all nav attributes -->
239
+ @nav_entity_attribs&.each do |attr, klass|
240
+ if expand_list.key?(attr)
241
+ expand_e[attr] = klass.output_template(expand_list: expand_list[attr])
242
+ else
243
+ deferr << attr
244
+ end
245
+ end
246
+
247
+ @nav_collection_attribs&.each do |attr, klass|
248
+ if expand_list.key?(attr)
249
+ expand_c[attr] = klass.output_template(expand_list: expand_list[attr])
250
+ else
251
+ deferr << attr
252
+ end
253
+ end
254
+
255
+ else
256
+ template[:selected_vals] = @columns_str & select.props
257
+
258
+ # include only selected nav attribs-->need additional intersection step
259
+ if @nav_entity_attribs
260
+ selected_nav_e = @nav_entity_attribs_keys & select.props
261
+
262
+ selected_nav_e&.each do |attr|
263
+ if expand_list.key?(attr)
264
+ klass = @nav_entity_attribs[attr]
265
+ expand_e[attr] = klass.output_template(expand_list: expand_list[attr])
266
+ else
267
+ deferr << attr
268
+ end
269
+ end
270
+ end
271
+ if @nav_collection_attribs
272
+ selected_nav_c = @nav_collection_attribs_keys & select.props
273
+ selected_nav_c&.each do |attr|
274
+ if expand_list.key?(attr)
275
+ klass = @nav_collection_attribs[attr]
276
+ expand_c[attr] = klass.output_template(expand_list: expand_list[attr])
277
+ else
278
+ deferr << attr
279
+ end
280
+ end
281
+ end
282
+ end
283
+ template[:expand_e] = expand_e
284
+ template[:expand_c] = expand_c
285
+ template[:deferr] = deferr
286
+ template
287
+ end
288
+
289
+ # this functionally similar to the Sequel Rels (many_to_one etc)
290
+ # We need to base this on the Sequel rels, or extend them
291
+ def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
292
+ @nav_collection_attribs = (@nav_collection_attribs || {})
293
+ @nav_collection_attribs_keys = (@nav_collection_attribs_keys || [])
294
+ # DONE: Error handling. This requires that associations
295
+ # have been properly defined with Sequel before
296
+ assoc = all_association_reflections.find do |a|
297
+ a[:name] == assoc_symb && a[:model] == self
298
+ end
299
+
300
+ raise Safrano::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
301
+
302
+ attr_class = assoc[:class_name].constantize
303
+ lattr_name_str = (attr_name_str || assoc_symb.to_s)
304
+
305
+ # check duplicate attributes names
306
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
307
+
308
+ if @nav_entity_attribs_keys
309
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_entity_attribs_keys.include? lattr_name_str
310
+ end
311
+
312
+ @nav_collection_attribs[lattr_name_str] = attr_class
313
+ @nav_collection_attribs_keys << lattr_name_str
314
+ @nav_collection_url_regexp = @nav_collection_attribs_keys.join('|')
315
+ end
316
+
317
+ def add_nav_prop_single(assoc_symb, attr_name_str = nil)
318
+ @nav_entity_attribs = (@nav_entity_attribs || {})
319
+ @nav_entity_attribs_keys = (@nav_entity_attribs_keys || [])
320
+ # DONE: Error handling. This requires that associations
321
+ # have been properly defined with Sequel before
322
+ assoc = all_association_reflections.find do |a|
323
+ a[:name] == assoc_symb && a[:model] == self
324
+ end
325
+
326
+ raise Safrano::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
327
+
328
+ attr_class = assoc[:class_name].constantize
329
+ lattr_name_str = (attr_name_str || assoc_symb.to_s)
330
+
331
+ # check duplicate attributes names
332
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
333
+
334
+ if @nav_collection_attribs_keys
335
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_collection_attribs_keys.include? lattr_name_str
336
+ end
337
+
338
+ @nav_entity_attribs[lattr_name_str] = attr_class
339
+ @nav_entity_attribs_keys << lattr_name_str
340
+ @nav_entity_url_regexp = @nav_entity_attribs_keys.join('|')
341
+ end
342
+
343
+ EMPTYH = {}.freeze
344
+
345
+ def build_default_template
346
+ @default_template = { all_values: EMPTYH }
347
+ if @nav_entity_attribs || @nav_collection_attribs
348
+ @default_template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
349
+ end
350
+ end
351
+
352
+ def finalize_publishing
353
+ build_type_name
354
+
355
+ # build default output template structure
356
+ build_default_template
357
+
358
+ # Time columns
359
+ @time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
360
+
361
+ # add edm_types into schema
362
+ db_schema.each do |_col, props|
363
+ props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type])
364
+ end
365
+
366
+ # and finally build the path lists and allowed tr's
367
+ build_attribute_path_list
368
+ build_expand_path_list
369
+ build_all_props_list
370
+
371
+ build_allowed_transitions
372
+ build_entity_allowed_transitions
373
+
374
+ # for media
375
+ finalize_media if self.respond_to? :finalize_media
376
+ end
377
+
378
+ KEYPRED_URL_REGEXP = /\A\(\s*'?([\w=,'\s]+)'?\s*\)(.*)/.freeze
379
+ def prepare_pk
380
+ if primary_key.is_a? Array
381
+ @pk_names = []
382
+ @pk_cast_from_string = {}
383
+ odata_upk_build = []
384
+ primary_key.each { |pk|
385
+ @pk_names << pk.to_s
386
+ kvpredicate = case db_schema[pk][:type]
387
+ when :integer
388
+ @pk_cast_from_string[pk] = ->(str) { Integer(str) }
389
+ '?'
390
+ else
391
+ "'?'"
392
+ end
393
+ odata_upk_build << "#{pk}=#{kvpredicate}"
394
+ }
395
+ @odata_upk_parts = odata_upk_build.join(',').split('?')
396
+
397
+ # regex parts for unordered matching
398
+ @iuk_rgx_parts = primary_key.map { |pk|
399
+ kvpredicate = case db_schema[pk][:type]
400
+ when :integer
401
+ "(\\d+)"
402
+ else
403
+ "'(\\w+)'"
404
+ end
405
+ [pk, "#{pk}=#{kvpredicate}"]
406
+ }.to_h
407
+
408
+ # single regex assuming the key fields are ordered !
409
+ @iuk_rgx = /\A#{@iuk_rgx_parts.values.join(',\s*')}\z/
410
+
411
+ @iuk_rgx_parts.transform_values! { |v| /\A#{v}\z/ }
412
+
413
+ @entity_id_url_regexp = KEYPRED_URL_REGEXP
414
+ else
415
+ @pk_names = [primary_key.to_s]
416
+ @pk_cast_from_string = nil
417
+ kvpredicate = case db_schema[primary_key][:type]
418
+ when :integer
419
+ @pk_cast_from_string = ->(str) { Integer(str) }
420
+ "(\\d+)"
421
+ else
422
+ "'(\\w+)'"
423
+ end
424
+ @iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
425
+ @entity_id_url_regexp = KEYPRED_URL_REGEXP
426
+ end
427
+ end
428
+
429
+ def prepare_fields
430
+ # columns as strings
431
+ @columns_str = @columns.map(&:to_s)
432
+
433
+ @data_fields = db_schema.map do |col, cattr|
434
+ cattr[:primary_key] ? nil : col
435
+ end.select { |col| col }
436
+ end
437
+
438
+ def invalid_hash_data?(data)
439
+ data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
440
+ end
441
+
442
+ ## A regexp matching all allowed attributes of the Entity
443
+ ## (eg ID|name|size etc... ) at start position and returning the rest
444
+ def transition_attribute_regexp
445
+ # db_schema.map { |sch| sch[0] }.join('|')
446
+ # @columns is from Sequel Model
447
+ %r{\A/(#{@columns.join('|')})(.*)\z}
448
+ end
449
+
450
+ # super-minimal type check, but better as nothing
451
+ def cast_odata_val(val, pk_cast)
452
+ pk_cast ? Contract.valid(pk_cast.call(val)) : Contract.valid(val) # no cast needed, eg for string
453
+ rescue StandardError => e
454
+ RubyStandardErrorException.new(e)
455
+ end
456
+
457
+ CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
458
+ # in-changeset requests get their own transaction
459
+ case assoc[:type]
460
+ when :one_to_many, :one_to_one
461
+ Safrano.create_nav_relation(new_entity, assoc, parent)
462
+ new_entity.save(transaction: false)
463
+ when :many_to_one
464
+ new_entity.save(transaction: false)
465
+ Safrano.create_nav_relation(new_entity, assoc, parent)
466
+ parent.save(transaction: false)
467
+ # else # not supported
468
+ end
469
+ end
470
+ def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
471
+ if req.in_changeset
472
+ # in-changeset requests get their own transaction
473
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
474
+ else
475
+ db.transaction do
476
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
477
+ end
478
+ end
479
+ end
480
+ # methods related to transitions to next state (cf. walker)
481
+ module Transitions
482
+ def allowed_transitions
483
+ @allowed_transitions
484
+ end
485
+
486
+ def entity_allowed_transitions
487
+ @entity_allowed_transitions
488
+ end
489
+
490
+ def build_allowed_transitions
491
+ @allowed_transitions = [Safrano::TransitionEnd,
492
+ Safrano::TransitionCount,
493
+ Safrano::Transition.new(entity_id_url_regexp,
494
+ trans: 'transition_id')].freeze
495
+ end
496
+
497
+ def build_entity_allowed_transitions
498
+ @entity_allowed_transitions = [
499
+ Safrano::TransitionEnd,
500
+ Safrano::TransitionCount,
501
+ Safrano::TransitionLinks,
502
+ Safrano::TransitionValue,
503
+ Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
504
+ ]
505
+ if (ncurgx = @nav_collection_url_regexp)
506
+ @entity_allowed_transitions <<
507
+ Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
508
+ end
509
+ if (neurgx = @nav_entity_url_regexp)
510
+ @entity_allowed_transitions <<
511
+ Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
512
+ end
513
+ @entity_allowed_transitions << Safrano::Transition.new(%r{\A/(\w+)(.*)\z}, trans: 'transition_invalid_attribute')
514
+ @entity_allowed_transitions.freeze
515
+ @entity_allowed_transitions
516
+ end
517
+ end
518
+ include Transitions
519
+ end
520
+
521
+ # special handling for composite key
522
+ module EntityClassMultiPK
523
+ include EntityClassBase
524
+ def pk_lookup_expr(ids)
525
+ primary_key.zip(ids)
526
+ end
527
+
528
+ # input fx='aas',fy_w='0001'
529
+ # output true, ['aas', '0001'] ... or false when typ-error
530
+ def parse_odata_key(mid)
531
+ # @iuk_rgx is (needs to be) built on start with
532
+ # collklass.prepare_pk
533
+
534
+ # first try to match single regex assuming orderd key fields
535
+ if (md = @iuk_rgx.match(mid))
536
+ md = md.captures
537
+ mdc = []
538
+ primary_key.each_with_index do |pk, i|
539
+ mdc << if (pk_cast = @pk_cast_from_string[pk])
540
+ pk_cast.call(md[i])
541
+ else
542
+ md[i] # no cast needed, eg for string
543
+ end
544
+ end
545
+
546
+ else
547
+
548
+ # order key fields didnt match--> try and collect/check each parts unordered
549
+ scan_rgx_parts = @iuk_rgx_parts.dup
550
+ mdch = {}
551
+
552
+ mid.split(/\s*,\s*/).each { |midpart|
553
+ mval = nil
554
+ mpk, mrgx = scan_rgx_parts.find { |pk, rgx|
555
+ if (md = rgx.match(midpart))
556
+ mval = md[1]
557
+ end
558
+ }
559
+ if mpk and mval
560
+ mdch[mpk] = if (pk_cast = @pk_cast_from_string[mpk])
561
+ pk_cast.call(mval)
562
+ else
563
+ mval # no cast needed, eg for string
564
+ end
565
+ scan_rgx_parts.delete(mpk)
566
+ else
567
+ return Contract::NOK
568
+ end
569
+ }
570
+ # normally arriving here we have mdch filled with key values pairs,
571
+ # but not in the model key ordering. lets just re-order the values
572
+ mdc = @iuk_rgx_parts.keys.map { |pk| mdch[pk] }
573
+
574
+ end
575
+ Contract.valid(mdc)
576
+ # catch remaining convertion errors that we failed to prevent
577
+ rescue StandardError => e
578
+ RubyStandardErrorException.new(e)
579
+ end
580
+ end
581
+
582
+ # special handling for single key
583
+ module EntityClassSinglePK
584
+ include EntityClassBase
585
+
586
+ def parse_odata_key(rawid)
587
+ if (md = @iuk_rgx.match(rawid))
588
+ if (@pk_cast_from_string)
589
+ Contract.valid(@pk_cast_from_string.call(md[1]))
590
+ else
591
+ Contract.valid(md[1]) # no cast needed, eg for string
592
+ end
593
+ else
594
+ Contract::NOK
595
+ end
596
+ rescue StandardError => e
597
+ RubyStandardErrorException.new(e)
598
+ end
599
+
600
+ def pk_lookup_expr(id)
601
+ id
602
+ end
603
+ end
604
+
605
+ # normal handling for non-media entity
606
+ module EntityClassNonMedia
607
+ # POST for non-media entity collection -->
608
+ # 1. Create and add entity from payload
609
+ # 2. Create relationship if needed
610
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
611
+ # TODO: this is for v2 only...
612
+ req.with_parsed_data do |data|
613
+ data.delete('__metadata')
614
+
615
+ # validate payload column names
616
+ if (invalid = invalid_hash_data?(data))
617
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
618
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
619
+ end
620
+
621
+ if req.accept?(APPJSON)
622
+ new_entity = new_from_hson_h(data)
623
+ if parent
624
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
625
+ else
626
+ # in-changeset requests get their own transaction
627
+ new_entity.save(transaction: !req.in_changeset)
628
+ end
629
+ req.register_content_id_ref(new_entity)
630
+ new_entity.copy_request_infos(req)
631
+ # json is default content type so we dont need to specify it here again
632
+ # TODO quirks array mode !
633
+ # [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
634
+ [201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)]
635
+ else # TODO: other formats
636
+ 415
637
+ end
638
+ end
639
+ end
640
+ end
641
+ end