safrano 0.5.0 → 0.5.4

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