# frozen_string_literal: true require "core/extension" require "core/local" module Is # [public] Makes objects stateful. # module Stateful # Adding core-copy as a dependency creates a recursive dependency, so just bundle it. # module Copy DEFAULT = ::Object.new refine ::Object do if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3" def copy(freeze: DEFAULT) should_freeze = resolve_freeze_argument(freeze) value = clone(freeze: should_freeze) value.freeze if should_freeze value end else def copy(freeze: DEFAULT) clone(freeze: resolve_freeze_argument(freeze)) end end private def resolve_freeze_argument(value) case value when DEFAULT frozen? else !!value end end end refine Array do def copy(freeze: DEFAULT) unless Stateful.copying?(self) Stateful.prevent_recursion(self) do array = map { |value| value.copy(freeze: freeze) } array.freeze if resolve_freeze_argument(freeze) array end end end end refine Hash do def copy(freeze: DEFAULT) unless Stateful.copying?(self) Stateful.prevent_recursion(self) do hash = {} each_pair do |key, value| hash[key.copy(freeze: freeze)] = value.copy(freeze: freeze) end hash.freeze if resolve_freeze_argument(freeze) hash end end end end end extend Is::Extension using 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, state[:value]) 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 Is::Localized 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