safrano 0.5.0 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|