Skip to content

Instantly share code, notes, and snippets.

@denisoster
Created September 21, 2020 05:43
Show Gist options
  • Select an option

  • Save denisoster/ad01a27809dad695e6657644d550e48b to your computer and use it in GitHub Desktop.

Select an option

Save denisoster/ad01a27809dad695e6657644d550e48b to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
module Callable
# Makes object callable.
# Instance method #call must be defined.
# Usage:
#
# class SomeService
# include Callable
#
# def initialize(foo, bar)
# @foo = foo
# @bar = bar
# end
#
# def call
# return @foo + @bar
# end
# end
#
# SomeService.call(1, 2) # returns 3
#
extend ActiveSupport::Concern
class_methods do
def call(*args)
new(*args).call
end
end
end
# frozen_string_literal: true
module Loggable
# Adds logger on instance an class level.
# By default it delegates to `Rails.logger`.
#
# class SomeService
# include Loggable
# end
#
# SomeService.logger.debug { 'foo' }
# SomeService.new.logger.debug { 'bar' }
#
extend ActiveSupport::Concern
included do
class_attribute :logger, instance_writer: false, default: Rails.logger
end
end
# frozen_string_literal: true
module Memoizable
# Allows to define memoized methods with ease.
# Usage:
#
# class SomeService
# include Memoizable
#
# def initialize(customer_id)
# @customer_id = customer_id
# end
#
# define_memoizable :customer do
# customer.find(customer_id: @customer_id) if @customer_id
# end
# end
#
# s = SomeService.new(5) # no sql query
# s.customer # one sql query
# s.customer # no sql query - memoized value returned
# s = SomeService.new(nil) # no sql query
# s.customer # no sql query but value is memoized
# s.customer # no sql query - memoized value returned
#
extend ActiveSupport::Concern
class_methods do
# Creates memoized method which only executes once and return cached value after that.
# @param name [Symbol, String] - name of method
# @param variable [Symbol, String] - name of instance variable (default `__memoized_#{name}`)
# @param apply [Proc] - provide lambda instead of block here if needed
# @yield once per instance
# @yieldreturn value that will be memoized and returned on next accesses to this method
def define_memoizable(name, variable: nil, apply: nil, &block)
variable_name = :"@#{variable || "__memoized_#{name.to_s.gsub(/[!?]+/, '__')}"}"
apply = block if block_given?
raise ArgumentError, 'provide :apply callable object or block' if apply.nil?
define_method(name) do
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name)
instance_variable_set variable_name, instance_exec(&apply)
end
name.to_sym
end
end
end
# frozen_string_literal: true
module Servicable
# Base mixin for services.
# Usage:
#
# class SomeService
# include Servicable
# parameter :foo, required: true
# parameter :is_baz, reader: baz?
# parameter :reason, default: 'whatever'
# parameter :by_who, default: -> { Thread.current[:by_who] }
# parameter(:start_date) { |val| val.is_a?(Time) ? val : Time.parse(val) }
#
# def call
# SomeApi.do_something!(
# foo: foo,
# is_baz: baz?,
# reason: reason,
# by_who: by_who,
# start_date: start_date
# )
# end
# end
#
# SomeService.call(foo: 'bar', baz: true)
extend ActiveSupport::Concern
include Callable
include Loggable
include Memoizable
include ErrorNotificationMethods
class_methods do
# allows to define multiple param getters with same options
# @see #parameter
# @param names [Array<Symbol,Hash>] - list of names with optional options at the end.
def parameters(*names, &block)
options = names.extract_options!
options.assert_valid_keys(:required, :default, :param_name)
raise ArgumentError, 'must be at least one name' if names.size == 0
names.each do |name|
parameter(name, options, &block)
end
end
# Defines parameter getter.
# Keep in mind that default value will not call apply/block.
# @param name [Symbol, String] - getter name
# @param required [TrueClass,FalseClass] - is param required or not (default false)
# @param default [Object] - default value for param (will be ignored when used with `required: true`)
# @param reader [Symbol] - name for reader method (default `name`)
# @yield optional formatter for param value
# @yieldparam [Object] yields value of param or default
# @yieldreturn [Object] value that will be memoized and returned by `name` method
def parameter(name, required: false, default: nil, reader: nil, &block)
name = name.to_sym
reader ||= name
self._parameters = _parameters.merge(name => { required: required, default: default, reader: reader })
default_proc = default.is_a?(Proc) ? default : proc { default }
if block_given?
define_memoizable(reader) do
value = params.fetch(name) { instance_exec(&default_proc) }
params.key?(name) ? instance_exec(value, &block) : value
end
else
define_memoizable(reader) do
params.fetch(name) { instance_exec(&default_proc) }
end
end
end
# Marks parameters as allowed without defining method
# @param names [Array<Symbol, String>] - parameter names
def allowed_parameters(*names)
extra = names.uniq.map { |name| [name.to_sym, extra: true] }.to_h
self._parameters = _parameters.merge(extra)
end
# Remove parameters from list of allowed params
# @param names [Array<Symbol, String>] - parameter names
def remove_parameters(*names)
names = names.map(&:to_sym)
self._parameters = _parameters.except(*names)
end
# Allows params keys that not explicitly defined using `parameter(s)` or `allowed_parameters`.
# Without calling this method extra parameters will be disallowed by default.
# @param flag [TrueClass,FalseClass] - is extra parameters allowed or not (default true)
def allow_extra_parameters(flag = true)
self._allow_extra_parameters = flag
end
end
included do
class_attribute :_parameters, instance_writer: false, default: {}
class_attribute :_allow_extra_parameters, instance_writer: false, default: false
attr_reader :params
end
# @param params [Hash<Symbol,String>] - service params
def initialize(params = {})
params = params.symbolize_keys
params.assert_valid_keys(*_parameters.keys) unless _allow_extra_parameters
missing_keys = _parameters.select { |_, opts| opts[:required] }.keys - params.keys
raise ArgumentError, "params #{missing_keys.join(', ')} are required but missing" if missing_keys.any?
@params = params
end
def has_parameter?(name)
params.key?(name.to_sym)
end
end
# frozen_string_literal: true
module ServicableWithEntity
# Mixin for services with entity.
# Usage:
#
# class SomeService
# include ServicableWithEntity
# service_entity :customer
# parameter :foo, required: true
#
# def call
# customer.do_some_stuff!(foo)
# end
# end
#
# customer = customer.find(1)
# SomeService.call(customer, foo: 'bar')
extend ActiveSupport::Concern
include Servicable
class_methods do
# alias entity with provided name
# @param name [Symbol] - alias name for the entity
def service_entity(name)
define_method(name) { @entity }
end
end
included do
attr_reader :entity
end
# @param entity [Object] - service entity
# @param params [Hash<Symbol,String>] - service params
def initialize(entity, params = {})
@entity = entity
super(params)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment