safrano 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
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