Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
This gem creates a thin shell to encapsulate primitive literal types such as integers, floats and symbols.
There are a family of wrappers which mimic the behavior of what they contain.
Primitive types have several drawbacks: no constructor to call, can't create instance variables, and can't create singleton methods.
There is some utility in wrapping a primitive type. You can simulate a call by reference for example.
You can also simulate mutability, and pointers.
Some wrappers are dedicated to holding a single type while others may hold a family of types such as the Number
wrapper.
What is interesting to note is Number objects do not derive from Numeric
, but instead derive from Value
(the wrapper base class);
but at the same time, Number
objects mimic the methods of Fixnum
, Complex
, Float
, etc.
Many of the wrappers can be used in an expression without having to call an access method.
There are also new types: Bool
which wraps true,false
and Property
which wraps Hash
types.
The Property
object auto-methodizes the key names of the Hash
.
Add this line to your application's Gemfile:
gem 'primitive_wrapper'
And then execute:
$ bundle
Or install it yourself as:
$ gem install primitive_wrapper
Or try it out inside irb:
$ require "primitive_wrapper"
There are 12 wrapper classes that hold these pesky primitive literal objects. Unlike other class objects, these primitives have no constructor and attempt to look like the bottom turtle literals used in compiled languages like C/C++. Such primitive literals are completely immutable. The wrappers used in this gem transform these primitive types into mutable objects. In fact, these objects cannot be frozen as that would defeat their purpose. The freeze method on these wrapper classes is disabled. The wrapped literal now behaves like a true ruby object such as Array, or Hash. See the example below:
five = Integer.new(5) # ERROR :: undefined method `new' for Integer:Class
five = Fixnum.new(5) # ERROR :: undefined method `new' for Fixnum:Class
five = 5 # ok, this one works!
def five.tripple
self + self + self
end # ERROR :: TypeError: can't define singleton
require "primitive_wrapper"
five = Int.new(5) # we now have a wrapped Integer object
five + 5 + five # 15 ... wrapper has the methods of an Integer object and can be used in an expression
def five.tripple
self + self + self
end
five.tripple # 15
five.kind_of? Integer # false
five.kind_of? Value # true
5.kind_of? Integer # true
5.kind_of? Value # false
five.type_of? Integer # true
5.type_of? Integer # true
As we can see from the above code, the Int class does not derive from Integer or anything that would appear numeric.
Our Int
class is simply a container which happens to have the methods of Integer
but does not derive itself from Integer
.
All of these container objects derive from the Value class.
Also note the new method #type_of
which allows us test the inner-most entity.
There are six instance methods added to the base Object class necessary to implement this system.
The most important method is the #to_wrapper
method.
When an instance variable calls this method, one of the wrapper containers will generate a new instance.
The one that gets picked is the one best suited for usability.
We also have a test method called #wrapped?
to see if we are dealing with the original entity or its thinly wrapped sibling.
Another method named #prim_value
will return the inner container value or simply return self
if it is not a container derived from the Value
class.
This is mostly used internally by the container objects. Now that we have wrapped versions of the base elemental objects,
we need a type checking method that can work on both wrapped and unwrapped primitives: called #type_of?
;
this works like #kind_of?
but instead works on the inner-most element (if Value
derived type).
Also, #type
gets the effective class and evaluates as #prim_value.class
.
Also needed is a method that can duplicate a primitive or its wrapped version; this is called #pw_copy
which works like dup.
Note that you can't call #dup
on an Integer without raising an exception.
Below shows which objects get mapped to which containers:
nil => Bit
true, false => Bool
Integer => Int
Float => FloatW
Complex => Number
Rational => Number
Symbol => SymbolW
Hash => Property
String => Datum
Array => XArray
Range => XRange
Rational => Fraction
Some examples are as follows:
n = nil.to_wrapper # n.class == Bit
b = true.to_wrapper # b.class == Bool
y = 13.to_wrapper # y.class == Int
t = 3e-9.to_wrapper # t.class == FloatW
z = (1+6i).to_wrapper # z.class == Number
str = "yo!".to_wrapper # str.class == Datum
me = :me.to_wrapper # me.class == SymbolW
prop = {}.to_wrapper # prop.class == Property
ary = [1,2].to_wrapper # ary.class == XArray ... X for extended functionality
ran = (3..0).to_wrapper # ran.class == XRange
aw = /[az]/.to_wrapper # aw.class == Value ... everything else goes inside the Value container
The next sections will detail which objects are allowed to be contained within each container objects.
This is the base class for all the containers created by this gem. Any object or primitive type can be held in this container.
To access the contents, simply call #val
, #prim_val
, #unwrap
, or for non-numeric types use the tilde ~
prefix operator.
Note that only Value
derived objects can call #val
while every object can call #prim_val
.
You can also replace the contents with the #replace
method or the #val=
method.
The Value
container has no restrictions on what it can hold. Other wrappers will raise an exception if the wrong type is added.
The comparison binary operators ==
, and !=
are used to compare equality and inequality; this is done on the inner contained element.
The value container can only contain a single object; it is like an Array object that is only allowed to hold one item.
See the example code below:
require 'PrimitiveWrapper'
# simulation of a pointer
a = Value.new(15)
b = a
b.val # 15
a.val # 15
a.val = 2 # assign new inner value
b.val # a == 2 ... a,b point to the same data
# simulation of pass by reference
def pass_by_reference(a, ref)
ref.val += 15 + a
return :yes
end
t = pass_by_reference(1, bjc = Value.new(10)) # t==:yes, bjc.val==26
The Value class has the following instance methods:
:val, :prim_val, :unwrap, :~ # Returns the internal data object (~ overriden by Int)
:val=, :replace # Sets the internal data object
:valid_type(inst) # Returns true if `inst` variable is compatible to wrapper object
# Child classes override this method
:freeze # Does nothing ... defined to prevent Object#freeze from being called
:== # Compares equality of the inner elements
:!= # Compares inequality of the inner elements
:inspect # "(Value)==>data" where Value is the class name of the Value derived object, and data is #val.inspect
:to_s # returns #to_s on contained entity
:to_wrapper # returns self ... prevents wrapping a wrapped entity
:wrapped? # also on object, tests for wrapped behavior
:type_of? # also added to object, tests innermost entity or self if primitive
:type # returns the class of the wrapped primitive or self if primitive
:inspect # string representing wrapped primitive
The Bool container is allowed to contain only true
or false
primitive literals.
The container adds three binary operators: &
, |
, ^
and the prefix operator !
.
Equality operators are inherited from the base class Value
.
When an operator is called, a new Bool type is created.
Most of the other wrapper classes will return a standard type in order to make expressions less promblematic.
You can mix Bool types with true
and false
in an expression, but you must make sure the Bool type comes first or unexpected results may occur.
As with all wrapped containers, left-to-right expressions are owned by the left object. If this cannot be avoided, then calling #val
will solve the problem.
This class is derived from Value. See the example below:
aaa = Bool.new(true)
bbb = Bool.new(false)
aaa & !bbb | bbb & !aaa == aaa ^ bbb # definition of XOR
t = aaa ^ true # creates a new Bool type with value of false
t = bbb ^ true # creates a new Bool type with value of true
t = true ^ aaa # incorrect order returns false ... not a Bool type
t = true ^ bbb # incorrect order returns false ... not a Bool type
t = true ^ "?" # false ... any object will return false except nil and false
t = true ^ 0 # false
t = true ^ false # true ... this is why Bool must come first when mixing Bool with true or false
t = true ^ nil # true ... one of the only two primitive types that will provide a true result
# other methods
.to_i # returns 0 if false, 1 if true
.to_int # returns an Int object wrapper filled with either 0 or 1
.val # returns true or false
Note that the ~
prefix operator returns the primitive value.
The operator was also added to: Symbol, TrueClass, FalseClass, and NilClass which simply return self;
these classes previously did not define this operator. Only Integer and Int derived objects define this as being a one's complement operator.
This ~
prefix operator permits a terse way of getting to the primitive and can be used by Value derived objects or primitives described above.
This is necessary when using Bool types in conditional statements. Alternatively, you can use the #val method.
The Bit
container is derived from Bool, but overrides the operators to return primitive values.
The Bit container also allows nil
as a valid container entity. Bit objects are therefore thinner than Bool objects
and are most likely easier to work in expressions. There are also FixedBit
derived classes called: Null
, TrueW
, FalseW
.
These are essentially Bit
objects restricted to a single type. See the example below:
a = Bit.new(nil)
aa = Null.new # a==aa ... ~a => nil ~aa => nil ~nil => nil
b = Bit.new(true)
bb = TrueW.new # b==bb ... ~b => true ~bb => true ~true => true
c = Bit.new(false)
cc = FalseW.new # c==cc ... ~c => false ~bb => false ~false => false
bb ^ b # false
y = ~b ? 3:4 # y == 3 ... ~b shortcut for b.val
FixedBit.new # *** RuntimeError Exception: FixedBit cannot create instance
aa.val = nil # *** RuntimeError Exception: can't assign primitive type
The Int
container looks like a duck, quacks and waddles like a duck, swims like a duck, but it ain't a duck! The ancestor chain of Int
only shares Value
and does not include Numeric
. With a little meta-magic, the Int
class mimics the methods of Integer
so that it can be directly used
within an expression or without having to call #val
to get the contained entity. Note that ~
does not get the contained entity, but instead returns the one's complement.
You can have your cake and eat it too by using two tildes ~~
. This undoes the one's complement and results in the primitive value.
The #to_int
method on Int
returns self, while #to_i
accesses the primitive value.
Most of the time if Integer
does not recognize something it calls coerce on the foreign object (which we grabbed from Integer).
This means that most expressions should work in whatever order you wish: if not, you will have to convert the Int object into an Integer object.
Pretty much all expressions with Int will create a primitive Integer.
We also have new methods #inc
and #dec
which work like the C/C++
post increment operators.
Prefix version methods are #pre_inc
and #pre_dec
.
See the example below:
aaa = 100.to_wrapper # method added to Object
bbb = Int.new(200) # standard constructor
ttt = aaa + bbb # ttt==300 ... ttt.class == Fixnum
~aaa # -101
~~aaa # 100
aaa.to_int # returns self, an Int object
aaa.to_i # returns 100
100.to_i # returns 100
100.to_int # returns 100
3 + aaa # returns 103
aaa + 3 # returns 103
The FloatW
container wraps Float
types and possesses the thin wrapper similar to how Int
works.
This can also be directly used within an expression. Only the Float
derived objects may be placed inside this container.
The wrapper is so thin, you might think that you are actually a Float
type.
See the example below:
aaa = 3.1415926.to_wrapper
bbb = FloatW.new(3e0)
aaa.to_i # 3
aaa.to_int #
aaa ** bbb # 31.006275093569673
aaa.to_i # 3
aaa.to_int # (Int==>3)
The Number
container object can contain any object that is derived from Numeric
.
This includes Float
, Fixnum
, Bignum
, Complex
, Rational
, or even something that you create yourself so long as it derives from Numeric
.
The class generator for Number
grabs method names from all the standard library of Numeric derived classes.
This means that there is a possibility that the contained entity will not support some of the method calls.
This is no different than calling an undefined method. Int
, FloatW
, Number
, Datum
all derive from ValueAdd
which is a child of Value
.
You can't create an instance of ValueAdd
, but it is used to store common methods as well as some class construction methods.
These will be described later on. This will come handy if you have a custom Numeric
class and wish to import some of the methods defined there.
This works just like the other specific number wrappers previously described.
The Datum
container holds everything a Number
can hold and allows String
data to be added. This represents things that a human can type into a form as a general data thing.
The methods of String
are also added. Errors will occur if you try to call string methods on a Datum
that is holding a number and visa versa.
The Fraction
container holds a Rational
primitive. All the methods of Rational
are mimicked with some important modifications.
All operators of Fraction
return a Fraction
type. Note that Rational
types when operated with Float
types will return Float
types.
The mutating methods of the Value
base class as well as some new mutating methods have also been added.
The constructor can take 1 to 3 parameters. The main idea behind this wrapper is to support mixed fractions as well as proper fractions.
See below:
# Construction: mixed fractions using 3 parameters
frac = Fraction.new(3,4,5) # frac.inspect == "(Fraction==>(19/5))" ... frac.to_s == "(3 4/5)" ... frac.to_s(false) == "3 4/5"
frac = Fraction(5,6,7) # shorter function call ... (Fraction==>(41/7))
frac = Fraction(-5,-6,7) # negative ... (Fraction==>(-41/7))
# note: always negate first two parameters to define negative fractions
# Construction: proper fractions using 2 parameters
frac = Fraction(27,7) # frac.inspect == "(Fraction==>(27/7))" ... frac.to_s false == "3 6/7"
# Construction: using float with/without approximation
frac = Fraction(Math::PI, 0.1) # fract.inspect == "(Fraction==>(22/7))" ... second parameter => percent error
frac = Fraction(Math::PI, 0.0001) # fract.inspect == "(Fraction==>(355/113))"
frac = Fraction(Math::PI) # fract.inspect == "(Fraction==>(884279719003555/281474976710656))"
frac = Math::PI.to_fraction(0.0001) # (Fraction==>(355/113))
frac = Math::PI.to_fraction # (Fraction==>(884279719003555/281474976710656))
# note: Rational does not have approximate construction
# Construction: using float with target denominator
frac = Fraction(Math::PI, 113) # (Fraction==>(355/113))
frac = Fraction(Math::PI, 7) # (Fraction==>(22/7))
# mutating methods:
frac = Math::PI.to_fraction(0.0001)
frac.negate! # make negative
frac.to_s(false) # "-3 -16/113" ... note: true add parenthesis, false removes parenthesis
frac.abs! # make positive
frac.to_s(false) # "3 16/113"
frac.to_r.to_s # "355/113" ... convert to rational, then to string for proper fraction
frac *= 113 # (Fraction==>(355/1)) ... operators return type of Fraction
frac.to_s # 355 ... no parenthesis if whole number
The SymbolW
class container holds a Symbol
primitive. It derives directly from Value
.
Although rarely used, Symbol
types do have some immutable methods that have some string-like behavior.
The SymbolW
class implements most of these, and adds some mutating methods as well.
This makes the SymolW object appear to be completely mutable; however, the inner element remains immutable.
See below:
# non-mutating methods taken from Symbol:
# :<, :<=, :>, :>=, :[], :between?, :capitalize, :downcase, :empty?, :length, :next, :size, :succ, :pred, :swapcase, :upcase
# non-mutating methods taken from String:
# :append, :include?, :prepend ... actually append renamed from :<<
# mutating methods of SymbolW
# :[]=, :append!, :capitalize!, :downcase!, :next!, :prepend!, :replace, :succ!, :pred!, :swapcase!, :upcase!
# construction:
wsym = :my_symbol.to_wrapper
tsym = SymbolW.new(:my_other_symbol)
# example:
wsym[1] = :e # wsym == (SymbolW==>:me_symbol)
The Property
class container holds a Hash object.
You can also place a Hash
object inside the Value
container, but there are no exposed methods there.
You can initialize the property class with a Hash instance, or start with a blank slate.
What is cool about the Property object is the internal hash keys become setters and getters on the instance so long as they obey the rules.
Additionally the internal hash is the original hash and not a copy;
so if the contained hash object is changed outside of the Property container,
new access methods will automatically be generated.
Hash keys that comprise upper and lowercase characters, underscore, and digits (with the first character not a number)
will become methods on the Property
instance.
Note that these names cannot compete with existing instance method names of Object or Property including private methods.
Nested hierarchical names are not added: only the first-level names.
Non-compliant names are still added to the internal Hash
instance; they just don't have the corresponding method names.
Another bonus is we have an unmolested Hash
unlike other competing gems that trash the Hash
with property getters and setters.
Most Hash methods except for :[], :[]=
have not been implemented in order to allow for more property names.
You can quickly get to the internal Hash by using the ~
tilde prefix operator however.
See the code below:
abc = Property.new
xyz = Property.new {:fred => "freddy", :george => "georgey"}
abc.seven = 7
abc.property? :seven # true
abc.seven # 7
abc.val # {:seven => 7}
abc.to_s=15 # this won't create a property because :to_s is reserved
abc.property? :to_s # false
abc.deferred? :to_s # true ... internal hash owns this, but no property is created
abc.to_s # "{:seven=>7, :to_s=>15}"
abc.defined_properties! # [:seven]
xyz.defined_properties! # [:fred, :george]
abc.deferred_properties! # [:to_s]
xyz.deferred_properties! # []
abc.keys # NoMethodError Exception: undefined method `keys' for (Property==>{...}):Property
(~abc).keys # [:seven, :to_s] ... Note ~abc.keys does not work as this is interpreted as: ~(abc.keys)
abc.val.keys # [:seven, :to_s] ... probably best syntax
hash = ~abc # grab internal Hash object
hash[:cool] = "yes!"
abc.cool # "yes!" ... wrapper behavior looks pointer-ish
abc[:works2] = "yep!"
abc.works2 # "yep!" ... wrapper supports array access
abc[:seven] # 7
abc.sam = {}
abc.defined_properties! # [:cool, :sam, :seven, :works2]
abc.val = {}
abc.defined_properties! # [] ... you just zapped the entire hash after abc.val = {}
# Property Instance methods
# :~ :unwrap :val :prim_value ... returns internal hash
# :val= :replace ... assigns new hash to wrapper
# :[] :[]= ... access internal hash element
# :deferred? key ... true if key is in internal hash but will not create a property access variable
# :property? key ... true if key is registered as a property access variable
# :split! ... returns an array of two Hash instances with [valid_property_hash, deferred_hash]
# :rekey! ... rekeys property and deferred keys ... called internally
# :valid_type inst_var ... used internally for #replace or #val= ... true if Hash or Property instance
# :method_missing ... used internally to simulate property methods
# :import_hash! ext_hash ... merge foreign hash into self
# :ensure_valid ... used internally ... raises error on type mismatch from :valid_type
# :deferred_properties! ... returns list of keys that do not have property methods
# :defined_properties! ... returns list of keys that have property methods
# :define_properties! [] , dflt ... assignes default properties to a list of property keys
# Property Class methods
# ::good_candidate_name? name ... test to see if name is formatted corectly, and is of type Hash
# ::good_key? name ... true if properly formatted and not a reserved word
# ::bad_key? name ... true if wrong type, or reserved word, or not formatted correctly
# ::reserve_property_names! [] ... adds additional reserved words
While inheritance could have accomplished similar goals, this gem is all about wrapped objects.
Mutability would not be possible with an inherited version. Note that Range objects are not completely immutable;
range_instance.first.upcase!
will modify the range that is defined with strings.
The XRange
object redefines much of the behavior of Range
with regard to ranges that descend rather than ascend.
Note that descending Range objects will return false on methods such as #include?
and #cover?
and will not enumerate with #each
.
There is great utility in having a functional downward range that actually does what would naturally be expected.
This gem includes the gem pred
which makes reverse behavior possible.
Integers and Strings use the method #succ
within the classic Range
to get the next sequence value.
The method #pred
is added to to Integers and Strings to get the inverse sequence value.
The XRange
container captures the methods of Range
but redefines most of the methods to define reverse behavior.
Below is a list of what has changed:
# :reverse Range, XRange ... reverses first and last, creates a new object of same type as caller
# :reverse! XRange ... self-modified version of :reverse
# :reverse? Range, XRange ... true if descending
# :simplify Range, XRange ... converts if possible excluded to non-excluded range with same behavior
# :simplefy! XRange ... self-modified version of :simplify
# :reorder Range, XRange ... creates ascending Range or XRange
# :reorder! XRange ... self-modified version of :reorder
# :to_range Range, XRange ... accesses inner element or returns self if Range
# :to_xr Range, XRange ... creates an XRange object or returns self if XRange
# :size, :count XRange ... redefined from nil to actual size if descending were reversed
# :include? XRange ... redefined from false to behave as if reversed
# :cover? XRange ... redefined from false to behave as if reversed
# :member? XRange ... redefined from false to behave as if reversed
# :to_a XRange ... redefined from empty [] to list ordered sequence
# :max, :min XRange ... redefined from nil to return maximum/minimum range values
# :eql? :== XRange ... compares using contained entity
# :step(n) XRange ... redefined to work with descending sequences
# :reverse_step(n) XRange ... reverses sequence before stepping
# :each XRange ... redefined to work with descending sequences
# :reverse_each XRange ... reverse version of :each
# :re_range(ng) XRange ... creates new range converting negative start, end to positive given size==ng
# used by XArray access operators that use descending ranges.
This creates a wrapped Array
with behavior that enhances the access operators of Array
;
additionally a few new methods were added to the wrapped version as well as the original Array
class.
XArray
mimics the behavior of Array
but redefines :[]
and :[]=
to include lists which comprise indexes, ranges, and arrays of indexes.
There is also behavior that defines many-to-one, one-to-many, and many-to-many operations. Note that one-to-one behavior has not changed.
An example best explains how this works:
ary = [2,3,5,7,11,13,17,19,23,29,31,37,41].to_xa
tst = ary[5..0,10,-1] # tst == [13, 11, 7, 5, 3, 2, 31, 41] ... note reverse range
# many-to-many
ary[4,5,1] = "a", "b", "c", "d" # ary == [2, "c", 5, 7, "a", "b", 17, 19, 23, 29, 31, 37, 41]
# note above, unused ignored
# one-to-many
ary[8..-1] = :s # ary == [2, 3, 5, 7, 11, 13, 17, 19, :s, :s, :s, :s, :s]
# note: ary.prim_value[8..-1] = :s returns this: [2, 3, 5, 7, 11, 13, 17, 19, :s] ... which is useless
# many-to-one
ary[-1] = 1,2,3 # ary == [2, "c", 5, 7, "a", "b", 17, 19, :s, :s, :s, :s, [1, 2, 3]]
ary[-2] = [:a, :b] # ary == [2, "c", 5, 7, "a", "b", 17, 19, :s, :s, :s, [:a, :b], [1, 2, 3]]
The are a few more upgrades as well as follows:
# :to_xa Array, XArray ... Converts Array to XArray unless already an XArray
# :include? XArray ... Updated to support a list of items all of which must be included to return true
# :include_any? XArray ... true if any of the list of itmes are included
# :delete_at XArray ... enhanced version can delete a list of indexes or ranges or arrays of indexes
# returns what was deleted in the order of the list
# :ssort, :ssort! XArray ... sorts with each element called with method :to_s
# :isort, :isort! XArray ... sorts with each element called with method :inspect
Things should work out of the box, until they don't. This section will detail what customization is possible with what container. First, Property has one method to reserve allowed property key names. This is done as follows:
Property.reserve_property_names! [:Fred, :Wilma, :BamBam, :Barney, :Betty, :Pebbles]
bedrock = Property.new
bedrock.Fred = "Yaba Daba Doo!"
bedrock.property? :Fred # false
bedrock.deferred? :Fred # true
bedrock[:Fred] # "Yaba Daba Doo!"
bedrock.Fred # *** NoMethodError Exception: undefined method `Fred' for (Property==>{...}):Property
bedrock.Test = "pass"
bedrock.Test # "pass"
The containers Int
, FloatW
, Number
, and Datum
all derive from ValueAdd
which has two class methods.
The ValueAdd
class derives from Value
, but cannot create an instance. Its purpose is to generate methods
that belong somewhere else. If we peer into the code we see this:
class ValueAdd < Value
def valid_type(prm) # must override
false
end
def self.bestow_methods(*args)
args = args.first if args.first.kind_of? Array
args.each do |meth|
define_method meth do |*args, &block|
@value.send(meth, *args, &block)
end
end
end
def self.capture_base_methods(type, except=Object)
add_me = type.instance_methods - except.instance_methods - Value.instance_methods - [:singleton_method_added]
bestow_methods add_me
end
end
If we call Int.capture_base_methods(Fixnum)
, all of the methods that a Fixnum can do is bestowed to the Int class.
If you define a new method to Integer called :do_my_math_thing
, you can teach Int to do it by calling Int.bestow_methods(:do_my_math_thing)
.
You should also add it to Number
and Datum
. Let's say you have a really cool Matrix class that you would like to teach the Number wrapper.
Here is how you would do it:
class Number
capture_base_methods(Matrix, self)
end
Some of the most extensive customizations involve the SymbolW wrapper class. Only a small subset of String
methods were added to this wrapper.
They were chosen to complement the non-mutating String
like functions already available to Symbols.
For example, #upcase!
was added to complement #upcase
. Note that the Symbol class does not and cannot have a mutating method.
There are several Class methods on SymbolW
that capture Symbol
and String
methods in several configurable ways.
If we look under the hood at the source code we will see how they got defined by example:
# ClassW class methods:
#
# -- single defines
# ::bestow_symbol_method(meth_def_name, meth_call_name, mutate_self=false)
# ::bestow_string_non_mutate_method(meth_def_name, meth_call_name, return_type = SymbolW)
# ::bestow_string_mutate_method(meth_def_name, meth_call_name)
#
# -- group defines
# ::bestow_string_mutate_methods(meths)
# ::bestow_string_non_mutate_methods(meths, return_type = SymbolW)
# ::bestow_symbol_methods(meths, mutate_self=false)
#
The group defines are used when you don't need to rename a method and wish to install a bunch of them.
As an example SymbolW.bestow_symbol_methods [:next, :succ, :[], :length, :size, :upcase, :downcase, ...]
was called to install the standard set of methods that exist on Symbol
.
The last parameter when set to true will mutate the inner Symbol which means that it gets replaced with the result of the method.
Some methods that do not return a string like #length
should not be used as a source for mutating methods.
Passing true
as the second parameter will auto-bang the name turning #upcase
into #upcase!
. The singular version of the method gives you more control allowing you to completely rename the method.
The number of Symbol methods is fairly small when compared to the number of String methods.
It was intentional to leave most of these open as it there are many ways to transform these methods. The self mutating version is the most straight forward as we don't need to worry about the return type.
See the code below to see how it was used:
SymbolW.bestow_string_mutate_method(:prepend!, :prepend)
SymbolW.bestow_string_mutate_method(:append!, :<<)
SymbolW.bestow_string_mutate_method(:[]=, :[]=)
Looking at the code, we see that the #append!
method is a renamed version of :<<
. What is interesting to note is that both in the String
and the Symbol
classes #append
is not found.
Only string implements :<<
which is a binary operator that does not mutate. Because we are bestowing this self-mutating method to SymbolW
, we don't need to consider what the return type is as self
is the only logical choice.
The non-mutating version must choose what kind of type is returned. We have four choices for return types: (1) force a conversion to SymbolW
, (2) return Symbol
, (3) return String
, or (4) return whatever the string method would otherwise return.
We would choose the 4th version for any non-string return method such as #match
. To select this we pass nil
to the return type which says let the string method decide.
Now we are left with the head-scratcher on how to decide which is the best return type to give to a string method bestowed upon a wrapped symbol thing.
This can be mostly a matter of taste, but we can choose custom names that convey this information. Now if you have an extension library such as used by the gstring
gem, you have an even larger list to choose from.
So it is left to you to decide.
#delete_at
bugsAfter checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
I need to control this for the time being. You are welcome to shoot me an EMAIL if you have any issues or suggestions.
The gem is available as open source under the terms of the MIT License.
FAQs
Unknown package
We found that primitive_wrapper demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.