summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2018-08-11 17:10:47 -0400
committerGitHub <noreply@github.com>2018-08-11 17:10:47 -0400
commit7b415991772fa3f6ac30ca44b34c2c92b0114b08 (patch)
tree9ad9681b3c1090f8c251a651c146aba434515f0f
parent6ab1e45ef74526fe983b4ed29174b781db00cd24 (diff)
parent91a5153fa3dd908dea19d4162253e4e45e2d6605 (diff)
downloadhashie-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.yml13
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/hashie/dash.rb22
-rw-r--r--lib/hashie/extensions/dash/property_translation.rb73
-rw-r--r--spec/hashie/trash_spec.rb60
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