safrano 0.3.4 → 0.4.4

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