hashie 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,7 @@ module Hashie
14
14
  # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
15
15
  # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
16
16
  # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
17
+ # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key. Used to test existance in deep Mashes.
17
18
  #
18
19
  # == Basic Example
19
20
  #
@@ -42,7 +43,18 @@ module Hashie
42
43
  # mash.author!.name = "Michael Bleigh"
43
44
  # mash.author # => <Mash name="Michael Bleigh">
44
45
  #
45
- class Mash < Hashie::Hash
46
+ # == Under Bang Example
47
+ #
48
+ # mash = Mash.new
49
+ # mash.author # => nil
50
+ # mash.author_ # => <Mash>
51
+ # mash.author_.name # => nil
52
+ #
53
+ # mash = Mash.new
54
+ # mash.author_.name = "Michael Bleigh" (assigned to temp object)
55
+ # mash.author # => <Mash>
56
+ #
57
+ class Mash < Hash
46
58
  include Hashie::PrettyInspect
47
59
  alias_method :to_s, :inspect
48
60
 
@@ -55,20 +67,18 @@ module Hashie
55
67
  default ? super(default) : super(&blk)
56
68
  end
57
69
 
58
- class << self
59
- alias [] new
60
-
61
- def subkey_class
62
- self
63
- end
64
- end
70
+ class << self; alias [] new; end
65
71
 
66
72
  def id #:nodoc:
67
- key?("id") ? self["id"] : super
73
+ self["id"]
68
74
  end
69
75
 
70
76
  def type #:nodoc:
71
- key?("type") ? self["type"] : super
77
+ self["type"]
78
+ end
79
+
80
+ def object_id #:nodoc:
81
+ self["object_id"]
72
82
  end
73
83
 
74
84
  alias_method :regular_reader, :[]
@@ -97,6 +107,21 @@ module Hashie
97
107
  regular_reader(ck)
98
108
  end
99
109
 
110
+ # This is the under bang method reader, it will return a temporary new Mash
111
+ # if there isn't a value already assigned to the key requested.
112
+ def underbang_reader(key)
113
+ ck = convert_key(key)
114
+ if key?(ck)
115
+ regular_reader(ck)
116
+ else
117
+ self.class.new
118
+ end
119
+ end
120
+
121
+ def fetch(key, default_value = nil)
122
+ self[key] || block_given? && yield(key) || default_value || super(key)
123
+ end
124
+
100
125
  def delete(key)
101
126
  super(convert_key(key))
102
127
  end
@@ -116,20 +141,22 @@ module Hashie
116
141
 
117
142
  # Performs a deep_update on a duplicate of the
118
143
  # current mash.
119
- def deep_merge(other_hash)
120
- dup.deep_update(other_hash)
144
+ def deep_merge(other_hash, &blk)
145
+ dup.deep_update(other_hash, &blk)
121
146
  end
122
147
  alias_method :merge, :deep_merge
123
148
 
124
149
  # Recursively merges this mash with the passed
125
150
  # in hash, merging each hash in the hierarchy.
126
- def deep_update(other_hash)
151
+ def deep_update(other_hash, &blk)
127
152
  other_hash.each_pair do |k,v|
128
153
  key = convert_key(k)
129
154
  if regular_reader(key).is_a?(Mash) and v.is_a?(::Hash)
130
- regular_reader(key).deep_update(v)
155
+ regular_reader(key).deep_update(v, &blk)
131
156
  else
132
- regular_writer(key, convert_value(v, true))
157
+ value = convert_value(v, true)
158
+ value = blk.call(key, self[k], value) if blk
159
+ regular_writer(key, value)
133
160
  end
134
161
  end
135
162
  self
@@ -161,7 +188,7 @@ module Hashie
161
188
 
162
189
  def method_missing(method_name, *args, &blk)
163
190
  return self.[](method_name, &blk) if key?(method_name)
164
- match = method_name.to_s.match(/(.*?)([?=!]?)$/)
191
+ match = method_name.to_s.match(/(.*?)([?=!_]?)$/)
165
192
  case match[2]
166
193
  when "="
167
194
  self[match[1]] = args.first
@@ -169,6 +196,8 @@ module Hashie
169
196
  !!self[match[1]]
170
197
  when "!"
171
198
  initializing_reader(match[1])
199
+ when "_"
200
+ underbang_reader(match[1])
172
201
  else
173
202
  default(method_name, *args, &blk)
174
203
  end
@@ -184,9 +213,11 @@ module Hashie
184
213
  case val
185
214
  when self.class
186
215
  val.dup
216
+ when Hash
217
+ duping ? val.dup : val
187
218
  when ::Hash
188
219
  val = val.dup if duping
189
- self.class.subkey_class.new.merge(val)
220
+ self.class.new(val)
190
221
  when Array
191
222
  val.collect{ |e| convert_value(e) }
192
223
  else
@@ -7,23 +7,39 @@ module Hashie
7
7
  # Trashes are useful when you need to read data from another application,
8
8
  # such as a Java api, where the keys are named differently from how we would
9
9
  # in Ruby.
10
- class Trash < Hashie::Dash
10
+ class Trash < Dash
11
11
 
12
12
  # Defines a property on the Trash. Options are as follows:
13
13
  #
14
14
  # * <tt>:default</tt> - Specify a default value for this property, to be
15
15
  # returned before a value is set on the property in a new Dash.
16
16
  # * <tt>:from</tt> - Specify the original key name that will be write only.
17
+ # * <tt>:with</tt> - Specify a lambda to be used to convert value.
18
+ # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
19
+ # without using the :from option. It transform the property itself.
17
20
  def self.property(property_name, options = {})
18
21
  super
19
22
 
20
23
  if options[:from]
24
+ if property_name.to_sym == options[:from].to_sym
25
+ raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
26
+ end
21
27
  translations << options[:from].to_sym
22
- class_eval <<-RUBY
23
- def #{options[:from]}=(val)
24
- self[:#{property_name}] = val
28
+ if options[:with].respond_to? :call
29
+ class_eval do
30
+ define_method "#{options[:from]}=" do |val|
31
+ self[property_name.to_sym] = options[:with].call(val)
32
+ end
25
33
  end
26
- RUBY
34
+ else
35
+ class_eval <<-RUBY
36
+ def #{options[:from]}=(val)
37
+ self[:#{property_name}] = val
38
+ end
39
+ RUBY
40
+ end
41
+ elsif options[:transform_with].respond_to? :call
42
+ transforms[property_name.to_sym] = options[:transform_with]
27
43
  end
28
44
  end
29
45
 
@@ -32,6 +48,8 @@ module Hashie
32
48
  def []=(property, value)
33
49
  if self.class.translations.include? property.to_sym
34
50
  send("#{property}=", value)
51
+ elsif self.class.transforms.key? property.to_sym
52
+ super property, self.class.transforms[property.to_sym].call(value)
35
53
  elsif property_exists? property
36
54
  super
37
55
  end
@@ -43,6 +61,10 @@ module Hashie
43
61
  @translations ||= []
44
62
  end
45
63
 
64
+ def self.transforms
65
+ @transforms ||= {}
66
+ end
67
+
46
68
  # Raises an NoMethodError if the property doesn't exist
47
69
  #
48
70
  def property_exists?(property)
@@ -51,5 +73,19 @@ module Hashie
51
73
  end
52
74
  true
53
75
  end
76
+
77
+ private
78
+
79
+ # Deletes any keys that have a translation
80
+ def initialize_attributes(attributes)
81
+ return unless attributes
82
+ attributes_copy = attributes.dup.delete_if do |k,v|
83
+ if self.class.translations.include?(k.to_sym)
84
+ self[k] = v
85
+ true
86
+ end
87
+ end
88
+ super attributes_copy
89
+ end
54
90
  end
55
91
  end
@@ -1,3 +1,3 @@
1
1
  module Hashie
2
- VERSION = '1.2.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -22,6 +22,14 @@ class Subclassed < DashTest
22
22
  property :last_name, :required => true
23
23
  end
24
24
 
25
+ class DashDefaultTest < Hashie::Dash
26
+ property :aliases, :default => ["Snake"]
27
+ end
28
+
29
+ class DeferredTest < Hashie::Dash
30
+ property :created_at, :default => Proc.new { Time.now }
31
+ end
32
+
25
33
  describe DashTest do
26
34
 
27
35
  subject { DashTest.new(:first_name => 'Bob', :email => 'bob@example.com') }
@@ -96,6 +104,17 @@ describe DashTest do
96
104
  end
97
105
  end
98
106
 
107
+ context 'reading from deferred properties' do
108
+ it 'should evaluate proc after initial read' do
109
+ DeferredTest.new['created_at'].should be_instance_of(Time)
110
+ end
111
+
112
+ it "should not evalute proc after subsequent reads" do
113
+ deferred = DeferredTest.new
114
+ deferred['created_at'].object_id.should == deferred['created_at'].object_id
115
+ end
116
+ end
117
+
99
118
  describe '.new' do
100
119
  it 'fails with non-existent properties' do
101
120
  lambda { described_class.new(:bork => '') }.should raise_error(NoMethodError)
@@ -120,6 +139,12 @@ describe DashTest do
120
139
  expect { DashTest.new }.to raise_error(ArgumentError)
121
140
  end
122
141
 
142
+ it "does not overwrite default values" do
143
+ obj1 = DashDefaultTest.new
144
+ obj1.aliases << "El Rey"
145
+ obj2 = DashDefaultTest.new
146
+ obj2.aliases.should_not include "El Rey"
147
+ end
123
148
  end
124
149
 
125
150
  describe 'properties' do
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::Coercion do
4
+ class Initializable
5
+ def initialize(obj, coerced = false)
6
+ @coerced = coerced
7
+ @value = obj.class.to_s
8
+ end
9
+ def coerced?; @coerced end
10
+ attr_reader :value
11
+ end
12
+
13
+ class Coercable < Initializable
14
+ def self.coerce(obj)
15
+ new(obj, true)
16
+ end
17
+ end
18
+
19
+ before(:each) do
20
+ class ExampleCoercableHash < Hash
21
+ include Hashie::Extensions::Coercion
22
+ include Hashie::Extensions::MergeInitializer
23
+ end
24
+ end
25
+ subject { ExampleCoercableHash }
26
+ let(:instance){ subject.new }
27
+
28
+ describe '.coerce_key' do
29
+ it { subject.should be_respond_to(:coerce_key) }
30
+
31
+ it 'should run through coerce on a specified key' do
32
+ subject.coerce_key :foo, Coercable
33
+
34
+ instance[:foo] = "bar"
35
+ instance[:foo].should be_coerced
36
+ end
37
+
38
+ it "should support an array of keys" do
39
+ subject.coerce_keys :foo, :bar, Coercable
40
+
41
+ instance[:foo] = "bar"
42
+ instance[:bar] = "bax"
43
+ instance[:foo].should be_coerced
44
+ instance[:bar].should be_coerced
45
+ end
46
+
47
+ it 'should just call #new if no coerce method is available' do
48
+ subject.coerce_key :foo, Initializable
49
+
50
+ instance[:foo] = "bar"
51
+ instance[:foo].value.should == "String"
52
+ instance[:foo].should_not be_coerced
53
+ end
54
+
55
+ it "should coerce when the merge initializer is used" do
56
+ subject.coerce_key :foo, Coercable
57
+ instance = subject.new(:foo => "bar")
58
+
59
+ instance[:foo].should be_coerced
60
+ end
61
+ end
62
+
63
+ describe '.coerce_value' do
64
+ context 'with :strict => true' do
65
+ it 'should coerce any value of the exact right class' do
66
+ subject.coerce_value String, Coercable
67
+
68
+ instance[:foo] = "bar"
69
+ instance[:bar] = "bax"
70
+ instance[:foo].should be_coerced
71
+ instance[:bar].should be_coerced
72
+ end
73
+
74
+ it 'should not coerce superclasses' do
75
+ klass = Class.new(String)
76
+ subject.coerce_value klass, Coercable
77
+
78
+ instance[:foo] = "bar"
79
+ instance[:foo].should_not be_kind_of(Coercable)
80
+ instance[:foo] = klass.new
81
+ instance[:foo].should be_kind_of(Coercable)
82
+ end
83
+ end
84
+ end
85
+
86
+ after(:each) do
87
+ Object.send(:remove_const, :ExampleCoercableHash)
88
+ end
89
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::DeepMerge do
4
+ class DeepMergeHash < Hash; include Hashie::Extensions::DeepMerge end
5
+
6
+ subject{ DeepMergeHash }
7
+
8
+ let(:h1) { subject.new.merge(:a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } }) }
9
+ let(:h2) { { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } } }
10
+ let(:expected_hash) { { :a => 1, :b => "b", :c => { :c1 => 2, :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } } }
11
+
12
+ it 'should deep merge two hashes' do
13
+ h1.deep_merge(h2).should == expected_hash
14
+ end
15
+
16
+ it 'should deep merge two hashes with bang method' do
17
+ h1.deep_merge!(h2)
18
+ h1.should == expected_hash
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::IndifferentAccess do
4
+ class IndifferentHash < Hash
5
+ include Hashie::Extensions::MergeInitializer
6
+ include Hashie::Extensions::IndifferentAccess
7
+ end
8
+ subject{ IndifferentHash }
9
+
10
+ it 'should be able to access via string or symbol' do
11
+ h = subject.new(:abc => 123)
12
+ h[:abc].should == 123
13
+ h['abc'].should == 123
14
+ end
15
+
16
+ describe '#values_at' do
17
+ it 'should indifferently find values' do
18
+ h = subject.new(:foo => 'bar', 'baz' => 'qux')
19
+ h.values_at('foo', :baz).should == %w(bar qux)
20
+ end
21
+ end
22
+
23
+ describe '#fetch' do
24
+ it 'should work like normal fetch, but indifferent' do
25
+ h = subject.new(:foo => 'bar')
26
+ h.fetch(:foo).should == h.fetch('foo')
27
+ h.fetch(:foo).should == 'bar'
28
+ end
29
+ end
30
+
31
+ describe '#delete' do
32
+ it 'should delete indifferently' do
33
+ h = subject.new(:foo => 'bar', 'baz' => 'qux')
34
+ h.delete('foo')
35
+ h.delete(:baz)
36
+ h.should be_empty
37
+ end
38
+ end
39
+
40
+ describe '#key?' do
41
+ let(:h) { subject.new(:foo => 'bar') }
42
+
43
+ it 'should find it indifferently' do
44
+ h.should be_key(:foo)
45
+ h.should be_key('foo')
46
+ end
47
+
48
+ %w(include? member? has_key?).each do |key_alias|
49
+ it "should be aliased as #{key_alias}" do
50
+ h.send(key_alias.to_sym, :foo).should be(true)
51
+ h.send(key_alias.to_sym, 'foo').should be(true)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#update' do
57
+ subject{ IndifferentHash.new(:foo => 'bar') }
58
+ it 'should allow keys to be indifferent still' do
59
+ subject.update(:baz => 'qux')
60
+ subject['foo'].should == 'bar'
61
+ subject['baz'].should == 'qux'
62
+ end
63
+
64
+ it 'should recursively inject indifference into sub-hashes' do
65
+ subject.update(:baz => {:qux => 'abc'})
66
+ subject['baz']['qux'].should == 'abc'
67
+ end
68
+
69
+ it 'should not change the ancestors of the injected object class' do
70
+ subject.update(:baz => {:qux => 'abc'})
71
+ Hash.new.should_not be_respond_to(:indifferent_access?)
72
+ end
73
+ end
74
+ end