safrano 0.4.1 → 0.4.6

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 +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  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 +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  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 +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  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 +113 -63
  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 +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -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