familia 1.0.0.pre.rc3 → 1.0.0.pre.rc4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +198 -48
- data/VERSION.yml +1 -1
- data/lib/familia/base.rb +29 -1
- data/lib/familia/features/expiration.rb +90 -0
- data/lib/familia/features/quantization.rb +56 -0
- data/lib/familia/features/safe_dump.rb +2 -33
- data/lib/familia/features.rb +5 -4
- data/lib/familia/horreum/class_methods.rb +112 -46
- data/lib/familia/horreum/commands.rb +9 -3
- data/lib/familia/horreum/relations_management.rb +2 -2
- data/lib/familia/horreum/serialization.rb +23 -42
- data/lib/familia/horreum/settings.rb +0 -8
- data/lib/familia/horreum/utils.rb +0 -1
- data/lib/familia/horreum.rb +1 -1
- data/lib/familia/logging.rb +26 -4
- data/lib/familia/redistype/serialization.rb +60 -38
- data/lib/familia/redistype.rb +45 -17
- data/lib/familia/settings.rb +11 -1
- data/lib/familia/tools.rb +68 -0
- data/lib/familia/types/hashkey.rb +5 -5
- data/lib/familia/types/list.rb +2 -2
- data/lib/familia/types/sorted_set.rb +12 -12
- data/lib/familia/types/string.rb +1 -1
- data/lib/familia/types/unsorted_set.rb +2 -2
- data/lib/familia/utils.rb +106 -51
- data/lib/familia/version.rb +2 -2
- data/try/10_familia_try.rb +4 -4
- data/try/20_redis_type_try.rb +9 -6
- data/try/26_redis_bool_try.rb +1 -1
- data/try/27_redis_horreum_try.rb +1 -1
- data/try/30_familia_object_try.rb +3 -2
- data/try/40_customer_try.rb +3 -3
- data/try/test_helpers.rb +9 -2
- metadata +5 -5
- data/lib/familia/features/api_version.rb +0 -19
- data/lib/familia/features/atomic_saves.rb +0 -8
- data/lib/familia/features/quantizer.rb +0 -35
@@ -4,63 +4,80 @@ class Familia::RedisType
|
|
4
4
|
|
5
5
|
module Serialization
|
6
6
|
|
7
|
-
# Serializes
|
8
|
-
#
|
9
|
-
# This method prepares a value for storage in Redis by converting it to a string representation.
|
10
|
-
# If a class option is specified, it uses that class's serialization method.
|
11
|
-
# Otherwise, it relies on the value's own `to_s` method for serialization.
|
7
|
+
# Serializes a value for storage in Redis.
|
12
8
|
#
|
13
9
|
# @param val [Object] The value to be serialized.
|
14
|
-
# @param strict_values [Boolean] Whether to enforce strict value
|
15
|
-
#
|
10
|
+
# @param strict_values [Boolean] Whether to enforce strict value
|
11
|
+
# serialization (default: true).
|
12
|
+
# @return [String, nil] The serialized representation of the value, or nil
|
13
|
+
# if serialization fails.
|
16
14
|
#
|
17
|
-
# @note When
|
18
|
-
#
|
15
|
+
# @note When a class option is specified, it uses that class's
|
16
|
+
# serialization method. Otherwise, it relies on Familia.distinguisher for
|
17
|
+
# serialization.
|
19
18
|
#
|
20
19
|
# @example With a class option
|
21
|
-
# to_redis(User.new(name: "
|
22
|
-
# to_redis(nil, strict_values: false) #=> "" (empty string)
|
23
|
-
# to_redis(true, strict_values: false) #=> "true"
|
20
|
+
# to_redis(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
|
24
21
|
#
|
25
|
-
# @example Without a class option
|
26
|
-
# to_redis(123) #=> "123"
|
22
|
+
# @example Without a class option
|
23
|
+
# to_redis(123) #=> "123"
|
27
24
|
# to_redis("hello") #=> "hello"
|
28
|
-
# to_redis(nil) # raises an exception
|
29
|
-
# to_redis(true) # raises an exception
|
30
25
|
#
|
31
|
-
# @raise [Familia::HighRiskFactor]
|
26
|
+
# @raise [Familia::HighRiskFactor] If serialization fails under strict
|
27
|
+
# mode.
|
32
28
|
#
|
33
29
|
def to_redis(val, strict_values = true)
|
34
|
-
|
30
|
+
prepared = nil
|
35
31
|
|
36
32
|
Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
|
37
33
|
|
38
34
|
if opts[:class]
|
39
|
-
|
40
|
-
Familia.ld " from opts[class] <#{opts[:class]}>: #{
|
35
|
+
prepared = Familia.distinguisher(opts[:class], strict_values)
|
36
|
+
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
|
41
37
|
end
|
42
38
|
|
43
|
-
if
|
39
|
+
if prepared.nil?
|
44
40
|
# Enforce strict values when no class option is specified
|
45
|
-
|
46
|
-
Familia.ld " from
|
41
|
+
prepared = Familia.distinguisher(val, true)
|
42
|
+
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
47
43
|
end
|
48
44
|
|
49
|
-
Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{
|
45
|
+
Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>", caller(1..1) if Familia.debug?
|
50
46
|
|
51
|
-
Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if
|
52
|
-
|
47
|
+
Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if prepared.nil?
|
48
|
+
prepared
|
53
49
|
end
|
54
50
|
|
51
|
+
# Deserializes multiple values from Redis, removing nil values.
|
52
|
+
#
|
53
|
+
# @param values [Array<String>] The values to deserialize.
|
54
|
+
# @return [Array<Object>] Deserialized objects, with nil values removed.
|
55
|
+
#
|
56
|
+
# @see #multi_from_redis_with_nil
|
57
|
+
#
|
55
58
|
def multi_from_redis(*values)
|
56
|
-
# Avoid using compact! here. Using compact! as the last expression in the
|
57
|
-
# can unintentionally return nil if no changes are made, which is
|
58
|
-
# Instead, use compact to ensure the method returns the
|
59
|
+
# Avoid using compact! here. Using compact! as the last expression in the
|
60
|
+
# method can unintentionally return nil if no changes are made, which is
|
61
|
+
# not desirable. Instead, use compact to ensure the method returns the
|
62
|
+
# expected value.
|
59
63
|
multi_from_redis_with_nil(*values).compact
|
60
64
|
end
|
61
65
|
|
66
|
+
# Deserializes multiple values from Redis, preserving nil values.
|
67
|
+
#
|
68
|
+
# @param values [Array<String>] The values to deserialize.
|
69
|
+
# @return [Array<Object, nil>] Deserialized objects, including nil values.
|
70
|
+
#
|
71
|
+
# @raise [Familia::Problem] If the specified class doesn't respond to the
|
72
|
+
# load method.
|
73
|
+
#
|
74
|
+
# @note This method attempts to deserialize each value using the specified
|
75
|
+
# class's load method. If deserialization fails for a value, it's
|
76
|
+
# replaced with nil.
|
77
|
+
#
|
62
78
|
# NOTE: `multi` in this method name refers to multiple values from
|
63
79
|
# redis and not the Redis server MULTI command.
|
80
|
+
#
|
64
81
|
def multi_from_redis_with_nil(*values)
|
65
82
|
Familia.ld "multi_from_redis: (#{@opts}) #{values}"
|
66
83
|
return [] if values.empty?
|
@@ -89,6 +106,20 @@ class Familia::RedisType
|
|
89
106
|
values
|
90
107
|
end
|
91
108
|
|
109
|
+
# Deserializes a single value from Redis.
|
110
|
+
#
|
111
|
+
# @param val [String, nil] The value to deserialize.
|
112
|
+
# @return [Object, nil] The deserialized object, the default value if
|
113
|
+
# val is nil, or nil if deserialization fails.
|
114
|
+
#
|
115
|
+
# @note If no class option is specified, the original value is
|
116
|
+
# returned unchanged.
|
117
|
+
#
|
118
|
+
# NOTE: Currently only the RedisType class uses this method. Horreum
|
119
|
+
# fields are a newer addition and don't support the full range of
|
120
|
+
# deserialization options that RedisType supports. It uses to_redis
|
121
|
+
# for serialization since everything becomes a string in Redis.
|
122
|
+
#
|
92
123
|
def from_redis(val)
|
93
124
|
return @opts[:default] if val.nil?
|
94
125
|
return val unless @opts[:class]
|
@@ -96,15 +127,6 @@ class Familia::RedisType
|
|
96
127
|
ret = multi_from_redis val
|
97
128
|
ret&.first # return the object or nil
|
98
129
|
end
|
99
|
-
|
100
|
-
def update_expiration(ttl = nil)
|
101
|
-
ttl ||= opts[:ttl]
|
102
|
-
return if ttl.to_i.zero? # nil will be zero
|
103
|
-
|
104
|
-
Familia.ld "#{rediskey} to #{ttl}"
|
105
|
-
expire ttl.to_i
|
106
|
-
end
|
107
|
-
|
108
130
|
end
|
109
131
|
|
110
132
|
end
|
data/lib/familia/redistype.rb
CHANGED
@@ -13,31 +13,29 @@ module Familia
|
|
13
13
|
# @abstract Subclass and implement Redis data type specific methods
|
14
14
|
class RedisType
|
15
15
|
include Familia::Base
|
16
|
+
extend Familia::Features
|
16
17
|
|
17
18
|
@registered_types = {}
|
18
19
|
@valid_options = %i[class parent ttl default db key redis]
|
19
20
|
@db = nil
|
20
|
-
|
21
|
+
|
22
|
+
feature :expiration
|
23
|
+
feature :quantization
|
21
24
|
|
22
25
|
class << self
|
23
26
|
attr_reader :registered_types, :valid_options
|
24
27
|
attr_accessor :parent
|
25
|
-
attr_writer :
|
28
|
+
attr_writer :db, :uri
|
26
29
|
|
27
30
|
# To be called inside every class that inherits RedisType
|
28
31
|
# +methname+ is the term used for the class and instance methods
|
29
32
|
# that are created for the given +klass+ (e.g. set, list, etc)
|
30
33
|
def register(klass, methname)
|
31
|
-
Familia.ld "[#{self}] Registering #{klass} as #{methname}"
|
34
|
+
Familia.ld "[#{self}] Registering #{klass} as #{methname.inspect}"
|
32
35
|
|
33
36
|
@registered_types[methname] = klass
|
34
37
|
end
|
35
38
|
|
36
|
-
def ttl(val = nil)
|
37
|
-
@ttl = val unless val.nil?
|
38
|
-
@ttl || parent&.ttl
|
39
|
-
end
|
40
|
-
|
41
39
|
def db(val = nil)
|
42
40
|
@db = val unless val.nil?
|
43
41
|
@db || parent&.db
|
@@ -49,8 +47,9 @@ module Familia
|
|
49
47
|
end
|
50
48
|
|
51
49
|
def inherited(obj)
|
50
|
+
Familia.trace :REDISTYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
|
52
51
|
obj.db = db
|
53
|
-
obj.ttl = ttl
|
52
|
+
obj.ttl = ttl # method added via Features::Expiration
|
54
53
|
obj.uri = uri
|
55
54
|
obj.parent = self
|
56
55
|
super(obj)
|
@@ -101,6 +100,12 @@ module Familia
|
|
101
100
|
@opts = opts || {}
|
102
101
|
@opts = RedisType.valid_keys_only(@opts)
|
103
102
|
|
103
|
+
# Apply the options to instance method setters of the same name
|
104
|
+
@opts.each do |k, v|
|
105
|
+
Familia.ld " [setting] #{k} #{v}"
|
106
|
+
send(:"#{k}=", v) if respond_to? :"#{k}="
|
107
|
+
end
|
108
|
+
|
104
109
|
init if respond_to? :init
|
105
110
|
end
|
106
111
|
|
@@ -110,10 +115,37 @@ module Familia
|
|
110
115
|
parent? ? parent.redis : Familia.redis(opts[:db])
|
111
116
|
end
|
112
117
|
|
113
|
-
# Produces the full
|
118
|
+
# Produces the full Redis key for this object.
|
119
|
+
#
|
120
|
+
# @return [String] The full Redis key.
|
121
|
+
#
|
122
|
+
# This method determines the appropriate Redis key based on the context of the RedisType object:
|
123
|
+
#
|
124
|
+
# 1. If a hardcoded key is set in the options, it returns that key.
|
125
|
+
# 2. For instance-level RedisType objects, it uses the parent instance's rediskey method.
|
126
|
+
# 3. For class-level RedisType objects, it uses the parent class's rediskey method.
|
127
|
+
# 4. For standalone RedisType objects, it uses the keystring as the full Redis key.
|
128
|
+
#
|
129
|
+
# For class-level RedisType objects (parent_class? == true):
|
130
|
+
# - The suffix is optional and used to differentiate between different types of objects.
|
131
|
+
# - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
|
132
|
+
# - If a nil suffix is explicitly passed, it won't appear in the resulting Redis key.
|
133
|
+
# - Passing nil as the suffix is how class-level RedisType objects are created without
|
134
|
+
# the global default 'object' suffix.
|
135
|
+
#
|
136
|
+
# @example Instance-level RedisType
|
137
|
+
# user_instance.some_redistype.rediskey # => "user:123:some_redistype"
|
138
|
+
#
|
139
|
+
# @example Class-level RedisType
|
140
|
+
# User.some_redistype.rediskey # => "user:some_redistype"
|
141
|
+
#
|
142
|
+
# @example Standalone RedisType
|
143
|
+
# RedisType.new("mykey").rediskey # => "mykey"
|
144
|
+
#
|
145
|
+
# @example Class-level RedisType with explicit nil suffix
|
146
|
+
# User.rediskey("123", nil) # => "user:123"
|
147
|
+
#
|
114
148
|
def rediskey
|
115
|
-
Familia.ld "[rediskey] #{keystring} for #{self.class} (#{opts})"
|
116
|
-
|
117
149
|
# Return the hardcoded key if it's set. This is useful for
|
118
150
|
# support legacy keys that aren't derived in the same way.
|
119
151
|
return opts[:key] if opts[:key]
|
@@ -128,7 +160,7 @@ module Familia
|
|
128
160
|
parent.rediskey(keystring, nil)
|
129
161
|
else
|
130
162
|
# This is a standalone RedisType object where it's keystring
|
131
|
-
# is the full key.
|
163
|
+
# is the full redis key.
|
132
164
|
keystring
|
133
165
|
end
|
134
166
|
end
|
@@ -153,10 +185,6 @@ module Familia
|
|
153
185
|
@opts[:parent]
|
154
186
|
end
|
155
187
|
|
156
|
-
def ttl
|
157
|
-
@opts[:ttl] || self.class.ttl
|
158
|
-
end
|
159
|
-
|
160
188
|
def db
|
161
189
|
@opts[:db] || self.class.db
|
162
190
|
end
|
data/lib/familia/settings.rb
CHANGED
@@ -5,7 +5,7 @@ module Familia
|
|
5
5
|
@delim = ':'
|
6
6
|
@prefix = nil
|
7
7
|
@suffix = :object
|
8
|
-
@ttl = nil
|
8
|
+
@ttl = 0 # see update_expiration. Zero is skip. nil is an exception.
|
9
9
|
@db = nil
|
10
10
|
|
11
11
|
module Settings
|
@@ -27,6 +27,16 @@ module Familia
|
|
27
27
|
@suffix
|
28
28
|
end
|
29
29
|
|
30
|
+
def ttl(v = nil)
|
31
|
+
@ttl = v unless v.nil?
|
32
|
+
@ttl
|
33
|
+
end
|
34
|
+
|
35
|
+
def db(v = nil)
|
36
|
+
@db = v unless v.nil?
|
37
|
+
@db
|
38
|
+
end
|
39
|
+
|
30
40
|
# We define this do-nothing method because it reads better
|
31
41
|
# than simply Familia.suffix in some contexts.
|
32
42
|
def default_suffix
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Tools
|
5
|
+
extend self
|
6
|
+
def move_keys(filter, source_uri, target_uri, &each_key)
|
7
|
+
raise "Source and target are the same (#{target_uri})" if target_uri == source_uri
|
8
|
+
|
9
|
+
Familia.connect target_uri
|
10
|
+
source_keys = Familia.redis(source_uri).keys(filter)
|
11
|
+
puts "Moving #{source_keys.size} keys from #{source_uri} to #{target_uri} (filter: #{filter})"
|
12
|
+
source_keys.each_with_index do |key, idx|
|
13
|
+
type = Familia.redis(source_uri).type key
|
14
|
+
ttl = Familia.redis(source_uri).ttl key
|
15
|
+
if source_uri.host == target_uri.host && source_uri.port == target_uri.port
|
16
|
+
Familia.redis(source_uri).move key, target_uri.db
|
17
|
+
else
|
18
|
+
case type
|
19
|
+
when 'string'
|
20
|
+
Familia.redis(source_uri).get key
|
21
|
+
when 'list'
|
22
|
+
Familia.redis(source_uri).lrange key, 0, -1
|
23
|
+
when 'set'
|
24
|
+
Familia.redis(source_uri).smembers key
|
25
|
+
else
|
26
|
+
raise Familia::Problem, "unknown key type: #{type}"
|
27
|
+
end
|
28
|
+
raise 'Not implemented'
|
29
|
+
end
|
30
|
+
yield(idx, type, key, ttl) unless each_key.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Use the return value from each_key as the new key name
|
35
|
+
def rename(filter, source_uri, target_uri = nil, &each_key)
|
36
|
+
target_uri ||= source_uri
|
37
|
+
move_keys filter, source_uri, target_uri if source_uri != target_uri
|
38
|
+
source_keys = Familia.redis(source_uri).keys(filter)
|
39
|
+
puts "Renaming #{source_keys.size} keys from #{source_uri} (filter: #{filter})"
|
40
|
+
source_keys.each_with_index do |key, idx|
|
41
|
+
Familia.trace :RENAME1, Familia.redis(source_uri), "#{key}", ''
|
42
|
+
type = Familia.redis(source_uri).type key
|
43
|
+
ttl = Familia.redis(source_uri).ttl key
|
44
|
+
newkey = yield(idx, type, key, ttl) unless each_key.nil?
|
45
|
+
Familia.trace :RENAME2, Familia.redis(source_uri), "#{key} -> #{newkey}", caller(1..1).first
|
46
|
+
Familia.redis(source_uri).renamenx key, newkey
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_any(keyname, uri = nil)
|
51
|
+
type = Familia.redis(uri).type keyname
|
52
|
+
case type
|
53
|
+
when 'string'
|
54
|
+
Familia.redis(uri).get keyname
|
55
|
+
when 'list'
|
56
|
+
Familia.redis(uri).lrange(keyname, 0, -1) || []
|
57
|
+
when 'set'
|
58
|
+
Familia.redis(uri).smembers(keyname) || []
|
59
|
+
when 'zset'
|
60
|
+
Familia.redis(uri).zrange(keyname, 0, -1) || []
|
61
|
+
when 'hash'
|
62
|
+
Familia.redis(uri).hgetall(keyname) || {}
|
63
|
+
else
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -49,12 +49,12 @@ module Familia
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def values
|
52
|
-
|
53
|
-
multi_from_redis(*
|
52
|
+
elements = redis.hvals(rediskey)
|
53
|
+
multi_from_redis(*elements)
|
54
54
|
end
|
55
55
|
|
56
56
|
def hgetall
|
57
|
-
# TODO: Use from_redis. Also
|
57
|
+
# TODO: Use from_redis. Also alias `all` is confusing with
|
58
58
|
# Onetime::Customer.all which returns all customers.
|
59
59
|
redis.hgetall rediskey
|
60
60
|
end
|
@@ -98,8 +98,8 @@ module Familia
|
|
98
98
|
alias merge! update
|
99
99
|
|
100
100
|
def values_at *fields
|
101
|
-
|
102
|
-
multi_from_redis(*
|
101
|
+
elements = redis.hmget(rediskey, *fields.flatten.compact)
|
102
|
+
multi_from_redis(*elements)
|
103
103
|
end
|
104
104
|
|
105
105
|
Familia::RedisType.register self, :hash # legacy, deprecated
|
data/lib/familia/types/list.rb
CHANGED
@@ -75,8 +75,8 @@ module Familia
|
|
75
75
|
|
76
76
|
def members(count = -1, opts = {})
|
77
77
|
count -= 1 if count.positive?
|
78
|
-
|
79
|
-
multi_from_redis(*
|
78
|
+
elements = membersraw count, opts
|
79
|
+
multi_from_redis(*elements)
|
80
80
|
end
|
81
81
|
alias to_a members
|
82
82
|
alias all members
|
@@ -88,8 +88,8 @@ module Familia
|
|
88
88
|
|
89
89
|
def revmembers(count = -1, opts = {})
|
90
90
|
count -= 1 if count.positive?
|
91
|
-
|
92
|
-
multi_from_redis(*
|
91
|
+
elements = revmembersraw count, opts
|
92
|
+
multi_from_redis(*elements)
|
93
93
|
end
|
94
94
|
|
95
95
|
def revmembersraw(count = -1, opts = {})
|
@@ -131,8 +131,8 @@ module Familia
|
|
131
131
|
|
132
132
|
def range(sidx, eidx, opts = {})
|
133
133
|
echo :range, caller(1..1).first if Familia.debug
|
134
|
-
|
135
|
-
multi_from_redis(*
|
134
|
+
elements = rangeraw(sidx, eidx, opts)
|
135
|
+
multi_from_redis(*elements)
|
136
136
|
end
|
137
137
|
|
138
138
|
def rangeraw(sidx, eidx, opts = {})
|
@@ -148,8 +148,8 @@ module Familia
|
|
148
148
|
|
149
149
|
def revrange(sidx, eidx, opts = {})
|
150
150
|
echo :revrange, caller(1..1).first if Familia.debug
|
151
|
-
|
152
|
-
multi_from_redis(*
|
151
|
+
elements = revrangeraw(sidx, eidx, opts)
|
152
|
+
multi_from_redis(*elements)
|
153
153
|
end
|
154
154
|
|
155
155
|
def revrangeraw(sidx, eidx, opts = {})
|
@@ -159,8 +159,8 @@ module Familia
|
|
159
159
|
# e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]
|
160
160
|
def rangebyscore(sscore, escore, opts = {})
|
161
161
|
echo :rangebyscore, caller(1..1).first if Familia.debug
|
162
|
-
|
163
|
-
multi_from_redis(*
|
162
|
+
elements = rangebyscoreraw(sscore, escore, opts)
|
163
|
+
multi_from_redis(*elements)
|
164
164
|
end
|
165
165
|
|
166
166
|
def rangebyscoreraw(sscore, escore, opts = {})
|
@@ -171,8 +171,8 @@ module Familia
|
|
171
171
|
# e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]
|
172
172
|
def revrangebyscore(sscore, escore, opts = {})
|
173
173
|
echo :revrangebyscore, caller(1..1).first if Familia.debug
|
174
|
-
|
175
|
-
multi_from_redis(*
|
174
|
+
elements = revrangebyscoreraw(sscore, escore, opts)
|
175
|
+
multi_from_redis(*elements)
|
176
176
|
end
|
177
177
|
|
178
178
|
def revrangebyscoreraw(sscore, escore, opts = {})
|
data/lib/familia/types/string.rb
CHANGED