gorillib 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/CHANGELOG.textile +6 -0
  2. data/README.textile +34 -11
  3. data/VERSION +1 -1
  4. data/gorillib.gemspec +63 -5
  5. data/lib/gorillib/enumerable/sum.rb +2 -2
  6. data/lib/gorillib/hash/compact.rb +2 -29
  7. data/lib/gorillib/hash/deep_compact.rb +2 -12
  8. data/lib/gorillib/hash/deep_dup.rb +4 -0
  9. data/lib/gorillib/hash/deep_merge.rb +2 -14
  10. data/lib/gorillib/hash/indifferent_access.rb +207 -0
  11. data/lib/gorillib/hash/keys.rb +2 -40
  12. data/lib/gorillib/hash/reverse_merge.rb +2 -24
  13. data/lib/gorillib/hash/slice.rb +2 -51
  14. data/lib/gorillib/hash/tree_merge.rb +4 -0
  15. data/lib/gorillib/hashlike.rb +824 -0
  16. data/lib/gorillib/hashlike/compact.rb +60 -0
  17. data/lib/gorillib/hashlike/deep_compact.rb +18 -0
  18. data/lib/gorillib/hashlike/deep_dup.rb +15 -0
  19. data/lib/gorillib/hashlike/deep_merge.rb +20 -0
  20. data/lib/gorillib/hashlike/hashlike_via_accessors.rb +169 -0
  21. data/lib/gorillib/hashlike/keys.rb +59 -0
  22. data/lib/gorillib/hashlike/reverse_merge.rb +31 -0
  23. data/lib/gorillib/hashlike/slice.rb +67 -0
  24. data/lib/gorillib/hashlike/tree_merge.rb +76 -0
  25. data/lib/gorillib/metaprogramming/mattr_accessor.rb +1 -1
  26. data/lib/gorillib/receiver.rb +315 -0
  27. data/lib/gorillib/receiver/active_model_shim.rb +19 -0
  28. data/lib/gorillib/receiver/acts_as_hash.rb +191 -0
  29. data/lib/gorillib/receiver/acts_as_loadable.rb +42 -0
  30. data/lib/gorillib/receiver/tree_diff.rb +74 -0
  31. data/lib/gorillib/receiver/validations.rb +30 -0
  32. data/lib/gorillib/struct/acts_as_hash.rb +108 -0
  33. data/lib/gorillib/struct/hashlike_iteration.rb +0 -0
  34. data/notes/fancy_hashes_and_receivers.textile +120 -0
  35. data/notes/hash_rdocs.textile +97 -0
  36. data/spec/hash/deep_merge_spec.rb +0 -2
  37. data/spec/hash/indifferent_access_spec.rb +391 -0
  38. data/spec/hash/slice_spec.rb +35 -12
  39. data/spec/hashlike/behave_same_as_hash_spec.rb +105 -0
  40. data/spec/hashlike/hashlike_behavior_spec.rb +824 -0
  41. data/spec/hashlike/hashlike_via_accessors_fuzzing_spec.rb +37 -0
  42. data/spec/hashlike/hashlike_via_accessors_spec.rb +262 -0
  43. data/spec/hashlike_spec.rb +302 -0
  44. data/spec/metaprogramming/aliasing_spec.rb +3 -0
  45. data/spec/metaprogramming/cattr_accessor_spec.rb +2 -0
  46. data/spec/metaprogramming/class_attribute_spec.rb +2 -0
  47. data/spec/metaprogramming/delegation_spec.rb +2 -0
  48. data/spec/metaprogramming/mattr_accessor_spec.rb +2 -0
  49. data/spec/metaprogramming/singleton_class_spec.rb +3 -0
  50. data/spec/receiver/acts_as_hash_spec.rb +286 -0
  51. data/spec/receiver_spec.rb +478 -0
  52. data/spec/spec_helper.rb +11 -6
  53. data/spec/string/truncate_spec.rb +1 -0
  54. data/spec/struct/acts_as_hash_fuzz_spec.rb +67 -0
  55. data/spec/struct/acts_as_hash_spec.rb +426 -0
  56. data/spec/support/hashlike_fuzzing_helper.rb +127 -0
  57. data/spec/support/hashlike_helper.rb +75 -0
  58. data/spec/support/hashlike_struct_helper.rb +37 -0
  59. data/spec/support/hashlike_via_delegation.rb +30 -0
  60. data/spec/support/matchers/be_array_eql.rb +12 -0
  61. data/spec/support/matchers/be_hash_eql.rb +14 -0
  62. data/spec/support/matchers/enumerate_method.rb +10 -0
  63. data/spec/support/matchers/evaluate_to_true.rb +5 -0
  64. metadata +62 -4
@@ -0,0 +1,60 @@
1
+ require 'gorillib/object/blank'
2
+ module Gorillib
3
+ module Hashlike
4
+ module Compact
5
+
6
+ # returns a compact!ed copy (contains no key/value pairs having nil? values)
7
+ #
8
+ # @example
9
+ # hsh = { :a => 100, :b => nil, :c => false, :d => "" }
10
+ # hsh.compact # => { :a => 100, :c => false, :d => "" }
11
+ # hsh # => { :a => 100, :b => nil, :c => false, :d => "" }
12
+ #
13
+ # @return [Hashlike]
14
+ #
15
+ def compact
16
+ reject{|key,val| val.nil? }
17
+ end
18
+
19
+ # Removes all key/value pairs having nil? values
20
+ #
21
+ # @example
22
+ # hsh = { :a => 100, :b => nil, :c => false, :d => "" }
23
+ # hsh.compact # => { :a => 100, :c => false, :d => "" }
24
+ # hsh # => { :a => 100, :c => false, :d => "" }
25
+ #
26
+ # @return [Hashlike]
27
+ #
28
+ def compact!
29
+ delete_if{|key,val| val.nil? }
30
+ end
31
+
32
+ # returns a compact!ed copy (contains no key/value pairs having blank? values)
33
+ #
34
+ # @example
35
+ # hsh = { :a => 100, :b => nil, :c => false, :d => "" }
36
+ # hsh.compact # => { :a => 100 }
37
+ # hsh # => { :a => 100, :b => nil, :c => false, :d => "" }
38
+ #
39
+ # @return [Hashlike]
40
+ #
41
+ def compact_blank
42
+ reject{|key,val| val.blank? }
43
+ end
44
+
45
+ # Removes all key/value pairs having blank? values
46
+ #
47
+ # @example
48
+ # hsh = { :a => 100, :b => nil, :c => false, :d => "" }
49
+ # hsh.compact # => { :a => 100 }
50
+ # hsh # => { :a => 100 }
51
+ #
52
+ # @return [Hashlike]
53
+ #
54
+ def compact_blank!
55
+ delete_if{|key,val| val.blank? }
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,18 @@
1
+ require 'gorillib/object/blank'
2
+ module Gorillib
3
+ module Hashlike
4
+ module DeepCompact
5
+
6
+ #
7
+ # deep_compact! removes all keys with 'blank?' values in the hash, in place, recursively
8
+ #
9
+ def deep_compact!
10
+ self.each do |key, val|
11
+ val.deep_compact! if val.respond_to?(:deep_compact!)
12
+ self.delete(key) if val.blank?
13
+ end
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module DeepDup
4
+ # Returns a deep copy of hash.
5
+ def deep_dup
6
+ duplicate = self.dup
7
+ duplicate.each_pair do |k,v|
8
+ tv = duplicate[k]
9
+ duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v
10
+ end
11
+ duplicate
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module DeepMerge
4
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
5
+ def deep_merge(other_hash)
6
+ dup.deep_merge!(other_hash)
7
+ end unless method_defined?(:deep_merge)
8
+
9
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
10
+ # Modifies the receiver in place.
11
+ def deep_merge!(other_hash)
12
+ other_hash.each_pair do |k,v|
13
+ tv = self[k]
14
+ self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
15
+ end
16
+ self
17
+ end unless method_defined?(:deep_merge!)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,169 @@
1
+ require 'enumerator'
2
+ module Gorillib
3
+ module Hashlike
4
+ #
5
+ # Makes a Receiver thingie behave mostly like a hash.
6
+ #
7
+ # By default, the hashlike methods iterate over the receiver attributes:
8
+ # instance #keys delegates to self.class.keys which calls
9
+ # receiver_attr_names. If you want to filter our add to the keys list, you
10
+ # can just override the class-level keys method (and call super, or not):
11
+ #
12
+ # def self.keys
13
+ # super + [:firstname, :lastname] - [:fullname]
14
+ # end
15
+ #
16
+ # in addition to the below, by including Enumerable, this also adds
17
+ #
18
+ # :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object,
19
+ # :map, :collect, :collect_concat, :entries, :to_a, :flat_map, :inject, :reduce,
20
+ # :group_by, :chunk, :cycle, :partition, :reverse_each, :slice_before, :drop,
21
+ # :drop_while, :take, :take_while, :detect, :find, :find_all, :find_index, :grep,
22
+ # :all?, :any?, :none?, :one?, :first, :count, :zip :max, :max_by, :min, :min_by,
23
+ # :minmax, :minmax_by, :sort, :sort_by
24
+ #
25
+ # As opposed to hash, does *not* define
26
+ #
27
+ # default, default=, default_proc, default_proc=, shift, flatten, compare_by_identity
28
+ # compare_by_identity? rehash
29
+ #
30
+ module HashlikeViaAccessors
31
+
32
+ # Hashlike#[]
33
+ #
34
+ # Element Reference -- Retrieves the value stored for +key+.
35
+ #
36
+ # In a normal hash, a default value can be set; none is provided here.
37
+ #
38
+ # Delegates to self.send(key)
39
+ #
40
+ # @example
41
+ # hsh = { :a => 100, :b => 200 }
42
+ # hsh[:a] # => 100
43
+ # hsh[:c] # => nil
44
+ #
45
+ # @param key [Object] key to retrieve
46
+ # @return [Object] the value stored for key, nil if missing
47
+ #
48
+ def [](key)
49
+ key = convert_key(key)
50
+ self.send(key)
51
+ end
52
+
53
+ # Hashlike#[]=
54
+ # Hashlike#store
55
+ #
56
+ # Element Assignment -- Associates the value given by +val+ with the key
57
+ # given by +key+.
58
+ #
59
+ # key should not have its value changed while it is in use as a key. In a
60
+ # normal hash, a String passed as a key will be duplicated and frozen. No such
61
+ # guarantee is provided here
62
+ #
63
+ # Delegates to self.send("key=", val)
64
+ #
65
+ # @example
66
+ # hsh = { :a => 100, :b => 200 }
67
+ # hsh[:a] = 9
68
+ # hsh[:c] = 4
69
+ # hsh # => { :a => 9, :b => 200, :c => 4 }
70
+ #
71
+ # hsh[key] = val -> val
72
+ # hsh.store(key, val) -> val
73
+ #
74
+ # @param key [Object] key to associate
75
+ # @param val [Object] value to associate it with
76
+ # @return [Object]
77
+ #
78
+ def []=(key, val)
79
+ key = convert_key(key)
80
+ self.send("#{key}=", val)
81
+ end
82
+
83
+ # Hashlike#delete
84
+ #
85
+ # Deletes and returns the value from +hsh+ whose key is equal to +key+. If the
86
+ # optional code block is given and the key is not found, pass in the key and
87
+ # return the result of +block+.
88
+ #
89
+ # In a normal hash, a default value can be set; none is provided here.
90
+ #
91
+ # @example
92
+ # hsh = { :a => 100, :b => 200 }
93
+ # hsh.delete(:a) # => 100
94
+ # hsh.delete(:z) # => nil
95
+ # hsh.delete(:z){|el| "#{el} not found" } # => "z not found"
96
+ #
97
+ # @overload hsh.delete(key) -> val
98
+ # @param key [Object] key to remove
99
+ # @return [Object, Nil] the removed object, nil if missing
100
+ #
101
+ # @overload hsh.delete(key){|key| block } -> val
102
+ # @param key [Object] key to remove
103
+ # @yield [Object] called (with key) if key is missing
104
+ # @yieldparam key
105
+ # @return [Object, Nil] the removed object, or if missing, the return value
106
+ # of the block
107
+ #
108
+ def delete(key, &block)
109
+ key = convert_key(key)
110
+ if has_key?(key)
111
+ val = self[key]
112
+ self.send(:remove_instance_variable, "@#{key}")
113
+ val
114
+ elsif block_given?
115
+ block.call(key)
116
+ else
117
+ nil
118
+ end
119
+ end
120
+
121
+ # # Hashlike#==
122
+ # #
123
+ # # Equality -- Two hashes are equal if they contain the same number of keys,
124
+ # # and the value corresponding to each key in the first hash is equal (using
125
+ # # <tt>==</tt>) to the value for the same key in the second. If +obj+ is not a
126
+ # # Hashlike, attempt to convert it using +to_hash+ and return <tt>obj ==
127
+ # # hsh</tt>.
128
+ # #
129
+ # # Does not take a default value comparion into account.
130
+ # #
131
+ # # @example
132
+ # # h1 = { :a => 1, :c => 2 }
133
+ # # h2 = { 7 => 35, :c => 2, :a => 1 }
134
+ # # h3 = { :a => 1, :c => 2, 7 => 35 }
135
+ # # h4 = { :a => 1, :d => 2, :f => 35 }
136
+ # # h1 == h2 # => false
137
+ # # h2 == h3 # => true
138
+ # # h3 == h4 # => false
139
+ # #
140
+ # def ==(other_hash)
141
+ # (length == other_hash.length) &&
142
+ # all?{|k,v| v == other_hash[k] }
143
+ # end
144
+
145
+ # Hashlike#keys
146
+ #
147
+ # Returns a new array populated with the keys from this hashlike.
148
+ #
149
+ # @see Hashlike#values.
150
+ #
151
+ # @example
152
+ # hsh = { :a => 100, :b => 200, :c => 300, :d => 400 }
153
+ # hsh.keys # => [:a, :b, :c, :d]
154
+ #
155
+ # @return [Array] list of keys
156
+ #
157
+ def keys
158
+ instance_variables.map{|s| convert_key(s[1..-1]) }
159
+ end
160
+
161
+ protected
162
+ def convert_key(key)
163
+ raise ArgumentError, "Keys for #{self.class} must be symbols, strings or respond to #to_sym" unless key.respond_to?(:to_sym)
164
+ key.to_sym
165
+ end
166
+
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,59 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module Keys
4
+ # Return a new hash with all keys converted to strings.
5
+ def stringify_keys
6
+ dup.stringify_keys!
7
+ end unless method_defined?(:stringify_keys)
8
+
9
+ # Destructively convert all keys to strings.
10
+ def stringify_keys!
11
+ keys.each do |key|
12
+ self[key.to_s] = delete(key)
13
+ end
14
+ self
15
+ end unless method_defined?(:stringify_keys!)
16
+
17
+ # Return a new hash with all keys converted to symbols, as long as
18
+ # they respond to +to_sym+.
19
+ def symbolize_keys
20
+ dup.symbolize_keys!
21
+ end unless method_defined?(:symbolize_keys)
22
+
23
+ # Destructively convert all keys to symbols, as long as they respond
24
+ # to +to_sym+.
25
+ def symbolize_keys!
26
+ keys.each do |key|
27
+ self[(key.to_sym rescue key) || key] = delete(key)
28
+ end
29
+ self
30
+ end unless method_defined?(:symbolize_keys!)
31
+
32
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
33
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
34
+ # as keys, this will fail.
35
+ #
36
+ # ==== Examples
37
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
38
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
39
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
40
+ def assert_valid_keys(*valid_keys)
41
+ unknown_keys = keys - [valid_keys].flatten
42
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
43
+ end unless method_defined?(:assert_valid_keys)
44
+ end
45
+ # # @return [Hash] the object as a Hash with symbolized keys.
46
+ # def symbolize_keys() to_hash ; end
47
+ # # @return [Hash] the object as a Hash with string keys.
48
+ # def stringify_keys() to_hash.stringify_keys ; end
49
+ #
50
+ # # Used to provide the same interface as Hash.
51
+ # # @return This object unchanged.
52
+ # def symbolize_keys!; self end
53
+ #
54
+ # # Used to provide the same interface as Hash.
55
+ # # @return This object unchanged.
56
+ # def stringify_keys!; self end
57
+ #
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module ReverseMerge
4
+ # Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
5
+ # in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
6
+ #
7
+ # def setup(options = {})
8
+ # options.reverse_merge! :size => 25, :velocity => 10
9
+ # end
10
+ #
11
+ # Using <tt>merge</tt>, the above example would look as follows:
12
+ #
13
+ # def setup(options = {})
14
+ # { :size => 25, :velocity => 10 }.merge(options)
15
+ # end
16
+ #
17
+ # The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
18
+ # have the respective key.
19
+ def reverse_merge(other_hash)
20
+ other_hash.merge(self)
21
+ end unless method_defined?(:reverse_merge)
22
+
23
+ # Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
24
+ # Modifies the receiver in place.
25
+ def reverse_merge!(other_hash)
26
+ merge!( other_hash ){|k,o,n| o }
27
+ end unless method_defined?(:reverse_merge!)
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,67 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module Slice
4
+ # Slice a hash to include only the given allowed_keys. This is useful for
5
+ # limiting an options hash to valid keys before passing to a method:
6
+ #
7
+ # def search(criteria = {})
8
+ # assert_valid_keys(:mass, :velocity, :time)
9
+ # end
10
+ #
11
+ # search(options.slice(:mass, :velocity, :time))
12
+ #
13
+ # If you have an array of keys you want to limit to, you should splat them:
14
+ #
15
+ # valid_keys = [:mass, :velocity, :time]
16
+ # search(options.slice(*valid_keys))
17
+ def slice(*allowed_keys)
18
+ allowed_keys = allowed_keys.map!{|key| convert_key(key) } if respond_to?(:convert_key)
19
+ hash = self.class.new
20
+ allowed_keys.each{|k| hash[k] = self[k] if has_key?(k) }
21
+ hash
22
+ end unless method_defined?(:slice)
23
+
24
+ # Replaces the hash with only the given allowed_keys.
25
+ # Returns a hash containing the removed key/value pairs
26
+ # @example
27
+ # hsh = {:a => 1, :b => 2, :c => 3, :d => 4}
28
+ # hsh.slice!(:a, :b)
29
+ # # => {:c => 3, :d =>4}
30
+ # hsh
31
+ # # => {:a => 1, :b => 2}
32
+ def slice!(*allowed_keys)
33
+ allowed_keys = allowed_keys.map!{|key| convert_key(key) } if respond_to?(:convert_key)
34
+ omit = slice(*self.keys - allowed_keys)
35
+ hash = slice(*allowed_keys)
36
+ replace(hash)
37
+ omit
38
+ end unless method_defined?(:slice!)
39
+
40
+ # This also works, and doesn't require #replace method, but is uglier and
41
+ # wasn't written by Railsians. I'm not sure that slice! is interesting if
42
+ # you're a duck-typed Hash but not is_a?(Hash), so we'll just leave it at the
43
+ # active_record implementation.
44
+ #
45
+ # def slice!(*allowed_keys)
46
+ # allowed_keys = allowed_keys.map!{|key| convert_key(key) } if respond_to?(:convert_key)
47
+ # omit_keys = self.keys - allowed_keys
48
+ # omit = slice(*omit_keys)
49
+ # omit_keys.each{|k| delete(k) }
50
+ # omit
51
+ # end
52
+
53
+ # Removes the given allowed_keys from the hash
54
+ # Returns a hash containing the removed key/value pairs
55
+ #
56
+ # @example
57
+ # hsh = {:a => 1, :b => 2, :c => 3, :d => 4}
58
+ # hsh.extract!(:a, :b)
59
+ # # => {:a => 1, :b => 2}
60
+ # hsh
61
+ # # => {:c => 3, :d =>4}
62
+ def extract!(*allowed_keys)
63
+ slice!(*self.keys - allowed_keys)
64
+ end unless method_defined?(:extract!)
65
+ end
66
+ end
67
+ end