lib/agnostic/duplicate.rb in agnostic-duplicate-1.0.0 vs lib/agnostic/duplicate.rb in agnostic-duplicate-1.0.1

- old
+ new

@@ -82,21 +82,40 @@ # # ... # attr_duplicable :images # # ... # end # ``` + # + # If you want to apply the `duplicate` over a custom instance object instead + # of the default template for the current configuration, then you can pass a + # `dup_template` option on the method call + # + # ```ruby + # otherobject # => Object sharing duplicable attributes with 'myobject' + # myobject.duplicate dup_template: otherobject + # ``` + # + # As the object passed to dup_template should be compliant with the duplicable + # attribute list, if there is an error during the process an exception will + # be raise according to the type of error: + # - Agnostic::Duplicate::ChangeSet::AttributeNotFound + # - Agnostic::Duplicate::ChangeSet::CopyError + # module Duplicate def self.included(base) base.extend(ClassMethods) base.instance_variable_set '@duplicable_changesets', [] base.instance_variable_set '@duplicable_options', {} end # Duplicates the object # @return [Duplicate] the new instance object - def duplicate - dup_template.tap do |model| + # @return opts [Hash] the options for duplicating + # @option [Object] dup_template The object over attributes are going to be + # copied + def duplicate(opts = {}) + (opts[:dup_template] || dup_template).tap do |model| apply_changesets!(model) hook_after_duplicate!(model) if respond_to? :hook_after_duplicate! end end @@ -111,38 +130,60 @@ end # Contains all kinds of changesets that can be applied to a duplicable # object module ChangeSet + # Raised when there is an error while trying to copy an attribute + class CopyError < StandardError + end + # Raised when a non existing attribute is tried to be duplicated + class AttributeNotFound < StandardError + end + # Base class for all changesets. Subclasses should implement method # `apply` (see #apply) class Base attr_reader :attributes def initialize(attributes) @attributes = attributes end + + private + + def raise_copy_error_for(attribute) + msg = "It wasn't possible to copy attribute '#{attribute}'" + fail CopyError, msg, caller + end end # Defines a changeset where a deep copy wants to be applied to all # attributes class DeepCopy < Base # Applies changes needed on the duplicated new instance object # @param parent [Duplicate] the original object to be duplicated # @param model [Duplicate] the duplicated new instance object def apply(parent, model) attributes.each do |attribute| - setter_method = "#{attribute}=" - if model.respond_to?(setter_method) - model.send(setter_method, dup_attribute(parent, attribute)) - else - fail "Invalid duplicable attribute '#{attribute}'" + unless model.respond_to? "#{attribute}=" + fail AttributeNotFound, "Attribute: '#{attribute}'", caller end + deep_copy = dup_attribute(parent, attribute) + copy_attribute(attribute, model, deep_copy) end end private + # @param attribute [Symbol] attribute to be copied + # @param parent [Duplicable] the original object to be duplicated + # @param model [Duplicable] the duplicated new instance object + def copy_attribute(attribute, model, deep_copy) + model.send("#{attribute}=", deep_copy) + rescue + raise_copy_error_for(attribute) + end + # @param parent [Duplicate] the original object to be duplicated # @param attribute [Symbol] the attribute to be duplicated # @return from a duplicable object the duplicated value for the # attribute specified def dup_attribute(parent, attribute) @@ -175,34 +216,45 @@ # Though if the field value is a memory address it copies the memory # address, and if the field value is a primitive type it copies the value # of the primitive type. class ShallowCopy < Base # Applies changes needed on the duplicated new instance object - # @param parent [Duplicate] the original object to be duplicated - # @param model [Duplicate] the duplicated new instance object + # @param parent [Duplicable] the original object to be duplicated + # @param model [Duplicable] the duplicated new instance object def apply(parent, model) attributes.each do |attribute| - model.send("#{attribute}=", parent.send(attribute)) + copy_attribute(attribute, parent, model) end end + + private + + # @param attribute [Symbol] attribute to be copied + # @param parent [Duplicable] the original object to be duplicated + # @param model [Duplicable] the duplicated new instance object + def copy_attribute(attribute, parent, model) + model.send("#{attribute}=", parent.send(attribute)) + rescue + raise_copy_error_for(attribute) + end end end private - # @return [Duplicate] a new instance object based on global duplicable + # @return [Duplicable] a new instance object based on global duplicable # configuration def dup_template klass = self.class if klass.duplicable_option? :new_instance klass.new else dup end end - # Methods added to classes including Duplicate module + # Methods added to classes including Duplicable module module ClassMethods attr_accessor :duplicable_changesets, :duplicable_options # Adds a new duplicable changeset for the class. # @@ -218,18 +270,17 @@ duplicable_changesets << changeset_class.new(args) end # Sets global options for applying changesets # - # ## Options available: - # - `new_instance`: if `true` the duplicated instance is created calling - # in first place `new` method over the class. if `false` the duplicated - # instance is created calling to `dup` method over the instance object. - # - # @param options [Hash] - def duplicable_config(options) - if options.is_a? Hash - @duplicable_options.merge! options + # @param opts [Hash] The options for duplicable configuration + # @option opts [Boolean] :new_instance If `true` the duplicated instance + # is created calling in first place `new` method over the class. if + # `false` the duplicated instance is created calling to `dup` method + # over the instance object. + def duplicable_config(opts) + if opts.is_a? Hash + @duplicable_options.merge! opts keep_valid_options else fail ArgumentError, 'Invalid options configuration' end end