safrano 0.4.1 → 0.4.6

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 +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