safrano 0.4.2 → 0.5.0

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 +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  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 +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  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 +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  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 +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'complex_type'
4
+ require_relative 'edm/primitive_types'
5
+ require_relative 'transition'
6
+
7
+ module Safrano
8
+ def self.FunctionImport(name)
9
+ FunctionImport::Function.new(name)
10
+ end
11
+
12
+ module FunctionImport
13
+ class Function
14
+ @allowed_transitions = [Safrano::TransitionEnd]
15
+ attr_reader :name
16
+ attr_reader :proc
17
+
18
+ def initialize(name)
19
+ @name = name
20
+ @http_method = 'GET'
21
+ end
22
+
23
+ def allowed_transitions
24
+ [Safrano::TransitionEnd]
25
+ end
26
+
27
+ def input(**parmtypes)
28
+ @input = {}
29
+ parmtypes.each do |k, t|
30
+ @input[k] = case t.name
31
+ when 'Integer'
32
+ Safrano::Edm::Edm::Int32
33
+ when 'String'
34
+ Safrano::Edm::Edm::String
35
+ when 'Float'
36
+ Safrano::Edm::Edm::Double
37
+ when 'DateTime'
38
+ Safrano::Edm::Edm::DateTime
39
+ else
40
+ t
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def return(klassmod, &proc)
47
+ raise('Please provide a code block') unless block_given?
48
+
49
+ @returning = if klassmod.respond_to? :return_as_instance_descriptor
50
+ klassmod.return_as_instance_descriptor
51
+ else
52
+ # if it's neither a ComplexType nor a Model-Entity
53
+ # --> assume it is a Primitive
54
+ ResultAsPrimitiveType.new(klassmod)
55
+ end
56
+ @proc = proc
57
+ self
58
+ end
59
+
60
+ def return_collection(klassmod, &proc)
61
+ raise('Please provide a code block') unless block_given?
62
+
63
+ @returning = if klassmod.respond_to? :return_as_collection_descriptor
64
+ klassmod.return_as_collection_descriptor
65
+ else
66
+ # if it's neither a ComplexType nor a Modle-Entity
67
+ # --> assume it is a Primitive
68
+ ResultAsPrimitiveTypeColl.new(klassmod)
69
+ end
70
+ @proc = proc
71
+ self
72
+ end
73
+ # def initialize_params
74
+ # @uparms = UrlParameters4Func.new(@model, @params)
75
+ # end
76
+
77
+ def check_missing_params
78
+ # do we have all parameters provided ? use Set difference to check
79
+ pkeys = @params.keys.map(&:to_sym).to_set
80
+ unless (idiff = @input.keys.to_set - pkeys).empty?
81
+
82
+ Safrano::ServiceOperationParameterMissing.new(
83
+ missing: idiff.to_a,
84
+ sopname: @name
85
+ )
86
+ else
87
+ Contract::OK
88
+ end
89
+ end
90
+
91
+ def check_url_func_params
92
+ @funcparams = {}
93
+ return nil unless @input # anything to check ?
94
+
95
+ # do we have all parameters provided ?
96
+ check_missing_params.tap_error { |error| return error }
97
+ # ==> all params were provided
98
+
99
+ # now we shall check the content and type of the parameters
100
+ @input.each do |ksym, typ|
101
+ typ.convert_from_urlparam(v = @params[ksym.to_s])
102
+ .tap_valid do |retval|
103
+ @funcparams[ksym] = retval
104
+ end
105
+ .tap_error do
106
+ # return is really needed here, or we end up returning nil below
107
+ return parameter_convertion_error(ksym, typ, v)
108
+ end
109
+ end
110
+ nil
111
+ end
112
+
113
+ def parameter_convertion_error(param, type, val)
114
+ Safrano::ServiceOperationParameterError.new(type: type,
115
+ value: val,
116
+ param: param,
117
+ sopname: @name)
118
+ end
119
+
120
+ def add_metadata_rexml(ec)
121
+ ## https://services.odata.org/V2/OData/Safrano.svc/$metadata
122
+ # <FunctionImport Name="GetProductsByRating" EntitySet="Products" ReturnType="Collection(ODataDemo.Product)" m:HttpMethod="GET">
123
+ # <Parameter Name="rating" Type="Edm.Int32" Mode="In"/>
124
+ # </FunctionImport>
125
+ funky = ec.add_element('FunctionImport',
126
+ 'Name' => @name.to_s,
127
+ # EntitySet= @entity_set ,
128
+ 'ReturnType' => @returning.type_metadata,
129
+ 'm:HttpMethod' => @http_method)
130
+ @input.each do |iname, type|
131
+ funky.add_element('Parameter',
132
+ 'Name' => iname.to_s,
133
+ 'Type' => type.type_name,
134
+ 'Mode' => 'In')
135
+ end if @input
136
+ funky
137
+ end
138
+
139
+ def with_validated_get(req)
140
+ # initialize_params
141
+ return yield unless (@error = check_url_func_params)
142
+
143
+ @error.odata_get(req) if @error
144
+ end
145
+
146
+ def to_odata_json(req)
147
+ result = @proc.call(**@funcparams)
148
+ @returning.to_odata_json(result, req)
149
+ end
150
+
151
+ def odata_get_output(req)
152
+ [200, EMPTY_HASH, [to_odata_json(req)]]
153
+ end
154
+
155
+ def odata_get(req)
156
+ @params = req.params
157
+
158
+ with_validated_get(req) do
159
+ odata_get_output(req)
160
+ end
161
+ end
162
+
163
+ def transition_end(_match_result)
164
+ Transition::RESULT_END
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,639 @@
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
+
303
+ # check duplicate attributes names
304
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
305
+
306
+ if @nav_entity_attribs_keys
307
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_entity_attribs_keys.include? lattr_name_str
308
+ end
309
+
310
+ @nav_collection_attribs[lattr_name_str] = attr_class
311
+ @nav_collection_attribs_keys << lattr_name_str
312
+ @nav_collection_url_regexp = @nav_collection_attribs_keys.join('|')
313
+ end
314
+
315
+ def add_nav_prop_single(assoc_symb, attr_name_str = nil)
316
+ @nav_entity_attribs = (@nav_entity_attribs || {})
317
+ @nav_entity_attribs_keys = (@nav_entity_attribs_keys || [])
318
+ # DONE: Error handling. This requires that associations
319
+ # have been properly defined with Sequel before
320
+ assoc = all_association_reflections.find do |a|
321
+ a[:name] == assoc_symb && a[:model] == self
322
+ end
323
+
324
+ raise Safrano::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
325
+
326
+ attr_class = assoc[:class_name].constantize
327
+ lattr_name_str = (attr_name_str || assoc_symb.to_s)
328
+
329
+ # check duplicate attributes names
330
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
331
+
332
+ if @nav_collection_attribs_keys
333
+ raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_collection_attribs_keys.include? lattr_name_str
334
+ end
335
+
336
+ @nav_entity_attribs[lattr_name_str] = attr_class
337
+ @nav_entity_attribs_keys << lattr_name_str
338
+ @nav_entity_url_regexp = @nav_entity_attribs_keys.join('|')
339
+ end
340
+
341
+ EMPTYH = {}.freeze
342
+
343
+ def build_default_template
344
+ @default_template = { all_values: EMPTYH }
345
+ if @nav_entity_attribs || @nav_collection_attribs
346
+ @default_template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
347
+ end
348
+ end
349
+
350
+ def finalize_publishing
351
+ build_type_name
352
+
353
+ # build default output template structure
354
+ build_default_template
355
+
356
+ # Time columns
357
+ @time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
358
+
359
+ # add edm_types into schema
360
+ db_schema.each do |_col, props|
361
+ props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type])
362
+ end
363
+
364
+ # and finally build the path lists and allowed tr's
365
+ build_attribute_path_list
366
+ build_expand_path_list
367
+ build_all_props_list
368
+
369
+ build_allowed_transitions
370
+ build_entity_allowed_transitions
371
+
372
+ # for media
373
+ finalize_media if self.respond_to? :finalize_media
374
+ end
375
+
376
+ KEYPRED_URL_REGEXP = /\A\(\s*'?([\w=,'\s]+)'?\s*\)(.*)/.freeze
377
+ def prepare_pk
378
+ if primary_key.is_a? Array
379
+ @pk_names = []
380
+ @pk_cast_from_string = {}
381
+ odata_upk_build = []
382
+ primary_key.each { |pk|
383
+ @pk_names << pk.to_s
384
+ kvpredicate = case db_schema[pk][:type]
385
+ when :integer
386
+ @pk_cast_from_string[pk] = ->(str) { Integer(str) }
387
+ '?'
388
+ else
389
+ "'?'"
390
+ end
391
+ odata_upk_build << "#{pk}=#{kvpredicate}"
392
+ }
393
+ @odata_upk_parts = odata_upk_build.join(',').split('?')
394
+
395
+ # regex parts for unordered matching
396
+ @iuk_rgx_parts = primary_key.map { |pk|
397
+ kvpredicate = case db_schema[pk][:type]
398
+ when :integer
399
+ "(\\d+)"
400
+ else
401
+ "'(\\w+)'"
402
+ end
403
+ [pk, "#{pk}=#{kvpredicate}"]
404
+ }.to_h
405
+
406
+ # single regex assuming the key fields are ordered !
407
+ @iuk_rgx = /\A#{@iuk_rgx_parts.values.join(',\s*')}\z/
408
+
409
+ @iuk_rgx_parts.transform_values! { |v| /\A#{v}\z/ }
410
+
411
+ @entity_id_url_regexp = KEYPRED_URL_REGEXP
412
+ else
413
+ @pk_names = [primary_key.to_s]
414
+ @pk_cast_from_string = nil
415
+ kvpredicate = case db_schema[primary_key][:type]
416
+ when :integer
417
+ @pk_cast_from_string = ->(str) { Integer(str) }
418
+ "(\\d+)"
419
+ else
420
+ "'(\\w+)'"
421
+ end
422
+ @iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
423
+ @entity_id_url_regexp = KEYPRED_URL_REGEXP
424
+ end
425
+ end
426
+
427
+ def prepare_fields
428
+ # columns as strings
429
+ @columns_str = @columns.map(&:to_s)
430
+
431
+ @data_fields = db_schema.map do |col, cattr|
432
+ cattr[:primary_key] ? nil : col
433
+ end.select { |col| col }
434
+ end
435
+
436
+ def invalid_hash_data?(data)
437
+ data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
438
+ end
439
+
440
+ ## A regexp matching all allowed attributes of the Entity
441
+ ## (eg ID|name|size etc... ) at start position and returning the rest
442
+ def transition_attribute_regexp
443
+ # db_schema.map { |sch| sch[0] }.join('|')
444
+ # @columns is from Sequel Model
445
+ %r{\A/(#{@columns.join('|')})(.*)\z}
446
+ end
447
+
448
+ # super-minimal type check, but better as nothing
449
+ def cast_odata_val(val, pk_cast)
450
+ pk_cast ? Contract.valid(pk_cast.call(val)) : Contract.valid(val) # no cast needed, eg for string
451
+ rescue StandardError => e
452
+ RubyStandardErrorException.new(e)
453
+ end
454
+
455
+ CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
456
+ # in-changeset requests get their own transaction
457
+ case assoc[:type]
458
+ when :one_to_many, :one_to_one
459
+ Safrano.create_nav_relation(new_entity, assoc, parent)
460
+ new_entity.save(transaction: false)
461
+ when :many_to_one
462
+ new_entity.save(transaction: false)
463
+ Safrano.create_nav_relation(new_entity, assoc, parent)
464
+ parent.save(transaction: false)
465
+ # else # not supported
466
+ end
467
+ end
468
+ def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
469
+ if req.in_changeset
470
+ # in-changeset requests get their own transaction
471
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
472
+ else
473
+ db.transaction do
474
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
475
+ end
476
+ end
477
+ end
478
+ # methods related to transitions to next state (cf. walker)
479
+ module Transitions
480
+ def allowed_transitions
481
+ @allowed_transitions
482
+ end
483
+
484
+ def entity_allowed_transitions
485
+ @entity_allowed_transitions
486
+ end
487
+
488
+ def build_allowed_transitions
489
+ @allowed_transitions = [Safrano::TransitionEnd,
490
+ Safrano::TransitionCount,
491
+ Safrano::Transition.new(entity_id_url_regexp,
492
+ trans: 'transition_id')].freeze
493
+ end
494
+
495
+ def build_entity_allowed_transitions
496
+ @entity_allowed_transitions = [
497
+ Safrano::TransitionEnd,
498
+ Safrano::TransitionCount,
499
+ Safrano::TransitionLinks,
500
+ Safrano::TransitionValue,
501
+ Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
502
+ ]
503
+ if (ncurgx = @nav_collection_url_regexp)
504
+ @entity_allowed_transitions <<
505
+ Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
506
+ end
507
+ if (neurgx = @nav_entity_url_regexp)
508
+ @entity_allowed_transitions <<
509
+ Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
510
+ end
511
+ @entity_allowed_transitions << Safrano::Transition.new(%r{\A/(\w+)(.*)\z}, trans: 'transition_invalid_attribute')
512
+ @entity_allowed_transitions.freeze
513
+ @entity_allowed_transitions
514
+ end
515
+ end
516
+ include Transitions
517
+ end
518
+
519
+ # special handling for composite key
520
+ module EntityClassMultiPK
521
+ include EntityClassBase
522
+ def pk_lookup_expr(ids)
523
+ primary_key.zip(ids)
524
+ end
525
+
526
+ # input fx='aas',fy_w='0001'
527
+ # output true, ['aas', '0001'] ... or false when typ-error
528
+ def parse_odata_key(mid)
529
+ # @iuk_rgx is (needs to be) built on start with
530
+ # collklass.prepare_pk
531
+
532
+ # first try to match single regex assuming orderd key fields
533
+ if (md = @iuk_rgx.match(mid))
534
+ md = md.captures
535
+ mdc = []
536
+ primary_key.each_with_index do |pk, i|
537
+ mdc << if (pk_cast = @pk_cast_from_string[pk])
538
+ pk_cast.call(md[i])
539
+ else
540
+ md[i] # no cast needed, eg for string
541
+ end
542
+ end
543
+
544
+ else
545
+
546
+ # order key fields didnt match--> try and collect/check each parts unordered
547
+ scan_rgx_parts = @iuk_rgx_parts.dup
548
+ mdch = {}
549
+
550
+ mid.split(/\s*,\s*/).each { |midpart|
551
+ mval = nil
552
+ mpk, mrgx = scan_rgx_parts.find { |pk, rgx|
553
+ if (md = rgx.match(midpart))
554
+ mval = md[1]
555
+ end
556
+ }
557
+ if mpk and mval
558
+ mdch[mpk] = if (pk_cast = @pk_cast_from_string[mpk])
559
+ pk_cast.call(mval)
560
+ else
561
+ mval # no cast needed, eg for string
562
+ end
563
+ scan_rgx_parts.delete(mpk)
564
+ else
565
+ return Contract::NOK
566
+ end
567
+ }
568
+ # normally arriving here we have mdch filled with key values pairs,
569
+ # but not in the model key ordering. lets just re-order the values
570
+ mdc = @iuk_rgx_parts.keys.map { |pk| mdch[pk] }
571
+
572
+ end
573
+ Contract.valid(mdc)
574
+ # catch remaining convertion errors that we failed to prevent
575
+ rescue StandardError => e
576
+ RubyStandardErrorException.new(e)
577
+ end
578
+ end
579
+
580
+ # special handling for single key
581
+ module EntityClassSinglePK
582
+ include EntityClassBase
583
+
584
+ def parse_odata_key(rawid)
585
+ if (md = @iuk_rgx.match(rawid))
586
+ if (@pk_cast_from_string)
587
+ Contract.valid(@pk_cast_from_string.call(md[1]))
588
+ else
589
+ Contract.valid(md[1]) # no cast needed, eg for string
590
+ end
591
+ else
592
+ Contract::NOK
593
+ end
594
+ rescue StandardError => e
595
+ RubyStandardErrorException.new(e)
596
+ end
597
+
598
+ def pk_lookup_expr(id)
599
+ id
600
+ end
601
+ end
602
+
603
+ # normal handling for non-media entity
604
+ module EntityClassNonMedia
605
+ # POST for non-media entity collection -->
606
+ # 1. Create and add entity from payload
607
+ # 2. Create relationship if needed
608
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
609
+ # TODO: this is for v2 only...
610
+ req.with_parsed_data do |data|
611
+ data.delete('__metadata')
612
+
613
+ # validate payload column names
614
+ if (invalid = invalid_hash_data?(data))
615
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
616
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
617
+ end
618
+
619
+ if req.accept?(APPJSON)
620
+ new_entity = new_from_hson_h(data)
621
+ if parent
622
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
623
+ else
624
+ # in-changeset requests get their own transaction
625
+ new_entity.save(transaction: !req.in_changeset)
626
+ end
627
+ req.register_content_id_ref(new_entity)
628
+ new_entity.copy_request_infos(req)
629
+ # json is default content type so we dont need to specify it here again
630
+ # TODO quirks array mode !
631
+ # [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
632
+ [201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)]
633
+ else # TODO: other formats
634
+ 415
635
+ end
636
+ end
637
+ end
638
+ end
639
+ end