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 +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
|