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 +4 -4
- data/lib/core_ext/Numeric/convert.rb +6 -2
- data/lib/odata/edm/primitive_types.rb +75 -74
- data/lib/odata/model_ext.rb +60 -20
- data/lib/safrano/service.rb +11 -2
- data/lib/safrano/type_mapping.rb +149 -0
- data/lib/safrano/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb412667db93e26340fc21440c89572fbff42fe52ae36807614f0fd11aac808c
|
4
|
+
data.tar.gz: 90883acded679aa860346e49242b1149933ccdb474e3e39eb6a893da5958dde7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
13
|
+
def toDecimalPrecisionString(precision)
|
14
14
|
p = Integer(precision)
|
15
|
+
BigDecimal(self, p).to_s('F')
|
16
|
+
end
|
15
17
|
|
16
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
return if
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
return if
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
return if
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
data/lib/odata/model_ext.rb
CHANGED
@@ -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' =>
|
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
|
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
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
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
|
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
|
data/lib/safrano/service.rb
CHANGED
@@ -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
|
data/lib/safrano/version.rb
CHANGED
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
|
+
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-
|
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
|