Skip to content

Instantly share code, notes, and snippets.

@wojtha
Created January 14, 2017 19:39
Show Gist options
  • Select an option

  • Save wojtha/130e6533b9215dffde7c6951efa4a3ac to your computer and use it in GitHub Desktop.

Select an option

Save wojtha/130e6533b9215dffde7c6951efa4a3ac to your computer and use it in GitHub Desktop.
Status factory lib for Ruby
class Result
include StatusFactory.new(:create, :update, :delete, :identical, callbacks: true)
end
class Result
def self.create; new(:create); end
def self.update; new(:update); end
def self.delete; new(:delete); end
def self.identical; new(:identical); end
def initialize(status)
@status = status
end
def create?
@status == :create
end
def update?
@status == :update
end
def delete?
@status == :delete
end
def identical?
@status == :identical
end
def on_create
yield if create?
end
def on_update
yield if update?
end
def on_delete
yield if delete?
end
def on_identical
yield if identical?
end
end
# 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
require 'spec_helper'
describe StatusFactory, '.new' do
context 'with statuses :succes, and :failure' do
let(:status_class) {
Class.new do
include StatusFactory.new(:success, :failure, query: true, callbacks: true, factory: true, equality: true)
end
}
describe 'factory methods' do
it 'defines class factory methods for all statuses' do
expect( status_class ).to respond_to(:success)
expect( status_class ).to respond_to(:failure)
end
end
context 'when target class initialized with :success' do
subject { status_class.new(:success) }
it 'returns true for #success? and false for #failure' do
is_expected.to have_attributes(
success?: true,
failure?: false,
)
end
it 'yields for #on_success' do
expect { |b| subject.on_success(&b) }.to yield_with_no_args
end
it 'does not yield for #on_failure' do
expect { |b| subject.on_failure(&b) }.not_to yield_control
end
end
describe '#eql? equality' do
it 'status :success is equal to :success' do
status = status_class.new(:success)
other = status_class.new(:success)
expect( status ).to eql other
end
it 'status :success is not equal to :failure' do
status = status_class.new(:success)
other = status_class.new(:failure)
expect( status ).not_to eql other
end
end
describe '#== equality' do
it 'status :success is equal to :success' do
status = status_class.new(:success)
other = status_class.new(:success)
expect( status ).to eq other
end
it 'status :success is not equal to :failure' do
status = status_class.new(:success)
other = status_class.new(:failure)
expect( status ).not_to eq other
end
end
describe '#hash'do
it 'status :success is equal to :success' do
status = status_class.new(:success)
other = status_class.new(:success)
expect( status.hash ).to eq other.hash
end
it 'status :success is not equal to :failure' do
status = status_class.new(:success)
other = status_class.new(:failure)
expect( status.hash ).not_to eq other.hash
end
end
end
describe 'with method override' do
let(:status_class) {
Class.new do
include StatusFactory.new(:success, :failure)
def success?
'hell yes!'
end
def on_success(&block)
block.call('hell yes!') if success?
end
end
}
subject { status_class.new(:success) }
it 'returns overriden result' do
expect( subject.success? ).to eq 'hell yes!'
end
end
end
describe StatusFactory::Builder, '.new' do
context 'with every option on' do
let(:status_class) {
status_builder = StatusFactory::Builder.new(query: true, callbacks: true, factory: true, equality: true)
Class.new do
include status_builder.new(:success)
end
}
let(:status_instance) { status_class.new(:success) }
it 'defines class factory methods' do
expect( status_class ).to respond_to(:success)
end
it 'defines query methods' do
expect( status_instance ).to respond_to(:success?)
end
it 'defines callback methods' do
expect( status_instance ).to respond_to(:success?)
end
end
context 'with every option off' do
let(:status_class) {
status_builder = StatusFactory::Builder.new(query: false, callbacks: false, factory: false, equality: false)
Class.new do
include status_builder.new(:success)
end
}
let(:status_instance) { status_class.new(:success) }
it 'defines class factory methods' do
expect( status_class ).to_not respond_to(:success)
end
it 'defines query methods' do
expect( status_instance ).to_not respond_to(:success?)
end
it 'defines callback methods' do
expect( status_instance ).to_not respond_to(:success?)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment