diff options
author | Daniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org> | 2018-08-11 17:10:47 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-11 17:10:47 -0400 |
commit | 7b415991772fa3f6ac30ca44b34c2c92b0114b08 (patch) | |
tree | 9ad9681b3c1090f8c251a651c146aba434515f0f | |
parent | 6ab1e45ef74526fe983b4ed29174b781db00cd24 (diff) | |
parent | 91a5153fa3dd908dea19d4162253e4e45e2d6605 (diff) | |
download | hashie-7b415991772fa3f6ac30ca44b34c2c92b0114b08.tar.gz |
Merge pull request #457 from michaelherold/copying-in-trash
Allow Trash to copy properties from other properties
-rw-r--r-- | .rubocop_todo.yml | 13 | ||||
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | lib/hashie/dash.rb | 22 | ||||
-rw-r--r-- | lib/hashie/extensions/dash/property_translation.rb | 73 | ||||
-rw-r--r-- | spec/hashie/trash_spec.rb | 60 |
5 files changed, 133 insertions, 36 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5900e3f..e4e05c7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,16 +1,16 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-02-07 19:58:08 -0600 using RuboCop version 0.52.1. +# on 2018-08-11 15:53:09 -0500 using RuboCop version 0.52.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 9 +# Offense count: 8 Metrics/AbcSize: - Max: 26 + Max: 24 -# Offense count: 57 +# Offense count: 58 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: Max: 620 @@ -42,14 +42,13 @@ Metrics/PerceivedComplexity: Style/Documentation: Enabled: false -# Offense count: 2 +# Offense count: 1 # Cop supports --auto-correct. Style/IfUnlessModifier: Exclude: - - 'lib/hashie/extensions/dash/property_translation.rb' - 'lib/hashie/extensions/strict_key_access.rb' -# Offense count: 256 +# Offense count: 261 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: diff --git a/CHANGELOG.md b/CHANGELOG.md index f04a249..365ecfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ scheme are considered to be bugs. * [#436](https://github.com/intridea/hashie/pull/436): Ensure that `Hashie::Extensions::IndifferentAccess` injects itself after a non-destructive merge - [@michaelherold](https://github.com/michaelherold). * [#437](https://github.com/intridea/hashie/pull/437): Allow codependent properties to be set on Dash - [@michaelherold](https://github.com/michaelherold). * [#438](https://github.com/intridea/hashie/pull/438): Fix: `NameError (uninitialized constant Hashie::Extensions::Parsers::YamlErbParser::Pathname)` in `Hashie::Mash.load` - [@onk](https://github.com/onk). +* [#457](https://github.com/intridea/hashie/pull/457): Fix `Trash` to allow it to copy properties from other properties - [@michaelherold](https://github.com/michaelherold). * Your contribution here. ### Security diff --git a/lib/hashie/dash.rb b/lib/hashie/dash.rb index ee27b11..4e366ed 100644 --- a/lib/hashie/dash.rb +++ b/lib/hashie/dash.rb @@ -42,11 +42,8 @@ module Hashie defaults.delete property_name end - unless instance_methods.map(&:to_s).include?("#{property_name}=") - define_method(property_name) { |&block| self.[](property_name, &block) } - property_assignment = "#{property_name}=".to_sym - define_method(property_assignment) { |value| self.[]=(property_name, value) } - end + define_getter_for(property_name) + define_setter_for(property_name) @subclasses.each { |klass| klass.property(property_name, options) } if defined? @subclasses @@ -61,9 +58,11 @@ module Hashie class << self attr_reader :properties, :defaults + attr_reader :getters attr_reader :required_properties end instance_variable_set('@properties', Set.new) + instance_variable_set('@getters', Set.new) instance_variable_set('@defaults', {}) instance_variable_set('@required_properties', {}) @@ -71,6 +70,7 @@ module Hashie super (@subclasses ||= Set.new) << klass klass.instance_variable_set('@properties', properties.dup) + klass.instance_variable_set('@getters', getters.dup) klass.instance_variable_set('@defaults', defaults.dup) klass.instance_variable_set('@required_properties', required_properties.dup) end @@ -87,6 +87,18 @@ module Hashie required_properties.key? name end + private_class_method def self.define_getter_for(property_name) + return if getters.include?(property_name) + define_method(property_name) { |&block| self.[](property_name, &block) } + getters << property_name + end + + private_class_method def self.define_setter_for(property_name) + setter = :"#{property_name}=" + return if instance_methods.include?(setter) + define_method(setter) { |value| self.[]=(property_name, value) } + end + # You may initialize a Dash with an attributes hash # just like you would many other kinds of data objects. def initialize(attributes = {}, &block) diff --git a/lib/hashie/extensions/dash/property_translation.rb b/lib/hashie/extensions/dash/property_translation.rb index 2d28080..85bfb89 100644 --- a/lib/hashie/extensions/dash/property_translation.rb +++ b/lib/hashie/extensions/dash/property_translation.rb @@ -40,7 +40,7 @@ module Hashie module PropertyTranslation def self.included(base) base.instance_variable_set(:@transforms, {}) - base.instance_variable_set(:@translations_hash, {}) + base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} }) base.extend(ClassMethods) base.send(:include, InstanceMethods) end @@ -72,21 +72,16 @@ module Hashie def property(property_name, options = {}) super - if options[:from] - if property_name == options[:from] - raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same" - end - - translations_hash[options[:from]] ||= {} - translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with] + from = options[:from] + converter = options[:with] + transformer = options[:transform_with] - define_method "#{options[:from]}=" do |val| - self.class.translations_hash[options[:from]].each do |name, with| - self[name] = with.respond_to?(:call) ? with.call(val) : val - end - end - elsif options[:transform_with].respond_to? :call - transforms[property_name] = options[:transform_with] + if from + fail_self_transformation_error!(property_name) if property_name == from + define_translation(from, property_name, converter || transformer) + define_writer_for_source_property(from) + elsif valid_transformer?(transformer) + transforms[property_name] = transformer end end @@ -103,26 +98,49 @@ module Hashie end def translations - @translations ||= {}.tap do |h| + @translations ||= {}.tap do |translations| translations_hash.each do |(property_name, property_translations)| - h[property_name] = if property_translations.size > 1 - property_translations.keys - else - property_translations.keys.first - end + translations[property_name] = + if property_translations.size > 1 + property_translations.keys + else + property_translations.keys.first + end end end end def inverse_translations - @inverse_translations ||= {}.tap do |h| + @inverse_translations ||= {}.tap do |translations| translations_hash.each do |(property_name, property_translations)| - property_translations.each_key do |k| - h[k] = property_name + property_translations.each_key do |key| + translations[key] = property_name end end end end + + private + + def define_translation(from, property_name, translator) + translations_hash[from][property_name] = translator + end + + def define_writer_for_source_property(property) + define_method "#{property}=" do |val| + __translations[property].each do |name, with| + self[name] = with.respond_to?(:call) ? with.call(val) : val + end + end + end + + def fail_self_transformation_error!(property_name) + raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same" + end + + def valid_transformer?(transformer) + transformer.respond_to? :call + end end module InstanceMethods @@ -132,6 +150,7 @@ module Hashie def []=(property, value) if self.class.translation_exists? property send("#{property}=", value) + super(property, value) if self.class.properties.include?(property) elsif self.class.transformation_exists? property super property, self.class.transformed_property(property, value) elsif property_exists? property @@ -156,6 +175,12 @@ module Hashie fail_no_property_error!(property) unless self.class.property?(property) true end + + private + + def __translations + self.class.translations_hash + end end end end diff --git a/spec/hashie/trash_spec.rb b/spec/hashie/trash_spec.rb index 20b717c..043cbfd 100644 --- a/spec/hashie/trash_spec.rb +++ b/spec/hashie/trash_spec.rb @@ -265,4 +265,64 @@ describe Hashie::Trash do expect(subject.first_name).to eq('Frodo') end end + + context 'when copying properties from other properties' do + it 'retains the original and also sets the copy' do + simple = Class.new(Hashie::Trash) do + property :id + property :copy_of_id, from: :id + end + + subject = simple.new(id: 1) + + expect(subject.id).to eq(1) + expect(subject.copy_of_id).to eq(1) + end + + it 'grabs the default for the original if it is not set' do + with_default = Class.new(Hashie::Trash) do + property :id, default: 0 + property :copy_of_id, from: :id + end + + subject = with_default.new + + expect(subject.id).to eq(0) + expect(subject.copy_of_id).to eq(0) + end + + it 'can be a required value' do + with_required = Class.new(Hashie::Trash) do + property :id + property :copy_of_id, from: :id, required: true, message: 'must be set' + end + + expect { with_required.new }.to raise_error(ArgumentError, "The property 'copy_of_id' must be set") + end + + it 'does not set properties that do not exist' do + from_non_property = Class.new(Hashie::Trash) do + property :copy_of_value, from: :value + end + + subject = from_non_property.new(value: 0) + + expect(subject).not_to respond_to(:value) + expect { subject[:value] }.to raise_error(NoMethodError, "The property 'value' is not defined for .") + expect(subject.to_h[:value]).to eq(nil) + expect(subject.copy_of_value).to eq(0) + end + + it 'is not order-dependent in definition' do + simple = Class.new(Hashie::Trash) do + property :copy_of_id, from: :id + property :id + end + + subject = simple.new(id: 1) + + expect(subject.id).to eq(1) + expect(subject.copy_of_id).to eq(1) + end + end end |