safrano 0.5.3 → 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 +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
|