# frozen_string_literal: true require "core/copy" require "core/extension" require "core/local" module Core # [public] Make objects stateful. # module State require_relative "state/version" extend Core::Extension using Core::Copy ALLOWED_FLAGS = %i[class instance].freeze applies do prepend_defined_state_modules end extends :definition do # [public] Defines state by name. # # flags - Changes how the state is defined. Possible values include: # # * `:class` - Defines state at the class level. # # * `:instance` - Defines state at the instance level. # # Dependencies are applied with both the `:class` and `:instance` flags by default. # # default - The default value for the defined state. # # private - If `true`, the reader/writer will be defined as private. # # reader - If `true`, defines a reader for the state. # # writer - If `true`, defines a reader for the state. # def state(name, *flags, default: nil, private: false, reader: true, writer: true) flags = flags.map(&:to_sym) enforce_allowed_flags(flags) state = { value: default, class: define_class_state?(flags), instance: define_instance_state?(flags) } method_name = name.to_sym ivar_name = "@#{method_name}" defined_state[ivar_name] = state defined_state_isolations << ivar_name if state[:class] instance_variable_set(ivar_name, default) end prefix = if private "private " else "" end if reader if state[:class] defined_state_class_module.module_eval <<~CODE, __FILE__, __LINE__ + 1 #{prefix}def #{method_name} unless defined_state_isolations.include?(#{ivar_name.inspect}) #{ivar_name} = #{ivar_name}.copy defined_state_isolations << #{ivar_name.inspect} end #{ivar_name} end CODE end if state[:instance] defined_state_instance_module.module_eval <<~CODE, __FILE__, __LINE__ + 1 #{prefix}def #{method_name} unless defined_state_isolations.include?(#{ivar_name.inspect}) #{ivar_name} = #{ivar_name}.copy defined_state_isolations << #{ivar_name.inspect} end #{ivar_name} end CODE end end if writer if state[:class] defined_state_class_module.module_eval <<~CODE, __FILE__, __LINE__ + 1 #{prefix}def #{method_name}=(value) defined_state[#{ivar_name.inspect}][:value] = value #{ivar_name} = value end CODE end if state[:instance] defined_state_instance_module.module_eval <<~CODE, __FILE__, __LINE__ + 1 #{prefix}def #{method_name}=(value) #{ivar_name} = value end CODE end end if state[:class] instance_variable_set(ivar_name, default) end end def inherited(subclass) defined_state.each_pair do |name, state| if state[:class] subclass.instance_variable_set(name, instance_variable_get(name)) end end subclass.__send__(:prepend_defined_state_modules) subclass.instance_variable_set(:@__defined_state__, @__defined_state__.each_with_object({}) { |(key, value), state| state[key] = value.dup }) super end private def defined_state @__defined_state__ ||= {} end private def defined_state_class_module @__defined_state_class_module__ ||= Module.new end private def defined_state_instance_module @__defined_state_instance_module__ ||= Module.new end private def prepend_defined_state_modules prepend(defined_state_instance_module) singleton_class.prepend(defined_state_class_module) end private def enforce_allowed_flags(flags) flags.each do |flag| unless allowed_flag?(flag) raise ArgumentError, "Expected flag `#{flag.inspect}' to be one of: #{allowed_flags_string}" end end end private def allowed_flag?(flag) ALLOWED_FLAGS.include?(flag) end private def allowed_flags_string ALLOWED_FLAGS.map { |allowed_flag| "`#{allowed_flag.inspect}'" }.join(", ") end private def define_class_state?(flags) flags.empty? || flags.include?(:class) end private def define_instance_state?(flags) flags.empty? || flags.include?(:instance) end end extends :implementation, prepend: true do def initialize(...) self_class = self.class self_class.__send__(:defined_state).each_pair do |name, state| if state[:instance] if state[:class] instance_variable_set(name, self_class.instance_variable_get(name)) else instance_variable_set(name, state[:value]) end end end super end def initialize_copy(...) @__defined_state_isolations__ = [] super end end extends :definition, :implementation do # [public] Safely mutates state by name, yielding a copy of the current value to the block. # def mutate_state(name, &block) __send__("#{name}=", block.call(__send__(name))) end private def defined_state_isolations @__defined_state_isolations__ ||= [] end end class << self include Core::Local def prevent_recursion(object) object_id = object.object_id copied_objects[object_id] = true yield ensure copied_objects.delete(object_id) end def copying?(object) copied_objects[object.object_id] end def copied_objects localized(:__corerb_copied_objects) || localize(:__corerb_copied_objects, {}) end end end end