safrano 0.4.2 → 0.5.0

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