safrano 0.4.3 → 0.4.4

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 +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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 497482614bfc6a87f8a42b9378a4fc8099ddb5b4f180d747b3717a6952f027b0
4
- data.tar.gz: cd5236302671643fd9fbd9bcd200034f305ce7069e5d56ab2db2225dcfb4ce87
3
+ metadata.gz: 88f62ce611a5030382ea19c2e7c22b47f454b85c51261e5bad060d82e4680dff
4
+ data.tar.gz: '088e66ed4a107a3bef8dd6db68c8ca626f7d3acef6ed2e58eb53d871e6349613'
5
5
  SHA512:
6
- metadata.gz: 4415ca4dc24362f3c179f0cc40f1d73fac6fb7207c0571ef63f1af318fc345e0be8043d8aa1b5ab48e0ead1f5c904dcab07b84061b3ca47f6b7d3c38696d4ae8
7
- data.tar.gz: 6646d12aadb3827b43c013a3da6c6223d072e5676f8676eac0db59ad8e3ac80d1a64ceb0f5e87d52bb75e5b556eecda701e2d473c4bdd3b650cb680b6ce19b96
6
+ metadata.gz: 2d719e317178a4baebc2e40cc50b95559921fe2936a6a03437f7f92714d173ef6160b10620ce5476ea54a7433b3dd1366b49ec37a48b121b8f808ad030e2c364
7
+ data.tar.gz: 535ee0f1d4fed30a0c94918c31149a2e95fce90051548a2ccca6bffb71930c0767543255695496c3623249070b5efb1b6a203a1dae6f868c1e4da9a126a85346
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # needed for ruby < 2.5
4
+ module Safrano
5
+ module CoreExt
6
+ module Dir
7
+ module Iter
8
+ def each_child(dir)
9
+ ::Dir.foreach(dir) do |x|
10
+ next if (x == '.') || (x == '..')
11
+
12
+ yield x
13
+ end
14
+ end unless ::Dir.respond_to? :each_child
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # picked from activsupport; needed for ruby < 2.5
2
+ # Destructively converts all keys using the +block+ operations.
3
+ # Same as +transform_keys+ but modifies +self+.
4
+ module Safrano
5
+ module CoreIncl
6
+ module Hash
7
+ module Transform
8
+ def transform_keys!
9
+ keys.each do |key|
10
+ self[yield(key)] = delete(key)
11
+ end
12
+ self
13
+ end unless method_defined? :transform_keys!
14
+
15
+ def symbolize_keys!
16
+ transform_keys! { |key| key.to_sym rescue key }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreExt
5
+ module Integer
6
+ module Edm
7
+ def type_name
8
+ 'Edm.Int32'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module Safrano
2
+ module CoreIncl
3
+ module REXML
4
+ module Document
5
+ module Output
6
+ def to_pretty_xml
7
+ formatter = ::REXML::Formatters::Pretty.new(2)
8
+ formatter.compact = true
9
+ formatter.write(root, strio = '')
10
+ strio
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreIncl
5
+ module String
6
+ module Convert
7
+ # thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
8
+ def constantize
9
+ names = split('::')
10
+ names.shift if names.empty? || names.first.empty?
11
+
12
+ const = Object
13
+ names.each do |name|
14
+ const = if const.const_defined?(name)
15
+ const.const_get(name)
16
+ else
17
+ const.const_missing(name)
18
+ end
19
+ end
20
+ const
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Safrano
4
+ module CoreExt
5
+ module String
6
+ module Edm
7
+ def type_name
8
+ 'Edm.String'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'Dir/iter'
2
+
3
+ Dir.extend Safrano::CoreExt::Dir::Iter
@@ -0,0 +1,3 @@
1
+ require_relative 'Hash/transform'
2
+
3
+ Hash.include Safrano::CoreIncl::Hash::Transform
@@ -0,0 +1,3 @@
1
+ require_relative 'Integer/edm'
2
+
3
+ Integer.extend Safrano::CoreExt::Integer::Edm
@@ -0,0 +1,3 @@
1
+ require_relative 'REXML/Document/output'
2
+
3
+ REXML::Document.include Safrano::CoreIncl::REXML::Document::Output
@@ -0,0 +1,5 @@
1
+ require_relative 'String/edm'
2
+ require_relative 'String/convert'
3
+
4
+ String.extend Safrano::CoreExt::String::Edm
5
+ String.include Safrano::CoreIncl::String::Convert
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require_relative '../safrano/core.rb'
3
5
  require_relative './entity.rb'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  # Represents a named and valued attribute of an Entity
7
9
  class Attribute
8
10
  attr_reader :name
9
11
  attr_reader :entity
12
+
10
13
  def initialize(entity, name)
11
14
  @entity = entity
12
15
  @name = name
@@ -30,7 +33,8 @@ module OData
30
33
  if req.walker.raw_value
31
34
  [200, CT_TEXT, value.to_s]
32
35
  elsif req.accept?(APPJSON)
33
- [200, CT_JSON, to_odata_json(service: req.service)]
36
+ # json is default content type so we dont need to specify it here again
37
+ [200, EMPTY_HASH, to_odata_json(service: req.service)]
34
38
  else # TODO: other formats
35
39
  406
36
40
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../safrano/rack_app.rb'
2
4
  require_relative '../safrano/core.rb'
3
5
  require 'rack/body_proxy'
4
6
  require_relative './common_logger.rb'
5
7
 
6
- module OData
8
+ module Safrano
7
9
  # Support for OData multipart $batch Requests
8
10
  class Request
9
11
  def create_batch_app
@@ -20,7 +22,7 @@ module OData
20
22
 
21
23
  module Batch
22
24
  # Mayonaise
23
- class MyOApp < OData::ServerApp
25
+ class MyOApp < Safrano::ServerApp
24
26
  attr_reader :full_req
25
27
  attr_reader :response
26
28
  attr_reader :db
@@ -41,8 +43,8 @@ module OData
41
43
  env = batch_env(part_req)
42
44
  env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
43
45
  began_at = Rack::Utils.clock_time
44
- @request = OData::Request.new(env)
45
- @response = OData::Response.new
46
+ @request = Safrano::Request.new(env)
47
+ @response = Safrano::Response.new
46
48
 
47
49
  if part_req.level == 2
48
50
  @request.in_changeset = true
@@ -98,7 +100,7 @@ module OData
98
100
  end
99
101
 
100
102
  def transition_end(_match_result)
101
- [nil, :end]
103
+ Safrano::Transition::RESULT_END
102
104
  end
103
105
  end
104
106
  # jaune d'oeuf
@@ -126,13 +128,13 @@ module OData
126
128
  def odata_post(req)
127
129
  @request = req
128
130
 
129
- if @request.media_type == OData::MP_MIXED
131
+ if @request.media_type == Safrano::MP_MIXED
130
132
 
131
133
  batcha = @request.create_batch_app
132
134
  @mult_request = @request.parse_multipart
133
135
 
134
136
  @mult_request.prepare_content_id_refs
135
- @mult_response = OData::Response.new
137
+ @mult_response = Safrano::Response.new
136
138
 
137
139
  resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
138
140
 
@@ -1,708 +1,202 @@
1
- # Design: Collections are nothing more as Sequel based model classes that have
2
- # somehow the character of an array (Enumerable)
3
- # Thus Below we have called that "EntityClass". It's meant as "Collection"
4
-
5
- require 'json'
6
- require 'rexml/document'
7
- require_relative '../safrano/core.rb'
8
- require_relative 'error.rb'
9
- require_relative 'collection_filter.rb'
10
- require_relative 'collection_order.rb'
11
- require_relative 'expand.rb'
12
- require_relative 'select.rb'
13
- require_relative 'url_parameters.rb'
14
- require_relative 'collection_media.rb'
15
-
16
- # small helper method
17
- # http://stackoverflow.com/
18
- # questions/24980295/strictly-convert-string-to-integer-or-nil
19
- def number_or_nil(str)
20
- num = str.to_i
21
- num if num.to_s == str
22
- end
1
+ # frozen_string_literal: true
23
2
 
24
- # another helper
25
- # thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
26
- class String
27
- def constantize
28
- names = split('::')
29
- names.shift if names.empty? || names.first.empty?
30
-
31
- const = Object
32
- names.each do |name|
33
- const = if const.const_defined?(name)
34
- const.const_get(name)
35
- else
36
- const.const_missing(name)
37
- end
38
- end
39
- const
40
- end
41
- end
3
+ require_relative 'model_ext'
42
4
 
43
- module OData
44
- # class methods. They Make heavy use of Sequel::Model functionality
45
- # we will add this to our Model classes with "extend" --> self is the Class
46
- module EntityClassBase
47
- SINGLE_PK_URL_REGEXP = /\A\(\s*'?([\w\s]+)'?\s*\)(.*)/.freeze
48
- ONLY_INTEGER_RGX = /\A[+-]?\d+\z/.freeze
49
-
50
- attr_reader :nav_collection_url_regexp
51
- attr_reader :nav_entity_url_regexp
52
- attr_reader :entity_id_url_regexp
53
- attr_reader :nav_collection_attribs
54
- attr_reader :nav_entity_attribs
55
- attr_reader :data_fields
56
- attr_reader :inlinecount
57
- attr_reader :default_template
58
- attr_reader :uri
59
- attr_reader :time_cols
60
-
61
- # Sequel associations pointing to this model. Sequel provides association
62
- # reflection information on the "from" side. But in some cases
63
- # we will need the reverted way
64
- # finally not needed and not used yet
65
- # attr_accessor :assocs_to
66
-
67
- # set to parent entity in case the collection is a nav.collection
68
- # nil otherwise
69
- attr_reader :nav_parent
70
-
71
- attr_accessor :namespace
72
-
73
- # dataset
74
- attr_accessor :cx
75
-
76
- # url params
77
- attr_reader :params
78
-
79
- # url parameters processing object (mostly covert to sequel exprs).
80
- # exposed for testing only
81
- attr_reader :uparms
82
-
83
- # initialising block of code to be executed at end of
84
- # ServerApp.publish_service after all model classes have been registered
85
- # (without the associations/relationships)
86
- # typically the block should contain the publication of the associations
87
- attr_accessor :deferred_iblock
88
-
89
- # convention: entityType is the Ruby Model class --> name is just to_s
90
- # Warning: for handling Navigation relations, we use anonymous collection classes
91
- # dynamically subtyped from a Model class, and in such an anonymous class
92
- # the class-name is not the OData Type. In these subclass we redefine "type_name"
93
- # thus when we need the Odata type name, we shall use this method instead of just the collection class name
94
- def type_name
95
- @type_name
96
- end
5
+ module Safrano
6
+ module OData
7
+ class Collection
8
+ attr_accessor :cx
97
9
 
98
- def build_type_name
99
- @type_name = to_s
100
- end
10
+ # url params
11
+ attr_reader :params
101
12
 
102
- # convention: default for entity_set_name is the type name
103
- def entity_set_name
104
- @entity_set_name = (@entity_set_name || type_name)
105
- end
13
+ # url parameters processing object (mostly convert to sequel exprs).
14
+ # exposed for testing only
15
+ attr_reader :uparms
106
16
 
107
- def reset
108
- # TODO: automatically reset all attributes?
109
- @deferred_iblock = nil
110
- @entity_set_name = nil
111
- @uri = nil
112
- @uparms = nil
113
- @params = nil
114
- @cx = nil
115
- @@time_cols = nil
116
- end
17
+ attr_reader :inlinecount
117
18
 
118
- def build_uri(uribase)
119
- @uri = "#{uribase}/#{entity_set_name}"
120
- end
19
+ attr_reader :modelk
121
20
 
122
- def execute_deferred_iblock
123
- instance_eval { @deferred_iblock.call } if @deferred_iblock
124
- end
125
-
126
- # Factory json-> Model Object instance
127
- def new_from_hson_h(hash)
128
- enty = new
129
- enty.set_fields(hash, @data_fields, missing: :skip)
130
- enty
131
- end
132
-
133
- CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
134
- # in-changeset requests get their own transaction
135
- case assoc[:type]
136
- when :one_to_many, :one_to_one
137
- OData.create_nav_relation(new_entity, assoc, parent)
138
- new_entity.save(transaction: false)
139
- when :many_to_one
140
- new_entity.save(transaction: false)
141
- OData.create_nav_relation(new_entity, assoc, parent)
142
- parent.save(transaction: false)
143
- # else # not supported
21
+ def initialize(modelk)
22
+ @modelk = modelk
144
23
  end
145
- end
146
- def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
147
- if req.in_changeset
148
- # in-changeset requests get their own transaction
149
- CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
150
- else
151
- db.transaction do
152
- CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
153
- end
154
- end
155
- end
156
-
157
- def odata_get_inlinecount_w_sequel
158
- return unless (icp = @params['$inlinecount'])
159
-
160
- @inlinecount = if icp == 'allpages'
161
- if is_a? Sequel::Model::ClassMethods
162
- @cx.count
163
- else
164
- @cx.dataset.count
165
- end
166
- end
167
- end
168
-
169
- def attrib_path_valid?(path)
170
- @attribute_path_list.include? path
171
- end
172
-
173
- def odata_get_apply_params
174
- @cx = @uparms.apply_to_dataset(@cx)
175
- odata_get_inlinecount_w_sequel
176
-
177
- @cx = @cx.offset(@params['$skip']) if @params['$skip']
178
- @cx = @cx.limit(@params['$top']) if @params['$top']
179
- @cx
180
- end
181
-
182
- # url params validation methods.
183
- # nil is the expected return for no errors
184
- # an error class is returned in case of errors
185
- # this way we can combine multiple validation calls with logical ||
186
- def check_u_p_top
187
- return unless @params['$top']
188
-
189
- itop = number_or_nil(@params['$top'])
190
- return BadRequestError if itop.nil? || itop.zero?
191
- end
192
-
193
- def check_u_p_skip
194
- return unless @params['$skip']
195
-
196
- iskip = number_or_nil(@params['$skip'])
197
- return BadRequestError if iskip.nil? || iskip.negative?
198
- end
199
-
200
- def check_u_p_inlinecount
201
- return unless (icp = @params['$inlinecount'])
202
-
203
- return BadRequestInlineCountParamError unless (icp == 'allpages') || (icp == 'none')
204
24
 
205
- nil
206
- end
207
-
208
- def check_u_p_filter
209
- @uparms.check_filter
210
- end
211
-
212
- def check_u_p_orderby
213
- @uparms.check_order
214
- end
215
-
216
- def check_u_p_expand
217
- @uparms.check_expand
218
- end
219
-
220
- def build_attribute_path_list
221
- @attribute_path_list = attribute_path_list
222
- end
223
-
224
- MAX_DEPTH = 6
225
- def attribute_path_list(depth = 0)
226
- ret = @columns.map(&:to_s)
227
- # break circles
228
- return ret if depth > MAX_DEPTH
229
-
230
- depth += 1
231
-
232
- @nav_entity_attribs&.each do |a, k|
233
- ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
234
- end
235
- @nav_collection_attribs&.each do |a, k|
236
- ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
25
+ def allowed_transitions
26
+ @modelk.allowed_transitions
237
27
  end
238
- ret
239
- end
240
-
241
- def check_url_params
242
- return nil unless @params
243
-
244
- check_u_p_top || check_u_p_skip || check_u_p_orderby ||
245
- check_u_p_filter || check_u_p_expand || check_u_p_inlinecount
246
- end
247
-
248
- def initialize_dataset
249
- @cx = self
250
- @cx = navigated_dataset if @cx.nav_parent
251
- @model = if @cx.respond_to? :model
252
- @cx.model
253
- else
254
- @cx
255
- end
256
- @uparms = UrlParameters4Coll.new(@model, @params)
257
- end
258
28
 
259
- # finally return the requested output according to format, options etc
260
- def odata_get_output(req)
261
- return @error.odata_get(req) if @error
262
-
263
- if req.walker.do_count
264
- [200, CT_TEXT, @cx.count.to_s]
265
- elsif req.accept?(APPJSON)
266
- if req.walker.do_links
267
- [200, CT_JSON, [to_odata_links_json(service: req.service)]]
268
- else
269
- [200, CT_JSON, [to_odata_json(service: req.service)]]
270
- end
271
- else # TODO: other formats
272
- 406
29
+ def transition_end(_match_result)
30
+ Safrano::Transition::RESULT_END
273
31
  end
274
- end
275
-
276
- # validation/error handling methods.
277
- # normal processing is done in the passed proc
278
32
 
279
- def with_validated_get(req)
280
- begin
281
- initialize_dataset
282
- return yield unless (@error = check_url_params)
283
- rescue OData::Filter::Parser::ErrorWrongColumnName
284
- @error = BadRequestFilterParseError
285
- rescue OData::Filter::Parser::ErrorFunctionArgumentType
286
- @error = BadRequestFilterParseError
287
- rescue OData::Filter::FunctionNotImplemented => e
288
- @error = e.odata_error
289
- rescue OData::Filter::Parser::ErrorInvalidFunction => e
290
- @error = e.odata_error
33
+ def transition_count(_match_result)
34
+ [self, :end_with_count]
291
35
  end
292
36
 
293
- @error.odata_get(req) if @error
294
- end
295
-
296
- # on model class level we return the collection
297
- def odata_get(req)
298
- @params = req.params
299
-
300
- with_validated_get(req) do
301
- odata_get_apply_params
302
- odata_get_output(req)
37
+ ###########################################################
38
+ # this is redefined in NavigatedCollection
39
+ def dataset
40
+ @modelk.dataset
303
41
  end
304
- end
305
-
306
- def odata_post(req)
307
- odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
308
- end
309
42
 
310
- # add metadata xml to the passed REXML schema object
311
- def add_metadata_rexml(schema)
312
- enty = if @media_handler
313
- schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true')
314
- else
315
- schema.add_element('EntityType', 'Name' => to_s)
316
- end
317
- # with their properties
318
- db_schema.each do |pnam, prop|
319
- if prop[:primary_key] == true
320
- enty.add_element('Key').add_element('PropertyRef',
321
- 'Name' => pnam.to_s)
43
+ def transition_id(match_result)
44
+ if (rawid = match_result[1])
45
+ @modelk.parse_odata_key(rawid).tap_error do
46
+ return Safrano::Transition::RESULT_BAD_REQ_ERR
47
+ end.if_valid do |casted_id|
48
+ (y = find_by_odata_key(casted_id)) ? [y, :run] : Safrano::Transition::RESULT_NOT_FOUND_ERR
49
+ end
50
+ else
51
+ Safrano::Transition::RESULT_SERVER_TR_ERR
322
52
  end
323
- attrs = { 'Name' => pnam.to_s,
324
- 'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
325
- attrs['Nullable'] = 'false' if prop[:allow_null] == false
326
- enty.add_element('Property', attrs)
327
53
  end
328
- enty
329
- end
330
54
 
331
- # metadata REXML data for a single Nav attribute
332
- def metadata_nav_rexml_attribs(assoc, to_klass, relman, xnamespace)
333
- from = type_name
334
- to = to_klass.type_name
335
- relman.get_metadata_xml_attribs(from,
336
- to,
337
- association_reflection(assoc.to_sym)[:type],
338
- xnamespace,
339
- assoc)
340
- end
341
-
342
- # and their Nav attributes == Sequel Model association
343
- def add_metadata_navs_rexml(schema_enty, relman, xnamespace)
344
- @nav_entity_attribs&.each do |ne, klass|
345
- nattr = metadata_nav_rexml_attribs(ne,
346
- klass,
347
- relman,
348
- xnamespace)
349
- schema_enty.add_element('NavigationProperty', nattr)
55
+ # pkid can be a single value for single-pk models, or an array.
56
+ # type checking/convertion is done in check_odata_key_type
57
+ def find_by_odata_key(pkid)
58
+ lkup = @modelk.pk_lookup_expr(pkid)
59
+ dataset[lkup]
350
60
  end
351
61
 
352
- @nav_collection_attribs&.each do |nc, klass|
353
- nattr = metadata_nav_rexml_attribs(nc,
354
- klass,
355
- relman,
356
- xnamespace)
357
- schema_enty.add_element('NavigationProperty', nattr)
62
+ def initialize_dataset(dtset = nil)
63
+ @cx = dtset || @modelk
64
+ @uparms = UrlParameters4Coll.new(@cx, @params)
358
65
  end
359
- end
360
66
 
361
- D = 'd'.freeze
362
- DJ_OPEN = '{"d":'.freeze
363
- DJ_CLOSE = '}'.freeze
364
- def to_odata_links_json(service:)
365
- innerj = service.get_coll_odata_links_h(array: @cx.all,
366
- icount: @inlinecount).to_json
367
- "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
368
- end
369
-
370
- # def output_template(expand: nil, select: nil)
371
- def output_template(uparms)
372
- # output_template_deep(expand_list: expand_list, select: select)
373
- output_template_deep(expand_list: uparms.expand.template, select: uparms.select)
374
- end
375
-
376
- # Recursive
377
- def output_template_deep(expand_list:, select: OData::SelectBase::ALL)
378
- return default_template if expand_list.empty? && select.all_props?
379
-
380
- template = {}
381
- expand_e = {}
382
- expand_c = {}
383
- deferr = []
384
-
385
- # 1. handle non-navigation properties, only consider $select
386
- # 2. handle navigations properties, need to check $select and $expand
387
- if select.all_props?
388
- template[:all_values] = EMPTYH
389
- # include all nav attributes -->
390
- @nav_entity_attribs&.each do |attr, klass|
391
- if expand_list.key?(attr)
392
- expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
393
- else
394
- deferr << attr
395
- end
396
- end
397
-
398
- @nav_collection_attribs&.each do |attr, klass|
399
- if expand_list.key?(attr)
400
- expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
401
- else
402
- deferr << attr
67
+ def odata_get_apply_params
68
+ @uparms.apply_to_dataset(@cx).map_result! do |dataset|
69
+ @cx = dataset
70
+ odata_get_inlinecount_w_sequel
71
+ if (skipp = @params['$skip'])
72
+ @cx = @cx.offset(skipp) if skipp != '0'
403
73
  end
404
- end
74
+ @cx = @cx.limit(@params['$top']) if @params['$top']
405
75
 
406
- else
407
- template[:selected_vals] = @columns.map(&:to_s) & select.props
408
- # include only selected nav attribs-->need additional intersection step
409
- if @nav_entity_attribs
410
- selected_nav_e = @nav_entity_attribs.keys & select.props
411
-
412
- selected_nav_e&.each do |attr|
413
- if expand_list.key?(attr)
414
- klass = @nav_entity_attribs[attr]
415
- expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
416
- else
417
- deferr << attr
418
- end
419
- end
420
- end
421
- if @nav_collection_attribs
422
- selected_nav_c = @nav_collection_attribs.keys & select.props
423
- selected_nav_c&.each do |attr|
424
- if expand_list.key?(attr)
425
- klass = @nav_collection_attribs[attr]
426
- expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
427
- else
428
- deferr << attr
429
- end
430
- end
76
+ @cx
431
77
  end
432
78
  end
433
- template[:expand_e] = expand_e if expand_e
434
- template[:expand_c] = expand_c if expand_c
435
- template[:deferr] = deferr if deferr
436
- template
437
- end
438
79
 
439
- def to_odata_json(service:)
440
- template = output_template(@uparms)
441
- innerj = service.get_coll_odata_h(array: @cx.all,
442
- template: template,
443
- icount: @inlinecount).to_json
444
- "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
445
- end
80
+ D = 'd'.freeze
81
+ DJ_OPEN = '{"d":'.freeze
82
+ DJ_CLOSE = '}'.freeze
83
+ EMPTYH = {}.freeze
446
84
 
447
- # this functionally similar to the Sequel Rels (many_to_one etc)
448
- # We need to base this on the Sequel rels, or extend them
449
- def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
450
- @nav_collection_attribs = (@nav_collection_attribs || {})
451
- # DONE: Error handling. This requires that associations
452
- # have been properly defined with Sequel before
453
- assoc = all_association_reflections.find do |a|
454
- a[:name] == assoc_symb && a[:model] == self
85
+ def to_odata_json(request:)
86
+ template = @modelk.output_template(expand_list: @uparms.expand.template,
87
+ select: @uparms.select)
88
+ innerj = request.service.get_coll_odata_h(array: @cx.all,
89
+ template: template,
90
+ icount: @inlinecount).to_json
91
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
455
92
  end
456
93
 
457
- raise OData::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
458
-
459
- attr_class = assoc[:class_name].constantize
460
- lattr_name_str = (attr_name_str || assoc_symb.to_s)
461
- @nav_collection_attribs[lattr_name_str] = attr_class
462
- @nav_collection_url_regexp = @nav_collection_attribs.keys.join('|')
463
- end
464
-
465
- def add_nav_prop_single(assoc_symb, attr_name_str = nil)
466
- @nav_entity_attribs = (@nav_entity_attribs || {})
467
- # DONE: Error handling. This requires that associations
468
- # have been properly defined with Sequel before
469
- assoc = all_association_reflections.find do |a|
470
- a[:name] == assoc_symb && a[:model] == self
94
+ def to_odata_links_json(service:)
95
+ innerj = service.get_coll_odata_links_h(array: @cx.all,
96
+ icount: @inlinecount).to_json
97
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
471
98
  end
472
99
 
473
- raise OData::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
474
-
475
- attr_class = assoc[:class_name].constantize
476
- lattr_name_str = (attr_name_str || assoc_symb.to_s)
477
- @nav_entity_attribs[lattr_name_str] = attr_class
478
- @nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
479
- end
100
+ def odata_get_inlinecount_w_sequel
101
+ return unless (icp = @params['$inlinecount'])
480
102
 
481
- EMPTYH = {}.freeze
482
-
483
- def build_default_template
484
- template = { all_values: EMPTYH }
485
- if @nav_entity_attribs || @nav_collection_attribs
486
- template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
103
+ @inlinecount = if icp == 'allpages'
104
+ if @modelk.is_a? Sequel::Model::ClassMethods
105
+ @cx.count
106
+ else
107
+ @cx.dataset.count
108
+ end
109
+ end
487
110
  end
488
- template
489
- end
490
- # old names...
491
- # alias_method :add_nav_prop_collection, :addNavCollectionAttrib
492
- # alias_method :add_nav_prop_single, :addNavEntityAttrib
493
-
494
- def finalize_publishing
495
- build_type_name
496
111
 
497
- # finalize media handler
498
- @media_handler.register(self) if @media_handler
499
-
500
- # build default output template structure
501
- @default_template = build_default_template
502
-
503
- # Time columns
504
- @time_cols = db_schema.select { |c, v| v[:type] == :datetime }.map { |c, v| c }
505
-
506
- # and finally build the path list and allowed tr's
507
- build_attribute_path_list
112
+ # finally return the requested output according to format, options etc
113
+ def odata_get_output(req)
114
+ output = if req.walker.do_count
115
+ [200, CT_TEXT, @cx.count.to_s]
116
+ elsif req.accept?(APPJSON)
117
+ # json is default content type so we dont need to specify it here again
118
+ if req.walker.do_links
119
+ [200, EMPTYH, [to_odata_links_json(service: req.service)]]
120
+ else
121
+ [200, EMPTYH, [to_odata_json(request: req)]]
122
+ end
123
+ else # TODO: other formats
124
+ 406
125
+ end
126
+ Contract.valid(output)
127
+ end
128
+
129
+ # on model class level we return the collection
130
+ def odata_get(req)
131
+ @params = req.params
132
+ initialize_dataset
508
133
 
509
- build_allowed_transitions
510
- build_entity_allowed_transitions
511
- end
134
+ @uparms.check_all.if_valid { |_ret|
135
+ odata_get_apply_params.if_valid { |_ret|
136
+ odata_get_output(req)
137
+ }
138
+ }.tap_error { |e| return e.odata_get(req) }.result
139
+ end
512
140
 
513
- def prepare_pk
514
- if primary_key.is_a? Array
515
- @pk_names = []
516
- primary_key.each { |pk| @pk_names << pk.to_s }
517
- # TODO: better handle quotes based on type
518
- # (stringlike--> quote, int-like --> no quotes)
519
-
520
- iuk = @pk_names.map { |pk| "#{pk}='?(\\w+)'?" }
521
- @iuk_rgx = /\A#{iuk.join(',\s*')}\z/
522
-
523
- iuk = @pk_names.map { |pk| "#{pk}='?\\w+'?" }
524
- @entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
525
- else
526
- @pk_names = [primary_key.to_s]
527
- @entity_id_url_regexp = SINGLE_PK_URL_REGEXP
141
+ def odata_post(req)
142
+ @modelk.odata_create_entity_and_relation(req)
528
143
  end
529
144
  end
530
145
 
531
- def prepare_fields
532
- @data_fields = db_schema.map do |col, cattr|
533
- cattr[:primary_key] ? nil : col
534
- end.select { |col| col }
535
- end
146
+ class NavigatedCollection < Collection
147
+ include Safrano::NavigationInfo
536
148
 
537
- def invalid_hash_data?(data)
538
- data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
539
- end
149
+ def initialize(childattrib, parent)
150
+ childklass = parent.class.nav_collection_attribs[childattrib]
540
151
 
541
- ## A regexp matching all allowed attributes of the Entity
542
- ## (eg ID|name|size etc... ) at start position and returning the rest
543
- def transition_attribute_regexp
544
- # db_schema.map { |sch| sch[0] }.join('|')
545
- # @columns is from Sequel Model
546
- %r{\A/(#{@columns.join('|')})(.*)\z}
547
- end
152
+ super(childklass)
153
+ @parent = parent
548
154
 
549
- # pkid can be a single value for single-pk models, or an array.
550
- # type checking/convertion is done in check_odata_key_type
551
- def find_by_odata_key(pkid)
552
- # amazingly this works as expected from an Entity.get_related(...) anonymous class
553
- # without need to redefine primary_key_lookup (returns nil for valid but unrelated keys)
554
- primary_key_lookup(pkid)
555
- end
155
+ set_relation_info(@parent, childattrib)
556
156
 
557
- # super-minimal type check, but better as nothing
558
- def check_odata_val_type(val, type)
559
- case type
560
- when :integer
561
- val =~ ONLY_INTEGER_RGX ? [true, Integer(val)] : [false, val]
562
- when :string
563
- [true, val]
564
- else
565
- [true, val] # todo...
566
- end
567
- rescue StandardError
568
- [false, val]
569
- end
157
+ @child_method = parent.method(childattrib.to_sym)
158
+ @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
570
159
 
571
- # methods related to transitions to next state (cf. walker)
572
- module Transitions
573
- def transition_end(_match_result)
574
- [nil, :end]
160
+ @cx = navigated_dataset
575
161
  end
576
162
 
577
- def transition_count(_match_result)
578
- [self, :end_with_count]
163
+ def odata_post(req)
164
+ @modelk.odata_create_entity_and_relation(req,
165
+ @navattr_reflection,
166
+ @nav_parent)
579
167
  end
580
168
 
581
- def transition_id(match_result)
582
- if (id = match_result[1])
583
-
584
- ck, casted_id = check_odata_key(id)
585
-
586
- if ck
587
- if (y = find_by_odata_key(casted_id))
588
- [y, :run]
589
- else
590
- [nil, :error, ErrorNotFound]
591
- end
592
- else
593
- [nil, :error, BadRequestError]
594
- end
595
- else
596
- [nil, :error, ServerTransitionError]
597
- end
169
+ def initialize_dataset(dtset = nil)
170
+ @cx = dtset || navigated_dataset
171
+ @uparms = UrlParameters4Coll.new(@cx, @params)
598
172
  end
173
+ # redefinitions of the main methods for a navigated collection
174
+ # (eg. all Books of Author[2] is Author[2].Books.all )
599
175
 
600
- def allowed_transitions
601
- @allowed_transitions
176
+ def all
177
+ @child_method.call
602
178
  end
603
179
 
604
- def entity_allowed_transitions
605
- @entity_allowed_transitions
180
+ def count
181
+ @child_method.call.count
606
182
  end
607
183
 
608
- def build_allowed_transitions
609
- @allowed_transitions = [Safrano::TransitionEnd,
610
- Safrano::TransitionCount,
611
- Safrano::Transition.new(entity_id_url_regexp,
612
- trans: 'transition_id')].freeze
184
+ def dataset
185
+ @child_dataset_method.call
613
186
  end
614
187
 
615
- def build_entity_allowed_transitions
616
- @entity_allowed_transitions = [
617
- Safrano::TransitionEnd,
618
- Safrano::TransitionCount,
619
- Safrano::TransitionLinks,
620
- Safrano::TransitionValue,
621
- Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
622
- ]
623
- if (ncurgx = @nav_collection_url_regexp)
624
- @entity_allowed_transitions <<
625
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
626
- end
627
- if (neurgx = @nav_entity_url_regexp)
628
- @entity_allowed_transitions <<
629
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
630
- end
631
- @entity_allowed_transitions.freeze
632
- @entity_allowed_transitions
188
+ def navigated_dataset
189
+ @child_dataset_method.call
633
190
  end
634
- end
635
- include Transitions
636
- end
637
191
 
638
- # special handling for composite key
639
- module EntityClassMultiPK
640
- include EntityClassBase
641
- # input fx='aas',fy_w='0001'
642
- # output true, ['aas', '0001'] ... or false when typ-error
643
- def check_odata_key(mid)
644
- # @iuk_rgx is (needs to be) built on start with
645
- # collklass.prepare_pk
646
- md = @iuk_rgx.match(mid).to_a
647
- md.shift # remove first element which is the whole match
648
- mdc = []
649
- error = false
650
- primary_key.each_with_index do |pk, i|
651
- ck, casted = check_odata_val_type(md[i], db_schema[pk][:type])
652
- if ck
653
- mdc << casted
654
- else
655
- error = true
656
- break
657
- end
192
+ def each
193
+ y = @child_method.call
194
+ y.each { |enty| yield enty }
658
195
  end
659
- if error
660
- [false, md]
661
- else
662
- [true, mdc]
663
- end
664
- end
665
- end
666
-
667
- # special handling for single key
668
- module EntityClassSinglePK
669
- include EntityClassBase
670
196
 
671
- def check_odata_key(id)
672
- check_odata_val_type(id, db_schema[primary_key][:type])
673
- end
674
- end
675
-
676
- # normal handling for non-media entity
677
- module EntityClassNonMedia
678
- # POST for non-media entity collection -->
679
- # 1. Create and add entity from payload
680
- # 2. Create relationship if needed
681
- def odata_create_entity_and_relation(req, assoc, parent)
682
- # TODO: this is for v2 only...
683
- req.with_parsed_data do |data|
684
- data.delete('__metadata')
685
- # validate payload column names
686
- if (invalid = invalid_hash_data?(data))
687
- ::OData::Request::ON_CGST_ERROR.call(req)
688
- return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
689
- end
690
-
691
- if req.accept?(APPJSON)
692
- new_entity = new_from_hson_h(data)
693
- if parent
694
- odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
695
- else
696
- # in-changeset requests get their own transaction
697
- new_entity.save(transaction: !req.in_changeset)
698
- end
699
- req.register_content_id_ref(new_entity)
700
- new_entity.copy_request_infos(req)
701
-
702
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
703
- else # TODO: other formats
704
- 415
705
- end
197
+ def to_a
198
+ y = @child_method.call
199
+ y.to_a
706
200
  end
707
201
  end
708
202
  end