safrano 0.4.3 → 0.5.1
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 +8 -4
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +139 -642
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +196 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +78 -123
- data/lib/odata/error.rb +170 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +9 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +39 -25
- data/lib/odata/filter/sequel.rb +112 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +21 -18
- data/lib/odata/filter/tree.rb +78 -44
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +641 -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 +18 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +141 -0
- data/lib/safrano/core.rb +24 -106
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -24
- data/lib/safrano/rack_app.rb +62 -63
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +96 -38
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +156 -110
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +30 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b38827d37fa3bfed54a30aa61c04b5da27470071c5e3167fb5a08aa5c48a69af
|
4
|
+
data.tar.gz: 3754e63822b6c504b42bc698df360295ab92bcf96a2ed8fdef4af7f2ed5c8d85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c6a3949c741f120955b4582ddad95aa3fefe80f613030970c174b4fe000129e4d2f67dcb43deae5608a2a287a489a4c94d6ba0df5a613c5557b22b8ca4f625b
|
7
|
+
data.tar.gz: 7e5070fa657435fbc3a2932344392e58fa7105863bb045ad5a0aac7227a1e7ce5e4f54628915c81156da2f315eb359409c786243c47697877237ac08294a7872
|
@@ -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
|
@@ -49,7 +53,7 @@ module OData
|
|
49
53
|
# methods related to transitions to next state (cf. walker)
|
50
54
|
module Transitions
|
51
55
|
def transition_end(_match_result)
|
52
|
-
|
56
|
+
Transition::RESULT_END
|
53
57
|
end
|
54
58
|
|
55
59
|
def transition_value(_match_result)
|
@@ -60,7 +64,7 @@ module OData
|
|
60
64
|
Safrano::TransitionValue].freeze
|
61
65
|
|
62
66
|
def allowed_transitions
|
63
|
-
ALLOWED_TRANSITIONS
|
67
|
+
Transitions::ALLOWED_TRANSITIONS
|
64
68
|
end
|
65
69
|
end
|
66
70
|
include Transitions
|
data/lib/odata/batch.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../safrano/rack_app.rb'
|
2
4
|
require_relative '../safrano/core.rb'
|
3
5
|
require 'rack/body_proxy'
|
4
6
|
require_relative './common_logger.rb'
|
5
7
|
|
6
|
-
module
|
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,205 @@
|
|
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
|
97
|
-
|
98
|
-
def build_type_name
|
99
|
-
@type_name = to_s
|
100
|
-
end
|
101
|
-
|
102
|
-
# convention: default for entity_set_name is the type name
|
103
|
-
def entity_set_name
|
104
|
-
@entity_set_name = (@entity_set_name || type_name)
|
105
|
-
end
|
5
|
+
module Safrano
|
6
|
+
module OData
|
7
|
+
class Collection
|
8
|
+
attr_accessor :cx
|
106
9
|
|
107
|
-
|
108
|
-
|
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
|
10
|
+
# url params
|
11
|
+
attr_reader :params
|
117
12
|
|
118
|
-
|
119
|
-
|
120
|
-
|
13
|
+
# url parameters processing object (mostly convert to sequel exprs).
|
14
|
+
# exposed for testing only
|
15
|
+
attr_reader :uparms
|
121
16
|
|
122
|
-
|
123
|
-
instance_eval { @deferred_iblock.call } if @deferred_iblock
|
124
|
-
end
|
17
|
+
attr_reader :inlinecount
|
125
18
|
|
126
|
-
|
127
|
-
def new_from_hson_h(hash)
|
128
|
-
enty = new
|
129
|
-
enty.set_fields(hash, @data_fields, missing: :skip)
|
130
|
-
enty
|
131
|
-
end
|
19
|
+
attr_reader :modelk
|
132
20
|
|
133
|
-
|
134
|
-
|
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
24
|
|
160
|
-
|
161
|
-
|
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
|
-
|
205
|
-
nil
|
206
|
-
end
|
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
28
|
|
248
|
-
|
249
|
-
|
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
|
-
|
259
|
-
# finally return the requested output according to format, options etc
|
260
|
-
def odata_get_output(req)
|
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
32
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
def with_validated_get(req)
|
280
|
-
begin
|
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
|
-
|
361
|
-
D = 'd'.freeze
|
362
|
-
DJ_OPEN = '{"d":'.freeze
|
363
|
-
DJ_CLOSE = '}'.freeze
|
364
|
-
def to_odata_links_json(service:)
|
365
|
-
innerj = service.get_coll_odata_links_h(array: @cx.all,
|
366
|
-
icount: @inlinecount).to_json
|
367
|
-
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
368
|
-
end
|
369
66
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
446
|
-
|
447
|
-
# this functionally similar to the Sequel Rels (many_to_one etc)
|
448
|
-
# We need to base this on the Sequel rels, or extend them
|
449
|
-
def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
|
450
|
-
@nav_collection_attribs = (@nav_collection_attribs || {})
|
451
|
-
# DONE: Error handling. This requires that associations
|
452
|
-
# have been properly defined with Sequel before
|
453
|
-
assoc = all_association_reflections.find do |a|
|
454
|
-
a[:name] == assoc_symb && a[:model] == self
|
455
|
-
end
|
80
|
+
D = 'd'
|
81
|
+
DJ_OPEN = '{"d":'
|
82
|
+
DJ_CLOSE = '}'
|
83
|
+
EMPTYH = {}.freeze
|
456
84
|
|
457
|
-
|
85
|
+
def to_odata_json(request:)
|
86
|
+
template = @modelk.output_template(expand_list: @uparms.expand.template,
|
87
|
+
select: @uparms.select)
|
88
|
+
# TODO: Error handling if database contains binary BLOB data that cant be
|
89
|
+
# interpreted as UTF-8 then JSON will fail here
|
90
|
+
innerj = request.service.get_coll_odata_h(array: @cx.all,
|
91
|
+
template: template,
|
92
|
+
icount: @inlinecount).to_json
|
458
93
|
|
459
|
-
|
460
|
-
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
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
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
471
95
|
end
|
472
96
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
@nav_entity_attribs[lattr_name_str] = attr_class
|
478
|
-
@nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
|
479
|
-
end
|
480
|
-
|
481
|
-
EMPTYH = {}.freeze
|
482
|
-
|
483
|
-
def build_default_template
|
484
|
-
template = { all_values: EMPTYH }
|
485
|
-
if @nav_entity_attribs || @nav_collection_attribs
|
486
|
-
template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
|
97
|
+
def to_odata_links_json(service:)
|
98
|
+
innerj = service.get_coll_odata_links_h(array: @cx.all,
|
99
|
+
icount: @inlinecount).to_json
|
100
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
487
101
|
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
102
|
|
497
|
-
|
498
|
-
|
103
|
+
def odata_get_inlinecount_w_sequel
|
104
|
+
return unless (icp = @params['$inlinecount'])
|
499
105
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
106
|
+
@inlinecount = if icp == 'allpages'
|
107
|
+
if @modelk.is_a? Sequel::Model::ClassMethods
|
108
|
+
@cx.count
|
109
|
+
else
|
110
|
+
@cx.dataset.count
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
505
114
|
|
506
|
-
#
|
507
|
-
|
115
|
+
# finally return the requested output according to format, options etc
|
116
|
+
def odata_get_output(req)
|
117
|
+
output = if req.walker.do_count
|
118
|
+
[200, CT_TEXT, @cx.count.to_s]
|
119
|
+
elsif req.accept?(APPJSON)
|
120
|
+
# json is default content type so we dont need to specify it here again
|
121
|
+
if req.walker.do_links
|
122
|
+
[200, EMPTYH, [to_odata_links_json(service: req.service)]]
|
123
|
+
else
|
124
|
+
[200, EMPTYH, [to_odata_json(request: req)]]
|
125
|
+
end
|
126
|
+
else # TODO: other formats
|
127
|
+
406
|
128
|
+
end
|
129
|
+
Contract.valid(output)
|
130
|
+
end
|
131
|
+
|
132
|
+
# on model class level we return the collection
|
133
|
+
def odata_get(req)
|
134
|
+
@params = req.params
|
135
|
+
initialize_dataset
|
508
136
|
|
509
|
-
|
510
|
-
|
511
|
-
|
137
|
+
@uparms.check_all.if_valid { |_ret|
|
138
|
+
odata_get_apply_params.if_valid { |_ret|
|
139
|
+
odata_get_output(req)
|
140
|
+
}
|
141
|
+
}.tap_error { |e| return e.odata_get(req) }.result
|
142
|
+
end
|
512
143
|
|
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
|
144
|
+
def odata_post(req)
|
145
|
+
@modelk.odata_create_entity_and_relation(req)
|
528
146
|
end
|
529
147
|
end
|
530
148
|
|
531
|
-
|
532
|
-
|
533
|
-
cattr[:primary_key] ? nil : col
|
534
|
-
end.select { |col| col }
|
535
|
-
end
|
149
|
+
class NavigatedCollection < Collection
|
150
|
+
include Safrano::NavigationInfo
|
536
151
|
|
537
|
-
|
538
|
-
|
539
|
-
end
|
152
|
+
def initialize(childattrib, parent)
|
153
|
+
childklass = parent.class.nav_collection_attribs[childattrib]
|
540
154
|
|
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
|
155
|
+
super(childklass)
|
156
|
+
@parent = parent
|
548
157
|
|
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
|
158
|
+
set_relation_info(@parent, childattrib)
|
556
159
|
|
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
|
160
|
+
@child_method = parent.method(childattrib.to_sym)
|
161
|
+
@child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
|
570
162
|
|
571
|
-
|
572
|
-
module Transitions
|
573
|
-
def transition_end(_match_result)
|
574
|
-
[nil, :end]
|
163
|
+
@cx = navigated_dataset
|
575
164
|
end
|
576
165
|
|
577
|
-
def
|
578
|
-
|
166
|
+
def odata_post(req)
|
167
|
+
@modelk.odata_create_entity_and_relation(req,
|
168
|
+
@navattr_reflection,
|
169
|
+
@nav_parent)
|
579
170
|
end
|
580
171
|
|
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
|
172
|
+
def initialize_dataset(dtset = nil)
|
173
|
+
@cx = dtset || navigated_dataset
|
174
|
+
@uparms = UrlParameters4Coll.new(@cx, @params)
|
598
175
|
end
|
176
|
+
# redefinitions of the main methods for a navigated collection
|
177
|
+
# (eg. all Books of Author[2] is Author[2].Books.all )
|
599
178
|
|
600
|
-
def
|
601
|
-
@
|
179
|
+
def all
|
180
|
+
@child_method.call
|
602
181
|
end
|
603
182
|
|
604
|
-
def
|
605
|
-
@
|
183
|
+
def count
|
184
|
+
@child_method.call.count
|
606
185
|
end
|
607
186
|
|
608
|
-
def
|
609
|
-
@
|
610
|
-
Safrano::TransitionCount,
|
611
|
-
Safrano::Transition.new(entity_id_url_regexp,
|
612
|
-
trans: 'transition_id')].freeze
|
187
|
+
def dataset
|
188
|
+
@child_dataset_method.call
|
613
189
|
end
|
614
190
|
|
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
|
191
|
+
def navigated_dataset
|
192
|
+
@child_dataset_method.call
|
633
193
|
end
|
634
|
-
end
|
635
|
-
include Transitions
|
636
|
-
end
|
637
194
|
|
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
|
195
|
+
def each
|
196
|
+
y = @child_method.call
|
197
|
+
y.each { |enty| yield enty }
|
658
198
|
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
199
|
|
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
|
200
|
+
def to_a
|
201
|
+
y = @child_method.call
|
202
|
+
y.to_a
|
706
203
|
end
|
707
204
|
end
|
708
205
|
end
|