safrano 0.4.3 → 0.4.4

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