safrano 0.5.4 → 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: 89b3584ee45b115a0a463faea56586c06271c36ea080c4f73b64fb4a4a641802
4
- data.tar.gz: 977386e3bff89f18c6b596f1f4244623e85b2960ab9cb9eed6ebf7c9c1c0498f
3
+ metadata.gz: fb412667db93e26340fc21440c89572fbff42fe52ae36807614f0fd11aac808c
4
+ data.tar.gz: 90883acded679aa860346e49242b1149933ccdb474e3e39eb6a893da5958dde7
5
5
  SHA512:
6
- metadata.gz: e45fb7cd3af3aa3b0d918229f84760b9c779a01a55d5f46f593e93dd2f18dbefe5e3713141cf3d72523b3e2069b426130760531c991549993f861c26c9b8a2f0
7
- data.tar.gz: 943e0cb99f039eb41c69204dd348347965b188a0e334d13c075ffe06188b01a44e8bc757b86509de24ca843b9253f131ed3c30bbbc4eaaa2d176cc6b5e71249d
6
+ metadata.gz: f66a01241685f18493d0519cb009674f0764e260ccc79b048ebc7346b5d4c64bd78deeb9983a0b4893e4fcfc2d1361f7024ca212bcbdc8071191417e793d9427
7
+ data.tar.gz: 85e2cdd6c5c65e52f882fe2e8386cdf772c239ecdf27d979d473170f51bb1c0a37d8919cb8fc7422bb04eadf50baa8adb1ad4e712ea3952d3e2c0f7d9923bd48
@@ -10,10 +10,14 @@ module Safrano
10
10
  BigDecimal(to_s).to_s('F')
11
11
  end
12
12
 
13
- def toDecimalPrecisionString(precision, scale = nil)
13
+ def toDecimalPrecisionString(precision)
14
14
  p = Integer(precision)
15
+ BigDecimal(self, p).to_s('F')
16
+ end
15
17
 
16
- scale.nil? ? BigDecimal(self, p).to_s('F') : sprintf("%#{p + 2}.#{scale}f", self).strip
18
+ def toDecimalPrecisionScaleString(precision, scale)
19
+ p = Integer(precision)
20
+ sprintf("%#{p + 2}.#{scale}f", self).strip
17
21
  end
18
22
  end
19
23
  end
@@ -37,86 +37,87 @@ module Safrano
37
37
 
38
38
  # type mappings are hard, especially between "Standards" like SQL and OData V2 (might be a bit better in V4 ?)
39
39
  # this is all best effort/try to make it work logic
40
- def self.add_edm_types(props)
40
+ def self.add_edm_types(metadata, props)
41
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]
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
64
 
65
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]
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
79
 
80
80
  # try int-like with db_type:
81
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
119
- end
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
120
121
  end
121
122
 
122
123
  # use Edm twice so that we can do include Safrano::Edm and then
@@ -35,6 +35,12 @@ module Safrano
35
35
  attr_reader :odata_upk_parts
36
36
  attr_reader :casted_cols
37
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
38
44
 
39
45
  # initialising block of code to be executed at end of
40
46
  # ServerApp.publish_service after all model classes have been registered
@@ -75,6 +81,7 @@ module Safrano
75
81
  @uparms = nil
76
82
  @params = nil
77
83
  @cx = nil
84
+ @cols_metadata = {}
78
85
  end
79
86
 
80
87
  def build_uri(uribase)
@@ -177,13 +184,14 @@ module Safrano
177
184
  end
178
185
  # with their properties
179
186
  db_schema.each do |pnam, prop|
187
+ metadata = @cols_metadata[pnam]
180
188
  if prop[:primary_key] == true
181
189
  enty.add_element('Key').add_element('PropertyRef',
182
190
  'Name' => pnam.to_s)
183
191
  end
184
192
  attrs = { 'Name' => pnam.to_s,
185
193
  # 'Type' => Safrano.get_edm_type(db_type: prop[:db_type]) }
186
- 'Type' => prop[:edm_type] }
194
+ 'Type' => metadata[:edm_type] }
187
195
  attrs['Nullable'] = 'false' if prop[:allow_null] == false
188
196
  enty.add_element('Property', attrs)
189
197
  end
@@ -350,47 +358,79 @@ module Safrano
350
358
  end
351
359
  end
352
360
 
353
- def finalize_publishing
354
- build_type_name
355
-
356
- # build default output template structure
357
- build_default_template
358
-
359
- # add edm_types into schema
360
- db_schema.each do |_col, props|
361
- Safrano.add_edm_types(props)
362
- end
363
-
361
+ def build_casted_cols(service)
364
362
  # cols needed catsting before final json output
365
363
  @casted_cols = {}
366
364
  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
- }
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
373
393
 
374
394
  next
375
395
  end
376
- if props[:edm_type] == 'Edm.Decimal'
396
+ if metadata[:edm_type] == 'Edm.Decimal'
377
397
  @casted_cols[col] = ->(x) { x&.toDecimalString }
378
398
  next
379
399
  end
380
400
  # Odata V2 Spec:
381
401
  # Edm.Binary Base64 encoded value of an EDM.Binary value represented as a JSON string
382
402
  # See for example https://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)?$format=json
383
- if props[:edm_type] == 'Edm.Binary'
403
+ if metadata[:edm_type] == 'Edm.Binary'
384
404
  @casted_cols[col] = ->(x) { Base64.encode64(x) unless x.nil? } # Base64
385
405
  next
386
406
  end
387
407
  # TODO check this more in details
408
+ # NOTE: here we use :type which is the sequel defined ruby-type
388
409
  if props[:type] == :datetime
389
410
  @casted_cols[col] = ->(x) { x&.iso8601 }
390
411
 
391
412
  end
392
413
  }
414
+ end
415
+
416
+ def finalize_publishing(service)
417
+ build_type_name
418
+
419
+ # build default output template structure
420
+ build_default_template
421
+
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)
427
+ end
393
428
 
429
+ build_casted_cols(service)
430
+ # unless @casted_cols.empty?
431
+ # require 'pry'
432
+ # binding.pry
433
+ # end
394
434
  # and finally build the path lists and allowed tr's
395
435
  build_attribute_path_list
396
436
  build_expand_path_list
@@ -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'
@@ -138,6 +139,7 @@ module Safrano
138
139
  attr_accessor :complex_types
139
140
  attr_accessor :function_imports
140
141
  attr_accessor :function_import_keys
142
+ attr_accessor :type_mappings
141
143
 
142
144
  # Instance attributes for specialized Version specific Instances
143
145
  attr_accessor :v1
@@ -158,6 +160,7 @@ module Safrano
158
160
  @function_imports = {}
159
161
  @function_import_keys = []
160
162
  @cmap = {}
163
+ @type_mappings = {}
161
164
  instance_eval(&block) if block_given?
162
165
  end
163
166
 
@@ -242,6 +245,7 @@ module Safrano
242
245
  other.complex_types = @complex_types
243
246
  other.function_imports = @function_imports
244
247
  other.function_import_keys = @function_import_keys
248
+ other.type_mappings = @type_mappings
245
249
  other
246
250
  end
247
251
 
@@ -330,6 +334,11 @@ module Safrano
330
334
  funcimp
331
335
  end
332
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
+
333
342
  def cmap=(imap)
334
343
  @cmap = imap
335
344
  set_collections_sorted(@cmap.values)
@@ -370,10 +379,10 @@ module Safrano
370
379
 
371
380
  set_uribase
372
381
 
373
- @collections.each(&:finalize_publishing)
374
-
375
382
  # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
376
383
  @collections.each do |klass|
384
+ klass.finalize_publishing(self)
385
+
377
386
  klass.build_uri(@uribase)
378
387
 
379
388
  # Output create (POST) as single entity (Standard) or as array (non-standard buggy)
@@ -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.4'
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.4
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-07-17 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
@@ -166,6 +166,7 @@ files:
166
166
  - lib/safrano/response.rb
167
167
  - lib/safrano/sequel_join_by_paths.rb
168
168
  - lib/safrano/service.rb
169
+ - lib/safrano/type_mapping.rb
169
170
  - lib/safrano/version.rb
170
171
  - lib/sequel/plugins/join_by_paths.rb
171
172
  homepage: https://gitlab.com/dm0da/safrano