🤷 kind

A simple type system (at runtime) for Ruby - free of dependencies.


Gem Ruby Rails
Build Status Maintainability Test Coverage

**Motivation:** As a creator of Ruby gems, I have a common need that I have to handle in many of my projects: type checking of method arguments. One of the goals of this project is to do simple type checking like `"some string".is_a?(String)`, but, exposing useful abstractions around this. e.g: [Kind.\ methods](#verifying-the-kind-of-some-object), [active model validations](#kindvalidator-activemodelvalidations), [maybe monad](#kindmaybe). ## Documentation Version | Documentation ---------- | ------------- unreleased | https://github.com/serradura/u-case/blob/main/README.md 5.1.0 | https://github.com/serradura/u-case/blob/v5.x/README.md 4.1.0 | https://github.com/serradura/u-case/blob/v4.x/README.md 3.1.0 | https://github.com/serradura/u-case/blob/v3.x/README.md 2.3.0 | https://github.com/serradura/u-case/blob/v2.x/README.md 1.9.0 | https://github.com/serradura/u-case/blob/v1.x/README.md ## Table of Contents - [Compatibility](#compatibility) - [Installation](#installation) - [Usage](#usage) - [Kind.\[]](#kindtype) - [Kind::\.===()](#kindtype-1) - [Kind::\.value?()](#kindtypevalue) - [Kind::\.or_nil()](#kindtypeor_nil) - [Kind::\.or_undefined()](#kindtypeor_undefined) - [Kind::\.or()](#kindtypeor) - [Kind::\.value()](#kindtypevalue-1) - [Kind::\.maybe](#kindtypemaybe) - [Kind::\?](#kindtype-2) - [Kind::{Array,Hash,String,Set}.value_or_empty()](#kindarrayhashstringsetvalue_or_empty) - [List of all type checkers](#list-of-all-type-checkers) - [Core](#core) - [Stdlib](#stdlib) - [Custom](#custom) - [Creating type checkers](#creating-type-checkers) - [Dynamic creation](#dynamic-creation) - [Using a class or a module](#using-a-class-or-a-module) - [Using an object which responds to ===](#using-an-object-which-responds-to-) - [Kind:: module](#kindtype-module) - [Utility methods](#utility-methods) - [Kind.of_class?()](#kindof_class) - [Kind.of_module?()](#kindof_module) - [Kind.of_module_or_class()](#kindof_module_or_class) - [Kind.respond_to()](#kindrespond_to) - [Kind.of()](#kindof) - [Kind.of?()](#kindof-1) - [Kind.value()](#kindvalue) - [Kind.is()](#kindis) - [Utility modules](#utility-modules) - [Kind::Try](#kindtry) - [Kind::Dig](#kinddig) - [Kind::Presence](#kindpresence) - [Kind::Undefined](#kindundefined) - [Kind::Maybe](#kindmaybe) - [Replacing blocks by lambdas](#replacing-blocks-by-lambdas) - [Kind::Maybe[], Kind::Maybe.wrap() and Kind::Maybe#then method aliases](#kindmaybe-kindmaybewrap-and-kindmaybethen-method-aliases) - [Replacing blocks by lambdas](#replacing-blocks-by-lambdas-1) - [Kind::None() and Kind::Some()](#kindnone-and-kindsome) - [Kind::Optional](#kindoptional) - [Replacing blocks by lambdas](#replacing-blocks-by-lambdas-2) - [Kind::Maybe()](#kindmaybetype) - [Real world examples](#real-world-examples) - [Error handling](#error-handling) - [Kind::Maybe.wrap {}](#kindmaybewrap-) - [Kind::Maybe.map! or Kind::Maybe.then!](#kindmaybemap-or-kindmaybethen) - [Kind::Maybe#try](#kindmaybetry) - [Kind::Maybe#try!](#kindmaybetry-1) - [Kind::Maybe#dig](#kindmaybedig) - [Kind::Maybe#check](#kindmaybecheck) - [Kind::Maybe#presence](#kindmaybepresence) - [Kind::Empty](#kindempty) - [Defining Empty as Kind::Empty an alias](#defining-empty-as-kindempty-an-alias) - [Kind::Validator (ActiveModel::Validations)](#kindvalidator-activemodelvalidations) - [Usage](#usage-1) - [Object#===](#object) - [Kind.is](#kindis-1) - [Object#instance_of?](#objectinstance_of) - [Object#respond_to?](#objectrespond_to) - [Array.new.all? { |item| item.kind_of?(Class) }](#arraynewall--item-itemkind_ofclass-) - [Array.new.all? { |item| expected_values.include?(item) }](#arraynewall--item-expected_valuesincludeitem-) - [Defining the default validation strategy](#defining-the-default-validation-strategy) - [Using the `allow_nil` and `strict` options](#using-the-allow_nil-and-strict-options) - [Similar Projects](#similar-projects) - [Development](#development) - [Contributing](#contributing) - [License](#license) - [Code of Conduct](#code-of-conduct) ## Compatibility | u-case | branch | ruby | activemodel | | -------------- | ------- | -------- | -------------- | | unreleased | main | >= 2.1.0 | >= 3.2, <= 6.1 | | 5.1.0 | v5.x | >= 2.1.0 | >= 3.2, <= 6.1 | | 4.1.0 | v4.x | >= 2.2.0 | >= 3.2, <= 6.1 | | 3.1.0 | v3.x | >= 2.2.0 | >= 3.2, <= 6.1 | | 2.3.0 | v2.x | >= 2.2.0 | >= 3.2, <= 6.0 | | 1.9.0 | v1.x | >= 2.2.0 | >= 3.2, <= 6.0 | > Note: The activemodel is an optional dependency, it is related with the [Kind::Validator](#kindvalidator-activemodelvalidations). ## Installation Add this line to your application's Gemfile: ```ruby gem 'kind' ``` And then execute: $ bundle install Or install it yourself as: $ gem install kind [⬆️  Back to Top](#table-of-contents-) ## Usage With this gem you can add some kind of type checking at runtime. e.g: ```ruby def sum(a, b) Kind::Numeric[a] + Kind::Numeric[b] end sum(1, 1) # 2 sum('1', 1) # Kind::Error ("\"1\" expected to be a kind of Numeric") ``` [⬆️  Back to Top](#table-of-contents-) ### Kind.\[] 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. ```ruby Kind::Hash[nil] # Kind::Error (nil expected to be a kind of Hash) Kind::Hash[''] # Kind::Error ("" expected to be a kind of Hash) Kind::Hash[a: 1] # {a: 1} ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.===() Use this method to verify if the given object has the expected type. ```ruby Kind::Enumerable === {} # true Kind::Enumerable === '' # false ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.value?() This method works like `.===`, but the difference is what happens when you invoke it without arguments. ```ruby # Example of calling `.value?` with an argument: Kind::Enumerable.value?({}) # true Kind::Enumerable.value?('') # false ``` When `.value?` is called without an argument, it will return a lambda which will know how to perform the kind verification. ```ruby collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ] collection.select(&Kind::Enumerable.value?) # [{:number=>1}, {:number=>3}, [:number, 5]] ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.or_nil() But if you don't need a strict type verification, use the `.or_nil` method. ```ruby Kind::Hash.or_nil('') # nil Kind::Hash.or_nil({a: 1}) # {a: 1} ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.or_undefined() This method works like `.or_nil`, but it will return a [`Kind::Undefined`](#kindundefined) instead of `nil`. ```ruby Kind::Hash.or_undefined('') # Kind::Undefined Kind::Hash.or_undefined({a: 1}) # {a: 1} ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.or() This method can return a fallback if the given value isn't an instance of the expected kind. ```ruby Kind::Hash.or({}, []) # {} Kind::Hash.or(nil, []) # nil Kind::Hash.or(nil, {a: 1}) # {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. ```ruby collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ] collection.map(&Kind::Hash.or({})) # [{:number=>1}, {}, {:number=>3}, {}, {}] collection.map(&Kind::Hash.or(nil)) # [{:number=>1}, nil, {:number=>3}, nil, nil] ``` An error will be raised if the fallback didn't have the expected kind or if not `nil` / `Kind::Undefined`. ```ruby collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ] collection.map(&Kind::Hash.or(:foo)) # Kind::Error (:foo expected to be a kind of Hash) ``` [⬆️  Back to Top](#table-of-contents-) ### 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. ```ruby Kind::String.value(1, default: '') # "" Kind::String.value('1', default: '') # "1" Kind::String.value('1', default: 1) # Kind::Error (1 expected to be a kind of String) ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::\.maybe This method exposes a [typed `Kind::Maybe`](#kindmaybetype) and using it will be possible to apply a sequence of operations in the case of the wrapped value has the expected kind. ```ruby Double = ->(value) do Kind::Numeric.maybe(value) .then { |number| number * 2 } .value_or(0) end Double.('2') # 0 Double.(2) # 4 ``` 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. ```ruby Kind::Integer.maybe # Kind::Integer.maybe(0).some? # true Kind::Integer.maybe { 1 }.some? # true Kind::Integer.maybe(2) { |n| n * 2 }.some? # true Kind::Integer.maybe { 2 / 0 }.none? # true Kind::Integer.maybe(2) { |n| n / 0 }.none? # true Kind::Integer.maybe('2') { |n| n * n }.none? # true ``` > **Note:** You can use `Kind::\.optional` as an alias for `Kind::\.maybe`. [⬆️  Back to Top](#table-of-contents-) ### Kind::\? 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: ```ruby # Verifying one value Kind::Enumerable?({}) # true # Verifying multiple values Kind::Enumerable?({}, [], Set.new) # true ``` Like the `Kind::.value?` method, if the `Kind::?` doesn't receive an argument, it will return a lambda which will know how to perform the kind verification. ```ruby collection = [ {number: 1}, 'number 2', {number: 3}, :number_4, [:number, 5] ] collection.select(&Kind::Enumerable?) # [{:number=>1}, {:number=>3}, [:number, 5]] ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::{Array,Hash,String,Set}.value_or_empty() This method is available for some type checkers (`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. ```ruby Kind::Array.value_or_empty({}) # [] Kind::Array.value_or_empty({}).frozen? # true ``` [⬆️  Back to Top](#table-of-contents-) ### List of all type checkers #### 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](#table-of-contents-) ### Creating type checkers There are two ways to do this, you can create type checkers dynamically or defining a module. #### Dynamic creation ##### Using a class or a module ```ruby class User end user = User.new kind_of_user = Kind::Of(User) # kind_of_user.name # kind_of_user.kind # The type checker can return its kind and its name kind_of_user.name # "User" kind_of_user.kind # User # kind_of_user.=== # Can check if a given value is an instance of its kind. kind_of_user === 0 # false kind_of_user === User.new # true # kind_of_user.value?(value) # Can check if a given value is an instance of its kind. kind_of_user.value?('') # false kind_of_user.value?(User.new) # true # If it doesn't receive an argument, a lambda will be returned and it will know how to do the type verification. [0, User.new].select?(&kind_of_user.value?) # [#] # kind_of_user.or_nil(value) # Can return nil if the given value isn't an instance of its kind kind_of_user.or_nil({}) # nil kind_of_user.or_nil(User.new) # # # kind_of_user.or_undefined(value) # Can return Kind::Undefined if the given value isn't an instance of its kind kind_of_user.or_undefined([]) # Kind::Undefined kind_of_user.or_undefined(User.new) # # # kind_of_user.or(fallback, value) # Can return a fallback if the given value isn't an instance of its kind kind_of_user.or(nil, 0) # nil kind_of_user.or(nil, User.new) # # # 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 was wrong. [1, User.new].map(&kind_of_user.or(nil)) # [nil, #] # An error will be raised if the fallback didn't have the expected kind or if not nil / Kind::Undefined. [0, User.new].map(&kind_of_user.or(:foo)) # Kind::Error (:foo expected to be a kind of User) # kind_of_user[value] # Will raise Kind::Error if the given value isn't an instance of the expected kind kind_of_user[:foo] # Kind::Error (:foo expected to be a kind of User) kind_of_user[User.new] # # # kind_of_user.value(arg, default:) # 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_of_user.value(User.new, default: User.new) # # kind_of_user.value('1', default: User.new) # # kind_of_user.value('1', default: 1) # Kind::Error (1 expected to be a kind of User) # kind_of_user.maybe # This method returns a typed Kind::Maybe. kind_of_user.maybe('1').value_or(User.new) # # ``` [⬆️  Back to Top](#table-of-contents-) ##### Using an object which responds to === Example using a lambda (an object which responds to .===) and a hash with the kind name. ```ruby PositiveInteger = Kind::Of( -> value { value.kind_of?(Integer) && value > 0 }, name: 'PositiveInteger' ) # PositiveInteger.name # PositiveInteger.kind # The type checker can return its kind and its name PositiveInteger.name # "PositiveInteger" PositiveInteger.kind # # # PositiveInteger.=== # Can check if a given value is an instance of its kind. PositiveInteger === 1 # true PositiveInteger === 0 # false # PositiveInteger.value?(value) # Can check if a given value is an instance of its kind. PositiveInteger.value?(1) # true PositiveInteger.value?(-1) # false # If it doesn't receive an argument, a lambda will be returned and it will know how to do the type verification. [1, 2, 0, 3, -1].select?(&PositiveInteger.value?) # [1, 2, 3] # PositiveInteger.or_nil(value) # Can return nil if the given value isn't an instance of its kind PositiveInteger.or_nil(1) # 1 PositiveInteger.or_nil(0) # nil # PositiveInteger.or_undefined(value) # Can return Kind::Undefined if the given value isn't an instance of its kind PositiveInteger.or_undefined(2) # 2 PositiveInteger.or_undefined(-1) # Kind::Undefined # PositiveInteger.or(fallback, value) # Can return a fallback if the given value isn't an instance of its kind PositiveInteger.or(nil, 1) # 1 PositiveInteger.or(nil, 0) # nil # 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 was wrong. [1, 2, 0, 3, -1].map(&PositiveInteger.or(1)) # [1, 2, 1, 3, 1] [1, 2, 0, 3, -1].map(&PositiveInteger.or(nil)) # [1, 2, nil, 3, nil] # An error will be raised if the fallback didn't have the expected kind or if not nil / Kind::Undefined. [1, 2, 0, 3, -1].map(&PositiveInteger.or(:foo)) # Kind::Error (:foo expected to be a kind of PositiveInteger) # PositiveInteger[value] # Will raise Kind::Error if the given value isn't an instance of the expected kind PositiveInteger[1] # 1 PositiveInteger[:foo] # Kind::Error (:foo expected to be a kind of PositiveInteger) # PositiveInteger.value(arg, default:) # 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. PositiveInteger.value(2, default: 1) # 2 PositiveInteger.value('1', default: 1) # 1 PositiveInteger.value('1', default: 0) # Kind::Error (0 expected to be a kind of PositiveInteger) # PositiveInteger.maybe # This method returns a typed Kind::Maybe. PositiveInteger.maybe(0).value_or(1) # 1 PositiveInteger.maybe(2).value_or(1) # 2 ``` [⬆️  Back to Top](#table-of-contents-) #### Kind:: module The idea here is to create a type checker inside of the `Kind` namespace, so to do this, you will only need to use pure Ruby. e.g: ```ruby class User end module Kind module User extend self, TypeChecker # Define the expected kind of this type checker. def kind; ::User; end end # This how the Kind::? methods are defined. def self.User?(*values) KIND.of?(::User, values) end end # Doing this you will have the same methods of a standard type checker (like: `Kind::Symbol`). user = User.new Kind::User[user] # # Kind::User[{}] # Kind::Error ({} expected to be a kind of User) Kind::User?(user) # true Kind::User?({}) # false ``` The advantages of this approach are: 1. You will have a singleton (a unique instance) to be used, so the garbage collector will work less. 2. You can define additional methods to be used with this kind. The disadvantage is: 1. You could overwrite some standard type checker 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](#table-of-contents-) ### Utility methods #### Kind.of_class?() This method verify if a given value is a `Class`. ```ruby Kind.of_class?(Hash) # true Kind.of_class?(Enumerable) # false Kind.of_class?(1) # false ``` [⬆️  Back to Top](#table-of-contents-) #### Kind.of_module?() This method verify if a given value is a `Module`. ```ruby Kind.of_module?(Hash) # false Kind.of_module?(Enumerable) # true Kind.of_module?(1) # false ``` [⬆️  Back to Top](#table-of-contents-) #### 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. ```ruby Kind.of_module_or_class(String) # String Kind.of_module_or_class(1) # Kind::Error (1 expected to be a kind of Module/Class) ``` [⬆️  Back to Top](#table-of-contents-) #### 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. ```ruby Kind.respond_to('', :upcase) # "" Kind.respond_to('', :upcase, :strip) # "" Kind.respond_to(1, :upcase) # expected 1 to respond to :upcase Kind.respond_to(2, :to_s, :upcase) # expected 2 to respond to :upcase ``` [⬆️  Back to Top](#table-of-contents-) #### 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. ```ruby Kind.of(Hash, {}) # {} Kind.of(Hash, []) # Kind::Error ([] expected to be a kind of Hash) ``` [⬆️  Back to Top](#table-of-contents-) #### Kind.of?() This method can be used to check if one or multiple values have the expected kind. ```ruby # Checking one value Kind.of?(Array, []) # true Kind.of?(Array, {}) # false # Checking multiple values Kind.of?(Enumerable, [], {}) # true Kind.of?(Hash, {}, {}) # true Kind.of?(Array, [], {}) # false ``` 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. ```ruby [1, '2', 3].select(&Kind.of?(Numeric)) # [1, 3] ``` [⬆️  Back to Top](#table-of-contents-) #### 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. ```ruby Kind.value(String, '1', default: '') # "1" Kind.value(String, 1, default: '') # "" Kind.value(String, 1, default: 2) # Kind::Error (2 expected to be a kind of String) ``` [⬆️  Back to Top](#table-of-contents-) #### Kind.is() You can use `Kind.is` to verify if some class has the expected type as its ancestor. ```ruby Kind.is(Hash, String) # false Kind.is(Hash, Hash) # true Kind.is(Enumerable, Hash) # true ``` The `Kind.is` also could check the inheritance of Classes/Modules. ```ruby # # Verifying if a class is or inherits from the expected class. # class Human; end class Person < Human; end class User < Human; end Kind.is(Human, User) # true Kind.is(Human, Human) # true Kind.is(Human, Person) # true Kind.is(Human, Struct) # false # # Verifying if the classes included a module. # module Human; end class Person; include Human; end class User; include Human; end Kind.is(Human, User) # true Kind.is(Human, Human) # true Kind.is(Human, Person) # true Kind.is(Human, Struct) # false # # Verifying if a class is or inherits from the expected module. # module Human; end module Person; extend Human; end module User; extend Human; end Kind.is(Human, User) # true Kind.is(Human, Human) # true Kind.is(Human, Person) # true Kind.is(Human, Struct) # false ``` [⬆️  Back to Top](#table-of-contents-) ### 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. ```ruby Kind::Try.(' foo ', :strip) # "foo" Kind::Try.({a: 1}, :[], :a) # 1 Kind::Try.({a: 1}, :[], :b) # nil Kind::Try.({a: 1}, :fetch, :b, 2) # 2 Kind::Try.(:symbol, :strip) # nil Kind::Try.(:symbol, :fetch, :b, 2) # nil # It raises an exception if the method name isn't a string or a symbol Kind::Try.({a: 1}, 1, :a) # TypeError (1 is not a symbol nor a string) ``` This module has the method `[]` that knows how to create a lambda that will know how to perform the `try` strategy. ```ruby results = [ {}, {name: 'Foo Bar'}, {name: 'Rodrigo Serradura'}, ].map(&Kind::Try[:fetch, :name, 'John Doe']) p results # ["John Doe", "Foo Bar", "Rodrigo Serradura"] ``` [⬆️  Back to Top](#table-of-contents-) #### Kind::Dig The method `.call` of this module has the same behavior of Ruby dig methods ([Hash](https://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig), [Array](https://ruby-doc.org/core-2.3.0/Array.html#method-i-dig), [Struct](https://ruby-doc.org/core-2.3.0/Struct.html#method-i-dig), [OpenStruct](https://ruby-doc.org/stdlib-2.3.0/libdoc/ostruct/rdoc/OpenStruct.html#method-i-dig)), but it will not raise an error if some step can't be digged. ```ruby 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]) # 101 Kind::Dig.(o, [:c]) # 103 Kind::Dig.(d, [:struct, :b]) # 102 Kind::Dig.(d, [:data, 0, :b]) # 102 Kind::Dig.(d, [:data, 0, 'b']) # 102 Kind::Dig.(d, [:ostruct, :d]) # 104 Kind::Dig.(d, [:data, 1, :d]) # 104 Kind::Dig.(d, [:data, 1, 'd']) # 104 Kind::Dig.(d, [:struct, :f]) # nil Kind::Dig.(d, [:ostruct, :f]) # nil Kind::Dig.(d, [:data, 0, :f]) # nil Kind::Dig.(d, [:data, 1, :f]) # nil ``` Another difference between the `Kind::Dig` and the native Ruby dig, is that it knows how to extract values from regular objects. ```ruby class Person attr_reader :name def initialize(name) @name = name end end person = Person.new('Rodrigo') Kind::Dig.(person, [:name]) # "Rodrigo" Kind::Dig.({people: [person]}, [:people, 0, :name]) # "Rodrigo" ``` This module has the method `[]` that knows how to create a lambda that will know how to perform the `dig` strategy. ```ruby results = [ { person: {} }, { person: { name: 'Foo Bar'} }, { person: { name: 'Rodrigo Serradura'} }, ].map(&Kind::Dig[:person, :name]) p results # [nil, "Foo Bar", "Rodrigo Serradura"], ``` [⬆️  Back to Top](#table-of-contents-) #### Kind::Presence The method `.call` of this module returns the given value if it's present otherwise it will return `nil`. ```ruby Kind::Presence.(true) # true Kind::Presence.('foo') # "foo" Kind::Presence.([1, 2]) # [1, 2] Kind::Presence.({a: 3}) # {a: 3} Kind::Presence.(Set.new([4])) # # Kind::Presence.('') # nil Kind::Presence.(' ') # nil Kind::Presence.("\t\n\r") # nil Kind::Presence.("\u00a0") # nil Kind::Presence.([]) # nil Kind::Presence.({}) # nil Kind::Presence.(Set.new) # nil Kind::Presence.(nil) # nil Kind::Presence.(false) # nil # nil will be returned if the given object responds to the method blank? and this method result is true. 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) # nil 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: ```ruby ['', [], {}, '1', [2]].map(&Kind::Presence) # [nil, nil, nil, "1", [2]] ``` [⬆️  Back to Top](#table-of-contents-) ## Kind::Undefined The [`Kind::Undefined`](https://github.com/serradura/kind/blob/1674bab/lib/kind/undefined.rb) constant can be used to distinguish the usage of `nil`. If you are interested, check out [the tests](https://github.com/serradura/kind/blob/main/test/kind/undefined_test.rb) to understand its methods. [⬆️  Back to Top](#table-of-contents-) ## 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. ```ruby optional = Kind::Maybe.new(2) .map { |value| value * 2 } .map { |value| value * 2 } puts optional.value # 8 puts optional.some? # true puts optional.none? # false puts optional.value_or(0) # 8 puts optional.value_or { 0 } # 8 ################# # Returning nil # ################# optional = Kind::Maybe.new(3) .map { nil } .map { |value| value * 3 } puts optional.value # nil puts optional.some? # false puts optional.none? # true puts optional.value_or(0) # 0 puts optional.value_or { 0 } # 0 ############################# # Returning Kind::Undefined # ############################# optional = Kind::Maybe.new(4) .map { Kind::Undefined } .map { |value| value * 4 } puts optional.value # Kind::Undefined puts optional.some? # false puts optional.none? # true puts optional.value_or(1) # 1 puts optional.value_or { 1 } # 1 ``` [⬆️  Back to Top](#table-of-contents-) #### Replacing blocks by lambdas ```ruby 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) # 3 # -- Kind::Maybe.new([]).map(&Add).value_or(0) # 0 Kind::Maybe.new({}).map(&Add).value_or(0) # 0 Kind::Maybe.new(nil).map(&Add).value_or(0) # 0 ``` [⬆️  Back to Top](#table-of-contents-) ### 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. ```ruby result = Kind::Maybe[5] .then { |value| value * 5 } .then { |value| value + 17 } .value_or(0) puts result # 42 ``` You can also use `Kind::Maybe.wrap()` instead of the `.new` method. ```ruby result = Kind::Maybe .wrap(5) .then { |value| value * 5 } .then { |value| value + 17 } .value_or(0) puts result # 42 ``` [⬆️  Back to Top](#table-of-contents-) #### Replacing blocks by lambdas ```ruby 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) # 3 # -- Kind::Maybe[1].then(&Add).value_or(0) # 0 Kind::Maybe['2'].then(&Add).value_or(0) # 0 Kind::Maybe[nil].then(&Add).value_or(0) # 0 ``` [⬆️  Back to Top](#table-of-contents-) ### 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: ```ruby 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) # 3 Kind::Maybe[1].then(&Add).value_or(0) # 0 # -- Add.(a: 2, b: 2).then(&Double).value # 8 ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::Optional The `Kind::Optional` constant is an alias for `Kind::Maybe`. e.g: ```ruby result1 = Kind::Optional .new(5) .map { |value| value * 5 } .map { |value| value - 10 } .value_or(0) puts result1 # 15 # --- result2 = Kind::Optional[5] .then { |value| value * 5 } .then { |value| value + 10 } .value_or { 0 } puts result2 # 35 ``` [⬆️  Back to Top](#table-of-contents-) #### Replacing blocks by lambdas ```ruby 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) # 4 # -- Kind::Optional['2'].then(&Double).value_or(0) # 0 Kind::Optional[nil].then(&Double).value_or(0) # 0 ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::Maybe() You can use `Kind::Maybe()` or `Kind::Optional()` to create a maybe monad which will return None if the given input hasn't the expected type. e.g: ```ruby result1 = Kind::Maybe(Numeric) .wrap(5) .then { |value| value * 5 } .value_or { 0 } puts result1 # 25 # --- result2 = Kind::Optional(Numeric) .wrap('5') .then { |value| value * 5 } .value_or { 0 } puts result2 # 0 ``` This typed maybe has the same methods of `Kind::Maybe` class. e.g: ```ruby 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: ```ruby 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('') # "John Doe" person_name(nil) # "John Doe" person_name(first_name: 'Rodrigo') # "John Doe" person_name(last_name: 'Serradura') # "John Doe" person_name(first_name: 'Rodrigo', last_name: 'Serradura') # "Rodrigo Serradura" # # See below the previous implementation without using the maybe monad. # 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. ```ruby 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 # # See below the previous implementation without using an optional. # 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](#table-of-contents-) ### 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. ```ruby 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. ```ruby # Handling StandardError exceptions result1 = Kind::Maybe[2].map { |number| number / 0 } result1.none? # true result1.value # # result2 = Kind::Maybe[3].then { |number| number / 0 } result2.none? # true 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. ```ruby # Leaking StandardError exceptions Kind::Maybe[2].map! { |number| number / 0 } # ZeroDivisionError (divided by 0) Kind::Maybe[2].then! { |number| number / 0 } # ZeroDivisionError (divided by 0) ``` > **Note:** If an exception (at StandardError level) is returned by the methods `#then`, `#map` it will be resolved as None. [⬆️  Back to Top](#table-of-contents-) ### 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. ```ruby object = 'foo' Kind::Maybe[object].try(:upcase).value # "FOO" Kind::Maybe[{}].try(:fetch, :number, 0).value # 0 Kind::Maybe[{number: 1}].try(:fetch, :number).value # 1 Kind::Maybe[object].try { |value| value.upcase }.value # "FOO" ############# # Nil value # ############# object = nil Kind::Maybe[object].try(:upcase).value # nil Kind::Maybe[object].try { |value| value.upcase }.value # nil ######################### # Kind::Undefined value # ######################### object = Kind::Undefined Kind::Maybe[object].try(:upcase).value # nil Kind::Maybe[object].try { |value| value.upcase }.value # nil ``` > **Note:** You can use the `#try` method with `Kind::Optional` objects. [⬆️  Back to Top](#table-of-contents-) ### 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. ```ruby Kind::Maybe[{}].try(:upcase) # => # Kind::Maybe[{}].try!(:upcase) # => NoMethodError (undefined method `upcase' for {}:Hash) ``` > **Note:** You can also use the `#try!` method with `Kind::Optional` objects. [⬆️  Back to Top](#table-of-contents-) ### Kind::Maybe#dig Has the same behavior of Ruby dig methods ([Hash](https://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig), [Array](https://ruby-doc.org/core-2.3.0/Array.html#method-i-dig), [Struct](https://ruby-doc.org/core-2.3.0/Struct.html#method-i-dig), [OpenStruct](https://ruby-doc.org/stdlib-2.3.0/libdoc/ostruct/rdoc/OpenStruct.html#method-i-dig)), but it will not raise an error if some value can't be digged. ```ruby [nil, 1, '', /x/].each do |value| p Kind::Maybe[value].dig(:foo).value # nil end # -- a = [1, 2, 3] Kind::Maybe[a].dig(0).value # 1 Kind::Maybe[a].dig(3).value # nil # -- h = { foo: {bar: {baz: 1}}} Kind::Maybe[h].dig(:foo).value # {bar: {baz: 1}} Kind::Maybe[h].dig(:foo, :bar).value # {baz: 1} Kind::Maybe[h].dig(:foo, :bar, :baz).value # 1 Kind::Maybe[h].dig(:foo, :bar, 'baz').value # nil # -- i = { foo: [{'bar' => [1, 2]}, {baz: [3, 4]}] } Kind::Maybe[i].dig(:foo, 0, 'bar', 0).value # 1 Kind::Maybe[i].dig(:foo, 0, 'bar', 1).value # 2 Kind::Maybe[i].dig(:foo, 0, 'bar', -1).value # 2 Kind::Maybe[i].dig(:foo, 0, 'bar', 2).value # nil # -- 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 # 101 Kind::Maybe[b].dig(:struct, :b).value # 102 Kind::Maybe[b].dig(:data, 0, :b).value # 102 Kind::Maybe[b].dig(:data, 0, 'b').value # 102 Kind::Maybe[o].dig(:c).value # 103 Kind::Maybe[b].dig(:ostruct, :d).value # 104 Kind::Maybe[b].dig(:data, 1, :d).value # 104 Kind::Maybe[b].dig(:data, 1, 'd').value # 104 Kind::Maybe[s].dig(:f).value # nil Kind::Maybe[o].dig(:f).value # nil Kind::Maybe[b].dig(:struct, :f).value # nil Kind::Maybe[b].dig(:ostruct, :f).value # nil Kind::Maybe[b].dig(:data, 0, :f).value # nil Kind::Maybe[b].dig(:data, 1, :f).value # nil ``` > **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: ```ruby Kind::Maybe(Array) .wrap(['Rodrigo', 'Serradura']) .then { |names| names.map(&Kind::Presence).tap(&:compact!) } .check { |names| names.size == 2 } .value # ["Rodrigo", "Serradura"] ``` [⬆️  Back to Top](#table-of-contents-) ### Kind::Maybe#presence This method will return None if the wrapped value wasn't present. ```ruby result = Kind::Maybe(Hash).wrap(foo: '').dig(:foo).presence result.none? # true result.value # nil ``` [⬆️  Back to Top](#table-of-contents-) ## 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: ```ruby def something(params = {}) params.object_id end puts something # 70312470300460 puts something # 70312470295800 puts something # 70312470278400 puts something # 70312470273800 ``` 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: ```ruby 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: ```ruby 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](#table-of-contents-) ## Kind::Validator (ActiveModel::Validations) This module enables the capability to validate types via [`ActiveModel::Validations >= 3.2, < 6.1.0`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html). e.g ```ruby 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: ```ruby # In some Gemfile gem 'kind', require: 'kind/validator' # In some .rb file require 'kind/validator' ``` [⬆️  Back to Top](#table-of-contents-) ### Usage #### [Object#===](https://ruby-doc.org/core-3.0.0/Object.html#method-i-3D-3D-3D) ```ruby validates :name, kind: { of: String } ``` Use an array to verify if the attribute is an instance of one of the classes/modules. ```ruby validates :status, kind: { of: [String, Symbol]} ``` Because of kind verification be made via `===` you can use type checkers as the expected kinds. ```ruby validates :alive, kind: Kind::Boolean ``` [⬆️  Back to Top](#table-of-contents-) #### [Kind.is](#verifying-the-kind-of-some-classmodule) ```ruby # # Verifying if the attribute value is the class or a subclass. # class Human; end class Person < Human; end class User < Human; end validates :human_kind, kind: { is: Human } # # Verifying if the attribute value is the module or if it is a class that includes the module # module Human; end class Person; include Human; end class User; include Human; end validates :human_kind, kind: { is: Human } # # Verifying if the attribute value is the module or if it is a module that extends the module # module Human; end module Person; extend Human; end module User; extend Human; end validates :human_kind, kind: { is: Human } # or use an array to verify if the attribute # is a kind of one those classes/modules. validates :human_kind, kind: { is: [Person, User] } ``` [⬆️  Back to Top](#table-of-contents-) #### [Object#instance_of?](https://ruby-doc.org/core-3.0.0/Object.html#method-i-instance_of-3F) ```ruby validates :name, kind: { instance_of: String } # or use an array to verify if the attribute # is an instance of one of the classes/modules. validates :name, kind: { instance_of: [String, Symbol] } ``` [⬆️  Back to Top](#table-of-contents-) #### [Object#respond_to?](https://ruby-doc.org/core-3.0.0/Object.html#method-i-respond_to-3F) ```ruby validates :handler, kind: { respond_to: :call } ``` This validation can verify one or multiple methods. ```ruby validates :params, kind: { respond_to: [:[], :values_at] } ``` [⬆️  Back to Top](#table-of-contents-) #### Array.new.all? { |item| item.kind_of?(Class) } ```ruby validates :account_types, kind: { array_of: String } # or use an array to verify if the attribute # is an instance of one of the classes validates :account_types, kind: { array_of: [String, Symbol] } ``` [⬆️  Back to Top](#table-of-contents-) #### Array.new.all? { |item| expected_values.include?(item) } ```ruby # Verifies if the attribute value # is an array with some or all the expected values. validates :account_types, kind: { array_with: ['foo', 'bar'] } ``` [⬆️  Back to Top](#table-of-contents-) ### Defining the default validation strategy By default, you can define the attribute type directly (without a hash). e.g. ```ruby validates :name, kind: String # or validates :name, kind: [String, Symbol] ``` To changes this behavior you can set another strategy to validates the attributes types: ```ruby Kind::Validator.default_strategy = :instance_of # Tip: Create an initializer if you are in a Rails application. ``` And these are the available options to define the default strategy: - `kind_of` *(default)* - `instance_of` [⬆️  Back to Top](#table-of-contents-) ### Using the `allow_nil` and `strict` options You can use the `allow_nil` option with any of the kind validations. e.g. ```ruby 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. ```ruby validates :first_name, kind: String, strict: true # or validates! :last_name, kind: String ``` [⬆️  Back to Top](#table-of-contents-) ## Similar Projects - [dry-types](https://dry-rb.org/gems/dry-types) ## 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](https://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](https://github.com/serradura/kind/blob/master/CODE_OF_CONDUCT.md). ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## 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](https://github.com/serradura/kind/blob/master/CODE_OF_CONDUCT.md).