angry_hash 0.1.1 → 0.2.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.
data/Changelog.md ADDED
@@ -0,0 +1,5 @@
1
+ ## 0.2.0
2
+
3
+ Features:
4
+
5
+ - Extension tracking extracted from PeaceLove. PeaceLove will use this in favour of its own implementation in the its version.
data/License ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 PLUS2 Pty. Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile CHANGED
@@ -7,7 +7,8 @@ begin
7
7
  gemspec.email = "lachie@plus2.com.au"
8
8
  gemspec.homepage = "http://github.com/plus2/angry_hash"
9
9
  gemspec.authors = ["Lachie Cox"]
10
- gemspec.test_files = []
10
+ gemspec.test_files = Dir['examples/**/*']
11
+ gemspec.files -= gemspec.test_files
11
12
  gemspec.executables = []
12
13
  end
13
14
  Jeweler::GemcutterTasks.new
data/Readme.md ADDED
@@ -0,0 +1,114 @@
1
+ # AngryHash
2
+
3
+ A stabler mash with different emphases.
4
+
5
+ # Install
6
+
7
+ gem install angry_hash
8
+
9
+ # Usage
10
+ grr = AngryHash[] #=> {}
11
+ grr = AngryHash.new #=> {}
12
+
13
+ grr = AngryHash[ :look => { :a => ['hash'] } ]
14
+
15
+ # reach into the hash using dot notation
16
+ grr.look.a[0] #=> 'hash'
17
+
18
+ # instantiate a sub-hash (idempotently):
19
+ grr.look.another #=> nil
20
+ grr.look.another!.one = "nice"
21
+
22
+ grr.look.another!.one #=> "nice"
23
+ grr.look.another.one #=> "nice"
24
+
25
+ # truth in hashes
26
+ grr.green = true
27
+ grr.yellow = [:goldenrod,:canary]
28
+ grr.red = false
29
+
30
+ grr.green? #=> true
31
+ grr.yellow? #=> true
32
+ grr.red? #=> false
33
+ grr.blue? #=> false
34
+
35
+
36
+ ## Merging Deeply
37
+
38
+ ### Merge
39
+
40
+ Deep merges are non-destructive, as in normal hash merges.
41
+
42
+ Merge favours the other hash's keys. Reverse merge favours the target's keys.
43
+
44
+ grr = AngryHash[ :a => {:b => :c}, :z => :x ]
45
+ arr = AngryHash[ :a => {:d => :e}, :z => :y ]
46
+
47
+ grr.deep_merge( arr ) #=> {"a" => {"b" => :c, "d" => :e}, "z" => :y}
48
+ grr.reverse_deep_merge( arr ) #=> {"a" => {"b" => :c, "d" => :e}, "z" => :x}
49
+
50
+ # grr & arr are unmodified
51
+ grr #=> { "a" => {"b" => :c}, "z" => :x }
52
+ arr #=> { "a" => {"d" => :e}, "z" => :y }
53
+
54
+ ### Update
55
+
56
+ Deep updates replace the contents of the hash with the merged version.
57
+
58
+ They're also known as `deep_merge!` and `reverse_deep_merge!`
59
+
60
+ grr = AngryHash[ :a => {:b => :c}, :z => :x ]
61
+ arr = AngryHash[ :a => {:d => :e}, :z => :y ]
62
+
63
+ grr.deep_update( arr )
64
+
65
+ # grr is updated, arr is unmodified
66
+ grr #=> { "a" => {"b" => :c, "d" => :e}, "z" => :y }
67
+ arr #=> { "a" => {"d" => :e}, "z" => :y }
68
+
69
+ grr.reverse_deep_update( arr )
70
+
71
+ # grr is updated, arr is unmodified
72
+ grr #=> { "a" => {"b" => :c, "d" => :e}, "z" => :x }
73
+ arr #=> { "a" => {"d" => :e}, "z" => :y }
74
+
75
+ # Extensions
76
+
77
+ `TODO` write up this section.
78
+
79
+ # Gotchas
80
+
81
+ ## AngryHashes are Hashes
82
+
83
+ This is good and bad. Its good because you can use all the normal hash methods.
84
+
85
+ AngryHash[ :a => :b, :c => :d ].each {|k,v| ... }
86
+
87
+ Unfortunately this also means that that if your hash keys collide with the names of Hash methods (or those of its ancestors) you can't use dot notation:
88
+
89
+ h = AngryHash[ :hash => 'hooray' ]
90
+
91
+ h.hash #=> 12345...
92
+ h['hash'] #=> 'hooray'
93
+ h[:hash] #=> 'hooray'
94
+
95
+ I'm planning on creating a sanitised `AngryProxy` to wrap `AngryHash`es for the times you really want guaranteed access via dot notation.
96
+
97
+ ## id
98
+
99
+ Since `Object#id` is deprecated in Ruby 1.8 and has been removed in 1.9, I decided it was safe to override `id` in `AngryHash`:
100
+
101
+ h = AngryHash[ :id => 'abc' ]
102
+
103
+ h.id #=> 'abc'
104
+ h.__id__ #=> 12345...
105
+
106
+ # About
107
+
108
+ `AngryHash`'s code is hosted on GitHub and can be found at http://github.com/plus2/angry_hash
109
+
110
+ Please report problems at http://github.com/plus2/angry_hash/issues.
111
+
112
+ `AngryHash` is by Lachie Cox for PLUS2 & YesMaster.
113
+
114
+ You're free to use AngryHash under the MIT license; see License for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
data/angry_hash.gemspec CHANGED
@@ -5,27 +5,24 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{angry_hash}
8
- s.version = "0.1.1"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Lachie Cox"]
12
- s.date = %q{2010-08-23}
12
+ s.date = %q{2010-08-25}
13
13
  s.description = %q{A stabler mash with different emphases. Used in plus2 projects AngryMob and Igor.}
14
14
  s.email = %q{lachie@plus2.com.au}
15
15
  s.files = [
16
16
  ".gitignore",
17
+ "Changelog.md",
18
+ "License",
17
19
  "Rakefile",
20
+ "Readme.md",
18
21
  "VERSION",
19
22
  "angry_hash.gemspec",
20
- "examples/accessors_eg.rb",
21
- "examples/creation_eg.rb",
22
- "examples/dsl.eg.rb",
23
- "examples/dup_eg.rb",
24
- "examples/eg_helper.rb",
25
- "examples/merge_eg.rb",
26
23
  "lib/angry_hash.rb",
27
24
  "lib/angry_hash/dsl.rb",
28
- "lib/angry_hash/extension_tracking.rb",
25
+ "lib/angry_hash/extension.rb",
29
26
  "lib/angry_hash/merge_string.rb"
30
27
  ]
31
28
  s.homepage = %q{http://github.com/plus2/angry_hash}
@@ -33,6 +30,15 @@ Gem::Specification.new do |s|
33
30
  s.require_paths = ["lib"]
34
31
  s.rubygems_version = %q{1.3.6}
35
32
  s.summary = %q{A stabler mash with different emphases.}
33
+ s.test_files = [
34
+ "examples/accessors_eg.rb",
35
+ "examples/creation_eg.rb",
36
+ "examples/dsl.eg.rb",
37
+ "examples/dup_eg.rb",
38
+ "examples/eg_helper.rb",
39
+ "examples/extension_tracking.rb",
40
+ "examples/merge_eg.rb"
41
+ ]
36
42
 
37
43
  if s.respond_to? :specification_version then
38
44
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -0,0 +1,106 @@
1
+ require 'eg_helper'
2
+ require 'angry_hash/extension'
3
+
4
+ module Fixtures
5
+ module CreditCard
6
+ def valid?
7
+ number % 2 == 1
8
+ end
9
+ end
10
+ module Course
11
+ def shouty
12
+ name.upcase
13
+ end
14
+ end
15
+
16
+ module PhoneNumber
17
+ def country_code
18
+ number.to_s[/^\+(\d+)/,1].to_i
19
+ end
20
+ end
21
+
22
+ module User
23
+ include AngryHash::Extension
24
+
25
+ extend_value :credit_card, CreditCard
26
+ extend_array :courses, Course
27
+ extend_hash :phone_numbers, PhoneNumber
28
+
29
+ extend_hash :parents, User
30
+
31
+ def shouty
32
+ name.upcase
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ eg 'tracks extensions to values' do
39
+ user = AngryHash[ :name => 'Bob', :credit_card => {:number => 123} ]
40
+ user.extend Fixtures::User
41
+
42
+ Assert( user.shouty == 'BOB' )
43
+ Assert( user.credit_card.number == 123 )
44
+ Assert( user.credit_card.valid? )
45
+
46
+ user.credit_card.number = 456
47
+ Assert( ! user.credit_card.valid? )
48
+ end
49
+
50
+ eg 'tracks extensions to hashes' do
51
+ user = AngryHash[ :name => 'Bob', :credit_card => {:number => 123}, :phone_numbers => {:home => {:number => '0404555111'}, :chalet => {:number => '+12 9481 1111'}} ]
52
+ user.extend Fixtures::User
53
+
54
+ Assert( user.phone_numbers.home.country_code == 0 )
55
+ Assert( user.phone_numbers.chalet.country_code == 12 )
56
+ end
57
+
58
+ eg 'tracks extensions to arrays' do
59
+ user = AngryHash[ :name => 'Bob', :credit_card => {:number => 123}, :courses => [ {:name => 'Money for artists'}, {:name => 'Papier mache with your fortune'} ] ]
60
+ user.extend Fixtures::User
61
+
62
+ Assert( user.courses[0].shouty == 'MONEY FOR ARTISTS' )
63
+ Assert( user.courses[1].shouty == 'PAPIER MACHE WITH YOUR FORTUNE' )
64
+ end
65
+
66
+ eg 'tracks deep and reflexive extensions' do
67
+ user = AngryHash.new_extended( Fixtures::User, :name => 'Bob',
68
+ :parents => {
69
+ :mum => {:name => 'Maggie', :parents => {:dad => {:name => 'Bill'}, :mum => {:name => 'Nancy'}}},
70
+ :dad => {:name => 'John'}
71
+ }
72
+ )
73
+
74
+ Assert( user.parents.mum.parents.dad.shouty == 'BILL' )
75
+ end
76
+
77
+ eg 'extensions persist with dup_with_extension' do
78
+ user = AngryHash[ :name => 'Bob', :credit_card => {:number => 123} ]
79
+ user.extend Fixtures::User
80
+
81
+ Assert( user.credit_card.valid? )
82
+
83
+ # new_user is expected to be extended
84
+ new_user = user.dup_with_extension
85
+
86
+ # clean_user is expected *not* to be extended
87
+ clean_user = user.dup
88
+
89
+ # make sure dup worked
90
+ Assert( user.__id__ != new_user.__id__ )
91
+ Assert( user.__id__ != clean_user.__id__ )
92
+
93
+ # make sure dup worked deeply
94
+ Assert( user.credit_card.__id__ != new_user.credit_card.__id__ )
95
+ Assert( user.credit_card.__id__ != clean_user.credit_card.__id__ )
96
+
97
+ # the non-extended user's CC is invalid because `valid?` is accessing a nil value in the hash, rather than calling the method `valid?`
98
+ Assert( clean_user.credit_card.number == 123 )
99
+ Assert( ! clean_user.credit_card.valid? )
100
+ Assert( defined?( clean_user.credit_card.valid? ) == nil )
101
+
102
+ # extended user's CC is valid. `valid?` refers to the method on the `CreditCard` module.
103
+ Assert( new_user.credit_card.number == 123 )
104
+ Assert( new_user.credit_card.valid? )
105
+ Assert( defined?( new_user.credit_card.valid? ) == 'method' )
106
+ end
@@ -0,0 +1,162 @@
1
+ require 'angry_hash'
2
+
3
+ class AngryHash
4
+ def self.new_extended(mod,seed={})
5
+ self[ seed ].tap {|hash| hash.extend(mod) }
6
+ end
7
+
8
+ module Extension
9
+ class << self
10
+ def included(base)
11
+ base.extend ClassMethods
12
+
13
+ base.module_eval do
14
+ def self.extend_object(obj)
15
+ super
16
+ Extension.mark_extension(obj,self)
17
+ end
18
+ end
19
+ end
20
+
21
+ def mark_extension(hash,mod)
22
+ #puts "mark_extension hash=#{hash.class} mod=#{mod}"
23
+
24
+ if (previous_mod = hash.__angry_hash_extension) && previous_mod != mod
25
+ raise "hash #{hash} has already been extended by a different AngryHash::Extension (was: #{previous_mod}, now: #{mod})"
26
+ end
27
+ hash.__angry_hash_extension = mod
28
+
29
+ setup_extended_hash(hash,mod)
30
+ end
31
+
32
+ # A record of extensions to fields of classes.
33
+ def mixin_registry
34
+ @mixin_registry ||= Hash.new {|h,k| h[k] = {}}
35
+ end
36
+
37
+ # Register a value extension
38
+ def register_mixin(target_class,field,mod,options)
39
+ mixin_registry[target_class][field.to_s] = [:single, mod, options]
40
+ end
41
+
42
+ # Register an array extension
43
+ def register_mixin_array(target_class, field, mod, options)
44
+ mixin_registry[target_class][field.to_s] = [:array, mod, options]
45
+ end
46
+
47
+ # Register a hash extension
48
+ def register_mixin_hash(target_class, field, mod, options)
49
+ mixin_registry[target_class][field.to_s] = [:hash, mod, options]
50
+ end
51
+
52
+ def extend_hash(hash, mod, parent_hash)
53
+ if !parent_hash.nil? && hash.nil?
54
+ hash = AngryHash.new
55
+ end
56
+
57
+ hash.extend mod
58
+
59
+ hash.__parent_hash = parent_hash if hash.respond_to?(:__parent_hash=)
60
+ hash
61
+ end
62
+
63
+ def setup_extended_hash(hash, mod)
64
+ mod.fill_in_defaults(hash) if mod.respond_to?(:fill_in_defaults)
65
+ hash
66
+ end
67
+
68
+ def mixin_to(parent_obj, field, obj)
69
+ extension = parent_obj.__angry_hash_extension
70
+
71
+ if mixin = mixin_registry[extension][field.to_s]
72
+ kind,mod,options = *mixin
73
+
74
+ if options.key?(:default) && obj.nil?
75
+ obj = options[:default]
76
+ end
77
+
78
+ case kind
79
+ when :single
80
+ obj = extend_hash(obj,mod,parent_obj)
81
+ when :array
82
+ # XXX - this is ok for now... we really need to typecheck, perhaps wrap in a smart-array
83
+ obj ||= []
84
+ obj = obj.map {|elt| extend_hash(elt, mod, parent_obj)}
85
+ when :hash
86
+ obj ||= {}
87
+ obj = obj.inject(AngryHash.new) do |h,(k,elt)|
88
+ h[k] = extend_hash(elt,mod,parent_obj)
89
+ h
90
+ end
91
+ end
92
+ end
93
+
94
+ obj
95
+ end
96
+ end
97
+
98
+ def [](key)
99
+ Extension.mixin_to(self,key,super)
100
+ end
101
+
102
+ def id
103
+ self['id']
104
+ end
105
+
106
+ def dup_with_extension
107
+ dup.tap {|new_hash|
108
+ new_hash.extend(__angry_hash_extension) if __angry_hash_extension
109
+ }
110
+ end
111
+
112
+ def __parent_hash=(hash)
113
+ @__parent_hash = hash
114
+ end
115
+
116
+ def __parent_hash
117
+ @__parent_hash
118
+ end
119
+
120
+ def __angry_hash_extension=(mod)
121
+ @__angry_hash_extension = mod
122
+ end
123
+
124
+ def __angry_hash_extension
125
+ @__angry_hash_extension
126
+ end
127
+
128
+ module ClassMethods
129
+ def defaults(default_form=nil)
130
+ if default_form
131
+ @default_form = default_form
132
+ end
133
+ @default_form
134
+ end
135
+
136
+ def fill_in_defaults(hash)
137
+ if defaults
138
+ hash.reverse_deep_update(defaults)
139
+ end
140
+ end
141
+
142
+ def extend_value(field,mod,options={})
143
+ Extension.register_mixin(self,field,mod,options)
144
+ end
145
+
146
+ def extend_array(field,mod,options={})
147
+ Extension.register_mixin_array(self,field,mod,options)
148
+ end
149
+
150
+ def extend_hash(field,mod,options={})
151
+ Extension.register_mixin_hash(self,field,mod,options)
152
+ end
153
+
154
+ def build(seed={})
155
+ AngryHash[ seed ].tap {|hash|
156
+ self.fill_in_defaults(hash)
157
+ hash.extend self
158
+ }
159
+ end
160
+ end
161
+ end
162
+ end
data/lib/angry_hash.rb CHANGED
@@ -1,10 +1,15 @@
1
1
 
2
2
  class AngryHash < Hash
3
+ # config
3
4
  require 'angry_hash/dsl'
4
5
  include AngryHash::DSL
5
6
 
6
- def self.[](other)
7
- super(__convert(other))
7
+ def self.[](other=nil)
8
+ if other
9
+ super(__convert(other))
10
+ else
11
+ new
12
+ end
8
13
  end
9
14
 
10
15
  alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
8
- - 1
9
- version: 0.1.1
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Lachie Cox
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-08-23 00:00:00 +10:00
17
+ date: 2010-08-25 00:00:00 +10:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -28,18 +28,15 @@ extra_rdoc_files: []
28
28
 
29
29
  files:
30
30
  - .gitignore
31
+ - Changelog.md
32
+ - License
31
33
  - Rakefile
34
+ - Readme.md
32
35
  - VERSION
33
36
  - angry_hash.gemspec
34
- - examples/accessors_eg.rb
35
- - examples/creation_eg.rb
36
- - examples/dsl.eg.rb
37
- - examples/dup_eg.rb
38
- - examples/eg_helper.rb
39
- - examples/merge_eg.rb
40
37
  - lib/angry_hash.rb
41
38
  - lib/angry_hash/dsl.rb
42
- - lib/angry_hash/extension_tracking.rb
39
+ - lib/angry_hash/extension.rb
43
40
  - lib/angry_hash/merge_string.rb
44
41
  has_rdoc: true
45
42
  homepage: http://github.com/plus2/angry_hash
@@ -71,5 +68,11 @@ rubygems_version: 1.3.6
71
68
  signing_key:
72
69
  specification_version: 3
73
70
  summary: A stabler mash with different emphases.
74
- test_files: []
75
-
71
+ test_files:
72
+ - examples/accessors_eg.rb
73
+ - examples/creation_eg.rb
74
+ - examples/dsl.eg.rb
75
+ - examples/dup_eg.rb
76
+ - examples/eg_helper.rb
77
+ - examples/extension_tracking.rb
78
+ - examples/merge_eg.rb
@@ -1,12 +0,0 @@
1
- class AngryHash
2
- module ExtensionTracking
3
- def self.included(base)
4
- # base.extend ClassMethods
5
- end
6
- end
7
-
8
- def extend(mod)
9
- puts "extending AH #{__id__} with #{mod}"
10
- super
11
- end
12
- end