safrano 0.5.3 → 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 +4 -4
- data/lib/odata/complex_type.rb +44 -48
- data/lib/odata/edm/primitive_types.rb +97 -25
- data/lib/odata/entity.rb +2 -35
- data/lib/odata/function_import.rb +8 -9
- data/lib/odata/model_ext.rb +38 -17
- data/lib/odata/transition.rb +11 -7
- data/lib/odata/walker.rb +8 -8
- data/lib/safrano/service.rb +20 -6
- 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,13 +60,13 @@ module Safrano
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def initialize_dataset(dtset = nil)
|
63
|
-
@cx = @cx ||
|
63
|
+
@cx = @cx || dtset || @modelk
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
def initialize_uparms
|
67
67
|
@uparms = UrlParameters4Coll.new(@cx, @params)
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
def odata_get_apply_params
|
71
71
|
@uparms.apply_to_dataset(@cx).map_result! do |dataset|
|
72
72
|
@cx = dataset
|
@@ -135,7 +135,7 @@ module Safrano
|
|
135
135
|
# on model class level we return the collection
|
136
136
|
def odata_get(req)
|
137
137
|
@params = @params || req.params
|
138
|
-
initialize_dataset
|
138
|
+
initialize_dataset
|
139
139
|
initialize_uparms
|
140
140
|
@uparms.check_all.if_valid { |_ret|
|
141
141
|
odata_get_apply_params.if_valid { |_ret|
|
data/lib/odata/complex_type.rb
CHANGED
@@ -12,153 +12,148 @@ module Safrano
|
|
12
12
|
VALUEK = 'value'
|
13
13
|
RESULTSK = 'results'
|
14
14
|
COLLECTION = 'Collection'
|
15
|
-
|
15
|
+
|
16
16
|
def allowed_transitions
|
17
17
|
[Safrano::TransitionEnd]
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def transition_end(_match_result)
|
21
21
|
Safrano::Transition::RESULT_END
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
# we will have this on class and instance level for making things simpler first
|
25
25
|
def self.klassmod
|
26
26
|
@klassmod
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
# return a subclass of ResultAsComplexType
|
30
30
|
def self.asComplexType(klassmod)
|
31
31
|
Class.new(ResultAsComplexType) do
|
32
32
|
@klassmod = klassmod
|
33
33
|
end
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
# return a subclass of ResultAsComplexType
|
37
37
|
def self.asComplexTypeColl(klassmod)
|
38
38
|
Class.new(ResultAsComplexTypeColl) do
|
39
39
|
@klassmod = klassmod
|
40
40
|
end
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def self.asPrimitiveType(klassmod)
|
44
44
|
Class.new(ResultAsPrimitiveType) do
|
45
45
|
@klassmod = klassmod
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
def self.asPrimitiveTypeColl(klassmod)
|
50
50
|
Class.new(ResultAsPrimitiveTypeColl) do
|
51
51
|
@klassmod = klassmod
|
52
52
|
end
|
53
|
-
end
|
54
|
-
|
53
|
+
end
|
54
|
+
|
55
55
|
def self.asEntity(klassmod)
|
56
56
|
Class.new(ResultAsEntity) do
|
57
57
|
@klassmod = klassmod
|
58
58
|
end
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
def self.asEntityColl(klassmod)
|
62
62
|
Class.new(ResultAsEntityColl) do
|
63
63
|
@klassmod = klassmod
|
64
64
|
end
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
def initialize(value)
|
68
68
|
@value = value
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def odata_get(req)
|
72
72
|
[200, EMPTY_HASH, [to_odata_json(req)]]
|
73
73
|
end
|
74
|
+
|
74
75
|
def self.type_metadata
|
75
76
|
@klassmod.type_name
|
76
77
|
end
|
78
|
+
|
77
79
|
def type_metadata
|
78
80
|
self.class.type_metadata
|
79
81
|
end
|
80
|
-
|
82
|
+
|
81
83
|
# needed for ComplexType result
|
82
84
|
def to_odata_json(_req)
|
83
85
|
"#{DJ_OPEN}#{@value.odata_h.to_json}#{DJ_CLOSE}"
|
84
|
-
end
|
85
|
-
|
86
|
-
# wrapper
|
86
|
+
end
|
87
|
+
|
88
|
+
# wrapper
|
87
89
|
# for OData Entity and Collections, return them directly
|
88
90
|
# for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
|
89
|
-
def self.do_execute_func_result(result, _req, apply_query_params=false)
|
91
|
+
def self.do_execute_func_result(result, _req, apply_query_params = false)
|
90
92
|
self.new(result)
|
91
93
|
end
|
92
|
-
|
93
94
|
end
|
94
|
-
|
95
|
+
|
95
96
|
class ResultAsComplexType < ResultDefinition
|
96
97
|
def self.type_metadata
|
97
98
|
@klassmod.type_name
|
98
99
|
end
|
99
100
|
end
|
100
|
-
|
101
|
+
|
101
102
|
class ResultAsComplexTypeColl < ResultDefinition
|
102
103
|
def self.type_metadata
|
103
104
|
"Collection(#{@klassmod.type_name})"
|
104
105
|
end
|
105
106
|
|
106
107
|
def to_odata_json(req)
|
107
|
-
# "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
108
|
+
# "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
|
108
109
|
template = self.class.klassmod.output_template
|
109
110
|
# TODO: Error handling if database contains binary BLOB data that cant be
|
110
111
|
# interpreted as UTF-8 then JSON will fail here
|
111
112
|
|
112
113
|
innerh = req.service.get_coll_odata_h(array: @value,
|
113
114
|
template: template)
|
114
|
-
|
115
|
+
|
115
116
|
innerj = innerh.to_json
|
116
117
|
|
117
118
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
118
119
|
end
|
119
120
|
end
|
120
|
-
|
121
|
+
|
121
122
|
class ResultAsEntity < ResultDefinition
|
122
|
-
|
123
123
|
def self.type_metadata
|
124
124
|
@klassmod.type_name
|
125
125
|
end
|
126
126
|
|
127
|
-
|
128
|
-
# wrapper
|
127
|
+
# wrapper
|
129
128
|
# for OData Entity return them directly
|
130
|
-
def self.do_execute_func_result(result, _req, apply_query_params=false)
|
131
|
-
# note: Sequel entities instances seem to be thread safe, so we can
|
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
|
132
131
|
# safely add request-dependant data (eg. req.params) there
|
133
132
|
apply_query_params ? result : result.inactive_query_params
|
134
133
|
end
|
135
|
-
|
136
134
|
end
|
137
|
-
|
135
|
+
|
138
136
|
class ResultAsEntityColl < ResultDefinition
|
139
|
-
|
140
137
|
def self.type_metadata
|
141
138
|
"Collection(#{@klassmod.type_name})"
|
142
139
|
end
|
143
|
-
|
144
|
-
# wrapper
|
140
|
+
|
141
|
+
# wrapper
|
145
142
|
# for OData Entity Collection return them directly
|
146
|
-
def self.do_execute_func_result(result, req, apply_query_params=false)
|
143
|
+
def self.do_execute_func_result(result, req, apply_query_params = false)
|
147
144
|
coll = Safrano::OData::Collection.new(@klassmod)
|
148
145
|
# instance_exec has other instance variables; @values would be nil in the block below
|
149
146
|
# need to pass a local copy
|
150
147
|
dtset = result
|
151
148
|
coll.instance_exec do
|
152
|
-
|
153
|
-
@params = apply_query_params ? req.params : EMPTY_HASH
|
149
|
+
@params = apply_query_params ? req.params : EMPTY_HASH
|
154
150
|
initialize_dataset(dtset)
|
155
151
|
initialize_uparms
|
156
152
|
end
|
157
153
|
coll
|
158
154
|
end
|
159
|
-
|
160
155
|
end
|
161
|
-
|
156
|
+
|
162
157
|
class ResultAsPrimitiveType < ResultDefinition
|
163
158
|
def self.type_metadata
|
164
159
|
@klassmod.type_name
|
@@ -169,7 +164,7 @@ module Safrano
|
|
169
164
|
VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
|
170
165
|
end
|
171
166
|
end
|
172
|
-
|
167
|
+
|
173
168
|
class ResultAsPrimitiveTypeColl < ResultDefinition
|
174
169
|
def self.type_metadata
|
175
170
|
"Collection(#{@klassmod.type_name})"
|
@@ -179,7 +174,6 @@ module Safrano
|
|
179
174
|
{ D => { METAK => { TYPEK => self.class.type_metadata },
|
180
175
|
RESULTSK => self.class.klassmod.odata_collection(@value) } }.to_json
|
181
176
|
end
|
182
|
-
|
183
177
|
end
|
184
178
|
end
|
185
179
|
|
@@ -188,8 +182,9 @@ module Safrano
|
|
188
182
|
# with added OData functionality
|
189
183
|
class ComplexType
|
190
184
|
attr_reader :values
|
185
|
+
|
191
186
|
EMPTYH = {}.freeze
|
192
|
-
|
187
|
+
|
193
188
|
@namespace = nil
|
194
189
|
def self.namespace
|
195
190
|
@namespace
|
@@ -198,28 +193,28 @@ module Safrano
|
|
198
193
|
def self.props
|
199
194
|
@props
|
200
195
|
end
|
201
|
-
|
196
|
+
|
202
197
|
def type_name
|
203
198
|
self.class.type_name
|
204
199
|
end
|
205
|
-
|
200
|
+
|
206
201
|
def metadata_h
|
207
202
|
{ type: type_name }
|
208
203
|
end
|
209
|
-
|
204
|
+
|
210
205
|
def casted_values
|
211
206
|
# MVP... TODO: handle time mappings like in Entity models
|
212
207
|
values
|
213
208
|
end
|
214
|
-
|
209
|
+
|
215
210
|
# needed for nested json output
|
216
211
|
# this is a simpler version of model_ext#output_template
|
217
212
|
def self.default_template
|
218
213
|
template = {}
|
219
214
|
expand_e = {}
|
220
|
-
|
215
|
+
|
221
216
|
template[:all_values] = EMPTYH
|
222
|
-
@props.each { |prop, kl|
|
217
|
+
@props.each { |prop, kl|
|
223
218
|
if kl.respond_to? :default_template
|
224
219
|
expand_e[prop] = kl.default_template
|
225
220
|
end
|
@@ -227,10 +222,11 @@ module Safrano
|
|
227
222
|
template[:expand_e] = expand_e
|
228
223
|
template
|
229
224
|
end
|
230
|
-
|
225
|
+
|
231
226
|
def self.output_template
|
232
227
|
default_template
|
233
228
|
end
|
229
|
+
|
234
230
|
def self.type_name
|
235
231
|
@namespace ? "#{@namespace}.#{self.to_s}" : self.to_s
|
236
232
|
end
|
@@ -15,35 +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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
47
119
|
end
|
48
120
|
end
|
49
121
|
|
data/lib/odata/entity.rb
CHANGED
@@ -151,11 +151,12 @@ module Safrano
|
|
151
151
|
@uparms.check_all.tap_valid { return odata_get_output(req) }
|
152
152
|
.tap_error { |e| return e.odata_get(req) }
|
153
153
|
end
|
154
|
+
|
154
155
|
def inactive_query_params
|
155
156
|
@inactive_query_params = true
|
156
157
|
self # chaining
|
157
158
|
end
|
158
|
-
|
159
|
+
|
159
160
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
160
161
|
Safrano.remove_nav_relation(assoc, parent)
|
161
162
|
entity.destroy(transaction: false)
|
@@ -310,40 +311,6 @@ module Safrano
|
|
310
311
|
end
|
311
312
|
end
|
312
313
|
|
313
|
-
module MappingBeforeOutput
|
314
|
-
# needed for proper datetime or Decimal output
|
315
|
-
def casted_values(cols = nil)
|
316
|
-
vals = case cols
|
317
|
-
when nil
|
318
|
-
# we need to dup the model values as we need to change it before passing to_json,
|
319
|
-
# but we dont want to interfere with Sequel's owned data
|
320
|
-
# (eg because then in worst case it could happen that we write back changed values to DB)
|
321
|
-
values_for_odata.dup
|
322
|
-
else
|
323
|
-
selected_values_for_odata(cols)
|
324
|
-
end
|
325
|
-
# TODO better design (perf/ do more during startup and less during request runtime )
|
326
|
-
# TODO replace the quick and dirty BigDecimal hack with something better
|
327
|
-
self.class.decimal_cols.each { |dc| vals[dc] = BigDecimal(vals[dc].to_s).to_s('F') if vals.key?(dc) }
|
328
|
-
|
329
|
-
self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
|
330
|
-
vals
|
331
|
-
end
|
332
|
-
end
|
333
|
-
module NoMappingBeforeOutput
|
334
|
-
# current model does not have eg. Time fields--> no special mapping, just to_json is fine
|
335
|
-
# --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
|
336
|
-
# need to change it, just output as is
|
337
|
-
def casted_values(cols = nil)
|
338
|
-
case cols
|
339
|
-
when nil
|
340
|
-
values_for_odata
|
341
|
-
else
|
342
|
-
selected_values_for_odata(cols)
|
343
|
-
end
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
314
|
module MediaEntity
|
348
315
|
# media entity metadata for json h
|
349
316
|
def metadata_h
|
@@ -42,13 +42,13 @@ module Safrano
|
|
42
42
|
end
|
43
43
|
self
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
def auto_query_parameters
|
47
47
|
@auto_query_params = true
|
48
|
-
self
|
48
|
+
self # chaining
|
49
49
|
end
|
50
|
-
alias auto_query_params auto_query_parameters
|
51
|
-
|
50
|
+
alias auto_query_params auto_query_parameters
|
51
|
+
|
52
52
|
def return(klassmod, &proc)
|
53
53
|
raise('Please provide a code block') unless block_given?
|
54
54
|
|
@@ -71,7 +71,7 @@ module Safrano
|
|
71
71
|
else
|
72
72
|
# if it's neither a ComplexType nor a Modle-Entity
|
73
73
|
# --> assume it is a Primitive
|
74
|
-
# ResultAsPrimitiveTypeColl.new(klassmod)
|
74
|
+
# ResultAsPrimitiveTypeColl.new(klassmod)
|
75
75
|
ResultDefinition.asPrimitiveTypeColl(klassmod)
|
76
76
|
end
|
77
77
|
@proc = proc
|
@@ -142,7 +142,7 @@ module Safrano
|
|
142
142
|
end if @input
|
143
143
|
funky
|
144
144
|
end
|
145
|
-
|
145
|
+
|
146
146
|
def with_transition_validated(req)
|
147
147
|
# initialize_params
|
148
148
|
@params = req.params
|
@@ -150,15 +150,14 @@ module Safrano
|
|
150
150
|
|
151
151
|
[nil, :error, @error] if @error
|
152
152
|
end
|
153
|
-
|
154
|
-
|
153
|
+
|
155
154
|
def do_execute_func(req)
|
156
155
|
with_transition_validated(req) do
|
157
156
|
result = @proc.call(**@funcparams)
|
158
157
|
[@returning.do_execute_func_result(result, req, @auto_query_params), :run]
|
159
158
|
end
|
160
159
|
end
|
161
|
-
|
160
|
+
|
162
161
|
def transition_execute_func(_match_result)
|
163
162
|
[self, :run_with_execute_func]
|
164
163
|
end
|
data/lib/odata/model_ext.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
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'
|
9
10
|
require 'bigdecimal'
|
10
11
|
require_relative '../safrano/core'
|
@@ -32,8 +33,7 @@ module Safrano
|
|
32
33
|
attr_reader :default_template
|
33
34
|
attr_reader :uri
|
34
35
|
attr_reader :odata_upk_parts
|
35
|
-
attr_reader :
|
36
|
-
attr_reader :decimal_cols
|
36
|
+
attr_reader :casted_cols
|
37
37
|
attr_reader :namespace
|
38
38
|
|
39
39
|
# initialising block of code to be executed at end of
|
@@ -75,7 +75,6 @@ module Safrano
|
|
75
75
|
@uparms = nil
|
76
76
|
@params = nil
|
77
77
|
@cx = nil
|
78
|
-
# @@time_cols = nil
|
79
78
|
end
|
80
79
|
|
81
80
|
def build_uri(uribase)
|
@@ -96,10 +95,10 @@ module Safrano
|
|
96
95
|
|
97
96
|
# Factory json-> Model Object instance
|
98
97
|
def new_from_hson_h(hash)
|
99
|
-
#enty = new
|
100
|
-
#enty.set_fields(hash, data_fields, missing: :skip)
|
98
|
+
# enty = new
|
99
|
+
# enty.set_fields(hash, data_fields, missing: :skip)
|
101
100
|
enty = create(hash)
|
102
|
-
#enty.set(hash)
|
101
|
+
# enty.set(hash)
|
103
102
|
enty
|
104
103
|
end
|
105
104
|
|
@@ -184,7 +183,7 @@ module Safrano
|
|
184
183
|
end
|
185
184
|
attrs = { 'Name' => pnam.to_s,
|
186
185
|
# 'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
|
187
|
-
'Type' => prop[:
|
186
|
+
'Type' => prop[:edm_type] }
|
188
187
|
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
189
188
|
enty.add_element('Property', attrs)
|
190
189
|
end
|
@@ -357,19 +356,41 @@ module Safrano
|
|
357
356
|
# build default output template structure
|
358
357
|
build_default_template
|
359
358
|
|
360
|
-
# Time columns
|
361
|
-
@time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
|
362
|
-
|
363
|
-
|
364
|
-
|
365
359
|
# add edm_types into schema
|
366
360
|
db_schema.each do |_col, props|
|
367
|
-
|
368
|
-
db_type: props[:db_type])
|
361
|
+
Safrano.add_edm_types(props)
|
369
362
|
end
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
+
|
373
394
|
# and finally build the path lists and allowed tr's
|
374
395
|
build_attribute_path_list
|
375
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
|
@@ -52,26 +52,30 @@ module Safrano
|
|
52
52
|
ctx.method(@trans).call(@match_result)
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
56
|
-
#Transition that does not move/change the input
|
57
|
-
class InplaceTransition < Transition
|
58
|
-
def initialize(trans:
|
55
|
+
|
56
|
+
# Transition that does not move/change the input
|
57
|
+
class InplaceTransition < Transition
|
58
|
+
def initialize(trans:)
|
59
59
|
@trans = trans
|
60
60
|
end
|
61
|
+
|
61
62
|
def do_match(str)
|
62
63
|
@str = str
|
63
64
|
end
|
65
|
+
|
64
66
|
def path_remain
|
65
|
-
@str
|
67
|
+
@str
|
66
68
|
end
|
69
|
+
|
67
70
|
def path_done
|
68
71
|
EMPTYSTR
|
69
72
|
end
|
73
|
+
|
70
74
|
def do_transition(ctx)
|
71
75
|
ctx.method(@trans).call(@str)
|
72
76
|
end
|
73
77
|
end
|
74
|
-
|
78
|
+
|
75
79
|
TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
|
76
80
|
TransitionExecuteFunc = InplaceTransition.new(trans: 'transition_execute_func')
|
77
81
|
TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
|
data/lib/odata/walker.rb
CHANGED
@@ -29,24 +29,24 @@ module Safrano
|
|
29
29
|
|
30
30
|
# are $links requested ?
|
31
31
|
attr_reader :do_links
|
32
|
-
|
33
|
-
attr_reader :request
|
32
|
+
|
33
|
+
attr_reader :request
|
34
34
|
|
35
35
|
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
36
36
|
EMPTYSTR = ''
|
37
37
|
SLASH = '/'
|
38
38
|
|
39
|
-
def initialize(service, path, request, content_id_refs = nil
|
39
|
+
def initialize(service, path, request, content_id_refs = nil)
|
40
40
|
raise NIL_SERVICE_FATAL unless service
|
41
41
|
|
42
42
|
path = URI.decode_www_form_component(path)
|
43
43
|
@context = service
|
44
44
|
@content_id_refs = content_id_refs
|
45
|
-
|
45
|
+
|
46
46
|
# needed because for function import we need access to the url parameters (req.params)
|
47
47
|
# who contains the functions params
|
48
48
|
@request = request
|
49
|
-
|
49
|
+
|
50
50
|
@contexts = [@context]
|
51
51
|
|
52
52
|
@path_start = @path_remain = if service
|
@@ -115,16 +115,16 @@ module Safrano
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
-
# execute function import with request parameters
|
118
|
+
# execute function import with request parameters
|
119
119
|
# input: @context containt the function to exectute,
|
120
120
|
# @request.params should normally contain the params
|
121
|
-
# result: validate the params for the given function, execute the function and
|
121
|
+
# result: validate the params for the given function, execute the function and
|
122
122
|
# return it's result back into @context,
|
123
123
|
# and finaly set status :end (or error if anyting went wrong )
|
124
124
|
def do_run_with_execute_func
|
125
125
|
@context, @status, @error = @context.do_execute_func(@request)
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
128
|
# little hacks... depending on returned state, set some attributes
|
129
129
|
def state_mappings
|
130
130
|
case @status
|
data/lib/safrano/service.rb
CHANGED
@@ -343,10 +343,10 @@ module Safrano
|
|
343
343
|
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
|
344
344
|
@collections
|
345
345
|
end
|
346
|
-
|
346
|
+
|
347
347
|
# need to be sorted by size too
|
348
348
|
def set_funcimports_sorted
|
349
|
-
@function_import_keys.sort_by!{|k| k.size}.reverse!
|
349
|
+
@function_import_keys.sort_by! { |k| k.size }.reverse!
|
350
350
|
end
|
351
351
|
# to be called at end of publishing block to ensure we get the right names
|
352
352
|
# and additionally build the list of valid attribute path's used
|
@@ -375,11 +375,25 @@ module Safrano
|
|
375
375
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
376
376
|
@collections.each do |klass|
|
377
377
|
klass.build_uri(@uribase)
|
378
|
-
# TODO perf
|
379
|
-
klass.include( (klass.time_cols.empty? && klass.decimal_cols.empty?) ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
380
378
|
|
381
379
|
# Output create (POST) as single entity (Standard) or as array (non-standard buggy)
|
382
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
|
383
397
|
end
|
384
398
|
|
385
399
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
@@ -505,8 +519,8 @@ module Safrano
|
|
505
519
|
doc.add_element('edmx:Edmx', 'Version' => '1.0')
|
506
520
|
doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
|
507
521
|
serv = doc.root.add_element('edmx:DataServices',
|
508
|
-
|
509
|
-
|
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
|
510
524
|
'm:DataServiceVersion' => '2.0')
|
511
525
|
# 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
|
512
526
|
# DataServiceVersion: This attribute MUST be in the data service
|
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-07-
|
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
|