gorillib 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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