safrano 0.4.3 → 0.5.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.
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