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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b38827d37fa3bfed54a30aa61c04b5da27470071c5e3167fb5a08aa5c48a69af
4
- data.tar.gz: 3754e63822b6c504b42bc698df360295ab92bcf96a2ed8fdef4af7f2ed5c8d85
3
+ metadata.gz: fb412667db93e26340fc21440c89572fbff42fe52ae36807614f0fd11aac808c
4
+ data.tar.gz: 90883acded679aa860346e49242b1149933ccdb474e3e39eb6a893da5958dde7
5
5
  SHA512:
6
- metadata.gz: 0c6a3949c741f120955b4582ddad95aa3fefe80f613030970c174b4fe000129e4d2f67dcb43deae5608a2a287a489a4c94d6ba0df5a613c5557b22b8ca4f625b
7
- data.tar.gz: 7e5070fa657435fbc3a2932344392e58fa7105863bb045ad5a0aac7227a1e7ce5e4f54628915c81156da2f315eb359409c786243c47697877237ac08294a7872
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
@@ -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,75 +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}"
36
- template = @klassmod.output_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: coll,
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 to_odata_json(result_entity, req)
50
- result_entity.instance_exec do
51
- copy_request_infos(req)
52
- to_odata_json(request: req)
53
- 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
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
- 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)
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(result_dataset)
149
+ @params = apply_query_params ? req.params : EMPTY_HASH
150
+ initialize_dataset(dtset)
151
+ initialize_uparms
66
152
  end
67
- coll.to_odata_json(request: req)
153
+ coll
68
154
  end
69
155
  end
156
+
70
157
  class ResultAsPrimitiveType < ResultDefinition
71
- def to_odata_json(result, _req)
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 => @klassmod.odata_value(result) } }.to_json
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(result, _req)
82
- { D => { METAK => { TYPEK => type_metadata },
83
- 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
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::ResultAsComplexTypeColl.new(self)
254
+ FunctionImport::ResultDefinition.asComplexTypeColl(self)
161
255
  end
162
256
 
163
257
  def self.return_as_instance_descriptor
164
- FunctionImport::ResultAsComplexType.new(self)
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
- 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'
41
- end
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::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,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 :time_cols
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
- @@time_cols = nil
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::ResultAsEntityColl.new(self)
92
+ Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
85
93
  end
86
94
 
87
95
  def return_as_instance_descriptor
88
- Safrano::FunctionImport::ResultAsEntity.new(self)
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' => prop[:odata_edm_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 finalize_publishing
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
- # Time columns
359
- @time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
360
-
361
- # add edm_types into schema
362
- db_schema.each do |_col, props|
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
@@ -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
@@ -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
 
@@ -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
- @function_imports.keys.join('|')
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
- 'm:DataServiceVersion' => '1.0')
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Safrano
4
- VERSION = '0.5.1'
4
+ VERSION = '0.5.5'
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.1
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-05-27 00:00:00.000000000 Z
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