🤷 kind
A development toolkit for Ruby with several small/cohesive abstractions to empower your development workflow - It's totally free of dependencies.
Motivation:
This project was born to help me with a simple task, create a light and fast type checker (at runtime) for Ruby. The initial idea was to have something to raise an exception when a method or function (procs) received a wrong input.
But through time it was natural the addition of more features to improve the development workflow, like monads (Kind::Maybe
, Kind::Either
/ Kind::Result
), enums (Kind::Enum
), immutable objects (Kind::ImmutableAttributes
), type validation via ActiveModel::Validation, and several abstractions to help the implementation of business logic (Kind::Functional::Steps
, Kind::Functional::Action
, Kind::Action
).
So, I invite you to check out these features to see how they could be useful for you. Enjoy!
Documentation
Table of Contents
Compatibility
kind | branch | ruby | activemodel |
---|
unreleased | main | >= 2.1.0, <= 3.0.0 | >= 3.2, < 7.0 |
5.10.0 | v5.x | >= 2.1.0, <= 3.0.0 | >= 3.2, < 7.0 |
4.1.0 | v4.x | >= 2.2.0, <= 3.0.0 | >= 3.2, < 7.0 |
3.1.0 | v3.x | >= 2.2.0, <= 2.7 | >= 3.2, < 7.0 |
2.3.0 | v2.x | >= 2.2.0, <= 2.7 | >= 3.2, <= 6.0 |
1.9.0 | v1.x | >= 2.2.0, <= 2.7 | >= 3.2, <= 6.0 |
Note: The activemodel is an optional dependency, it is related with the Kind::Validator.
Installation
Add this line to your application's Gemfile:
gem 'kind'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install kind
⬆️ Back to Top
Usage
With this gem you can add some kind of type checking at runtime. e.g:
def sum(a, b)
Kind::Numeric[a] + Kind::Numeric[b]
end
sum(1, 1)
sum('1', 1)
⬆️ Back to Top
Kind.<Type>[]
By default, basic verifications are strict. So, when you perform Kind::Hash[value]
the given value will be returned if it was a Hash, but if not, an error will be raised.
Kind::Hash[nil]
Kind::Hash['']
Kind::Hash[a: 1]
⬆️ Back to Top
Kind::<Type>.===()
Use this method to verify if the given object has the expected type.
Kind::Enumerable === {}
Kind::Enumerable === ''
⬆️ Back to Top
Kind::<Type>.value?()
This method works like .===
, but the difference is what happens when you invoke it without arguments.
Kind::Enumerable.value?({})
Kind::Enumerable.value?('')
When .value?
is called without an argument, it will return a lambda which will know how to perform the kind verification.
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ]
collection.select(&Kind::Enumerable.value?)
⬆️ Back to Top
Kind::<Type>.or_nil()
But if you don't need a strict type verification, use the .or_nil
method.
Kind::Hash.or_nil('')
Kind::Hash.or_nil({a: 1})
⬆️ Back to Top
Kind::<Type>.or_undefined()
This method works like .or_nil
, but it will return a Kind::Undefined
instead of nil
.
Kind::Hash.or_undefined('')
Kind::Hash.or_undefined({a: 1})
⬆️ Back to Top
Kind::<Type>.or()
This method can return a fallback if the given value isn't an instance of the expected kind.
Kind::Hash.or({}, [])
Kind::Hash.or(nil, [])
Kind::Hash.or(nil, {a: 1})
If it doesn't receive a second argument (the value), it will return a callable that knows how to expose an instance of the expected type or a fallback if the given value is wrong.
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ]
collection.map(&Kind::Hash.or({}))
collection.map(&Kind::Hash.or(nil))
An error will be raised if the fallback didn't have the expected kind or if not nil
/ Kind::Undefined
.
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ]
collection.map(&Kind::Hash.or(:foo))
⬆️ Back to Top
Kind::<Type>.value()
This method ensures that you will have a value of the expected kind. But, in the case of the given value be invalid, this method will require a default value (with the expected kind) to be returned.
Kind::String.value(1, default: '')
Kind::String.value('1', default: '')
Kind::String.value('1', default: 1)
⬆️ Back to Top
Kind::<Type>.maybe
This method exposes a typed Kind::Maybe
and using it will be possible to apply a sequence of operations in the case of the wrapped value has the expected kind.
Double = ->(value) do
Kind::Numeric.maybe(value)
.then { |number| number * 2 }
.value_or(0)
end
Double.('2')
Double.(2)
If it is invoked without arguments, it returns the typed Maybe. But, if it receives arguments, it will behave like the Kind::Maybe.wrap
method. e.g.
Kind::Integer.maybe #<Kind::Maybe::Typed:0x0000... @kind=Kind::Integer>
Kind::Integer.maybe(0).some?
Kind::Integer.maybe { 1 }.some?
Kind::Integer.maybe(2) { |n| n * 2 }.some?
Kind::Integer.maybe { 2 / 0 }.none?
Kind::Integer.maybe(2) { |n| n / 0 }.none?
Kind::Integer.maybe('2') { |n| n * n }.none?
Note: You can use Kind::\<Type\>.optional
as an alias for Kind::\<Type\>.maybe
.
⬆️ Back to Top
Kind::<Type>?
There is a second way to do a type verification and know if one or multiple values has the expected type. You can use the predicate kind methods (Kind::Hash?
). e.g:
Kind::Enumerable?({})
Kind::Enumerable?({}, [], Set.new)
Like the Kind::<Type>.value?
method, if the Kind::<Type>?
doesn't receive an argument, it will return a lambda which will know how to perform the kind verification.
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ]
collection.select(&Kind::Enumerable?)
⬆️ Back to Top
Kind::{Array,Hash,String,Set}.empty_or()
This method is available for some type handlers (Kind::Array
, Kind::Hash
, Kind::String
, Kind::Set
), and it will return an empty frozen value if the given one hasn't the expected kind.
Kind::Array.empty_or({})
Kind::Array.empty_or({}).frozen?
⬆️ Back to Top
List of all type handlers
Core
Kind::Array
Kind::Class
Kind::Comparable
Kind::Enumerable
Kind::Enumerator
Kind::File
Kind::Float
Kind::Hash
Kind::Integer
Kind::IO
Kind::Method
Kind::Module
Kind::Numeric
Kind::Proc
Kind::Queue
Kind::Range
Kind::Regexp
Kind::String
Kind::Struct
Kind::Symbol
Kind::Time
Stdlib
Kind::OpenStruct
Kind::Set
Custom
Kind::Boolean
Kind::Callable
Kind::Lambda
⬆️ Back to Top
Creating type handlers
There are two ways to do this, you can create type handlers dynamically or defining a module.
Dynamic creation
Using a class or a module
class User
end
user = User.new
kind_of_user = Kind[User]
kind_of_user.name
kind_of_user.kind
kind_of_user === 0
kind_of_user === User.new
kind_of_user.value?('')
kind_of_user.value?(User.new)
[0, User.new].select(&kind_of_user.value?)
kind_of_user.or_nil({})
kind_of_user.or_nil(User.new)
kind_of_user.or_undefined([])
kind_of_user.or_undefined(User.new)
kind_of_user.or(nil, 0)
kind_of_user.or(nil, User.new)
[1, User.new].map(&kind_of_user.or(nil))
[0, User.new].map(&kind_of_user.or(:foo))
kind_of_user[:foo]
kind_of_user[User.new]
kind_of_user.value(User.new, default: User.new)
kind_of_user.value('1', default: User.new)
kind_of_user.value('1', default: 1)
kind_of_user.maybe('1').value_or(User.new)
⬆️ Back to Top
Using Kind.object(name:, &block)
PositiveInteger = Kind.object(name: 'PositiveInteger') do |value|
value.kind_of?(Integer) && value > 0
end
PositiveInteger.name
PositiveInteger.kind
PositiveInteger === 1
PositiveInteger === 0
PositiveInteger.value?(1)
PositiveInteger.value?(-1)
[1, 2, 0, 3, -1].select(&PositiveInteger.value?)
PositiveInteger.or_nil(1)
PositiveInteger.or_nil(0)
PositiveInteger.or_undefined(2)
PositiveInteger.or_undefined(-1)
PositiveInteger.or(nil, 1)
PositiveInteger.or(nil, 0)
[1, 2, 0, 3, -1].map(&PositiveInteger.or(1))
[1, 2, 0, 3, -1].map(&PositiveInteger.or(nil))
[1, 2, 0, 3, -1].map(&PositiveInteger.or(:foo))
PositiveInteger[1]
PositiveInteger[:foo]
PositiveInteger.value(2, default: 1)
PositiveInteger.value('1', default: 1)
PositiveInteger.value('1', default: 0)
PositiveInteger.maybe(0).value_or(1)
PositiveInteger.maybe(2).value_or(1)
⬆️ Back to Top
Kind:: object
The idea here is to create a type handler inside of the Kind
namespace and to do this you will only need to use pure Ruby. e.g:
class User
end
module Kind
module User
extend self, ::Kind::Object
def kind; ::User; end
end
def self.User?(*values)
KIND.of?(::User, values)
end
end
user = User.new
Kind::User[user]
Kind::User[{}]
Kind::User?(user)
Kind::User?({})
The advantages of this approach are:
- You will have a singleton (a unique instance) to be used, so the garbage collector will work less.
- You can define additional methods to be used with this kind.
The disadvantage is:
- You could overwrite some standard type handler or constant. I believe that this will be hard to happen, but must be your concern if you decide to use this kind of approach.
⬆️ Back to Top
Utility methods
Kind.of_class?()
This method verify if a given value is a Class
.
Kind.of_class?(Hash)
Kind.of_class?(Enumerable)
Kind.of_class?(1)
⬆️ Back to Top
Kind.of_module?()
This method verify if a given value is a Module
.
Kind.of_module?(Hash)
Kind.of_module?(Enumerable)
Kind.of_module?(1)
⬆️ Back to Top
Kind.of_module_or_class()
This method return the given value if it is a module or a class. If not, a Kind::Error
will be raised.
Kind.of_module_or_class(String)
Kind.of_module_or_class(1)
⬆️ Back to Top
Kind.respond_to()
this method returns the given object if it responds to all of the method names. But if the object does not respond to some of the expected methods, an error will be raised.
Kind.respond_to('', :upcase)
Kind.respond_to('', :upcase, :strip)
Kind.respond_to(1, :upcase)
Kind.respond_to(2, :to_s, :upcase)
⬆️ Back to Top
Kind.of()
There is a second way to do a strict type verification, you can use the Kind.of()
method to do this. It receives the kind as the first argument and the value to be checked as the second one.
Kind.of(Hash, {})
Kind.of(Hash, [])
⬆️ Back to Top
Kind.of?()
This method can be used to check if one or multiple values have the expected kind.
Kind.of?(Array, [])
Kind.of?(Array, {})
Kind.of?(Enumerable, [], {})
Kind.of?(Hash, {}, {})
Kind.of?(Array, [], {})
If the method receives only the first argument (the kind) a lambda will be returned and it will know how to do the type verification.
[1, '2', 3].select(&Kind.of?(Numeric))
⬆️ Back to Top
Kind.value()
This method ensures that you will have a value of the expected kind. But, in the case of the given value be invalid, this method will require a default value (with the expected kind) to be returned.
Kind.value(String, '1', default: '')
Kind.value(String, 1, default: '')
Kind.value(String, 1, default: 2)
⬆️ Back to Top
Kind.is()
You can use Kind.is
to verify if some class has the expected type as its ancestor.
Kind.is(Hash, String)
Kind.is(Hash, Hash)
Kind.is(Enumerable, Hash)
The Kind.is
also could check the inheritance of Classes/Modules.
class Human; end
class Person < Human; end
class User < Human; end
Kind.is(Human, User)
Kind.is(Human, Human)
Kind.is(Human, Person)
Kind.is(Human, Struct)
module Human; end
class Person; include Human; end
class User; include Human; end
Kind.is(Human, User)
Kind.is(Human, Human)
Kind.is(Human, Person)
Kind.is(Human, Struct)
module Human; end
module Person; extend Human; end
module User; extend Human; end
Kind.is(Human, User)
Kind.is(Human, Human)
Kind.is(Human, Person)
Kind.is(Human, Struct)
⬆️ Back to Top
Utility modules
Kind::Try
The method .call
of this module invokes a public method with or without arguments like public_send
does, except that if the receiver does not respond to it the call returns nil
rather than raising an exception.
Kind::Try.(' foo ', :strip)
Kind::Try.({a: 1}, :[], :a)
Kind::Try.({a: 1}, :[], :b)
Kind::Try.({a: 1}, :fetch, :b, 2)
Kind::Try.(:symbol, :strip)
Kind::Try.(:symbol, :fetch, :b, 2)
Kind::Try.({a: 1}, 1, :a)
This module has the method []
that knows how to create a lambda that will know how to perform the try
strategy.
results =
[
{},
{name: 'Foo Bar'},
{name: 'Rodrigo Serradura'},
].map(&Kind::Try[:fetch, :name, 'John Doe'])
p results
⬆️ Back to Top
Kind::Dig
The method .call
of this module has the same behavior of Ruby dig methods (Hash, Array, Struct, OpenStruct), but it will not raise an error if some step can't be digged.
s = Struct.new(:a, :b).new(101, 102)
o = OpenStruct.new(c: 103, d: 104)
d = { struct: s, ostruct: o, data: [s, o]}
Kind::Dig.(s, [:a])
Kind::Dig.(o, [:c])
Kind::Dig.(d, [:struct, :b])
Kind::Dig.(d, [:data, 0, :b])
Kind::Dig.(d, [:data, 0, 'b'])
Kind::Dig.(d, [:ostruct, :d])
Kind::Dig.(d, [:data, 1, :d])
Kind::Dig.(d, [:data, 1, 'd'])
Kind::Dig.(d, [:struct, :f])
Kind::Dig.(d, [:ostruct, :f])
Kind::Dig.(d, [:data, 0, :f])
Kind::Dig.(d, [:data, 1, :f])
Another difference between the Kind::Dig
and the native Ruby dig, is that it knows how to extract values from regular objects.
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
person = Person.new('Rodrigo')
Kind::Dig.(person, [:name])
Kind::Dig.({people: [person]}, [:people, 0, :name])
This module has the method []
that knows how to create a lambda that will know how to perform the dig
strategy.
results = [
{ person: {} },
{ person: { name: 'Foo Bar'} },
{ person: { name: 'Rodrigo Serradura'} },
].map(&Kind::Dig[:person, :name])
p results
⬆️ Back to Top
Kind::Presence
The method .call
of this module returns the given value if it's present otherwise it will return nil
.
Kind::Presence.(true)
Kind::Presence.('foo')
Kind::Presence.([1, 2])
Kind::Presence.({a: 3})
Kind::Presence.(Set.new([4]))
Kind::Presence.('')
Kind::Presence.(' ')
Kind::Presence.("\t\n\r")
Kind::Presence.("\u00a0")
Kind::Presence.([])
Kind::Presence.({})
Kind::Presence.(Set.new)
Kind::Presence.(nil)
Kind::Presence.(false)
MyObject = Struct.new(:is_blank) do
def blank?
is_blank
end
end
my_object = MyObject.new
my_object.is_blank = true
Kind::Presence.(my_object)
my_object.is_blank = false
Kind::Presence.(my_object)
This module also has the method to_proc
, because of this you can make use of the Kind::Presence
in methods that receive a block as an argument. e.g:
['', [], {}, '1', [2]].map(&Kind::Presence)
⬆️ Back to Top
Kind::Undefined
The Kind::Undefined
constant can be used to distinguish the usage of nil
.
If you are interested, check out the tests to understand its methods.
⬆️ Back to Top
Kind::Maybe
The Kind::Maybe
is used when a series of computations (in a chain of map callings) could return nil
or Kind::Undefined
at any point.
optional =
Kind::Maybe.new(2)
.map { |value| value * 2 }
.map { |value| value * 2 }
puts optional.value
puts optional.some?
puts optional.none?
puts optional.value_or(0)
puts optional.value_or { 0 }
optional =
Kind::Maybe.new(3)
.map { nil }
.map { |value| value * 3 }
puts optional.value
puts optional.some?
puts optional.none?
puts optional.value_or(0)
puts optional.value_or { 0 }
optional =
Kind::Maybe.new(4)
.map { Kind::Undefined }
.map { |value| value * 4 }
puts optional.value
puts optional.some?
puts optional.none?
puts optional.value_or(1)
puts optional.value_or { 1 }
⬆️ Back to Top
Replacing blocks by lambdas
Add = -> params do
a, b = Kind::Hash.value_or_empty(params).values_at(:a, :b)
a + b if Kind::Numeric?(a, b)
end
Kind::Maybe.new(a: 1, b: 2).map(&Add).value_or(0)
Kind::Maybe.new([]).map(&Add).value_or(0)
Kind::Maybe.new({}).map(&Add).value_or(0)
Kind::Maybe.new(nil).map(&Add).value_or(0)
⬆️ Back to Top
Kind::Maybe[], Kind::Maybe.wrap() and Kind::Maybe#then method aliases
You can use Kind::Maybe[]
(brackets) instead of the .new
to transform values in a Kind::Maybe
. Another alias is .then
to the .map
method.
result =
Kind::Maybe[5]
.then { |value| value * 5 }
.then { |value| value + 17 }
.value_or(0)
puts result
You can also use Kind::Maybe.wrap()
instead of the .new
method.
result =
Kind::Maybe
.wrap(5)
.then { |value| value * 5 }
.then { |value| value + 17 }
.value_or(0)
puts result
⬆️ Back to Top
Replacing blocks by lambdas
Add = -> params do
a, b = Kind::Hash.value_or_empty(params).values_at(:a, :b)
a + b if Kind::Numeric?(a, b)
end
Kind::Maybe[a: 1, b: 2].then(&Add).value_or(0)
Kind::Maybe[1].then(&Add).value_or(0)
Kind::Maybe['2'].then(&Add).value_or(0)
Kind::Maybe[nil].then(&Add).value_or(0)
⬆️ Back to Top
Kind::None() and Kind::Some()
If you need to ensure the return of Kind::Maybe
results from your methods/lambdas,
you could use the methods Kind::None
and Kind::Some
to do this. e.g:
Double = ->(arg) do
number = Kind::Numeric.or_nil(arg)
Kind::Maybe[number].then { |number| number * 2 }
end
Add = -> params do
a, b = Kind::Hash.value_or_empty(params).values_at(:a, :b)
return Kind::None unless Kind::Numeric?(a, b)
Kind::Some(a + b)
end
Add.call(1)
Add.call({})
Add.call(a: 1)
Add.call(b: 2)
Add.call(a:1, b: 2)
Kind::Maybe[a: 1, b: 2].then(&Add).value_or(0)
Kind::Maybe[1].then(&Add).value_or(0)
Add.(a: 2, b: 2).then(&Double).value
⬆️ Back to Top
Kind::Optional
The Kind::Optional
constant is an alias for Kind::Maybe
. e.g:
result1 =
Kind::Optional
.new(5)
.map { |value| value * 5 }
.map { |value| value - 10 }
.value_or(0)
puts result1
result2 =
Kind::Optional[5]
.then { |value| value * 5 }
.then { |value| value + 10 }
.value_or { 0 }
puts result2
⬆️ Back to Top
Replacing blocks by lambdas
Double = ->(arg) do
number = Kind::Numeric.or_nil(arg)
Kind::Maybe[number].then { |number| number * 2 }
end
Kind::Optional[2].then(&Double).value_or(0)
Kind::Optional['2'].then(&Double).value_or(0)
Kind::Optional[nil].then(&Double).value_or(0)
⬆️ Back to Top
Kind::Maybe()
You can use Kind::Maybe(<Type>)
or Kind::Optional(<Type>)
to create a maybe monad which will return None if the given input hasn't the expected type. e.g:
result1 =
Kind::Maybe(Numeric)
.wrap(5)
.then { |value| value * 5 }
.value_or { 0 }
puts result1
result2 =
Kind::Optional(Numeric)
.wrap('5')
.then { |value| value * 5 }
.value_or { 0 }
puts result2
This typed maybe has the same methods of Kind::Maybe
class. e.g:
Kind::Maybe(Numeric)[5]
Kind::Maybe(Numeric).new(5)
Kind::Maybe(Numeric).wrap(5)
Kind::Optional(Numeric)[5]
Kind::Optional(Numeric).new(5)
Kind::Optional(Numeric).wrap(5)
Real world examples
It is very common the need to avoid some operation when a method receives the wrong input.
In these scenarios, you could create a maybe monad that will return None if the given input hasn't the expected type. e.g:
def person_name(params)
Kind::Maybe(Hash)
.wrap(params)
.then { |hash| hash.values_at(:first_name, :last_name) }
.then { |names| names.map(&Kind::Presence).tap(&:compact!) }
.check { |names| names.size == 2 }
.then { |(first_name, last_name)| "#{first_name} #{last_name}" }
.value_or { 'John Doe' }
end
person_name('')
person_name(nil)
person_name(first_name: 'Rodrigo')
person_name(last_name: 'Serradura')
person_name(first_name: 'Rodrigo', last_name: 'Serradura')
def person_name(params)
default = 'John Doe'
return default unless params.kind_of?(Hash)
names = params.values_at(:first_name, :last_name).map(&Kind::Presence).tap(&:compact!)
return default if names.size != 2
first_name, last_name = names
"#{first_name} #{last_name}"
end
To finish follows an example of how to use the Maybe monad to handle arguments in coupled methods.
module PersonIntroduction1
extend self
def call(params)
optional = Kind::Maybe(Hash).wrap(params)
"Hi my name is #{full_name(optional)}, I'm #{age(optional)} years old."
end
private
def full_name(optional)
optional.map { |hash| "#{hash[:first_name]} #{hash[:last_name]}".strip }
.presence
.value_or { 'John Doe' }
end
def age(optional)
optional.dig(:age).value_or(0)
end
end
module PersonIntroduction2
extend self
def call(params)
"Hi my name is #{full_name(params)}, I'm #{age(params)} years old."
end
private
def full_name(params)
default = 'John Doe'
case params
when Hash then
Kind::Presence.("#{params[:first_name]} #{params[:last_name]}".strip) || default
else default
end
end
def age(params)
case params
when Hash then params.fetch(:age, 0)
else 0
end
end
end
⬆️ Back to Top
Error handling
Kind::Maybe.wrap {}
The Kind::Maybe#wrap
can receive a block, and if an exception (at StandardError level
) happening, this will generate a None result.
Kind::Maybe(Numeric)
.wrap { 2 / 0 }
Kind::Maybe(Numeric)
.wrap(2) { |number| number / 0 }
Kind::Maybe.map! or Kind::Maybe.then!
By default the Kind::Maybe#map
and Kind::Maybe#then
intercept exceptions at the StandardError
level. So if an exception was intercepted a None will be returned.
result1 = Kind::Maybe[2].map { |number| number / 0 }
result1.none?
result1.value
result2 = Kind::Maybe[3].then { |number| number / 0 }
result2.none?
result2.value
But there are versions of these methods (Kind::Maybe#map!
and Kind::Maybe#then!
) that allow the exception leak, so, the user must handle the exception by himself or use this method when he wants to see the error be raised.
Kind::Maybe[2].map! { |number| number / 0 }
Kind::Maybe[2].then! { |number| number / 0 }
Note: If an exception (at StandardError level) is returned by the methods #then
, #map
it will be resolved as None.
⬆️ Back to Top
Kind::Maybe#try
If you don't want to use #map/#then
to access the value, you could use the #try
method to access it. So, if the value wasn't nil
or Kind::Undefined
, the some monad will be returned.
object = 'foo'
Kind::Maybe[object].try(:upcase).value
Kind::Maybe[{}].try(:fetch, :number, 0).value
Kind::Maybe[{number: 1}].try(:fetch, :number).value
Kind::Maybe[object].try { |value| value.upcase }.value
object = nil
Kind::Maybe[object].try(:upcase).value
Kind::Maybe[object].try { |value| value.upcase }.value
object = Kind::Undefined
Kind::Maybe[object].try(:upcase).value
Kind::Maybe[object].try { |value| value.upcase }.value
Note: You can use the #try
method with Kind::Optional
objects.
⬆️ Back to Top
Kind::Maybe#try!
Has the same behavior of its #try
, but it will raise an error if the value doesn't respond to the expected method.
Kind::Maybe[{}].try(:upcase)
Kind::Maybe[{}].try!(:upcase)
Note: You can also use the #try!
method with Kind::Optional
objects.
⬆️ Back to Top
Kind::Maybe#dig
Has the same behavior of Ruby dig methods (Hash, Array, Struct, OpenStruct), but it will not raise an error if some value can't be digged.
[nil, 1, '', /x/].each do |value|
p Kind::Maybe[value].dig(:foo).value
end
a = [1, 2, 3]
Kind::Maybe[a].dig(0).value
Kind::Maybe[a].dig(3).value
h = { foo: {bar: {baz: 1}}}
Kind::Maybe[h].dig(:foo).value
Kind::Maybe[h].dig(:foo, :bar).value
Kind::Maybe[h].dig(:foo, :bar, :baz).value
Kind::Maybe[h].dig(:foo, :bar, 'baz').value
i = { foo: [{'bar' => [1, 2]}, {baz: [3, 4]}] }
Kind::Maybe[i].dig(:foo, 0, 'bar', 0).value
Kind::Maybe[i].dig(:foo, 0, 'bar', 1).value
Kind::Maybe[i].dig(:foo, 0, 'bar', -1).value
Kind::Maybe[i].dig(:foo, 0, 'bar', 2).value
s = Struct.new(:a, :b).new(101, 102)
o = OpenStruct.new(c: 103, d: 104)
b = { struct: s, ostruct: o, data: [s, o]}
Kind::Maybe[s].dig(:a).value
Kind::Maybe[b].dig(:struct, :b).value
Kind::Maybe[b].dig(:data, 0, :b).value
Kind::Maybe[b].dig(:data, 0, 'b').value
Kind::Maybe[o].dig(:c).value
Kind::Maybe[b].dig(:ostruct, :d).value
Kind::Maybe[b].dig(:data, 1, :d).value
Kind::Maybe[b].dig(:data, 1, 'd').value
Kind::Maybe[s].dig(:f).value
Kind::Maybe[o].dig(:f).value
Kind::Maybe[b].dig(:struct, :f).value
Kind::Maybe[b].dig(:ostruct, :f).value
Kind::Maybe[b].dig(:data, 0, :f).value
Kind::Maybe[b].dig(:data, 1, :f).value
Note: You can also use the #dig
method with Kind::Optional
objects.
Kind::Maybe#check
This method will return the current Some after verify if the block output is truthy. e.g:
Kind::Maybe(Array)
.wrap(['Rodrigo', 'Serradura'])
.then { |names| names.map(&Kind::Presence).tap(&:compact!) }
.check { |names| names.size == 2 }
.value
⬆️ Back to Top
Kind::Maybe#presence
This method will return None if the wrapped value wasn't present.
result = Kind::Maybe(Hash).wrap(foo: '').dig(:foo).presence
result.none?
result.value
⬆️ Back to Top
Kind::Empty
When you define a method that has default arguments, for certain data types, you will always create a new object in memory. e.g:
def something(params = {})
params.object_id
end
puts something
puts something
puts something
puts something
So, to avoid an unnecessary allocation in memory, the kind
gem exposes some frozen objects to be used as default values.
Kind::Empty::SET
Kind::Empty::HASH
Kind::Empty::ARRAY
Kind::Empty::STRING
Usage example:
def do_something(value, with_options: Kind::Empty::HASH)
end
Defining Empty as Kind::Empty an alias
You can require kind/empty/constant
to define Empty
as a Kind::Empty
alias. But, a LoadError
will be raised if there is an already defined constant Empty
.
So if you required this file, the previous example could be written like this:
def do_something(value, with_options: Empty::HASH)
end
Follows the list of constants if the alias was defined:
Empty::SET
Empty::HASH
Empty::ARRAY
Empty::STRING
⬆️ Back to Top
Kind::Validator (ActiveModel::Validations)
This module enables the capability to validate types via ActiveModel::Validations >= 3.2, < 7.0
. e.g
class Person
include ActiveModel::Validations
attr_accessor :first_name, :last_name
validates :first_name, :last_name, kind: String
end
And to make use of it, you will need to do an explicitly require. e.g:
gem 'kind', require: 'kind/validator'
require 'kind/validator'
⬆️ Back to Top
Usage
validates :name, kind: { of: String }
Use an array to verify if the attribute is an instance of one of the classes/modules.
validates :status, kind: { of: [String, Symbol]}
Because of kind verification be made via ===
you can use type handlers as the expected kinds.
validates :alive, kind: Kind::Boolean
⬆️ Back to Top
class Human; end
class Person < Human; end
class User < Human; end
validates :human_kind, kind: { is: Human }
module Human; end
class Person; include Human; end
class User; include Human; end
validates :human_kind, kind: { is: Human }
module Human; end
module Person; extend Human; end
module User; extend Human; end
validates :human_kind, kind: { is: Human }
validates :human_kind, kind: { is: [Person, User] }
⬆️ Back to Top
validates :name, kind: { instance_of: String }
validates :name, kind: { instance_of: [String, Symbol] }
⬆️ Back to Top
validates :handler, kind: { respond_to: :call }
This validation can verify one or multiple methods.
validates :params, kind: { respond_to: [:[], :values_at] }
⬆️ Back to Top
Array.new.all? { |item| item.kind_of?(Class) }
validates :account_types, kind: { array_of: String }
validates :account_types, kind: { array_of: [String, Symbol] }
⬆️ Back to Top
Array.new.all? { |item| expected_values.include?(item) }
validates :account_types, kind: { array_with: ['foo', 'bar'] }
⬆️ Back to Top
Defining the default validation strategy
By default, you can define the attribute type directly (without a hash). e.g.
validates :name, kind: String
validates :name, kind: [String, Symbol]
To changes this behavior you can set another strategy to validates the attributes types:
Kind::Validator.default_strategy = :instance_of
And these are the available options to define the default strategy:
kind_of
(default)instance_of
⬆️ Back to Top
Using the allow_nil
and strict
options
You can use the allow_nil
option with any of the kind validations. e.g.
validates :name, kind: String, allow_nil: true
And as any active model validation, kind validations works with the strict: true
option and with the validates!
method. e.g.
validates :first_name, kind: String, strict: true
validates! :last_name, kind: String
⬆️ Back to Top
Similar Projects
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
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
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/kind. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Kind project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.