safrano 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +8 -4
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +139 -642
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +196 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +78 -123
  23. data/lib/odata/error.rb +170 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +9 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +39 -25
  28. data/lib/odata/filter/sequel.rb +112 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +21 -18
  31. data/lib/odata/filter/tree.rb +78 -44
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +641 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +18 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +141 -0
  42. data/lib/safrano/core.rb +24 -106
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -24
  46. data/lib/safrano/rack_app.rb +62 -63
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +96 -38
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +156 -110
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +30 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 497482614bfc6a87f8a42b9378a4fc8099ddb5b4f180d747b3717a6952f027b0
4
- data.tar.gz: cd5236302671643fd9fbd9bcd200034f305ce7069e5d56ab2db2225dcfb4ce87
3
+ metadata.gz: b38827d37fa3bfed54a30aa61c04b5da27470071c5e3167fb5a08aa5c48a69af
4
+ data.tar.gz: 3754e63822b6c504b42bc698df360295ab92bcf96a2ed8fdef4af7f2ed5c8d85
5
5
  SHA512:
6
- metadata.gz: 4415ca4dc24362f3c179f0cc40f1d73fac6fb7207c0571ef63f1af318fc345e0be8043d8aa1b5ab48e0ead1f5c904dcab07b84061b3ca47f6b7d3c38696d4ae8
7
- data.tar.gz: 6646d12aadb3827b43c013a3da6c6223d072e5676f8676eac0db59ad8e3ac80d1a64ceb0f5e87d52bb75e5b556eecda701e2d473c4bdd3b650cb680b6ce19b96
6
+ metadata.gz: 0c6a3949c741f120955b4582ddad95aa3fefe80f613030970c174b4fe000129e4d2f67dcb43deae5608a2a287a489a4c94d6ba0df5a613c5557b22b8ca4f625b
7
+ data.tar.gz: 7e5070fa657435fbc3a2932344392e58fa7105863bb045ad5a0aac7227a1e7ce5e4f54628915c81156da2f315eb359409c786243c47697877237ac08294a7872
@@ -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
@@ -49,7 +53,7 @@ module OData
49
53
  # methods related to transitions to next state (cf. walker)
50
54
  module Transitions
51
55
  def transition_end(_match_result)
52
- [nil, :end]
56
+ Transition::RESULT_END
53
57
  end
54
58
 
55
59
  def transition_value(_match_result)
@@ -60,7 +64,7 @@ module OData
60
64
  Safrano::TransitionValue].freeze
61
65
 
62
66
  def allowed_transitions
63
- ALLOWED_TRANSITIONS
67
+ Transitions::ALLOWED_TRANSITIONS
64
68
  end
65
69
  end
66
70
  include Transitions
data/lib/odata/batch.rb CHANGED
@@ -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,205 @@
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
97
-
98
- def build_type_name
99
- @type_name = to_s
100
- end
101
-
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
5
+ module Safrano
6
+ module OData
7
+ class Collection
8
+ attr_accessor :cx
106
9
 
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
10
+ # url params
11
+ attr_reader :params
117
12
 
118
- def build_uri(uribase)
119
- @uri = "#{uribase}/#{entity_set_name}"
120
- end
13
+ # url parameters processing object (mostly convert to sequel exprs).
14
+ # exposed for testing only
15
+ attr_reader :uparms
121
16
 
122
- def execute_deferred_iblock
123
- instance_eval { @deferred_iblock.call } if @deferred_iblock
124
- end
17
+ attr_reader :inlinecount
125
18
 
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
19
+ attr_reader :modelk
132
20
 
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
24
 
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
-
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
28
 
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
-
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
32
 
276
- # validation/error handling methods.
277
- # normal processing is done in the passed proc
278
-
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
-
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
66
 
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
446
-
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
455
- end
80
+ D = 'd'
81
+ DJ_OPEN = '{"d":'
82
+ DJ_CLOSE = '}'
83
+ EMPTYH = {}.freeze
456
84
 
457
- raise OData::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
85
+ def to_odata_json(request:)
86
+ template = @modelk.output_template(expand_list: @uparms.expand.template,
87
+ select: @uparms.select)
88
+ # TODO: Error handling if database contains binary BLOB data that cant be
89
+ # interpreted as UTF-8 then JSON will fail here
90
+ innerj = request.service.get_coll_odata_h(array: @cx.all,
91
+ template: template,
92
+ icount: @inlinecount).to_json
458
93
 
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
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
471
95
  end
472
96
 
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
480
-
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)
97
+ def to_odata_links_json(service:)
98
+ innerj = service.get_coll_odata_links_h(array: @cx.all,
99
+ icount: @inlinecount).to_json
100
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
487
101
  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
102
 
497
- # finalize media handler
498
- @media_handler.register(self) if @media_handler
103
+ def odata_get_inlinecount_w_sequel
104
+ return unless (icp = @params['$inlinecount'])
499
105
 
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 }
106
+ @inlinecount = if icp == 'allpages'
107
+ if @modelk.is_a? Sequel::Model::ClassMethods
108
+ @cx.count
109
+ else
110
+ @cx.dataset.count
111
+ end
112
+ end
113
+ end
505
114
 
506
- # and finally build the path list and allowed tr's
507
- build_attribute_path_list
115
+ # finally return the requested output according to format, options etc
116
+ def odata_get_output(req)
117
+ output = if req.walker.do_count
118
+ [200, CT_TEXT, @cx.count.to_s]
119
+ elsif req.accept?(APPJSON)
120
+ # json is default content type so we dont need to specify it here again
121
+ if req.walker.do_links
122
+ [200, EMPTYH, [to_odata_links_json(service: req.service)]]
123
+ else
124
+ [200, EMPTYH, [to_odata_json(request: req)]]
125
+ end
126
+ else # TODO: other formats
127
+ 406
128
+ end
129
+ Contract.valid(output)
130
+ end
131
+
132
+ # on model class level we return the collection
133
+ def odata_get(req)
134
+ @params = req.params
135
+ initialize_dataset
508
136
 
509
- build_allowed_transitions
510
- build_entity_allowed_transitions
511
- end
137
+ @uparms.check_all.if_valid { |_ret|
138
+ odata_get_apply_params.if_valid { |_ret|
139
+ odata_get_output(req)
140
+ }
141
+ }.tap_error { |e| return e.odata_get(req) }.result
142
+ end
512
143
 
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
144
+ def odata_post(req)
145
+ @modelk.odata_create_entity_and_relation(req)
528
146
  end
529
147
  end
530
148
 
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
149
+ class NavigatedCollection < Collection
150
+ include Safrano::NavigationInfo
536
151
 
537
- def invalid_hash_data?(data)
538
- data.keys.map(&:to_sym).find { |ksym| !(@columns.include? ksym) }
539
- end
152
+ def initialize(childattrib, parent)
153
+ childklass = parent.class.nav_collection_attribs[childattrib]
540
154
 
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
155
+ super(childklass)
156
+ @parent = parent
548
157
 
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
158
+ set_relation_info(@parent, childattrib)
556
159
 
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
160
+ @child_method = parent.method(childattrib.to_sym)
161
+ @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
570
162
 
571
- # methods related to transitions to next state (cf. walker)
572
- module Transitions
573
- def transition_end(_match_result)
574
- [nil, :end]
163
+ @cx = navigated_dataset
575
164
  end
576
165
 
577
- def transition_count(_match_result)
578
- [self, :end_with_count]
166
+ def odata_post(req)
167
+ @modelk.odata_create_entity_and_relation(req,
168
+ @navattr_reflection,
169
+ @nav_parent)
579
170
  end
580
171
 
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
172
+ def initialize_dataset(dtset = nil)
173
+ @cx = dtset || navigated_dataset
174
+ @uparms = UrlParameters4Coll.new(@cx, @params)
598
175
  end
176
+ # redefinitions of the main methods for a navigated collection
177
+ # (eg. all Books of Author[2] is Author[2].Books.all )
599
178
 
600
- def allowed_transitions
601
- @allowed_transitions
179
+ def all
180
+ @child_method.call
602
181
  end
603
182
 
604
- def entity_allowed_transitions
605
- @entity_allowed_transitions
183
+ def count
184
+ @child_method.call.count
606
185
  end
607
186
 
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
187
+ def dataset
188
+ @child_dataset_method.call
613
189
  end
614
190
 
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
191
+ def navigated_dataset
192
+ @child_dataset_method.call
633
193
  end
634
- end
635
- include Transitions
636
- end
637
194
 
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
195
+ def each
196
+ y = @child_method.call
197
+ y.each { |enty| yield enty }
658
198
  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
199
 
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
200
+ def to_a
201
+ y = @child_method.call
202
+ y.to_a
706
203
  end
707
204
  end
708
205
  end