safrano 0.3.4 → 0.4.4

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