Created
September 21, 2020 05:43
-
-
Save denisoster/ad01a27809dad695e6657644d550e48b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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