safrano 0.5.4 → 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: 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