safrano 0.4.1 → 0.4.6

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