safrano 0.5.1 → 0.5.5
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 +25 -0
- data/lib/core_ext/numeric.rb +3 -0
- data/lib/odata/collection.rb +7 -5
- data/lib/odata/complex_type.rb +131 -37
- data/lib/odata/edm/primitive_types.rb +99 -20
- data/lib/odata/entity.rb +6 -31
- data/lib/odata/function_import.rb +19 -21
- data/lib/odata/model_ext.rb +83 -15
- data/lib/odata/transition.rb +25 -1
- data/lib/odata/walker.rb +19 -1
- data/lib/safrano/request.rb +1 -0
- data/lib/safrano/service.rb +40 -5
- data/lib/safrano/type_mapping.rb +149 -0
- data/lib/safrano/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb412667db93e26340fc21440c89572fbff42fe52ae36807614f0fd11aac808c
|
4
|
+
data.tar.gz: 90883acded679aa860346e49242b1149933ccdb474e3e39eb6a893da5958dde7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f66a01241685f18493d0519cb009674f0764e260ccc79b048ebc7346b5d4c64bd78deeb9983a0b4893e4fcfc2d1361f7024ca212bcbdc8071191417e793d9427
|
7
|
+
data.tar.gz: 85e2cdd6c5c65e52f882fe2e8386cdf772c239ecdf27d979d473170f51bb1c0a37d8919cb8fc7422bb04eadf50baa8adb1ad4e712ea3952d3e2c0f7d9923bd48
|
@@ -0,0 +1,25 @@
|
|
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)
|
14
|
+
p = Integer(precision)
|
15
|
+
BigDecimal(self, p).to_s('F')
|
16
|
+
end
|
17
|
+
|
18
|
+
def toDecimalPrecisionScaleString(precision, scale)
|
19
|
+
p = Integer(precision)
|
20
|
+
sprintf("%#{p + 2}.#{scale}f", self).strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
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,75 +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}"
|
36
|
-
template =
|
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
|
37
110
|
# TODO: Error handling if database contains binary BLOB data that cant be
|
38
111
|
# interpreted as UTF-8 then JSON will fail here
|
39
112
|
|
40
|
-
innerh = req.service.get_coll_odata_h(array:
|
113
|
+
innerh = req.service.get_coll_odata_h(array: @value,
|
41
114
|
template: template)
|
42
|
-
|
115
|
+
|
43
116
|
innerj = innerh.to_json
|
44
117
|
|
45
118
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
46
119
|
end
|
47
120
|
end
|
121
|
+
|
48
122
|
class ResultAsEntity < ResultDefinition
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
54
133
|
end
|
55
134
|
end
|
135
|
+
|
56
136
|
class ResultAsEntityColl < ResultDefinition
|
57
|
-
def type_metadata
|
137
|
+
def self.type_metadata
|
58
138
|
"Collection(#{@klassmod.type_name})"
|
59
139
|
end
|
60
140
|
|
61
|
-
|
141
|
+
# wrapper
|
142
|
+
# for OData Entity Collection return them directly
|
143
|
+
def self.do_execute_func_result(result, req, apply_query_params = false)
|
62
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
|
63
148
|
coll.instance_exec do
|
64
|
-
@params = req.params
|
65
|
-
initialize_dataset(
|
149
|
+
@params = apply_query_params ? req.params : EMPTY_HASH
|
150
|
+
initialize_dataset(dtset)
|
151
|
+
initialize_uparms
|
66
152
|
end
|
67
|
-
coll
|
153
|
+
coll
|
68
154
|
end
|
69
155
|
end
|
156
|
+
|
70
157
|
class ResultAsPrimitiveType < ResultDefinition
|
71
|
-
def
|
158
|
+
def self.type_metadata
|
159
|
+
@klassmod.type_name
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_odata_json(_req)
|
72
163
|
{ D => { METAK => { TYPEK => type_metadata },
|
73
|
-
VALUEK =>
|
164
|
+
VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
|
74
165
|
end
|
75
166
|
end
|
167
|
+
|
76
168
|
class ResultAsPrimitiveTypeColl < ResultDefinition
|
77
|
-
def type_metadata
|
169
|
+
def self.type_metadata
|
78
170
|
"Collection(#{@klassmod.type_name})"
|
79
171
|
end
|
80
172
|
|
81
|
-
def to_odata_json(
|
82
|
-
{ D => { METAK => { TYPEK => type_metadata },
|
83
|
-
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
|
84
176
|
end
|
85
177
|
end
|
86
178
|
end
|
@@ -90,8 +182,9 @@ module Safrano
|
|
90
182
|
# with added OData functionality
|
91
183
|
class ComplexType
|
92
184
|
attr_reader :values
|
185
|
+
|
93
186
|
EMPTYH = {}.freeze
|
94
|
-
|
187
|
+
|
95
188
|
@namespace = nil
|
96
189
|
def self.namespace
|
97
190
|
@namespace
|
@@ -100,28 +193,28 @@ module Safrano
|
|
100
193
|
def self.props
|
101
194
|
@props
|
102
195
|
end
|
103
|
-
|
196
|
+
|
104
197
|
def type_name
|
105
198
|
self.class.type_name
|
106
199
|
end
|
107
|
-
|
200
|
+
|
108
201
|
def metadata_h
|
109
202
|
{ type: type_name }
|
110
203
|
end
|
111
|
-
|
204
|
+
|
112
205
|
def casted_values
|
113
206
|
# MVP... TODO: handle time mappings like in Entity models
|
114
207
|
values
|
115
208
|
end
|
116
|
-
|
209
|
+
|
117
210
|
# needed for nested json output
|
118
211
|
# this is a simpler version of model_ext#output_template
|
119
212
|
def self.default_template
|
120
213
|
template = {}
|
121
214
|
expand_e = {}
|
122
|
-
|
215
|
+
|
123
216
|
template[:all_values] = EMPTYH
|
124
|
-
@props.each { |prop, kl|
|
217
|
+
@props.each { |prop, kl|
|
125
218
|
if kl.respond_to? :default_template
|
126
219
|
expand_e[prop] = kl.default_template
|
127
220
|
end
|
@@ -129,10 +222,11 @@ module Safrano
|
|
129
222
|
template[:expand_e] = expand_e
|
130
223
|
template
|
131
224
|
end
|
132
|
-
|
225
|
+
|
133
226
|
def self.output_template
|
134
227
|
default_template
|
135
228
|
end
|
229
|
+
|
136
230
|
def self.type_name
|
137
231
|
@namespace ? "#{@namespace}.#{self.to_s}" : self.to_s
|
138
232
|
end
|
@@ -157,11 +251,11 @@ module Safrano
|
|
157
251
|
end
|
158
252
|
|
159
253
|
def self.return_as_collection_descriptor
|
160
|
-
FunctionImport::
|
254
|
+
FunctionImport::ResultDefinition.asComplexTypeColl(self)
|
161
255
|
end
|
162
256
|
|
163
257
|
def self.return_as_instance_descriptor
|
164
|
-
FunctionImport::
|
258
|
+
FunctionImport::ResultDefinition.asComplexType(self)
|
165
259
|
end
|
166
260
|
|
167
261
|
# add metadata xml to the passed REXML schema object
|
@@ -15,30 +15,109 @@ 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
|
-
|
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(metadata, props)
|
41
|
+
# try num/dec with db_type:
|
42
|
+
metadata[: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
|
+
metadata[:edm_precision] = prec
|
48
|
+
"Edm.Decimal(#{prec})"
|
49
|
+
else
|
50
|
+
# we have precision and scale
|
51
|
+
metadata[:edm_scale] = scale
|
52
|
+
metadata[:edm_precision] = prec
|
53
|
+
"Edm.Decimal(#{prec},#{scale})"
|
54
|
+
end
|
55
|
+
elsif prec
|
56
|
+
# we have precision only
|
57
|
+
metadata[:edm_precision] = prec
|
58
|
+
"Edm.Decimal(#{prec})"
|
59
|
+
else
|
60
|
+
'Edm.Decimal'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return if metadata[:edm_type]
|
64
|
+
|
65
|
+
# try float(prec) with db_type:
|
66
|
+
metadata[: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
|
+
metadata[:edm_precision] = prec
|
75
|
+
'Edm.Double'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return if metadata[:edm_type]
|
79
|
+
|
80
|
+
# try int-like with db_type:
|
81
|
+
# smallint|int|integer|bigint|serial|bigserial
|
82
|
+
metadata[: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 metadata[:edm_type]
|
98
|
+
|
99
|
+
# try with Sequel(ruby) type
|
100
|
+
metadata[:edm_type] = case props[:type]
|
101
|
+
when :integer
|
102
|
+
'Edm.Int32'
|
103
|
+
when :string
|
104
|
+
'Edm.String'
|
105
|
+
when :date
|
106
|
+
'Edm.DateTime'
|
107
|
+
when :datetime
|
108
|
+
'Edm.DateTime'
|
109
|
+
when :time
|
110
|
+
'Edm.Time'
|
111
|
+
when :boolean
|
112
|
+
'Edm.Boolean'
|
113
|
+
when :float
|
114
|
+
'Edm.Double'
|
115
|
+
when :decimal
|
116
|
+
'Edm.Decimal'
|
117
|
+
when :blob
|
118
|
+
'Edm.Binary'
|
119
|
+
else
|
120
|
+
end
|
42
121
|
end
|
43
122
|
|
44
123
|
# use Edm twice so that we can do include Safrano::Edm and then
|
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,8 +33,14 @@ 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
|
38
|
+
# store cols metata here in the model (sub)-class. Initially we stored this infos (eg. edm_types etc)
|
39
|
+
# directly into sequels db_schema hash. But this hash is on the upper Sequel::Model(table) class
|
40
|
+
# and is shared by all subclasses.
|
41
|
+
# By storing it separately here we are less dependant from Sequel, and have less issues with
|
42
|
+
# testing with multiples models class derived from same Sequel::Model(table)
|
43
|
+
attr_reader :cols_metadata
|
36
44
|
|
37
45
|
# initialising block of code to be executed at end of
|
38
46
|
# ServerApp.publish_service after all model classes have been registered
|
@@ -73,7 +81,7 @@ module Safrano
|
|
73
81
|
@uparms = nil
|
74
82
|
@params = nil
|
75
83
|
@cx = nil
|
76
|
-
|
84
|
+
@cols_metadata = {}
|
77
85
|
end
|
78
86
|
|
79
87
|
def build_uri(uribase)
|
@@ -81,11 +89,11 @@ module Safrano
|
|
81
89
|
end
|
82
90
|
|
83
91
|
def return_as_collection_descriptor
|
84
|
-
Safrano::FunctionImport::
|
92
|
+
Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
|
85
93
|
end
|
86
94
|
|
87
95
|
def return_as_instance_descriptor
|
88
|
-
Safrano::FunctionImport::
|
96
|
+
Safrano::FunctionImport::ResultDefinition.asEntity(self)
|
89
97
|
end
|
90
98
|
|
91
99
|
def execute_deferred_iblock
|
@@ -94,10 +102,10 @@ module Safrano
|
|
94
102
|
|
95
103
|
# Factory json-> Model Object instance
|
96
104
|
def new_from_hson_h(hash)
|
97
|
-
#enty = new
|
98
|
-
#enty.set_fields(hash, data_fields, missing: :skip)
|
105
|
+
# enty = new
|
106
|
+
# enty.set_fields(hash, data_fields, missing: :skip)
|
99
107
|
enty = create(hash)
|
100
|
-
#enty.set(hash)
|
108
|
+
# enty.set(hash)
|
101
109
|
enty
|
102
110
|
end
|
103
111
|
|
@@ -176,13 +184,14 @@ module Safrano
|
|
176
184
|
end
|
177
185
|
# with their properties
|
178
186
|
db_schema.each do |pnam, prop|
|
187
|
+
metadata = @cols_metadata[pnam]
|
179
188
|
if prop[:primary_key] == true
|
180
189
|
enty.add_element('Key').add_element('PropertyRef',
|
181
190
|
'Name' => pnam.to_s)
|
182
191
|
end
|
183
192
|
attrs = { 'Name' => pnam.to_s,
|
184
193
|
# 'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
|
185
|
-
'Type' =>
|
194
|
+
'Type' => metadata[:edm_type] }
|
186
195
|
attrs['Nullable'] = 'false' if prop[:allow_null] == false
|
187
196
|
enty.add_element('Property', attrs)
|
188
197
|
end
|
@@ -349,20 +358,79 @@ module Safrano
|
|
349
358
|
end
|
350
359
|
end
|
351
360
|
|
352
|
-
def
|
361
|
+
def build_casted_cols(service)
|
362
|
+
# cols needed catsting before final json output
|
363
|
+
@casted_cols = {}
|
364
|
+
db_schema.each { |col, props|
|
365
|
+
# first check if we have user-defined type mapping
|
366
|
+
usermap = nil
|
367
|
+
dbtyp = props[:db_type]
|
368
|
+
metadata = @cols_metadata[col]
|
369
|
+
if (service.type_mappings.values.find { |map| usermap = map.match(dbtyp) })
|
370
|
+
|
371
|
+
metadata[:edm_type] = usermap.edm_type
|
372
|
+
|
373
|
+
@casted_cols[col] = usermap.castfunc
|
374
|
+
next # this will override our rules below !
|
375
|
+
end
|
376
|
+
|
377
|
+
if (metadata[:edm_precision] && (metadata[:edm_type] =~ /\AEdm.Decimal\(/i))
|
378
|
+
# we save the precision and/or scale in the lambda (binding!)
|
379
|
+
|
380
|
+
@casted_cols[col] = if metadata[:edm_scale]
|
381
|
+
->(x) {
|
382
|
+
# not sure if these copies are really needed, but feels better that way
|
383
|
+
# output decimal with precision and scale
|
384
|
+
x&.toDecimalPrecisionScaleString(metadata[:edm_precision], metadata[:edm_scale])
|
385
|
+
}
|
386
|
+
else
|
387
|
+
->(x) {
|
388
|
+
# not sure if these copies are really needed, but feels better that way
|
389
|
+
# output decimal with precision only
|
390
|
+
x&.toDecimalPrecisionString(metadata[:edm_precision])
|
391
|
+
}
|
392
|
+
end
|
393
|
+
|
394
|
+
next
|
395
|
+
end
|
396
|
+
if metadata[:edm_type] == 'Edm.Decimal'
|
397
|
+
@casted_cols[col] = ->(x) { x&.toDecimalString }
|
398
|
+
next
|
399
|
+
end
|
400
|
+
# Odata V2 Spec:
|
401
|
+
# Edm.Binary Base64 encoded value of an EDM.Binary value represented as a JSON string
|
402
|
+
# See for example https://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)?$format=json
|
403
|
+
if metadata[:edm_type] == 'Edm.Binary'
|
404
|
+
@casted_cols[col] = ->(x) { Base64.encode64(x) unless x.nil? } # Base64
|
405
|
+
next
|
406
|
+
end
|
407
|
+
# TODO check this more in details
|
408
|
+
# NOTE: here we use :type which is the sequel defined ruby-type
|
409
|
+
if props[:type] == :datetime
|
410
|
+
@casted_cols[col] = ->(x) { x&.iso8601 }
|
411
|
+
|
412
|
+
end
|
413
|
+
}
|
414
|
+
end
|
415
|
+
|
416
|
+
def finalize_publishing(service)
|
353
417
|
build_type_name
|
354
418
|
|
355
419
|
# build default output template structure
|
356
420
|
build_default_template
|
357
421
|
|
358
|
-
#
|
359
|
-
@
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type])
|
422
|
+
# add edm_types into metadata store
|
423
|
+
@cols_metadata = {}
|
424
|
+
db_schema.each do |col, props|
|
425
|
+
metadata = @cols_metadata.key?(col) ? @cols_metadata[col] : (@cols_metadata[col] = {})
|
426
|
+
Safrano.add_edm_types(metadata, props)
|
364
427
|
end
|
365
428
|
|
429
|
+
build_casted_cols(service)
|
430
|
+
# unless @casted_cols.empty?
|
431
|
+
# require 'pry'
|
432
|
+
# binding.pry
|
433
|
+
# end
|
366
434
|
# and finally build the path lists and allowed tr's
|
367
435
|
build_attribute_path_list
|
368
436
|
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/request.rb
CHANGED
data/lib/safrano/service.rb
CHANGED
@@ -5,6 +5,7 @@ require 'odata/relations'
|
|
5
5
|
require 'odata/batch'
|
6
6
|
require 'odata/complex_type'
|
7
7
|
require 'odata/function_import'
|
8
|
+
require 'safrano/type_mapping'
|
8
9
|
require 'odata/error'
|
9
10
|
require 'odata/filter/sequel'
|
10
11
|
require 'set'
|
@@ -137,6 +138,8 @@ module Safrano
|
|
137
138
|
attr_accessor :relman
|
138
139
|
attr_accessor :complex_types
|
139
140
|
attr_accessor :function_imports
|
141
|
+
attr_accessor :function_import_keys
|
142
|
+
attr_accessor :type_mappings
|
140
143
|
|
141
144
|
# Instance attributes for specialized Version specific Instances
|
142
145
|
attr_accessor :v1
|
@@ -155,7 +158,9 @@ module Safrano
|
|
155
158
|
@relman = Safrano::RelationManager.new
|
156
159
|
@complex_types = Set.new
|
157
160
|
@function_imports = {}
|
161
|
+
@function_import_keys = []
|
158
162
|
@cmap = {}
|
163
|
+
@type_mappings = {}
|
159
164
|
instance_eval(&block) if block_given?
|
160
165
|
end
|
161
166
|
|
@@ -239,6 +244,8 @@ module Safrano
|
|
239
244
|
other.batch_handler = @batch_handler
|
240
245
|
other.complex_types = @complex_types
|
241
246
|
other.function_imports = @function_imports
|
247
|
+
other.function_import_keys = @function_import_keys
|
248
|
+
other.type_mappings = @type_mappings
|
242
249
|
other
|
243
250
|
end
|
244
251
|
|
@@ -322,9 +329,16 @@ module Safrano
|
|
322
329
|
def function_import(name)
|
323
330
|
funcimp = Safrano::FunctionImport(name)
|
324
331
|
@function_imports[name] = funcimp
|
332
|
+
@function_import_keys << name
|
333
|
+
set_funcimports_sorted
|
325
334
|
funcimp
|
326
335
|
end
|
327
336
|
|
337
|
+
def with_db_type(*dbtypnams, &proc)
|
338
|
+
m = TypeMapping.builder(*dbtypnams, &proc)
|
339
|
+
@type_mappings[m.db_types_rgx] = m
|
340
|
+
end
|
341
|
+
|
328
342
|
def cmap=(imap)
|
329
343
|
@cmap = imap
|
330
344
|
set_collections_sorted(@cmap.values)
|
@@ -339,6 +353,10 @@ module Safrano
|
|
339
353
|
@collections
|
340
354
|
end
|
341
355
|
|
356
|
+
# need to be sorted by size too
|
357
|
+
def set_funcimports_sorted
|
358
|
+
@function_import_keys.sort_by! { |k| k.size }.reverse!
|
359
|
+
end
|
342
360
|
# to be called at end of publishing block to ensure we get the right names
|
343
361
|
# and additionally build the list of valid attribute path's used
|
344
362
|
# for validation of $orderby or $filter params
|
@@ -361,15 +379,30 @@ module Safrano
|
|
361
379
|
|
362
380
|
set_uribase
|
363
381
|
|
364
|
-
@collections.each(&:finalize_publishing)
|
365
|
-
|
366
382
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
367
383
|
@collections.each do |klass|
|
384
|
+
klass.finalize_publishing(self)
|
385
|
+
|
368
386
|
klass.build_uri(@uribase)
|
369
|
-
klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
370
387
|
|
371
388
|
# Output create (POST) as single entity (Standard) or as array (non-standard buggy)
|
372
389
|
klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)
|
390
|
+
|
391
|
+
# define the most optimal casted_values method for the given model(klass)
|
392
|
+
if (klass.casted_cols.empty?)
|
393
|
+
klass.send(:define_method, :casted_values) do |cols = nil|
|
394
|
+
cols ? selected_values_for_odata(cols) : values_for_odata
|
395
|
+
end
|
396
|
+
else
|
397
|
+
klass.send(:define_method, :casted_values) do |cols = nil|
|
398
|
+
# we need to dup the model values as we need to change it before passing to_json,
|
399
|
+
# but we dont want to interfere with Sequel's owned data
|
400
|
+
# (eg because then in worst case it could happen that we write back changed values to DB)
|
401
|
+
vals = cols ? selected_values_for_odata(cols) : values_for_odata.dup
|
402
|
+
self.class.casted_cols.each { |cc, lambda| vals[cc] = lambda.call(vals[cc]) if vals.key?(cc) }
|
403
|
+
vals
|
404
|
+
end
|
405
|
+
end
|
373
406
|
end
|
374
407
|
|
375
408
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
@@ -401,7 +434,7 @@ module Safrano
|
|
401
434
|
end
|
402
435
|
|
403
436
|
def base_url_func_regexp
|
404
|
-
@
|
437
|
+
@function_import_keys.join('|')
|
405
438
|
end
|
406
439
|
|
407
440
|
def service
|
@@ -495,7 +528,9 @@ module Safrano
|
|
495
528
|
doc.add_element('edmx:Edmx', 'Version' => '1.0')
|
496
529
|
doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
|
497
530
|
serv = doc.root.add_element('edmx:DataServices',
|
498
|
-
|
531
|
+
# TODO: export the real version (result from version negotions)
|
532
|
+
# but currently we support only v1 and v2, and most users will use v2
|
533
|
+
'm:DataServiceVersion' => '2.0')
|
499
534
|
# 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
|
500
535
|
# DataServiceVersion: This attribute MUST be in the data service
|
501
536
|
# metadata namespace
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Note: Safrano has hardcoded mapping rules for most of types.
|
4
|
+
# But
|
5
|
+
# it might not be 100% complete
|
6
|
+
# it might not always do what is expected
|
7
|
+
# The type mapping functionality here allows Safrano users to design type mapping themselves
|
8
|
+
# and fill or fix the two above issues
|
9
|
+
module Safrano
|
10
|
+
# Base class
|
11
|
+
class TypeMapping
|
12
|
+
attr_reader :castfunc
|
13
|
+
attr_reader :db_types_rgx
|
14
|
+
attr_reader :edm_type
|
15
|
+
|
16
|
+
# wrapper to handle API
|
17
|
+
class Builder
|
18
|
+
attr_reader :xedm_type
|
19
|
+
attr_reader :castfunc
|
20
|
+
attr_reader :db_types_rgx
|
21
|
+
attr_reader :bui1
|
22
|
+
attr_reader :bui2
|
23
|
+
|
24
|
+
def initialize(*dbtyps)
|
25
|
+
@db_types_rgx = dbtyps.join('|')
|
26
|
+
@rgx = /\A\s*(?:#{@db_types_rgx})\s*\z/i
|
27
|
+
end
|
28
|
+
|
29
|
+
def edm_type(input)
|
30
|
+
@xedm_type = input
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_one_param(lambda = nil, &proc)
|
34
|
+
proc1 = block_given? ? proc : lambda
|
35
|
+
@bui1 = Builder1Par.new(@db_types_rgx, proc1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_two_params(lambda = nil, &proc)
|
39
|
+
proc2 = block_given? ? proc : lambda
|
40
|
+
@bui2 = Builder2Par.new(@db_types_rgx, proc2)
|
41
|
+
end
|
42
|
+
|
43
|
+
def json_value(lambda = nil, &proc)
|
44
|
+
@castfunc = block_given? ? proc : lambda
|
45
|
+
end
|
46
|
+
|
47
|
+
def match(curtyp)
|
48
|
+
if (@bui2 && (m = @bui2.match(curtyp)))
|
49
|
+
m
|
50
|
+
elsif (@bui1 && (m = @bui1.match(curtyp)))
|
51
|
+
m
|
52
|
+
elsif @rgx.match(curtyp)
|
53
|
+
type_mapping
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def type_mapping
|
58
|
+
# TODO perf; return always same object when called multiple times
|
59
|
+
FixedTypeMapping.new(self)
|
60
|
+
end
|
61
|
+
end # Builder
|
62
|
+
|
63
|
+
def self.builder(*dbtypnams, &proc)
|
64
|
+
builder = Builder.new(*dbtypnams)
|
65
|
+
builder.instance_eval(&proc)
|
66
|
+
builder
|
67
|
+
end
|
68
|
+
|
69
|
+
class Builder1Par < Builder
|
70
|
+
def initialize(db_ty_rgx, proc)
|
71
|
+
@db_types_rgx = db_ty_rgx
|
72
|
+
@proc = proc
|
73
|
+
@rgx = /\A\s*(?:#{@db_types_rgx})\s*\(\s*(\d+)\s*\)\z/i
|
74
|
+
end
|
75
|
+
|
76
|
+
def match(curtyp)
|
77
|
+
(@md = @rgx.match(curtyp)) ? type_mapping : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def json_value(lambda = nil, &proc)
|
81
|
+
@castfunc = block_given? ? proc : lambda
|
82
|
+
end
|
83
|
+
|
84
|
+
# this is a bit advanced/obscure programming
|
85
|
+
# the intance_exec here is required to produce correct results
|
86
|
+
# ie. it produces concrete instances of edm_type and json_val lambda
|
87
|
+
# for the later, it is kind of currying but with the *implicit* parameters
|
88
|
+
# from the calling context eg par1
|
89
|
+
|
90
|
+
# probably this is not best-practice programing as we
|
91
|
+
# have a mutating object (the builder) that
|
92
|
+
# produces different lambdas after each type_mapping(mutation) calls
|
93
|
+
def type_mapping
|
94
|
+
p1val = @md[1]
|
95
|
+
instance_exec p1val, &@proc
|
96
|
+
|
97
|
+
TypeMapping1Par.new(self)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
class Builder2Par < Builder
|
101
|
+
def initialize(db_ty_rgx, proc)
|
102
|
+
@db_types_rgx = db_ty_rgx
|
103
|
+
@proc = proc
|
104
|
+
@rgx = /\A\s*(?:#{@db_types_rgx})\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*\z/i
|
105
|
+
end
|
106
|
+
|
107
|
+
def match(curtyp)
|
108
|
+
(@md = @rgx.match(curtyp)) ? type_mapping : nil
|
109
|
+
end
|
110
|
+
|
111
|
+
# this is a bit advanced/obscure programming
|
112
|
+
# the intance_exec here is required to produce correct results
|
113
|
+
# ie. it produces concrete instances of edm_type and json_val lambda
|
114
|
+
# for the later, it is kind of currying but with the *implicit* parameters
|
115
|
+
# from the calling context eg par1 and par2
|
116
|
+
|
117
|
+
# probably this is not best-practice programing as we
|
118
|
+
# have a mutating object (the builder) that
|
119
|
+
# produces different lambdas after each type_mapping(mutation) calls
|
120
|
+
def type_mapping
|
121
|
+
p1val = @md[1]
|
122
|
+
p2val = @md[2]
|
123
|
+
instance_exec p1val, p2val, &@proc
|
124
|
+
TypeMapping2Par.new(self)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Fixed type (ie. without variable parts)
|
130
|
+
class FixedTypeMapping < TypeMapping
|
131
|
+
def initialize(builder)
|
132
|
+
@edm_type = builder.xedm_type
|
133
|
+
@castfunc = builder.castfunc
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class TypeMapping1Par < TypeMapping
|
138
|
+
def initialize(builder)
|
139
|
+
@edm_type = builder.xedm_type
|
140
|
+
@castfunc = builder.castfunc
|
141
|
+
end
|
142
|
+
end
|
143
|
+
class TypeMapping2Par < TypeMapping
|
144
|
+
def initialize(builder)
|
145
|
+
@edm_type = builder.xedm_type
|
146
|
+
@castfunc = builder.castfunc
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
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.5
|
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-08-28 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
|
@@ -164,6 +166,7 @@ files:
|
|
164
166
|
- lib/safrano/response.rb
|
165
167
|
- lib/safrano/sequel_join_by_paths.rb
|
166
168
|
- lib/safrano/service.rb
|
169
|
+
- lib/safrano/type_mapping.rb
|
167
170
|
- lib/safrano/version.rb
|
168
171
|
- lib/sequel/plugins/join_by_paths.rb
|
169
172
|
homepage: https://gitlab.com/dm0da/safrano
|