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