scale_rb 0.4.2 → 0.5.0
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/.devcontainer/devcontainer.json +21 -0
- data/Dockerfile +16 -0
- data/Gemfile +4 -4
- data/README.md +19 -7
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/examples/http_client_2.rb +0 -2
- data/exe/metadata +9 -11
- data/lib/address.rb +1 -1
- data/lib/custom_assign.rb +92 -0
- data/lib/scale_rb/call_helper.rb +42 -0
- data/lib/{client → scale_rb/client}/client_ext.rb +12 -13
- data/lib/{client → scale_rb/client}/http_client.rb +1 -1
- data/lib/{client → scale_rb/client}/ws_client.rb +11 -15
- data/lib/scale_rb/codec.rb +25 -0
- data/lib/scale_rb/codec_utils.rb +128 -0
- data/lib/scale_rb/decode.rb +164 -0
- data/lib/scale_rb/encode.rb +150 -0
- data/lib/{hasher.rb → scale_rb/hasher.rb} +10 -8
- data/lib/scale_rb/metadata/metadata.rb +114 -0
- data/lib/{metadata → scale_rb/metadata}/metadata_v10.rb +0 -17
- data/lib/{metadata → scale_rb/metadata}/metadata_v11.rb +0 -17
- data/lib/{metadata → scale_rb/metadata}/metadata_v12.rb +0 -17
- data/lib/{metadata → scale_rb/metadata}/metadata_v13.rb +0 -17
- data/lib/{metadata → scale_rb/metadata}/metadata_v14.rb +18 -18
- data/lib/{metadata → scale_rb/metadata}/metadata_v9.rb +0 -17
- data/lib/scale_rb/metadata/registry.rb +263 -0
- data/lib/scale_rb/metadata/type_exp.rb +286 -0
- data/lib/scale_rb/portable_registry.rb +133 -0
- data/lib/{storage_helper.rb → scale_rb/storage_helper.rb} +16 -4
- data/lib/scale_rb/types.rb +233 -0
- data/lib/scale_rb/utils.rb +125 -0
- data/lib/scale_rb/version.rb +1 -1
- data/lib/scale_rb.rb +20 -26
- data/lib/type_enforcer.rb +170 -0
- data/scale_rb.gemspec +3 -0
- metadata +57 -19
- data/lib/codec.rb +0 -450
- data/lib/metadata/metadata.rb +0 -137
- data/lib/monkey_patching.rb +0 -115
- data/lib/portable_codec.rb +0 -285
- data/lib/registry.rb +0 -13
@@ -20,7 +20,7 @@ module ScaleRb
|
|
20
20
|
]
|
21
21
|
else
|
22
22
|
[
|
23
|
-
registry[key[:type]].
|
23
|
+
registry[key[:type]].tuple.first(key[:value].length),
|
24
24
|
key[:value],
|
25
25
|
key[:hashers].first(key[:value].length)
|
26
26
|
]
|
@@ -32,12 +32,24 @@ module ScaleRb
|
|
32
32
|
raise "Key's value doesn't match key's type, key's value: #{key_values.inspect}, but key's type: #{key_types.inspect}. Please check your key's value."
|
33
33
|
end
|
34
34
|
|
35
|
-
storage_key +
|
35
|
+
storage_key + _encode_types_with_hashers(key_types, key_values, registry, key_hashers)
|
36
36
|
else
|
37
37
|
storage_key
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def _encode_types_with_hashers(type_ids, values, registry, hashers)
|
42
|
+
if !hashers.nil? && hashers.length != type_ids.length
|
43
|
+
raise "type_ids length: #{type_ids.length}, hashers length: #{hashers.length}"
|
44
|
+
end
|
45
|
+
|
46
|
+
type_ids
|
47
|
+
.map.with_index { |type_id, i| ScaleRb::Codec.encode(type_id, values[i], registry) }
|
48
|
+
.each_with_index.reduce([]) do |memo, (bytes, i)|
|
49
|
+
memo + Hasher.apply_hasher(hashers[i], bytes)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
41
53
|
# data: hex string
|
42
54
|
# type: portable type id
|
43
55
|
# optional: boolean
|
@@ -45,14 +57,14 @@ module ScaleRb
|
|
45
57
|
# returns nil or data
|
46
58
|
def decode_storage(data, type, optional, fallback, registry)
|
47
59
|
data ||= (optional ? nil : fallback)
|
48
|
-
|
60
|
+
ScaleRb::Codec.decode(type, Utils.hex_to_u8a(data), registry)[0] if data
|
49
61
|
end
|
50
62
|
|
51
63
|
# storage_item: the storage item from metadata
|
52
64
|
def decode_storage2(data, storage_item, registry)
|
53
65
|
modifier = storage_item._get(:modifier) # Default | Optional
|
54
66
|
fallback = storage_item._get(:fallback)
|
55
|
-
type = storage_item._get(:type
|
67
|
+
type = storage_item._get(:type, :plain) || storage_item._get(:type, :map, :value)
|
56
68
|
decode_storage(data, type, modifier == 'Optional', fallback, registry)
|
57
69
|
end
|
58
70
|
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-struct'
|
4
|
+
require 'dry-types'
|
5
|
+
|
6
|
+
module ScaleRb
|
7
|
+
module Types
|
8
|
+
include Dry.Types()
|
9
|
+
|
10
|
+
Primitive = Types::Strict::Symbol.enum(
|
11
|
+
:I8, :U8, :I16, :U16, :I32, :U32, :I64, :U64, :I128, :U128, :I256, :U256, :Bool, :Str, :Char
|
12
|
+
)
|
13
|
+
Ti = Types::Strict::Integer.constrained(gteq: 0)
|
14
|
+
U8 = Types::Strict::Integer.constrained(gteq: 0, lt: 256)
|
15
|
+
U8Array = Types::Strict::Array.of(U8)
|
16
|
+
Hex = Types::Strict::String.constrained(format: /\A0x[0-9a-fA-F]+\z/)
|
17
|
+
|
18
|
+
Registry = Types.Interface(:[])
|
19
|
+
|
20
|
+
HashMap = lambda do |key_type, value_type|
|
21
|
+
Types::Hash.map(key_type, value_type)
|
22
|
+
end
|
23
|
+
UnsignedInteger = Types::Strict::Integer.constrained(gteq: 0)
|
24
|
+
TypedArray = ->(type) { Types::Array.of(type) }
|
25
|
+
|
26
|
+
class Base < Dry::Struct
|
27
|
+
attribute? :registry, Registry
|
28
|
+
attribute? :path, Types::Strict::Array.of(Types::Strict::String)
|
29
|
+
|
30
|
+
def t(type_id)
|
31
|
+
raise 'No registry' unless registry
|
32
|
+
|
33
|
+
pt = registry[type_id]
|
34
|
+
raise "Unknown type: #{type_id}" unless pt
|
35
|
+
|
36
|
+
pt
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
to_string
|
41
|
+
end
|
42
|
+
|
43
|
+
MAX_DEPTH = 2
|
44
|
+
def to_string(_depth = 0)
|
45
|
+
raise NotImplementedError, "#{self.class} must implement to_string"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class PrimitiveType < Base
|
50
|
+
attribute :primitive, Primitive
|
51
|
+
|
52
|
+
def to_string(_depth = 0)
|
53
|
+
primitive.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class CompactType < Base
|
58
|
+
attribute? :type, Ti
|
59
|
+
|
60
|
+
def to_string(depth = 0)
|
61
|
+
if type
|
62
|
+
if depth > MAX_DEPTH
|
63
|
+
'Compact<...>'
|
64
|
+
else
|
65
|
+
"Compact<#{t(type).to_string(depth + 1)}>"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
'Compact'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class SequenceType < Base
|
74
|
+
attribute :type, Ti
|
75
|
+
|
76
|
+
def to_string(depth = 0)
|
77
|
+
if depth > MAX_DEPTH
|
78
|
+
'[...]'
|
79
|
+
else
|
80
|
+
"[#{t(type).to_string(depth + 1)}]"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class BitSequenceType < Base
|
86
|
+
attribute :bit_store_type, Ti
|
87
|
+
attribute :bit_order_type, Ti
|
88
|
+
|
89
|
+
def to_string(depth = 0)
|
90
|
+
if depth > MAX_DEPTH
|
91
|
+
'BitSequence<...>'
|
92
|
+
else
|
93
|
+
"BitSequence<#{t(bit_store_type).to_string(depth + 1)}, #{t(bit_order_type).to_string(depth + 1)}>"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class ArrayType < Base
|
99
|
+
attribute :len, Types::Strict::Integer
|
100
|
+
attribute :type, Ti
|
101
|
+
|
102
|
+
def to_string(depth = 0)
|
103
|
+
if depth > MAX_DEPTH
|
104
|
+
'[...]'
|
105
|
+
else
|
106
|
+
"[#{t(type).to_string(depth + 1)}; #{len}]"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class TupleType < Base
|
112
|
+
attribute :tuple, Types::Strict::Array.of(Ti)
|
113
|
+
|
114
|
+
def to_string(depth = 0)
|
115
|
+
if depth > MAX_DEPTH
|
116
|
+
'(...)'
|
117
|
+
else
|
118
|
+
"(#{tuple.map { |t| t(t).to_string(depth + 1) }.join(', ')})"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Field < Dry::Struct
|
124
|
+
attribute :name, Types::Strict::String
|
125
|
+
attribute :type, Ti
|
126
|
+
end
|
127
|
+
|
128
|
+
class StructType < Base
|
129
|
+
attribute :fields, Types::Strict::Array.of(Field)
|
130
|
+
|
131
|
+
def to_string(depth = 0)
|
132
|
+
if depth > MAX_DEPTH
|
133
|
+
'{ ... }'
|
134
|
+
else
|
135
|
+
"{ #{fields.map { |field| "#{field.name}: #{t(field.type).to_string(depth + 1)}" }.join(', ')} }"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class UnitType < Base
|
141
|
+
def to_string(_depth = 0)
|
142
|
+
'()'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class SimpleVariant < Dry::Struct
|
147
|
+
attribute :name, Types::Strict::Symbol
|
148
|
+
attribute :index, Types::Strict::Integer
|
149
|
+
end
|
150
|
+
|
151
|
+
class TupleVariant < Dry::Struct
|
152
|
+
attribute :name, Types::Strict::Symbol
|
153
|
+
attribute :index, Types::Strict::Integer
|
154
|
+
attribute :tuple, TupleType
|
155
|
+
end
|
156
|
+
|
157
|
+
class StructVariant < Dry::Struct
|
158
|
+
attribute :name, Types::Strict::Symbol
|
159
|
+
attribute :index, Types::Strict::Integer
|
160
|
+
attribute :struct, StructType
|
161
|
+
end
|
162
|
+
|
163
|
+
VariantKind = Instance(SimpleVariant) | Instance(TupleVariant) | Instance(StructVariant)
|
164
|
+
|
165
|
+
class VariantType < Base
|
166
|
+
attribute :variants, Types::Array.of(VariantKind)
|
167
|
+
|
168
|
+
def to_string(depth = 0)
|
169
|
+
if depth > MAX_DEPTH
|
170
|
+
variants.sort_by(&:index).map { |v| v.name.to_s }.join(' | ')
|
171
|
+
else
|
172
|
+
variants.sort_by(&:index).map do |v|
|
173
|
+
case v
|
174
|
+
when SimpleVariant
|
175
|
+
v.name.to_s
|
176
|
+
when TupleVariant
|
177
|
+
"#{v.name}#{v.tuple.to_string(depth + 1)}"
|
178
|
+
when StructVariant
|
179
|
+
"#{v.name} #{v.struct.to_string(depth + 1)}"
|
180
|
+
end
|
181
|
+
end.join(' | ')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.option(type, registry)
|
186
|
+
VariantType.new(
|
187
|
+
variants: [
|
188
|
+
SimpleVariant.new(name: :None, index: 0),
|
189
|
+
TupleVariant.new(name: :Some, index: 1, tuple: TupleType.new(tuple: [type], registry:))
|
190
|
+
],
|
191
|
+
registry:
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
def option?
|
196
|
+
variants.length == 2 &&
|
197
|
+
variants.any? { |v| v.is_a?(SimpleVariant) && v.name == :None && v.index == 0 } &&
|
198
|
+
variants.any? { |v| v.is_a?(TupleVariant) && v.name == :Some && v.index == 1 }
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.result(ok_type, err_type, registry)
|
202
|
+
VariantType.new(
|
203
|
+
variants: [
|
204
|
+
TupleVariant.new(name: :Ok, index: 0, tuple: TupleType.new(tuple: [ok_type], registry:)),
|
205
|
+
TupleVariant.new(name: :Err, index: 1, tuple: TupleType.new(tuple: [err_type], registry:))
|
206
|
+
],
|
207
|
+
registry:
|
208
|
+
)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
PortableType = Instance(VariantType) |
|
213
|
+
Instance(StructType) |
|
214
|
+
Instance(TupleType) |
|
215
|
+
Instance(ArrayType) |
|
216
|
+
Instance(CompactType) |
|
217
|
+
Instance(PrimitiveType) |
|
218
|
+
Instance(UnitType) |
|
219
|
+
Instance(SequenceType) |
|
220
|
+
Instance(BitSequenceType)
|
221
|
+
|
222
|
+
DecodeResult = lambda do |type|
|
223
|
+
Types::Array.of(Types::Any).constrained(size: 2).constructor do |arr|
|
224
|
+
[type[arr[0]], arr[1]] # U8Array[arr[1]], but performance is not good.
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# type = ScaleRb::Types::TypedArray[ScaleRb::Types::UnsignedInteger]
|
231
|
+
# p type
|
232
|
+
# p type[[1, 2]] # => [1, 2]
|
233
|
+
# # p type[[-1, -2]] # => -1 violates constraints (gteq?(0, -1) failed)
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
# Check if the key exists in the hash
|
5
|
+
# @param key [String | Symbol] Key to check
|
6
|
+
# @return [Boolean] True if the key exists, false otherwise
|
7
|
+
def _key?(key)
|
8
|
+
ScaleRb::Utils.key?(self, key)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get the value from the hash
|
12
|
+
# @param keys [Array<String | Symbol>] Keys to get the value from
|
13
|
+
# @return [Object | NilClass] Value if the key exists, nil otherwise
|
14
|
+
def _get(*keys)
|
15
|
+
ScaleRb::Utils.get(self, *keys)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ScaleRb
|
20
|
+
module Utils
|
21
|
+
class << self
|
22
|
+
# https://www.rubyguides.com/2017/01/read-binary-data/
|
23
|
+
def hex_to_u8a(str)
|
24
|
+
data = str.start_with?('0x') ? str[2..] : str
|
25
|
+
raise 'Not valid hex string' if data =~ /[^\da-f]+/i
|
26
|
+
|
27
|
+
data = "0#{data}" if data.length.odd?
|
28
|
+
data.scan(/../).map(&:hex)
|
29
|
+
end
|
30
|
+
|
31
|
+
def camelize(str)
|
32
|
+
str.split('_').collect(&:capitalize).join
|
33
|
+
end
|
34
|
+
|
35
|
+
def underscore(str)
|
36
|
+
str.gsub(/::/, '/')
|
37
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
38
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
39
|
+
.tr('-', '_')
|
40
|
+
.downcase
|
41
|
+
end
|
42
|
+
|
43
|
+
def int_to_u8a(int, bit_length = nil)
|
44
|
+
hex = bit_length ? int.to_s(16).rjust(bit_length / 4, '0') : int.to_s(16)
|
45
|
+
hex_to_u8a(hex)
|
46
|
+
end
|
47
|
+
|
48
|
+
def uint_to_int(unsigned, bit_length)
|
49
|
+
unsigned_mid = 2**(bit_length - 1)
|
50
|
+
unsigned_ceiling = 2**bit_length
|
51
|
+
unsigned >= unsigned_mid ? unsigned - unsigned_ceiling : unsigned
|
52
|
+
end
|
53
|
+
|
54
|
+
def int_to_uint(signed, bit_length)
|
55
|
+
unsigned_mid = 2**(bit_length - 1)
|
56
|
+
unsigned_ceiling = 2**bit_length
|
57
|
+
raise 'Out of scope' if signed >= unsigned_mid || signed <= -unsigned_mid
|
58
|
+
|
59
|
+
signed.negative? ? unsigned_ceiling + signed : signed
|
60
|
+
end
|
61
|
+
|
62
|
+
# unix timestamp to utc
|
63
|
+
def unix_to_utc(unix)
|
64
|
+
Time.at(unix).utc.asctime
|
65
|
+
end
|
66
|
+
|
67
|
+
# utc to unix timestamp
|
68
|
+
def utc_to_unix(utc_asctime)
|
69
|
+
Time.parse(utc_asctime)
|
70
|
+
end
|
71
|
+
|
72
|
+
def u8a?(arr)
|
73
|
+
arr.all? { |e| e >= 0 && e <= 255 }
|
74
|
+
end
|
75
|
+
|
76
|
+
def u8a_to_hex(u8a)
|
77
|
+
raise 'Not a byte array' unless u8a?(u8a)
|
78
|
+
|
79
|
+
u8a.reduce('0x') { |hex, u8| hex + u8.to_s(16).rjust(2, '0') }
|
80
|
+
end
|
81
|
+
|
82
|
+
def u8a_to_bin(u8a)
|
83
|
+
raise 'Not a byte array' unless u8a?(u8a)
|
84
|
+
|
85
|
+
u8a.reduce('0b') { |bin, u8| bin + u8.to_s(2).rjust(8, '0') }
|
86
|
+
end
|
87
|
+
|
88
|
+
def u8a_to_utf8(u8a)
|
89
|
+
raise 'Not a byte array' unless u8a?(u8a)
|
90
|
+
|
91
|
+
u8a.pack('C*').force_encoding('utf-8')
|
92
|
+
end
|
93
|
+
|
94
|
+
def u8a_to_uint(u8a)
|
95
|
+
u8a_to_hex(u8a).to_i(16)
|
96
|
+
end
|
97
|
+
|
98
|
+
def u8a_to_int(u8a, bit_length)
|
99
|
+
uint_to_int(u8a_to_uint(u8a), bit_length)
|
100
|
+
end
|
101
|
+
|
102
|
+
def key?(hash, key)
|
103
|
+
if key.instance_of?(String)
|
104
|
+
hash.key?(key) || hash.key?(key.to_sym)
|
105
|
+
else
|
106
|
+
hash.key?(key) || hash.key?(key.to_s)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def get(hash, *keys)
|
111
|
+
keys.reduce(hash) do |h, key|
|
112
|
+
break nil unless h.is_a?(Hash)
|
113
|
+
|
114
|
+
if key.instance_of?(String)
|
115
|
+
h[key] || h[key.to_sym]
|
116
|
+
elsif key.instance_of?(Symbol)
|
117
|
+
h[key] || h[key.to_s]
|
118
|
+
else
|
119
|
+
h[key]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/scale_rb/version.rb
CHANGED
data/lib/scale_rb.rb
CHANGED
@@ -3,32 +3,6 @@
|
|
3
3
|
require 'scale_rb/version'
|
4
4
|
require 'console'
|
5
5
|
|
6
|
-
# scale codec
|
7
|
-
require 'monkey_patching'
|
8
|
-
require 'codec'
|
9
|
-
require 'portable_codec'
|
10
|
-
|
11
|
-
# metadata types, decoding and helpers
|
12
|
-
require 'metadata/metadata_v9'
|
13
|
-
require 'metadata/metadata_v10'
|
14
|
-
require 'metadata/metadata_v11'
|
15
|
-
require 'metadata/metadata_v12'
|
16
|
-
require 'metadata/metadata_v13'
|
17
|
-
require 'metadata/metadata_v14'
|
18
|
-
require 'metadata/metadata'
|
19
|
-
|
20
|
-
require 'hasher'
|
21
|
-
require 'storage_helper'
|
22
|
-
|
23
|
-
# get registry from config
|
24
|
-
require 'registry'
|
25
|
-
|
26
|
-
require 'address'
|
27
|
-
|
28
|
-
# clients
|
29
|
-
require 'client/http_client'
|
30
|
-
require 'client/ws_client'
|
31
|
-
|
32
6
|
module ScaleRb
|
33
7
|
class << self
|
34
8
|
attr_accessor :logger
|
@@ -40,3 +14,23 @@ module ScaleRb
|
|
40
14
|
end
|
41
15
|
|
42
16
|
ScaleRb.logger = Console
|
17
|
+
|
18
|
+
require 'scale_rb/utils'
|
19
|
+
|
20
|
+
require 'type_enforcer'
|
21
|
+
|
22
|
+
require 'scale_rb/types'
|
23
|
+
require 'scale_rb/portable_registry'
|
24
|
+
require 'scale_rb/codec'
|
25
|
+
|
26
|
+
require 'scale_rb/metadata/metadata'
|
27
|
+
|
28
|
+
require 'scale_rb/hasher'
|
29
|
+
require 'scale_rb/storage_helper'
|
30
|
+
require 'scale_rb/call_helper'
|
31
|
+
|
32
|
+
require 'address'
|
33
|
+
|
34
|
+
# clients
|
35
|
+
require 'scale_rb/client/http_client'
|
36
|
+
require 'scale_rb/client/ws_client'
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require_relative 'custom_assign'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
# rubocop:disable all
|
5
|
+
module TypeEnforcer
|
6
|
+
|
7
|
+
def self.extended(base)
|
8
|
+
base.instance_variable_set(:@type_enforcements, {})
|
9
|
+
base.instance_variable_set(:@applying_enforcement, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
def __(method_name, param_types, return_type = nil, level: 1, skip: [])
|
13
|
+
return unless type_enforcement_enabled?
|
14
|
+
|
15
|
+
@type_enforcements[method_name] = {
|
16
|
+
params: param_types,
|
17
|
+
return: return_type,
|
18
|
+
level: level,
|
19
|
+
skip: skip
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_added(method_name)
|
24
|
+
super
|
25
|
+
apply_enforcement(method_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def apply_enforcement(method_name)
|
31
|
+
return unless type_enforcement_enabled?
|
32
|
+
return if @applying_enforcement
|
33
|
+
return unless @type_enforcements.key?(method_name)
|
34
|
+
|
35
|
+
@applying_enforcement = true
|
36
|
+
begin
|
37
|
+
result = @type_enforcements[method_name]
|
38
|
+
if result[:level] > type_enforcement_level
|
39
|
+
decorate(method_name, result[:params], result[:return], result[:skip])
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
@applying_enforcement = false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def type_enforcement_enabled?
|
47
|
+
return true if ENV['ENABLE_TYPE_ENFORCEMENT'] && ENV['ENABLE_TYPE_ENFORCEMENT'] == 'true'
|
48
|
+
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def type_enforcement_level
|
53
|
+
ENV['TYPE_ENFORCEMENT_LEVEL'].to_i || 2
|
54
|
+
end
|
55
|
+
|
56
|
+
def decorate(method_name, param_types, return_type, skip)
|
57
|
+
target = self
|
58
|
+
original_method = target.instance_method(method_name)
|
59
|
+
method_parameters = original_method.parameters
|
60
|
+
|
61
|
+
# only support positional args and keyword args for now
|
62
|
+
# TODO: support splat(rest), double splat(keyrest) args, and block
|
63
|
+
target.define_method(method_name) do |*args, **kwargs|
|
64
|
+
ScaleRb.logger.debug("----------------------------------------------------------")
|
65
|
+
ScaleRb.logger.debug("method: #{method_name}")
|
66
|
+
ScaleRb.logger.debug("params: args: #{args}, kwargs: #{kwargs}")
|
67
|
+
ScaleRb.logger.debug("param kinds: #{method_parameters}")
|
68
|
+
ScaleRb.logger.debug("param types: #{param_types}")
|
69
|
+
|
70
|
+
validated_args = []
|
71
|
+
validated_kwargs = {}
|
72
|
+
|
73
|
+
# build a hash of param_name => value | default_value
|
74
|
+
defaults = method_parameters.each_with_object({}) do |(_param_kind, param_name), memo|
|
75
|
+
type = param_types[param_name]
|
76
|
+
memo[param_name] = type.value if type.respond_to?(:value)
|
77
|
+
end
|
78
|
+
assigned_params = build_assigned_params(original_method, defaults, args, kwargs)
|
79
|
+
ScaleRb.logger.debug("assigned params: #{assigned_params}")
|
80
|
+
|
81
|
+
# validate each param
|
82
|
+
method_parameters.each do |param_kind, param_name|
|
83
|
+
case param_kind
|
84
|
+
when :req, :opt
|
85
|
+
value = assigned_params[param_name]
|
86
|
+
if skip.include?(param_name)
|
87
|
+
validated_args << value
|
88
|
+
else
|
89
|
+
type = param_types[param_name]
|
90
|
+
validated_args << type[value]
|
91
|
+
end
|
92
|
+
when :keyreq, :key
|
93
|
+
value = assigned_params[param_name]
|
94
|
+
if skip.include?(param_name)
|
95
|
+
validated_kwargs[param_name] = value
|
96
|
+
else
|
97
|
+
type = param_types[param_name]
|
98
|
+
validated_kwargs[param_name] = type[value]
|
99
|
+
end
|
100
|
+
when :rest, :keyrest
|
101
|
+
raise NotImplementedError, 'rest and keyrest args not supported'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
result = original_method.bind(self).call(*validated_args, **validated_kwargs)
|
106
|
+
|
107
|
+
if skip.include?(:returns) && return_type
|
108
|
+
return_type[result]
|
109
|
+
else
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# require 'dry-types'
|
117
|
+
|
118
|
+
# module Types
|
119
|
+
# include Dry.Types()
|
120
|
+
# end
|
121
|
+
|
122
|
+
# class Example
|
123
|
+
# extend TypeEnforcer
|
124
|
+
|
125
|
+
# __ :add, { a: Types::Strict::Integer, b: Types::Strict::Integer }, Types::Strict::Integer
|
126
|
+
# def self.add(a, b)
|
127
|
+
# a + b
|
128
|
+
# end
|
129
|
+
|
130
|
+
# __ :subtract, { a: Types::Strict::Integer, b: Types::Strict::Integer }, Types::Strict::Integer
|
131
|
+
# def subtract(a, b)
|
132
|
+
# a - b
|
133
|
+
# end
|
134
|
+
|
135
|
+
# __ :my_method, {
|
136
|
+
# a: Types::Strict::Integer,
|
137
|
+
# b: Types::Strict::Integer.default(2),
|
138
|
+
# c: Types::Strict::Integer,
|
139
|
+
# d: Types::Strict::Integer.default(4)
|
140
|
+
# }, Types::Strict::String
|
141
|
+
# def my_method(a, b = 2, c:, d: 4)
|
142
|
+
# "a: #{a}, b: #{b}, c: #{c}, d: #{d}"
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
|
146
|
+
# puts Example.add(1, 2) # => 3
|
147
|
+
|
148
|
+
# puts Example.new.subtract(3, 1) # => 2
|
149
|
+
|
150
|
+
# begin
|
151
|
+
# puts Example.new.subtract(3, '1')
|
152
|
+
# rescue StandardError => e
|
153
|
+
# puts e.class # => Dry::Types::ConstraintError
|
154
|
+
# puts e.message # => "1" violates constraints (type?(Integer, "1") failed)
|
155
|
+
# end
|
156
|
+
|
157
|
+
# puts Example.new.my_method(5, c: 3) # => "a: 5, b: 2, c: 3, d: 4"
|
158
|
+
# puts Example.new.my_method(5, 6, c: 3) # => "a: 5, b: 6, c: 3, d: 4"
|
159
|
+
# puts Example.new.my_method(5, 6, c: 3, d: 10) # => "a: 5, b: 6, c: 3, d: 10"
|
160
|
+
# begin
|
161
|
+
# puts Example.new.my_method(5, 6, c: 3, d: '10')
|
162
|
+
# rescue StandardError => e
|
163
|
+
# puts e.class # => Dry::Types::ConstraintError
|
164
|
+
# puts e.message # => "10" violates constraints (type?(Integer, "10") failed)
|
165
|
+
# end
|
166
|
+
# begin
|
167
|
+
# puts Example.new.my_method(5, 6, d: 10)
|
168
|
+
# rescue StandardError => e
|
169
|
+
# puts e.message # => Missing required keyword argument: c
|
170
|
+
# end
|
data/scale_rb.gemspec
CHANGED