Skip to content

Instantly share code, notes, and snippets.

@wojtha
Created March 31, 2020 09:43
Show Gist options
  • Save wojtha/a16e45a6171c75433c2a39f038521cfa to your computer and use it in GitHub Desktop.
Save wojtha/a16e45a6171c75433c2a39f038521cfa to your computer and use it in GitHub Desktop.
Ruby: Class StatusFactory is module factory to generate Status classes
# Class StatusFactory is module factory to generate Status classes
#
# @example Status class definition
#
# class Result
# include StatusFactory.new(:success, :failure, query: true, callbacks: true, factory: true, equality: true)
# end
#
# # is equivalent of:
#
# class Result
#
# # Factory methods
#
# def self.success, *args, **kwargs)
# new(:success, *args, **kwargs)
# end
#
# def self.failure, *args, **kwargs)
# new(:failure, *args, **kwargs)
# end
#
# # Base methods, included everytime
#
# def initialize(status, *, **)
# @status = status
# end
#
# def status
# @status
# end
#
# # Query methods
#
# def success?
# @status == :success
# end
#
# def failure?
# @status == :failure
# end
#
# # Equality methods
#
# def hash
# status.hash
# end
#
# def eql?(other)
# instance_of?(other.class) && @status.eql?(other.status)
# end
#
# def ==(other)
# other.kind_of?(self.class) && @status == other.status
# end
#
# end
#
# @example Use in the code
#
# class LuckService
# def call
# result =
# if rand(10) > 4
# Result.success
# else
# Result.failure
# end
# yield result if block_given?
# result
# end
# end
#
# result = LuckService.new.call
#
# if result.success?
# puts "tada"
# end
#
# case result.status
# when :success then puts "tada"
# when :failure then puts "oops"
# end
#
# LuckService.new.call do |r|
# r.on_success { puts "tada" }
# r.on_failure { puts "oops" }
# end
#
# @see Confident Ruby by Avdi Grimm, chapter Yield a status object
# @see Module Factory description https://blog.codeminer42.com/cracking-the-box-open-with-module-factories-2ef048193a7e#.jl953uu5v
# @see Equalizer for some inspiration https://github.com/dkubb/equalizer
#
class StatusFactory < Module
# Class Builder allows to prefabricate StatusFactory reusable configurations.
#
# @example
# SimpleStatusFactory = StatusFactory::Builder.new(query: true, factory: true)
# RichStatusFactory = StatusFactory::Builder.new(query: true, factory: true, callbacks: true, equality: true)
#
# class Result
# include SimpleStatusFactory.new(:success, :failure)
# # or
# include RichStatusFactory.new(:success, :failure)
# # or
# # Note: You can still override the builder defaults
# include RichStatusFactory.new(:success, :failure, equality: false)
# end
#
class Builder
# Initialize StatusFactory::Builder with the given options
#
# @param see StatusFactory#initialize
#
def initialize(query: false, factory: false, callbacks: false, equality: false)
@default_options = {
query: query,
callbacks: callbacks,
factory: factory,
equality: equality,
}
end
# Emulates Class#new
#
# @param [Array<Symbol>] *statuses
# The list of statuses
# @param [Hash] **overrides
# The overriden options see StatusFactory#initialize
#
# @return [StatusFactory] the factory module instance with applied defaults
def new(*statuses, **overrides)
StatusFactory.new(*statuses, **@default_options.merge(overrides))
end
end
# Predefined factory builder with all options disabled
#
# @return [StatusFactory::Builder]
Empty = Builder.new(query: false, callbacks: false, factory: false, equality: false)
# Predefined factory builder with all options enabled
#
# @return [StatusFactory::Builder]
Full = Builder.new(query: true, callbacks: true, factory: true, equality: true)
# Initialize StatusFactory with the given statuses and options
#
# @param [Array<Symbol>] *statuses
# The list of statuses
# @param [Boolean] query: true
# Whether to include query methods like #success?
# @param [Boolean] callbacks: false
# Whether to include callback methods like #on_success
# @param [Boolean] factory: false
# Whether to include class factory methods like .success
# @param [Boolean] equality: false <description>
# Whether to include class factory methods like .success
#
# @return [undefined]
#
# @api private
def initialize(*statuses, query: true, factory: true, callbacks: false, equality: false)
@statuses = [*statuses].map(&:to_sym)
@query_methods = query
@callback_methods = callbacks
@factory_methods = factory
@equality_methods = equality
define_query_methods if @query_methods
define_callback_methods if @callback_methods
freeze
end
private
# @api private
def included(descendant)
super
descendant.include Methods
descendant.include Equality if @equality_methods
define_factory_methods(descendant) if @factory_methods
end
# @api private
def define_query_methods
@statuses.each do |status|
define_method("#{status}?") do
@status == status
end
end
end
# @api private
def define_callback_methods
@statuses.each do |status|
# Interesting, can't be replaced with yield, with yield it gives the following:
# LocalJumpError: no block given (yield)
define_method("on_#{status}") do |&block|
block.call if @status == status
end
end
end
# @api private
def define_factory_methods(descendant)
@statuses.each do |status|
descendant.define_singleton_method(status) do |*args, **kwargs|
new(status, *args, **kwargs)
end
end
end
# Module StatusFactory::Methods contains methods included in every descendant.
#
module Methods
def initialize(status, *, **)
@status = status
end
# @return [Symbol]
# the status itself
#
# @api public
def status
@status
end
end
# Module StatusFactory::Equality contains methods included in descendats with enabled equality
#
module Equality
# @return [String]
# the status hash
#
# @api public
def hash
@status.hash
end
# Compare the object with other object for equality
#
# @example
# object.eql?(other) # => true or false
#
# @param [Object] other
# the other object to compare with
#
# @return [Boolean]
#
# @api public
def eql?(other)
instance_of?(other.class) && @status.eql?(other.status)
end
# Compare the object with other object for equivalency
#
# @example
# object == other # => true or false
#
# @param [Object] other
# the other object to compare with
#
# @return [Boolean]
#
# @api public
def ==(other)
other.kind_of?(self.class) && @status == other.status
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment