safrano 0.5.1 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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