safrano 0.5.0 → 0.5.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/Numeric/convert.rb +21 -0
- data/lib/core_ext/numeric.rb +3 -0
- data/lib/odata/collection.rb +7 -5
- data/lib/odata/complex_type.rb +165 -27
- data/lib/odata/edm/primitive_types.rb +97 -19
- data/lib/odata/entity.rb +6 -31
- data/lib/odata/function_import.rb +19 -21
- data/lib/odata/model_ext.rb +41 -11
- data/lib/odata/transition.rb +25 -1
- data/lib/odata/walker.rb +19 -1
- data/lib/safrano/contract.rb +2 -4
- data/lib/safrano/request.rb +1 -0
- data/lib/safrano/service.rb +30 -4
- data/lib/safrano/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89b3584ee45b115a0a463faea56586c06271c36ea080c4f73b64fb4a4a641802
|
4
|
+
data.tar.gz: 977386e3bff89f18c6b596f1f4244623e85b2960ab9cb9eed6ebf7c9c1c0498f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e45fb7cd3af3aa3b0d918229f84760b9c779a01a55d5f46f593e93dd2f18dbefe5e3713141cf3d72523b3e2069b426130760531c991549993f861c26c9b8a2f0
|
7
|
+
data.tar.gz: 943e0cb99f039eb41c69204dd348347965b188a0e334d13c075ffe06188b01a44e8bc757b86509de24ca843b9253f131ed3c30bbbc4eaaa2d176cc6b5e71249d
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# require 'bigdecimal/util'
|
4
|
+
|
5
|
+
module Safrano
|
6
|
+
module CoreIncl
|
7
|
+
module Numeric
|
8
|
+
module Convert
|
9
|
+
def toDecimalString
|
10
|
+
BigDecimal(to_s).to_s('F')
|
11
|
+
end
|
12
|
+
|
13
|
+
def toDecimalPrecisionString(precision, scale = nil)
|
14
|
+
p = Integer(precision)
|
15
|
+
|
16
|
+
scale.nil? ? BigDecimal(self, p).to_s('F') : sprintf("%#{p + 2}.#{scale}f", self).strip
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/odata/collection.rb
CHANGED
@@ -60,7 +60,10 @@ module Safrano
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def initialize_dataset(dtset = nil)
|
63
|
-
@cx = dtset || @modelk
|
63
|
+
@cx = @cx || dtset || @modelk
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize_uparms
|
64
67
|
@uparms = UrlParameters4Coll.new(@cx, @params)
|
65
68
|
end
|
66
69
|
|
@@ -131,9 +134,9 @@ module Safrano
|
|
131
134
|
|
132
135
|
# on model class level we return the collection
|
133
136
|
def odata_get(req)
|
134
|
-
@params = req.params
|
137
|
+
@params = @params || req.params
|
135
138
|
initialize_dataset
|
136
|
-
|
139
|
+
initialize_uparms
|
137
140
|
@uparms.check_all.if_valid { |_ret|
|
138
141
|
odata_get_apply_params.if_valid { |_ret|
|
139
142
|
odata_get_output(req)
|
@@ -170,8 +173,7 @@ module Safrano
|
|
170
173
|
end
|
171
174
|
|
172
175
|
def initialize_dataset(dtset = nil)
|
173
|
-
@cx = dtset || navigated_dataset
|
174
|
-
@uparms = UrlParameters4Coll.new(@cx, @params)
|
176
|
+
@cx = @cx || dtset || navigated_dataset
|
175
177
|
end
|
176
178
|
# redefinitions of the main methods for a navigated collection
|
177
179
|
# (eg. all Books of Author[2] is Author[2].Books.all )
|
data/lib/odata/complex_type.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Safrano
|
4
4
|
module FunctionImport
|
5
|
+
EMPTY_HASH = {}.freeze
|
5
6
|
class ResultDefinition
|
6
7
|
D = 'd'
|
7
8
|
DJ_OPEN = '{"d":'
|
@@ -12,65 +13,166 @@ module Safrano
|
|
12
13
|
RESULTSK = 'results'
|
13
14
|
COLLECTION = 'Collection'
|
14
15
|
|
15
|
-
def
|
16
|
-
|
16
|
+
def allowed_transitions
|
17
|
+
[Safrano::TransitionEnd]
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
|
20
|
+
def transition_end(_match_result)
|
21
|
+
Safrano::Transition::RESULT_END
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
|
+
# we will have this on class and instance level for making things simpler first
|
25
|
+
def self.klassmod
|
26
|
+
@klassmod
|
27
|
+
end
|
28
|
+
|
29
|
+
# return a subclass of ResultAsComplexType
|
30
|
+
def self.asComplexType(klassmod)
|
31
|
+
Class.new(ResultAsComplexType) do
|
32
|
+
@klassmod = klassmod
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# return a subclass of ResultAsComplexType
|
37
|
+
def self.asComplexTypeColl(klassmod)
|
38
|
+
Class.new(ResultAsComplexTypeColl) do
|
39
|
+
@klassmod = klassmod
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.asPrimitiveType(klassmod)
|
44
|
+
Class.new(ResultAsPrimitiveType) do
|
45
|
+
@klassmod = klassmod
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.asPrimitiveTypeColl(klassmod)
|
50
|
+
Class.new(ResultAsPrimitiveTypeColl) do
|
51
|
+
@klassmod = klassmod
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.asEntity(klassmod)
|
56
|
+
Class.new(ResultAsEntity) do
|
57
|
+
@klassmod = klassmod
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.asEntityColl(klassmod)
|
62
|
+
Class.new(ResultAsEntityColl) do
|
63
|
+
@klassmod = klassmod
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(value)
|
68
|
+
@value = value
|
69
|
+
end
|
70
|
+
|
71
|
+
def odata_get(req)
|
72
|
+
[200, EMPTY_HASH, [to_odata_json(req)]]
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.type_metadata
|
24
76
|
@klassmod.type_name
|
25
77
|
end
|
78
|
+
|
79
|
+
def type_metadata
|
80
|
+
self.class.type_metadata
|
81
|
+
end
|
82
|
+
|
83
|
+
# needed for ComplexType result
|
84
|
+
def to_odata_json(_req)
|
85
|
+
"#{DJ_OPEN}#{@value.odata_h.to_json}#{DJ_CLOSE}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# wrapper
|
89
|
+
# for OData Entity and Collections, return them directly
|
90
|
+
# for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
|
91
|
+
def self.do_execute_func_result(result, _req, apply_query_params = false)
|
92
|
+
self.new(result)
|
93
|
+
end
|
26
94
|
end
|
95
|
+
|
27
96
|
class ResultAsComplexType < ResultDefinition
|
97
|
+
def self.type_metadata
|
98
|
+
@klassmod.type_name
|
99
|
+
end
|
28
100
|
end
|
101
|
+
|
29
102
|
class ResultAsComplexTypeColl < ResultDefinition
|
30
|
-
def type_metadata
|
103
|
+
def self.type_metadata
|
31
104
|
"Collection(#{@klassmod.type_name})"
|
32
105
|
end
|
33
106
|
|
34
|
-
def to_odata_json(
|
35
|
-
"#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
107
|
+
def to_odata_json(req)
|
108
|
+
# "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
109
|
+
template = self.class.klassmod.output_template
|
110
|
+
# TODO: Error handling if database contains binary BLOB data that cant be
|
111
|
+
# interpreted as UTF-8 then JSON will fail here
|
112
|
+
|
113
|
+
innerh = req.service.get_coll_odata_h(array: @value,
|
114
|
+
template: template)
|
115
|
+
|
116
|
+
innerj = innerh.to_json
|
117
|
+
|
118
|
+
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
36
119
|
end
|
37
120
|
end
|
121
|
+
|
38
122
|
class ResultAsEntity < ResultDefinition
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
123
|
+
def self.type_metadata
|
124
|
+
@klassmod.type_name
|
125
|
+
end
|
126
|
+
|
127
|
+
# wrapper
|
128
|
+
# for OData Entity return them directly
|
129
|
+
def self.do_execute_func_result(result, _req, apply_query_params = false)
|
130
|
+
# note: Sequel entities instances seem to be thread safe, so we can
|
131
|
+
# safely add request-dependant data (eg. req.params) there
|
132
|
+
apply_query_params ? result : result.inactive_query_params
|
44
133
|
end
|
45
134
|
end
|
135
|
+
|
46
136
|
class ResultAsEntityColl < ResultDefinition
|
47
|
-
def type_metadata
|
137
|
+
def self.type_metadata
|
48
138
|
"Collection(#{@klassmod.type_name})"
|
49
139
|
end
|
50
140
|
|
51
|
-
|
141
|
+
# wrapper
|
142
|
+
# for OData Entity Collection return them directly
|
143
|
+
def self.do_execute_func_result(result, req, apply_query_params = false)
|
52
144
|
coll = Safrano::OData::Collection.new(@klassmod)
|
145
|
+
# instance_exec has other instance variables; @values would be nil in the block below
|
146
|
+
# need to pass a local copy
|
147
|
+
dtset = result
|
53
148
|
coll.instance_exec do
|
54
|
-
@params = req.params
|
55
|
-
initialize_dataset(
|
149
|
+
@params = apply_query_params ? req.params : EMPTY_HASH
|
150
|
+
initialize_dataset(dtset)
|
151
|
+
initialize_uparms
|
56
152
|
end
|
57
|
-
coll
|
153
|
+
coll
|
58
154
|
end
|
59
155
|
end
|
156
|
+
|
60
157
|
class ResultAsPrimitiveType < ResultDefinition
|
61
|
-
def
|
158
|
+
def self.type_metadata
|
159
|
+
@klassmod.type_name
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_odata_json(_req)
|
62
163
|
{ D => { METAK => { TYPEK => type_metadata },
|
63
|
-
VALUEK =>
|
164
|
+
VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
|
64
165
|
end
|
65
166
|
end
|
167
|
+
|
66
168
|
class ResultAsPrimitiveTypeColl < ResultDefinition
|
67
|
-
def type_metadata
|
169
|
+
def self.type_metadata
|
68
170
|
"Collection(#{@klassmod.type_name})"
|
69
171
|
end
|
70
172
|
|
71
|
-
def to_odata_json(
|
72
|
-
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
-
RESULTSK =>
|
173
|
+
def to_odata_json(_req)
|
174
|
+
{ D => { METAK => { TYPEK => self.class.type_metadata },
|
175
|
+
RESULTSK => self.class.klassmod.odata_collection(@value) } }.to_json
|
74
176
|
end
|
75
177
|
end
|
76
178
|
end
|
@@ -81,6 +183,8 @@ module Safrano
|
|
81
183
|
class ComplexType
|
82
184
|
attr_reader :values
|
83
185
|
|
186
|
+
EMPTYH = {}.freeze
|
187
|
+
|
84
188
|
@namespace = nil
|
85
189
|
def self.namespace
|
86
190
|
@namespace
|
@@ -90,8 +194,41 @@ module Safrano
|
|
90
194
|
@props
|
91
195
|
end
|
92
196
|
|
197
|
+
def type_name
|
198
|
+
self.class.type_name
|
199
|
+
end
|
200
|
+
|
201
|
+
def metadata_h
|
202
|
+
{ type: type_name }
|
203
|
+
end
|
204
|
+
|
205
|
+
def casted_values
|
206
|
+
# MVP... TODO: handle time mappings like in Entity models
|
207
|
+
values
|
208
|
+
end
|
209
|
+
|
210
|
+
# needed for nested json output
|
211
|
+
# this is a simpler version of model_ext#output_template
|
212
|
+
def self.default_template
|
213
|
+
template = {}
|
214
|
+
expand_e = {}
|
215
|
+
|
216
|
+
template[:all_values] = EMPTYH
|
217
|
+
@props.each { |prop, kl|
|
218
|
+
if kl.respond_to? :default_template
|
219
|
+
expand_e[prop] = kl.default_template
|
220
|
+
end
|
221
|
+
}
|
222
|
+
template[:expand_e] = expand_e
|
223
|
+
template
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.output_template
|
227
|
+
default_template
|
228
|
+
end
|
229
|
+
|
93
230
|
def self.type_name
|
94
|
-
"#{@namespace}.#{self.to_s}"
|
231
|
+
@namespace ? "#{@namespace}.#{self.to_s}" : self.to_s
|
95
232
|
end
|
96
233
|
|
97
234
|
def initialize
|
@@ -102,6 +239,7 @@ module Safrano
|
|
102
239
|
|
103
240
|
def odata_h
|
104
241
|
ret = { METAK => { TYPEK => self.class.type_name } }
|
242
|
+
|
105
243
|
@values.each { |k, v|
|
106
244
|
ret[k] = if v.respond_to? :odata_h
|
107
245
|
v.odata_h
|
@@ -113,11 +251,11 @@ module Safrano
|
|
113
251
|
end
|
114
252
|
|
115
253
|
def self.return_as_collection_descriptor
|
116
|
-
FunctionImport::
|
254
|
+
FunctionImport::ResultDefinition.asComplexTypeColl(self)
|
117
255
|
end
|
118
256
|
|
119
257
|
def self.return_as_instance_descriptor
|
120
|
-
FunctionImport::
|
258
|
+
FunctionImport::ResultDefinition.asComplexType(self)
|
121
259
|
end
|
122
260
|
|
123
261
|
# add metadata xml to the passed REXML schema object
|
@@ -15,29 +15,107 @@ module Safrano
|
|
15
15
|
# Classes specifying generic types that Sequel will convert to
|
16
16
|
# database-specific types.
|
17
17
|
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
18
|
-
|
18
|
+
DB_TYPE_NUMDEC_RGX = /\A(NUMERIC|DECIMAL)\s*(\(\s*((\d+)\s*(,\s*(\d+))?)\s*\))?\s*\z/i.freeze
|
19
|
+
# thank you rubular
|
20
|
+
# Test String: DECIMAL (55,2 )
|
21
|
+
# Match groups
|
22
|
+
# 1 DECIMAL
|
23
|
+
# 2 (55,2 )
|
24
|
+
# 3 55,2
|
25
|
+
# 4 55
|
26
|
+
# 5 ,2
|
27
|
+
# 6 2
|
28
|
+
|
29
|
+
DB_TYPE_FLOATP_RGX = /\A\s*(FLOAT)\s*(\(\s*(\d+)\s*\))?\s*\z/i.freeze
|
30
|
+
|
31
|
+
# Note: "char" (quoted!) is postgresql's byte type
|
32
|
+
DB_TYPE_INTLIKE_RGX = /\A\s*(smallserial|smallint|integer|int2|int4|int8|int|mediumint|bigint|serial|bigserial|tinyint)\s*/i.freeze
|
19
33
|
# used in $metadata
|
20
34
|
# cf. Sequel Database column_schema_default_to_ruby_value
|
21
35
|
# schema_column_type
|
22
36
|
# https://www.odata.org/documentation/odata-version-2-0/overview/
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
|
38
|
+
# type mappings are hard, especially between "Standards" like SQL and OData V2 (might be a bit better in V4 ?)
|
39
|
+
# this is all best effort/try to make it work logic
|
40
|
+
def self.add_edm_types(props)
|
41
|
+
# try num/dec with db_type:
|
42
|
+
props[:edm_type] = if (md = DB_TYPE_NUMDEC_RGX.match(props[:db_type]))
|
43
|
+
prec = md[4]
|
44
|
+
scale = md[6]
|
45
|
+
if (scale && prec)
|
46
|
+
if (scale == '0') # dont force default scale to 0 like SQL standard
|
47
|
+
props[:edm_precision] = prec
|
48
|
+
"Edm.Decimal(#{prec})"
|
49
|
+
else
|
50
|
+
# we have precision and scale
|
51
|
+
props[:edm_scale] = scale
|
52
|
+
props[:edm_precision] = prec
|
53
|
+
"Edm.Decimal(#{prec},#{scale})"
|
54
|
+
end
|
55
|
+
elsif prec
|
56
|
+
# we have precision only
|
57
|
+
props[:edm_precision] = prec
|
58
|
+
"Edm.Decimal(#{prec})"
|
59
|
+
else
|
60
|
+
'Edm.Decimal'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return if props[:edm_type]
|
64
|
+
|
65
|
+
# try float(prec) with db_type:
|
66
|
+
props[:edm_type] = if (md = DB_TYPE_FLOATP_RGX.match(props[:db_type]))
|
67
|
+
# FLOAT( 22) match groups
|
68
|
+
# 1 FLOAT
|
69
|
+
# 2 (22 )
|
70
|
+
# 3 22
|
71
|
+
|
72
|
+
if (prec = md[3])
|
73
|
+
# we have precision only
|
74
|
+
props[:edm_precision] = prec
|
75
|
+
'Edm.Double'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return if props[:edm_type]
|
79
|
+
|
80
|
+
# try int-like with db_type:
|
81
|
+
# smallint|int|integer|bigint|serial|bigserial
|
82
|
+
props[:edm_type] = if (md = DB_TYPE_INTLIKE_RGX.match(props[:db_type]))
|
83
|
+
|
84
|
+
if (itype = md[1])
|
85
|
+
case itype.downcase
|
86
|
+
when 'smallint', 'int2', 'smallserial'
|
87
|
+
'Edm.Int16'
|
88
|
+
when 'int', 'integer', 'serial', 'mediumint', 'int4'
|
89
|
+
'Edm.Int32'
|
90
|
+
when 'bigint', 'bigserial', 'int8'
|
91
|
+
'Edm.Int64'
|
92
|
+
when 'tinyint'
|
93
|
+
'Edm.Byte'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
return if props[:edm_type]
|
98
|
+
|
99
|
+
props[:edm_type] = case props[:type]
|
100
|
+
when :integer
|
101
|
+
'Edm.Int32'
|
102
|
+
when :string
|
103
|
+
'Edm.String'
|
104
|
+
when :date
|
105
|
+
'Edm.DateTime'
|
106
|
+
when :datetime
|
107
|
+
'Edm.DateTime'
|
108
|
+
when :time
|
109
|
+
'Edm.Time'
|
110
|
+
when :boolean
|
111
|
+
'Edm.Boolean'
|
112
|
+
when :float
|
113
|
+
'Edm.Double'
|
114
|
+
when :decimal
|
115
|
+
'Edm.Decimal'
|
116
|
+
when :blob
|
117
|
+
'Edm.Binary'
|
118
|
+
else
|
41
119
|
end
|
42
120
|
end
|
43
121
|
|
data/lib/odata/entity.rb
CHANGED
@@ -125,7 +125,7 @@ module Safrano
|
|
125
125
|
end
|
126
126
|
|
127
127
|
def copy_request_infos(req)
|
128
|
-
@params = req.params
|
128
|
+
@params = @inactive_query_params ? EMPTY_HASH : req.params
|
129
129
|
@do_links = req.walker.do_links
|
130
130
|
@uparms = UrlParameters4Single.new(self, @params)
|
131
131
|
end
|
@@ -152,6 +152,11 @@ module Safrano
|
|
152
152
|
.tap_error { |e| return e.odata_get(req) }
|
153
153
|
end
|
154
154
|
|
155
|
+
def inactive_query_params
|
156
|
+
@inactive_query_params = true
|
157
|
+
self # chaining
|
158
|
+
end
|
159
|
+
|
155
160
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
156
161
|
Safrano.remove_nav_relation(assoc, parent)
|
157
162
|
entity.destroy(transaction: false)
|
@@ -306,36 +311,6 @@ module Safrano
|
|
306
311
|
end
|
307
312
|
end
|
308
313
|
|
309
|
-
module MappingBeforeOutput
|
310
|
-
# needed for proper datetime output
|
311
|
-
def casted_values(cols = nil)
|
312
|
-
vals = case cols
|
313
|
-
when nil
|
314
|
-
# we need to dup the model values as we need to change it before passing to_json,
|
315
|
-
# but we dont want to interfere with Sequel's owned data
|
316
|
-
# (eg because then in worst case it could happen that we write back changed values to DB)
|
317
|
-
values_for_odata.dup
|
318
|
-
else
|
319
|
-
selected_values_for_odata(cols)
|
320
|
-
end
|
321
|
-
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
322
|
-
vals
|
323
|
-
end
|
324
|
-
end
|
325
|
-
module NoMappingBeforeOutput
|
326
|
-
# current model does not have eg. Time fields--> no special mapping, just to_json is fine
|
327
|
-
# --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
|
328
|
-
# need to change it, just output as is
|
329
|
-
def casted_values(cols = nil)
|
330
|
-
case cols
|
331
|
-
when nil
|
332
|
-
values_for_odata
|
333
|
-
else
|
334
|
-
selected_values_for_odata(cols)
|
335
|
-
end
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
314
|
module MediaEntity
|
340
315
|
# media entity metadata for json h
|
341
316
|
def metadata_h
|
@@ -21,7 +21,7 @@ module Safrano
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def allowed_transitions
|
24
|
-
[Safrano::
|
24
|
+
[Safrano::TransitionExecuteFunc]
|
25
25
|
end
|
26
26
|
|
27
27
|
def input(**parmtypes)
|
@@ -43,6 +43,12 @@ module Safrano
|
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
46
|
+
def auto_query_parameters
|
47
|
+
@auto_query_params = true
|
48
|
+
self # chaining
|
49
|
+
end
|
50
|
+
alias auto_query_params auto_query_parameters
|
51
|
+
|
46
52
|
def return(klassmod, &proc)
|
47
53
|
raise('Please provide a code block') unless block_given?
|
48
54
|
|
@@ -51,7 +57,7 @@ module Safrano
|
|
51
57
|
else
|
52
58
|
# if it's neither a ComplexType nor a Model-Entity
|
53
59
|
# --> assume it is a Primitive
|
54
|
-
|
60
|
+
ResultDefinition.asPrimitiveType(klassmod)
|
55
61
|
end
|
56
62
|
@proc = proc
|
57
63
|
self
|
@@ -65,7 +71,8 @@ module Safrano
|
|
65
71
|
else
|
66
72
|
# if it's neither a ComplexType nor a Modle-Entity
|
67
73
|
# --> assume it is a Primitive
|
68
|
-
ResultAsPrimitiveTypeColl.new(klassmod)
|
74
|
+
# ResultAsPrimitiveTypeColl.new(klassmod)
|
75
|
+
ResultDefinition.asPrimitiveTypeColl(klassmod)
|
69
76
|
end
|
70
77
|
@proc = proc
|
71
78
|
self
|
@@ -136,32 +143,23 @@ module Safrano
|
|
136
143
|
funky
|
137
144
|
end
|
138
145
|
|
139
|
-
def
|
146
|
+
def with_transition_validated(req)
|
140
147
|
# initialize_params
|
148
|
+
@params = req.params
|
141
149
|
return yield unless (@error = check_url_func_params)
|
142
150
|
|
143
|
-
@error
|
144
|
-
end
|
145
|
-
|
146
|
-
def to_odata_json(req)
|
147
|
-
result = @proc.call(**@funcparams)
|
148
|
-
@returning.to_odata_json(result, req)
|
151
|
+
[nil, :error, @error] if @error
|
149
152
|
end
|
150
153
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
def odata_get(req)
|
156
|
-
@params = req.params
|
157
|
-
|
158
|
-
with_validated_get(req) do
|
159
|
-
odata_get_output(req)
|
154
|
+
def do_execute_func(req)
|
155
|
+
with_transition_validated(req) do
|
156
|
+
result = @proc.call(**@funcparams)
|
157
|
+
[@returning.do_execute_func_result(result, req, @auto_query_params), :run]
|
160
158
|
end
|
161
159
|
end
|
162
160
|
|
163
|
-
def
|
164
|
-
|
161
|
+
def transition_execute_func(_match_result)
|
162
|
+
[self, :run_with_execute_func]
|
165
163
|
end
|
166
164
|
end
|
167
165
|
end
|
data/lib/odata/model_ext.rb
CHANGED
@@ -5,7 +5,9 @@
|
|
5
5
|
# Thus Below we have called that "EntityClass". It's meant as "Collection"
|
6
6
|
|
7
7
|
require 'json'
|
8
|
+
require 'base64'
|
8
9
|
require 'rexml/document'
|
10
|
+
require 'bigdecimal'
|
9
11
|
require_relative '../safrano/core'
|
10
12
|
require_relative 'error'
|
11
13
|
require_relative 'collection_filter'
|
@@ -31,7 +33,7 @@ module Safrano
|
|
31
33
|
attr_reader :default_template
|
32
34
|
attr_reader :uri
|
33
35
|
attr_reader :odata_upk_parts
|
34
|
-
attr_reader :
|
36
|
+
attr_reader :casted_cols
|
35
37
|
attr_reader :namespace
|
36
38
|
|
37
39
|
# initialising block of code to be executed at end of
|
@@ -73,7 +75,6 @@ module Safrano
|
|
73
75
|
@uparms = nil
|
74
76
|
@params = nil
|
75
77
|
@cx = nil
|
76
|
-
@@time_cols = nil
|
77
78
|
end
|
78
79
|
|
79
80
|
def build_uri(uribase)
|
@@ -81,11 +82,11 @@ module Safrano
|
|
81
82
|
end
|
82
83
|
|
83
84
|
def return_as_collection_descriptor
|
84
|
-
Safrano::FunctionImport::
|
85
|
+
Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
|
85
86
|
end
|
86
87
|
|
87
88
|
def return_as_instance_descriptor
|
88
|
-
Safrano::FunctionImport::
|
89
|
+
Safrano::FunctionImport::ResultDefinition.asEntity(self)
|
89
90
|
end
|
90
91
|
|
91
92
|
def execute_deferred_iblock
|
@@ -94,8 +95,10 @@ module Safrano
|
|
94
95
|
|
95
96
|
# Factory json-> Model Object instance
|
96
97
|
def new_from_hson_h(hash)
|
97
|
-
enty = new
|
98
|
-
enty.set_fields(hash, data_fields, missing: :skip)
|
98
|
+
# enty = new
|
99
|
+
# enty.set_fields(hash, data_fields, missing: :skip)
|
100
|
+
enty = create(hash)
|
101
|
+
# enty.set(hash)
|
99
102
|
enty
|
100
103
|
end
|
101
104
|
|
@@ -180,7 +183,7 @@ module Safrano
|
|
180
183
|
end
|
181
184
|
attrs = { 'Name' => pnam.to_s,
|
182
185
|
# 'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
|
183
|
-
'Type' => prop[:
|
186
|
+
'Type' => prop[:edm_type] }
|
184
187
|
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
185
188
|
enty.add_element('Property', attrs)
|
186
189
|
end
|
@@ -353,14 +356,41 @@ module Safrano
|
|
353
356
|
# build default output template structure
|
354
357
|
build_default_template
|
355
358
|
|
356
|
-
# Time columns
|
357
|
-
@time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
|
358
|
-
|
359
359
|
# add edm_types into schema
|
360
360
|
db_schema.each do |_col, props|
|
361
|
-
|
361
|
+
Safrano.add_edm_types(props)
|
362
362
|
end
|
363
363
|
|
364
|
+
# cols needed catsting before final json output
|
365
|
+
@casted_cols = {}
|
366
|
+
db_schema.each { |col, props|
|
367
|
+
if (props[:edm_precision] && (props[:edm_type] =~ /\AEdm.Decimal\(/i))
|
368
|
+
# we save the precision and scale in the lambda (binding!)
|
369
|
+
@casted_cols[col] = ->(x) {
|
370
|
+
# not sure if these copies are really needed, but feels better that way
|
371
|
+
x&.toDecimalPrecisionString(props[:edm_precision], props[:edm_scale])
|
372
|
+
}
|
373
|
+
|
374
|
+
next
|
375
|
+
end
|
376
|
+
if props[:edm_type] == 'Edm.Decimal'
|
377
|
+
@casted_cols[col] = ->(x) { x&.toDecimalString }
|
378
|
+
next
|
379
|
+
end
|
380
|
+
# Odata V2 Spec:
|
381
|
+
# Edm.Binary Base64 encoded value of an EDM.Binary value represented as a JSON string
|
382
|
+
# See for example https://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)?$format=json
|
383
|
+
if props[:edm_type] == 'Edm.Binary'
|
384
|
+
@casted_cols[col] = ->(x) { Base64.encode64(x) unless x.nil? } # Base64
|
385
|
+
next
|
386
|
+
end
|
387
|
+
# TODO check this more in details
|
388
|
+
if props[:type] == :datetime
|
389
|
+
@casted_cols[col] = ->(x) { x&.iso8601 }
|
390
|
+
|
391
|
+
end
|
392
|
+
}
|
393
|
+
|
364
394
|
# and finally build the path lists and allowed tr's
|
365
395
|
build_attribute_path_list
|
366
396
|
build_expand_path_list
|
data/lib/odata/transition.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative 'error'
|
|
6
6
|
module Safrano
|
7
7
|
# represents a state transition when navigating/parsing the url path
|
8
8
|
# from left to right
|
9
|
-
class Transition
|
9
|
+
class Transition
|
10
10
|
attr_accessor :trans
|
11
11
|
attr_accessor :match_result
|
12
12
|
attr_accessor :rgx
|
@@ -53,7 +53,31 @@ module Safrano
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
# Transition that does not move/change the input
|
57
|
+
class InplaceTransition < Transition
|
58
|
+
def initialize(trans:)
|
59
|
+
@trans = trans
|
60
|
+
end
|
61
|
+
|
62
|
+
def do_match(str)
|
63
|
+
@str = str
|
64
|
+
end
|
65
|
+
|
66
|
+
def path_remain
|
67
|
+
@str
|
68
|
+
end
|
69
|
+
|
70
|
+
def path_done
|
71
|
+
EMPTYSTR
|
72
|
+
end
|
73
|
+
|
74
|
+
def do_transition(ctx)
|
75
|
+
ctx.method(@trans).call(@str)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
56
79
|
TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
|
80
|
+
TransitionExecuteFunc = InplaceTransition.new(trans: 'transition_execute_func')
|
57
81
|
TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
|
58
82
|
trans: 'transition_metadata')
|
59
83
|
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
data/lib/odata/walker.rb
CHANGED
@@ -30,17 +30,23 @@ module Safrano
|
|
30
30
|
# are $links requested ?
|
31
31
|
attr_reader :do_links
|
32
32
|
|
33
|
+
attr_reader :request
|
34
|
+
|
33
35
|
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
34
36
|
EMPTYSTR = ''
|
35
37
|
SLASH = '/'
|
36
38
|
|
37
|
-
def initialize(service, path, content_id_refs = nil)
|
39
|
+
def initialize(service, path, request, content_id_refs = nil)
|
38
40
|
raise NIL_SERVICE_FATAL unless service
|
39
41
|
|
40
42
|
path = URI.decode_www_form_component(path)
|
41
43
|
@context = service
|
42
44
|
@content_id_refs = content_id_refs
|
43
45
|
|
46
|
+
# needed because for function import we need access to the url parameters (req.params)
|
47
|
+
# who contains the functions params
|
48
|
+
@request = request
|
49
|
+
|
44
50
|
@contexts = [@context]
|
45
51
|
|
46
52
|
@path_start = @path_remain = if service
|
@@ -109,6 +115,16 @@ module Safrano
|
|
109
115
|
end
|
110
116
|
end
|
111
117
|
|
118
|
+
# execute function import with request parameters
|
119
|
+
# input: @context containt the function to exectute,
|
120
|
+
# @request.params should normally contain the params
|
121
|
+
# result: validate the params for the given function, execute the function and
|
122
|
+
# return it's result back into @context,
|
123
|
+
# and finaly set status :end (or error if anyting went wrong )
|
124
|
+
def do_run_with_execute_func
|
125
|
+
@context, @status, @error = @context.do_execute_func(@request)
|
126
|
+
end
|
127
|
+
|
112
128
|
# little hacks... depending on returned state, set some attributes
|
113
129
|
def state_mappings
|
114
130
|
case @status
|
@@ -137,6 +153,8 @@ module Safrano
|
|
137
153
|
# entity reference here and place it in @context
|
138
154
|
when :run_with_content_id
|
139
155
|
do_run_with_content_id
|
156
|
+
when :run_with_execute_func
|
157
|
+
do_run_with_execute_func
|
140
158
|
end
|
141
159
|
|
142
160
|
@contexts << @context
|
data/lib/safrano/contract.rb
CHANGED
@@ -62,6 +62,8 @@ module Safrano
|
|
62
62
|
# All tap_valid* handlers are executed
|
63
63
|
# tap_error* handlers are not executed
|
64
64
|
class Valid
|
65
|
+
attr_reader :result
|
66
|
+
|
65
67
|
def initialize(result)
|
66
68
|
@result = result
|
67
69
|
end
|
@@ -100,10 +102,6 @@ module Safrano
|
|
100
102
|
def error
|
101
103
|
nil
|
102
104
|
end
|
103
|
-
|
104
|
-
def result
|
105
|
-
@result
|
106
|
-
end
|
107
105
|
end # class Valid
|
108
106
|
|
109
107
|
def self.valid(result)
|
data/lib/safrano/request.rb
CHANGED
data/lib/safrano/service.rb
CHANGED
@@ -112,7 +112,7 @@ module Safrano
|
|
112
112
|
include Safrano
|
113
113
|
include ExpandHandler
|
114
114
|
|
115
|
-
XML_PREAMBLE = %
|
115
|
+
XML_PREAMBLE = %(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
|
116
116
|
|
117
117
|
# This is just a hash of entity Set Names to the corresponding Class
|
118
118
|
# Example
|
@@ -137,6 +137,7 @@ module Safrano
|
|
137
137
|
attr_accessor :relman
|
138
138
|
attr_accessor :complex_types
|
139
139
|
attr_accessor :function_imports
|
140
|
+
attr_accessor :function_import_keys
|
140
141
|
|
141
142
|
# Instance attributes for specialized Version specific Instances
|
142
143
|
attr_accessor :v1
|
@@ -155,6 +156,7 @@ module Safrano
|
|
155
156
|
@relman = Safrano::RelationManager.new
|
156
157
|
@complex_types = Set.new
|
157
158
|
@function_imports = {}
|
159
|
+
@function_import_keys = []
|
158
160
|
@cmap = {}
|
159
161
|
instance_eval(&block) if block_given?
|
160
162
|
end
|
@@ -239,6 +241,7 @@ module Safrano
|
|
239
241
|
other.batch_handler = @batch_handler
|
240
242
|
other.complex_types = @complex_types
|
241
243
|
other.function_imports = @function_imports
|
244
|
+
other.function_import_keys = @function_import_keys
|
242
245
|
other
|
243
246
|
end
|
244
247
|
|
@@ -322,6 +325,8 @@ module Safrano
|
|
322
325
|
def function_import(name)
|
323
326
|
funcimp = Safrano::FunctionImport(name)
|
324
327
|
@function_imports[name] = funcimp
|
328
|
+
@function_import_keys << name
|
329
|
+
set_funcimports_sorted
|
325
330
|
funcimp
|
326
331
|
end
|
327
332
|
|
@@ -339,6 +344,10 @@ module Safrano
|
|
339
344
|
@collections
|
340
345
|
end
|
341
346
|
|
347
|
+
# need to be sorted by size too
|
348
|
+
def set_funcimports_sorted
|
349
|
+
@function_import_keys.sort_by! { |k| k.size }.reverse!
|
350
|
+
end
|
342
351
|
# to be called at end of publishing block to ensure we get the right names
|
343
352
|
# and additionally build the list of valid attribute path's used
|
344
353
|
# for validation of $orderby or $filter params
|
@@ -366,10 +375,25 @@ module Safrano
|
|
366
375
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
367
376
|
@collections.each do |klass|
|
368
377
|
klass.build_uri(@uribase)
|
369
|
-
klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
370
378
|
|
371
379
|
# Output create (POST) as single entity (Standard) or as array (non-standard buggy)
|
372
380
|
klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)
|
381
|
+
|
382
|
+
# define the most optimal casted_values method for the given model(klass)
|
383
|
+
if (klass.casted_cols.empty?)
|
384
|
+
klass.send(:define_method, :casted_values) do |cols = nil|
|
385
|
+
cols ? selected_values_for_odata(cols) : values_for_odata
|
386
|
+
end
|
387
|
+
else
|
388
|
+
klass.send(:define_method, :casted_values) do |cols = nil|
|
389
|
+
# we need to dup the model values as we need to change it before passing to_json,
|
390
|
+
# but we dont want to interfere with Sequel's owned data
|
391
|
+
# (eg because then in worst case it could happen that we write back changed values to DB)
|
392
|
+
vals = cols ? selected_values_for_odata(cols) : values_for_odata.dup
|
393
|
+
self.class.casted_cols.each { |cc, lambda| vals[cc] = lambda.call(vals[cc]) if vals.key?(cc) }
|
394
|
+
vals
|
395
|
+
end
|
396
|
+
end
|
373
397
|
end
|
374
398
|
|
375
399
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
@@ -401,7 +425,7 @@ module Safrano
|
|
401
425
|
end
|
402
426
|
|
403
427
|
def base_url_func_regexp
|
404
|
-
@
|
428
|
+
@function_import_keys.join('|')
|
405
429
|
end
|
406
430
|
|
407
431
|
def service
|
@@ -495,7 +519,9 @@ module Safrano
|
|
495
519
|
doc.add_element('edmx:Edmx', 'Version' => '1.0')
|
496
520
|
doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
|
497
521
|
serv = doc.root.add_element('edmx:DataServices',
|
498
|
-
|
522
|
+
# TODO: export the real version (result from version negotions)
|
523
|
+
# but currently we support only v1 and v2, and most users will use v2
|
524
|
+
'm:DataServiceVersion' => '2.0')
|
499
525
|
# 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
|
500
526
|
# DataServiceVersion: This attribute MUST be in the data service
|
501
527
|
# metadata namespace
|
data/lib/safrano/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -117,12 +117,14 @@ files:
|
|
117
117
|
- lib/core_ext/Dir/iter.rb
|
118
118
|
- lib/core_ext/Hash/transform.rb
|
119
119
|
- lib/core_ext/Integer/edm.rb
|
120
|
+
- lib/core_ext/Numeric/convert.rb
|
120
121
|
- lib/core_ext/REXML/Document/output.rb
|
121
122
|
- lib/core_ext/String/convert.rb
|
122
123
|
- lib/core_ext/String/edm.rb
|
123
124
|
- lib/core_ext/dir.rb
|
124
125
|
- lib/core_ext/hash.rb
|
125
126
|
- lib/core_ext/integer.rb
|
127
|
+
- lib/core_ext/numeric.rb
|
126
128
|
- lib/core_ext/rexml.rb
|
127
129
|
- lib/core_ext/string.rb
|
128
130
|
- lib/odata/attribute.rb
|