angry_hash 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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