safrano 0.4.3 → 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.
- checksums.yaml +4 -4
- data/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +6 -2
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +136 -642
- data/lib/odata/collection_filter.rb +16 -40
- data/lib/odata/collection_media.rb +56 -37
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +53 -117
- data/lib/odata/error.rb +142 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +4 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +33 -25
- data/lib/odata/filter/sequel.rb +97 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +10 -10
- data/lib/odata/filter/tree.rb +75 -41
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +9 -24
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +15 -7
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +12 -94
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +25 -20
- data/lib/safrano/rack_app.rb +61 -62
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +95 -37
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +132 -94
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +24 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88f62ce611a5030382ea19c2e7c22b47f454b85c51261e5bad060d82e4680dff
|
4
|
+
data.tar.gz: '088e66ed4a107a3bef8dd6db68c8ca626f7d3acef6ed2e58eb53d871e6349613'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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
|
data/lib/core_ext/dir.rb
ADDED
data/lib/odata/attribute.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
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
|
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
|
@@ -30,7 +33,8 @@ module OData
|
|
30
33
|
if req.walker.raw_value
|
31
34
|
[200, CT_TEXT, value.to_s]
|
32
35
|
elsif req.accept?(APPJSON)
|
33
|
-
|
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)]
|
34
38
|
else # TODO: other formats
|
35
39
|
406
|
36
40
|
end
|
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
|
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 <
|
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 =
|
45
|
-
@response =
|
46
|
+
@request = Safrano::Request.new(env)
|
47
|
+
@response = Safrano::Response.new
|
46
48
|
|
47
49
|
if part_req.level == 2
|
48
50
|
@request.in_changeset = true
|
@@ -98,7 +100,7 @@ module OData
|
|
98
100
|
end
|
99
101
|
|
100
102
|
def transition_end(_match_result)
|
101
|
-
|
103
|
+
Safrano::Transition::RESULT_END
|
102
104
|
end
|
103
105
|
end
|
104
106
|
# jaune d'oeuf
|
@@ -126,13 +128,13 @@ module OData
|
|
126
128
|
def odata_post(req)
|
127
129
|
@request = req
|
128
130
|
|
129
|
-
if @request.media_type ==
|
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 =
|
137
|
+
@mult_response = Safrano::Response.new
|
136
138
|
|
137
139
|
resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
|
138
140
|
|
data/lib/odata/collection.rb
CHANGED
@@ -1,708 +1,202 @@
|
|
1
|
-
#
|
2
|
-
# somehow the character of an array (Enumerable)
|
3
|
-
# Thus Below we have called that "EntityClass". It's meant as "Collection"
|
4
|
-
|
5
|
-
require 'json'
|
6
|
-
require 'rexml/document'
|
7
|
-
require_relative '../safrano/core.rb'
|
8
|
-
require_relative 'error.rb'
|
9
|
-
require_relative 'collection_filter.rb'
|
10
|
-
require_relative 'collection_order.rb'
|
11
|
-
require_relative 'expand.rb'
|
12
|
-
require_relative 'select.rb'
|
13
|
-
require_relative 'url_parameters.rb'
|
14
|
-
require_relative 'collection_media.rb'
|
15
|
-
|
16
|
-
# small helper method
|
17
|
-
# http://stackoverflow.com/
|
18
|
-
# questions/24980295/strictly-convert-string-to-integer-or-nil
|
19
|
-
def number_or_nil(str)
|
20
|
-
num = str.to_i
|
21
|
-
num if num.to_s == str
|
22
|
-
end
|
1
|
+
# frozen_string_literal: true
|
23
2
|
|
24
|
-
|
25
|
-
# thanks https://stackoverflow.com/questions/1448670/ruby-stringto-class
|
26
|
-
class String
|
27
|
-
def constantize
|
28
|
-
names = split('::')
|
29
|
-
names.shift if names.empty? || names.first.empty?
|
30
|
-
|
31
|
-
const = Object
|
32
|
-
names.each do |name|
|
33
|
-
const = if const.const_defined?(name)
|
34
|
-
const.const_get(name)
|
35
|
-
else
|
36
|
-
const.const_missing(name)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
const
|
40
|
-
end
|
41
|
-
end
|
3
|
+
require_relative 'model_ext'
|
42
4
|
|
43
|
-
module
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
SINGLE_PK_URL_REGEXP = /\A\(\s*'?([\w\s]+)'?\s*\)(.*)/.freeze
|
48
|
-
ONLY_INTEGER_RGX = /\A[+-]?\d+\z/.freeze
|
49
|
-
|
50
|
-
attr_reader :nav_collection_url_regexp
|
51
|
-
attr_reader :nav_entity_url_regexp
|
52
|
-
attr_reader :entity_id_url_regexp
|
53
|
-
attr_reader :nav_collection_attribs
|
54
|
-
attr_reader :nav_entity_attribs
|
55
|
-
attr_reader :data_fields
|
56
|
-
attr_reader :inlinecount
|
57
|
-
attr_reader :default_template
|
58
|
-
attr_reader :uri
|
59
|
-
attr_reader :time_cols
|
60
|
-
|
61
|
-
# Sequel associations pointing to this model. Sequel provides association
|
62
|
-
# reflection information on the "from" side. But in some cases
|
63
|
-
# we will need the reverted way
|
64
|
-
# finally not needed and not used yet
|
65
|
-
# attr_accessor :assocs_to
|
66
|
-
|
67
|
-
# set to parent entity in case the collection is a nav.collection
|
68
|
-
# nil otherwise
|
69
|
-
attr_reader :nav_parent
|
70
|
-
|
71
|
-
attr_accessor :namespace
|
72
|
-
|
73
|
-
# dataset
|
74
|
-
attr_accessor :cx
|
75
|
-
|
76
|
-
# url params
|
77
|
-
attr_reader :params
|
78
|
-
|
79
|
-
# url parameters processing object (mostly covert to sequel exprs).
|
80
|
-
# exposed for testing only
|
81
|
-
attr_reader :uparms
|
82
|
-
|
83
|
-
# initialising block of code to be executed at end of
|
84
|
-
# ServerApp.publish_service after all model classes have been registered
|
85
|
-
# (without the associations/relationships)
|
86
|
-
# typically the block should contain the publication of the associations
|
87
|
-
attr_accessor :deferred_iblock
|
88
|
-
|
89
|
-
# convention: entityType is the Ruby Model class --> name is just to_s
|
90
|
-
# Warning: for handling Navigation relations, we use anonymous collection classes
|
91
|
-
# dynamically subtyped from a Model class, and in such an anonymous class
|
92
|
-
# the class-name is not the OData Type. In these subclass we redefine "type_name"
|
93
|
-
# thus when we need the Odata type name, we shall use this method instead of just the collection class name
|
94
|
-
def type_name
|
95
|
-
@type_name
|
96
|
-
end
|
5
|
+
module Safrano
|
6
|
+
module OData
|
7
|
+
class Collection
|
8
|
+
attr_accessor :cx
|
97
9
|
|
98
|
-
|
99
|
-
|
100
|
-
end
|
10
|
+
# url params
|
11
|
+
attr_reader :params
|
101
12
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
13
|
+
# url parameters processing object (mostly convert to sequel exprs).
|
14
|
+
# exposed for testing only
|
15
|
+
attr_reader :uparms
|
106
16
|
|
107
|
-
|
108
|
-
# TODO: automatically reset all attributes?
|
109
|
-
@deferred_iblock = nil
|
110
|
-
@entity_set_name = nil
|
111
|
-
@uri = nil
|
112
|
-
@uparms = nil
|
113
|
-
@params = nil
|
114
|
-
@cx = nil
|
115
|
-
@@time_cols = nil
|
116
|
-
end
|
17
|
+
attr_reader :inlinecount
|
117
18
|
|
118
|
-
|
119
|
-
@uri = "#{uribase}/#{entity_set_name}"
|
120
|
-
end
|
19
|
+
attr_reader :modelk
|
121
20
|
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
# Factory json-> Model Object instance
|
127
|
-
def new_from_hson_h(hash)
|
128
|
-
enty = new
|
129
|
-
enty.set_fields(hash, @data_fields, missing: :skip)
|
130
|
-
enty
|
131
|
-
end
|
132
|
-
|
133
|
-
CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
|
134
|
-
# in-changeset requests get their own transaction
|
135
|
-
case assoc[:type]
|
136
|
-
when :one_to_many, :one_to_one
|
137
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
138
|
-
new_entity.save(transaction: false)
|
139
|
-
when :many_to_one
|
140
|
-
new_entity.save(transaction: false)
|
141
|
-
OData.create_nav_relation(new_entity, assoc, parent)
|
142
|
-
parent.save(transaction: false)
|
143
|
-
# else # not supported
|
21
|
+
def initialize(modelk)
|
22
|
+
@modelk = modelk
|
144
23
|
end
|
145
|
-
end
|
146
|
-
def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
147
|
-
if req.in_changeset
|
148
|
-
# in-changeset requests get their own transaction
|
149
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
150
|
-
else
|
151
|
-
db.transaction do
|
152
|
-
CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def odata_get_inlinecount_w_sequel
|
158
|
-
return unless (icp = @params['$inlinecount'])
|
159
|
-
|
160
|
-
@inlinecount = if icp == 'allpages'
|
161
|
-
if is_a? Sequel::Model::ClassMethods
|
162
|
-
@cx.count
|
163
|
-
else
|
164
|
-
@cx.dataset.count
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def attrib_path_valid?(path)
|
170
|
-
@attribute_path_list.include? path
|
171
|
-
end
|
172
|
-
|
173
|
-
def odata_get_apply_params
|
174
|
-
@cx = @uparms.apply_to_dataset(@cx)
|
175
|
-
odata_get_inlinecount_w_sequel
|
176
|
-
|
177
|
-
@cx = @cx.offset(@params['$skip']) if @params['$skip']
|
178
|
-
@cx = @cx.limit(@params['$top']) if @params['$top']
|
179
|
-
@cx
|
180
|
-
end
|
181
|
-
|
182
|
-
# url params validation methods.
|
183
|
-
# nil is the expected return for no errors
|
184
|
-
# an error class is returned in case of errors
|
185
|
-
# this way we can combine multiple validation calls with logical ||
|
186
|
-
def check_u_p_top
|
187
|
-
return unless @params['$top']
|
188
|
-
|
189
|
-
itop = number_or_nil(@params['$top'])
|
190
|
-
return BadRequestError if itop.nil? || itop.zero?
|
191
|
-
end
|
192
|
-
|
193
|
-
def check_u_p_skip
|
194
|
-
return unless @params['$skip']
|
195
|
-
|
196
|
-
iskip = number_or_nil(@params['$skip'])
|
197
|
-
return BadRequestError if iskip.nil? || iskip.negative?
|
198
|
-
end
|
199
|
-
|
200
|
-
def check_u_p_inlinecount
|
201
|
-
return unless (icp = @params['$inlinecount'])
|
202
|
-
|
203
|
-
return BadRequestInlineCountParamError unless (icp == 'allpages') || (icp == 'none')
|
204
24
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
def check_u_p_filter
|
209
|
-
@uparms.check_filter
|
210
|
-
end
|
211
|
-
|
212
|
-
def check_u_p_orderby
|
213
|
-
@uparms.check_order
|
214
|
-
end
|
215
|
-
|
216
|
-
def check_u_p_expand
|
217
|
-
@uparms.check_expand
|
218
|
-
end
|
219
|
-
|
220
|
-
def build_attribute_path_list
|
221
|
-
@attribute_path_list = attribute_path_list
|
222
|
-
end
|
223
|
-
|
224
|
-
MAX_DEPTH = 6
|
225
|
-
def attribute_path_list(depth = 0)
|
226
|
-
ret = @columns.map(&:to_s)
|
227
|
-
# break circles
|
228
|
-
return ret if depth > MAX_DEPTH
|
229
|
-
|
230
|
-
depth += 1
|
231
|
-
|
232
|
-
@nav_entity_attribs&.each do |a, k|
|
233
|
-
ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
|
234
|
-
end
|
235
|
-
@nav_collection_attribs&.each do |a, k|
|
236
|
-
ret.concat(k.attribute_path_list(depth).map { |kc| "#{a}/#{kc}" })
|
25
|
+
def allowed_transitions
|
26
|
+
@modelk.allowed_transitions
|
237
27
|
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_expand || check_u_p_inlinecount
|
246
|
-
end
|
247
|
-
|
248
|
-
def initialize_dataset
|
249
|
-
@cx = self
|
250
|
-
@cx = navigated_dataset if @cx.nav_parent
|
251
|
-
@model = if @cx.respond_to? :model
|
252
|
-
@cx.model
|
253
|
-
else
|
254
|
-
@cx
|
255
|
-
end
|
256
|
-
@uparms = UrlParameters4Coll.new(@model, @params)
|
257
|
-
end
|
258
28
|
|
259
|
-
|
260
|
-
|
261
|
-
return @error.odata_get(req) if @error
|
262
|
-
|
263
|
-
if req.walker.do_count
|
264
|
-
[200, CT_TEXT, @cx.count.to_s]
|
265
|
-
elsif req.accept?(APPJSON)
|
266
|
-
if req.walker.do_links
|
267
|
-
[200, CT_JSON, [to_odata_links_json(service: req.service)]]
|
268
|
-
else
|
269
|
-
[200, CT_JSON, [to_odata_json(service: req.service)]]
|
270
|
-
end
|
271
|
-
else # TODO: other formats
|
272
|
-
406
|
29
|
+
def transition_end(_match_result)
|
30
|
+
Safrano::Transition::RESULT_END
|
273
31
|
end
|
274
|
-
end
|
275
|
-
|
276
|
-
# validation/error handling methods.
|
277
|
-
# normal processing is done in the passed proc
|
278
32
|
|
279
|
-
|
280
|
-
|
281
|
-
initialize_dataset
|
282
|
-
return yield unless (@error = check_url_params)
|
283
|
-
rescue OData::Filter::Parser::ErrorWrongColumnName
|
284
|
-
@error = BadRequestFilterParseError
|
285
|
-
rescue OData::Filter::Parser::ErrorFunctionArgumentType
|
286
|
-
@error = BadRequestFilterParseError
|
287
|
-
rescue OData::Filter::FunctionNotImplemented => e
|
288
|
-
@error = e.odata_error
|
289
|
-
rescue OData::Filter::Parser::ErrorInvalidFunction => e
|
290
|
-
@error = e.odata_error
|
33
|
+
def transition_count(_match_result)
|
34
|
+
[self, :end_with_count]
|
291
35
|
end
|
292
36
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
def odata_get(req)
|
298
|
-
@params = req.params
|
299
|
-
|
300
|
-
with_validated_get(req) do
|
301
|
-
odata_get_apply_params
|
302
|
-
odata_get_output(req)
|
37
|
+
###########################################################
|
38
|
+
# this is redefined in NavigatedCollection
|
39
|
+
def dataset
|
40
|
+
@modelk.dataset
|
303
41
|
end
|
304
|
-
end
|
305
|
-
|
306
|
-
def odata_post(req)
|
307
|
-
odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
|
308
|
-
end
|
309
42
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
if prop[:primary_key] == true
|
320
|
-
enty.add_element('Key').add_element('PropertyRef',
|
321
|
-
'Name' => pnam.to_s)
|
43
|
+
def transition_id(match_result)
|
44
|
+
if (rawid = match_result[1])
|
45
|
+
@modelk.parse_odata_key(rawid).tap_error do
|
46
|
+
return Safrano::Transition::RESULT_BAD_REQ_ERR
|
47
|
+
end.if_valid do |casted_id|
|
48
|
+
(y = find_by_odata_key(casted_id)) ? [y, :run] : Safrano::Transition::RESULT_NOT_FOUND_ERR
|
49
|
+
end
|
50
|
+
else
|
51
|
+
Safrano::Transition::RESULT_SERVER_TR_ERR
|
322
52
|
end
|
323
|
-
attrs = { 'Name' => pnam.to_s,
|
324
|
-
'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
|
325
|
-
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
326
|
-
enty.add_element('Property', attrs)
|
327
53
|
end
|
328
|
-
enty
|
329
|
-
end
|
330
54
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
to,
|
337
|
-
association_reflection(assoc.to_sym)[:type],
|
338
|
-
xnamespace,
|
339
|
-
assoc)
|
340
|
-
end
|
341
|
-
|
342
|
-
# and their Nav attributes == Sequel Model association
|
343
|
-
def add_metadata_navs_rexml(schema_enty, relman, xnamespace)
|
344
|
-
@nav_entity_attribs&.each do |ne, klass|
|
345
|
-
nattr = metadata_nav_rexml_attribs(ne,
|
346
|
-
klass,
|
347
|
-
relman,
|
348
|
-
xnamespace)
|
349
|
-
schema_enty.add_element('NavigationProperty', nattr)
|
55
|
+
# pkid can be a single value for single-pk models, or an array.
|
56
|
+
# type checking/convertion is done in check_odata_key_type
|
57
|
+
def find_by_odata_key(pkid)
|
58
|
+
lkup = @modelk.pk_lookup_expr(pkid)
|
59
|
+
dataset[lkup]
|
350
60
|
end
|
351
61
|
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
relman,
|
356
|
-
xnamespace)
|
357
|
-
schema_enty.add_element('NavigationProperty', nattr)
|
62
|
+
def initialize_dataset(dtset = nil)
|
63
|
+
@cx = dtset || @modelk
|
64
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
358
65
|
end
|
359
|
-
end
|
360
66
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
368
|
-
end
|
369
|
-
|
370
|
-
# def output_template(expand: nil, select: nil)
|
371
|
-
def output_template(uparms)
|
372
|
-
# output_template_deep(expand_list: expand_list, select: select)
|
373
|
-
output_template_deep(expand_list: uparms.expand.template, select: uparms.select)
|
374
|
-
end
|
375
|
-
|
376
|
-
# Recursive
|
377
|
-
def output_template_deep(expand_list:, select: OData::SelectBase::ALL)
|
378
|
-
return default_template if expand_list.empty? && select.all_props?
|
379
|
-
|
380
|
-
template = {}
|
381
|
-
expand_e = {}
|
382
|
-
expand_c = {}
|
383
|
-
deferr = []
|
384
|
-
|
385
|
-
# 1. handle non-navigation properties, only consider $select
|
386
|
-
# 2. handle navigations properties, need to check $select and $expand
|
387
|
-
if select.all_props?
|
388
|
-
template[:all_values] = EMPTYH
|
389
|
-
# include all nav attributes -->
|
390
|
-
@nav_entity_attribs&.each do |attr, klass|
|
391
|
-
if expand_list.key?(attr)
|
392
|
-
expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
|
393
|
-
else
|
394
|
-
deferr << attr
|
395
|
-
end
|
396
|
-
end
|
397
|
-
|
398
|
-
@nav_collection_attribs&.each do |attr, klass|
|
399
|
-
if expand_list.key?(attr)
|
400
|
-
expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
|
401
|
-
else
|
402
|
-
deferr << attr
|
67
|
+
def odata_get_apply_params
|
68
|
+
@uparms.apply_to_dataset(@cx).map_result! do |dataset|
|
69
|
+
@cx = dataset
|
70
|
+
odata_get_inlinecount_w_sequel
|
71
|
+
if (skipp = @params['$skip'])
|
72
|
+
@cx = @cx.offset(skipp) if skipp != '0'
|
403
73
|
end
|
404
|
-
|
74
|
+
@cx = @cx.limit(@params['$top']) if @params['$top']
|
405
75
|
|
406
|
-
|
407
|
-
template[:selected_vals] = @columns.map(&:to_s) & select.props
|
408
|
-
# include only selected nav attribs-->need additional intersection step
|
409
|
-
if @nav_entity_attribs
|
410
|
-
selected_nav_e = @nav_entity_attribs.keys & select.props
|
411
|
-
|
412
|
-
selected_nav_e&.each do |attr|
|
413
|
-
if expand_list.key?(attr)
|
414
|
-
klass = @nav_entity_attribs[attr]
|
415
|
-
expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
|
416
|
-
else
|
417
|
-
deferr << attr
|
418
|
-
end
|
419
|
-
end
|
420
|
-
end
|
421
|
-
if @nav_collection_attribs
|
422
|
-
selected_nav_c = @nav_collection_attribs.keys & select.props
|
423
|
-
selected_nav_c&.each do |attr|
|
424
|
-
if expand_list.key?(attr)
|
425
|
-
klass = @nav_collection_attribs[attr]
|
426
|
-
expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
|
427
|
-
else
|
428
|
-
deferr << attr
|
429
|
-
end
|
430
|
-
end
|
76
|
+
@cx
|
431
77
|
end
|
432
78
|
end
|
433
|
-
template[:expand_e] = expand_e if expand_e
|
434
|
-
template[:expand_c] = expand_c if expand_c
|
435
|
-
template[:deferr] = deferr if deferr
|
436
|
-
template
|
437
|
-
end
|
438
79
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
icount: @inlinecount).to_json
|
444
|
-
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
445
|
-
end
|
80
|
+
D = 'd'.freeze
|
81
|
+
DJ_OPEN = '{"d":'.freeze
|
82
|
+
DJ_CLOSE = '}'.freeze
|
83
|
+
EMPTYH = {}.freeze
|
446
84
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
a[:name] == assoc_symb && a[:model] == self
|
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}"
|
455
92
|
end
|
456
93
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
@nav_collection_attribs[lattr_name_str] = attr_class
|
462
|
-
@nav_collection_url_regexp = @nav_collection_attribs.keys.join('|')
|
463
|
-
end
|
464
|
-
|
465
|
-
def add_nav_prop_single(assoc_symb, attr_name_str = nil)
|
466
|
-
@nav_entity_attribs = (@nav_entity_attribs || {})
|
467
|
-
# DONE: Error handling. This requires that associations
|
468
|
-
# have been properly defined with Sequel before
|
469
|
-
assoc = all_association_reflections.find do |a|
|
470
|
-
a[:name] == assoc_symb && a[:model] == self
|
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}"
|
471
98
|
end
|
472
99
|
|
473
|
-
|
474
|
-
|
475
|
-
attr_class = assoc[:class_name].constantize
|
476
|
-
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
477
|
-
@nav_entity_attribs[lattr_name_str] = attr_class
|
478
|
-
@nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
|
479
|
-
end
|
100
|
+
def odata_get_inlinecount_w_sequel
|
101
|
+
return unless (icp = @params['$inlinecount'])
|
480
102
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
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
|
487
110
|
end
|
488
|
-
template
|
489
|
-
end
|
490
|
-
# old names...
|
491
|
-
# alias_method :add_nav_prop_collection, :addNavCollectionAttrib
|
492
|
-
# alias_method :add_nav_prop_single, :addNavEntityAttrib
|
493
|
-
|
494
|
-
def finalize_publishing
|
495
|
-
build_type_name
|
496
111
|
|
497
|
-
#
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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)
|
127
|
+
end
|
128
|
+
|
129
|
+
# on model class level we return the collection
|
130
|
+
def odata_get(req)
|
131
|
+
@params = req.params
|
132
|
+
initialize_dataset
|
508
133
|
|
509
|
-
|
510
|
-
|
511
|
-
|
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
|
139
|
+
end
|
512
140
|
|
513
|
-
|
514
|
-
|
515
|
-
@pk_names = []
|
516
|
-
primary_key.each { |pk| @pk_names << pk.to_s }
|
517
|
-
# TODO: better handle quotes based on type
|
518
|
-
# (stringlike--> quote, int-like --> no quotes)
|
519
|
-
|
520
|
-
iuk = @pk_names.map { |pk| "#{pk}='?(\\w+)'?" }
|
521
|
-
@iuk_rgx = /\A#{iuk.join(',\s*')}\z/
|
522
|
-
|
523
|
-
iuk = @pk_names.map { |pk| "#{pk}='?\\w+'?" }
|
524
|
-
@entity_id_url_regexp = /\A\(\s*(#{iuk.join(',\s*')})\s*\)(.*)/
|
525
|
-
else
|
526
|
-
@pk_names = [primary_key.to_s]
|
527
|
-
@entity_id_url_regexp = SINGLE_PK_URL_REGEXP
|
141
|
+
def odata_post(req)
|
142
|
+
@modelk.odata_create_entity_and_relation(req)
|
528
143
|
end
|
529
144
|
end
|
530
145
|
|
531
|
-
|
532
|
-
|
533
|
-
cattr[:primary_key] ? nil : col
|
534
|
-
end.select { |col| col }
|
535
|
-
end
|
146
|
+
class NavigatedCollection < Collection
|
147
|
+
include Safrano::NavigationInfo
|
536
148
|
|
537
|
-
|
538
|
-
|
539
|
-
end
|
149
|
+
def initialize(childattrib, parent)
|
150
|
+
childklass = parent.class.nav_collection_attribs[childattrib]
|
540
151
|
|
541
|
-
|
542
|
-
|
543
|
-
def transition_attribute_regexp
|
544
|
-
# db_schema.map { |sch| sch[0] }.join('|')
|
545
|
-
# @columns is from Sequel Model
|
546
|
-
%r{\A/(#{@columns.join('|')})(.*)\z}
|
547
|
-
end
|
152
|
+
super(childklass)
|
153
|
+
@parent = parent
|
548
154
|
|
549
|
-
|
550
|
-
# type checking/convertion is done in check_odata_key_type
|
551
|
-
def find_by_odata_key(pkid)
|
552
|
-
# amazingly this works as expected from an Entity.get_related(...) anonymous class
|
553
|
-
# without need to redefine primary_key_lookup (returns nil for valid but unrelated keys)
|
554
|
-
primary_key_lookup(pkid)
|
555
|
-
end
|
155
|
+
set_relation_info(@parent, childattrib)
|
556
156
|
|
557
|
-
|
558
|
-
|
559
|
-
case type
|
560
|
-
when :integer
|
561
|
-
val =~ ONLY_INTEGER_RGX ? [true, Integer(val)] : [false, val]
|
562
|
-
when :string
|
563
|
-
[true, val]
|
564
|
-
else
|
565
|
-
[true, val] # todo...
|
566
|
-
end
|
567
|
-
rescue StandardError
|
568
|
-
[false, val]
|
569
|
-
end
|
157
|
+
@child_method = parent.method(childattrib.to_sym)
|
158
|
+
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
570
159
|
|
571
|
-
|
572
|
-
module Transitions
|
573
|
-
def transition_end(_match_result)
|
574
|
-
[nil, :end]
|
160
|
+
@cx = navigated_dataset
|
575
161
|
end
|
576
162
|
|
577
|
-
def
|
578
|
-
|
163
|
+
def odata_post(req)
|
164
|
+
@modelk.odata_create_entity_and_relation(req,
|
165
|
+
@navattr_reflection,
|
166
|
+
@nav_parent)
|
579
167
|
end
|
580
168
|
|
581
|
-
def
|
582
|
-
|
583
|
-
|
584
|
-
ck, casted_id = check_odata_key(id)
|
585
|
-
|
586
|
-
if ck
|
587
|
-
if (y = find_by_odata_key(casted_id))
|
588
|
-
[y, :run]
|
589
|
-
else
|
590
|
-
[nil, :error, ErrorNotFound]
|
591
|
-
end
|
592
|
-
else
|
593
|
-
[nil, :error, BadRequestError]
|
594
|
-
end
|
595
|
-
else
|
596
|
-
[nil, :error, ServerTransitionError]
|
597
|
-
end
|
169
|
+
def initialize_dataset(dtset = nil)
|
170
|
+
@cx = dtset || navigated_dataset
|
171
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
598
172
|
end
|
173
|
+
# redefinitions of the main methods for a navigated collection
|
174
|
+
# (eg. all Books of Author[2] is Author[2].Books.all )
|
599
175
|
|
600
|
-
def
|
601
|
-
@
|
176
|
+
def all
|
177
|
+
@child_method.call
|
602
178
|
end
|
603
179
|
|
604
|
-
def
|
605
|
-
@
|
180
|
+
def count
|
181
|
+
@child_method.call.count
|
606
182
|
end
|
607
183
|
|
608
|
-
def
|
609
|
-
@
|
610
|
-
Safrano::TransitionCount,
|
611
|
-
Safrano::Transition.new(entity_id_url_regexp,
|
612
|
-
trans: 'transition_id')].freeze
|
184
|
+
def dataset
|
185
|
+
@child_dataset_method.call
|
613
186
|
end
|
614
187
|
|
615
|
-
def
|
616
|
-
@
|
617
|
-
Safrano::TransitionEnd,
|
618
|
-
Safrano::TransitionCount,
|
619
|
-
Safrano::TransitionLinks,
|
620
|
-
Safrano::TransitionValue,
|
621
|
-
Safrano::Transition.new(transition_attribute_regexp, trans: 'transition_attribute')
|
622
|
-
]
|
623
|
-
if (ncurgx = @nav_collection_url_regexp)
|
624
|
-
@entity_allowed_transitions <<
|
625
|
-
Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z}, trans: 'transition_nav_collection')
|
626
|
-
end
|
627
|
-
if (neurgx = @nav_entity_url_regexp)
|
628
|
-
@entity_allowed_transitions <<
|
629
|
-
Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z}, trans: 'transition_nav_entity')
|
630
|
-
end
|
631
|
-
@entity_allowed_transitions.freeze
|
632
|
-
@entity_allowed_transitions
|
188
|
+
def navigated_dataset
|
189
|
+
@child_dataset_method.call
|
633
190
|
end
|
634
|
-
end
|
635
|
-
include Transitions
|
636
|
-
end
|
637
191
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
# input fx='aas',fy_w='0001'
|
642
|
-
# output true, ['aas', '0001'] ... or false when typ-error
|
643
|
-
def check_odata_key(mid)
|
644
|
-
# @iuk_rgx is (needs to be) built on start with
|
645
|
-
# collklass.prepare_pk
|
646
|
-
md = @iuk_rgx.match(mid).to_a
|
647
|
-
md.shift # remove first element which is the whole match
|
648
|
-
mdc = []
|
649
|
-
error = false
|
650
|
-
primary_key.each_with_index do |pk, i|
|
651
|
-
ck, casted = check_odata_val_type(md[i], db_schema[pk][:type])
|
652
|
-
if ck
|
653
|
-
mdc << casted
|
654
|
-
else
|
655
|
-
error = true
|
656
|
-
break
|
657
|
-
end
|
192
|
+
def each
|
193
|
+
y = @child_method.call
|
194
|
+
y.each { |enty| yield enty }
|
658
195
|
end
|
659
|
-
if error
|
660
|
-
[false, md]
|
661
|
-
else
|
662
|
-
[true, mdc]
|
663
|
-
end
|
664
|
-
end
|
665
|
-
end
|
666
|
-
|
667
|
-
# special handling for single key
|
668
|
-
module EntityClassSinglePK
|
669
|
-
include EntityClassBase
|
670
196
|
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
end
|
675
|
-
|
676
|
-
# normal handling for non-media entity
|
677
|
-
module EntityClassNonMedia
|
678
|
-
# POST for non-media entity collection -->
|
679
|
-
# 1. Create and add entity from payload
|
680
|
-
# 2. Create relationship if needed
|
681
|
-
def odata_create_entity_and_relation(req, assoc, parent)
|
682
|
-
# TODO: this is for v2 only...
|
683
|
-
req.with_parsed_data do |data|
|
684
|
-
data.delete('__metadata')
|
685
|
-
# validate payload column names
|
686
|
-
if (invalid = invalid_hash_data?(data))
|
687
|
-
::OData::Request::ON_CGST_ERROR.call(req)
|
688
|
-
return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
|
689
|
-
end
|
690
|
-
|
691
|
-
if req.accept?(APPJSON)
|
692
|
-
new_entity = new_from_hson_h(data)
|
693
|
-
if parent
|
694
|
-
odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
|
695
|
-
else
|
696
|
-
# in-changeset requests get their own transaction
|
697
|
-
new_entity.save(transaction: !req.in_changeset)
|
698
|
-
end
|
699
|
-
req.register_content_id_ref(new_entity)
|
700
|
-
new_entity.copy_request_infos(req)
|
701
|
-
|
702
|
-
[201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
|
703
|
-
else # TODO: other formats
|
704
|
-
415
|
705
|
-
end
|
197
|
+
def to_a
|
198
|
+
y = @child_method.call
|
199
|
+
y.to_a
|
706
200
|
end
|
707
201
|
end
|
708
202
|
end
|