smart_hash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ # General Ruby.
2
+ .ref-*
3
+ .old*
4
+ *-old*
5
+
6
+ # Project-specific.
7
+ /*.rb
8
+ /doc/
9
+ /pkg/
10
+ /.rvmrc
11
+ /.yardoc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -fn # Tree-like progress.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem dependencies in `PROJECT.gemspec`.
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ smart_hash (0.1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rspec (2.8.0)
11
+ rspec-core (~> 2.8.0)
12
+ rspec-expectations (~> 2.8.0)
13
+ rspec-mocks (~> 2.8.0)
14
+ rspec-core (2.8.0)
15
+ rspec-expectations (2.8.0)
16
+ diff-lcs (~> 1.1.2)
17
+ rspec-mocks (2.8.0)
18
+ yard (0.7.5)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ rspec
25
+ smart_hash!
26
+ yard
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Alex Fortuna
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+
2
+ A smarter alternative to OpenStruct
3
+ ===================================
4
+
5
+ Introduction
6
+ ------------
7
+
8
+ If you're unhappy with `OpenStruct` (like myself), you might consider using `SmartHash` because of these major features:
9
+
10
+ * You can access attributes as methods or keys. Both `person.name` and `person[:name]` will work.
11
+ * Attribute access is strict by default. `person.invalid_stuff` will raise an exception instead of returning the stupid `nil`.
12
+ * You can use **any** attribute names. `person.size = "XL"` will work as intended.
13
+ * `SmartHash` descends from `Hash` and inherits its rich feature set.
14
+
15
+
16
+ Setup
17
+ -----
18
+
19
+ ~~~
20
+ $ gem install smart_hash
21
+ ~~~
22
+
23
+ , or via Bundler's `Gemfile`:
24
+
25
+ ~~~
26
+ gem "smart_hash"
27
+ #gem "smart_hash", :git => "git://github.com/dadooda/smart_hash.git" # Edge version.
28
+ ~~~
29
+
30
+
31
+ Usage
32
+ -----
33
+
34
+ Create an object and set a few attributes:
35
+
36
+ ~~~
37
+ >> person = SmartHash[]
38
+ >> person.name = "John"
39
+ >> person.age = 25
40
+
41
+ >> person
42
+ => {:name=>"John", :age=>25}
43
+ ~~~
44
+
45
+ Read attributes:
46
+
47
+ ~~~
48
+ >> person.name
49
+ => "John"
50
+ >> person[:name]
51
+ => "John"
52
+ ~~~
53
+
54
+ Access an unset attribute:
55
+
56
+ ~~~
57
+ >> person.invalid_stuff
58
+ KeyError: key not found: :invalid_stuff
59
+ >> person[:invalid_stuff]
60
+ => nil
61
+ ~~~
62
+
63
+ Please note that `[]` access is always non-strict since `SmartHash` behaves as `Hash` here.
64
+
65
+ Manipulate attributes which exist as methods:
66
+
67
+ ~~~
68
+ >> person = SmartHash[:name => "John"]
69
+ >> person.size
70
+ => 1
71
+ >> person.size = "XL"
72
+ >> person.size
73
+ => "XL"
74
+ ~~~
75
+
76
+ **IMPORTANT:** You can use any attribute names excluding these: `default`, `default_proc`, `strict`.
77
+
78
+ Use `Hash` features, e.g. merge:
79
+
80
+ ~~~
81
+ >> person = SmartHash[:name => "John"]
82
+ >> person.merge(:surname => "Smith", :age => 25)
83
+ => {:name=>"John", :surname=>"Smith", :age=>25}
84
+ ~~~
85
+
86
+ , or iterate:
87
+
88
+ ~~~
89
+ >> person.each {|k, v| puts "#{k}: #{v}"}
90
+ name: John
91
+ surname: Smith
92
+ age: 25
93
+ ~~~
94
+
95
+ Suppose you want to disable strict mode:
96
+
97
+ ~~~
98
+ >> person = SmartHash[]
99
+ >> person.strict = false
100
+
101
+ >> person.name
102
+ => nil
103
+ >> person.age
104
+ => nil
105
+ ~~~
106
+
107
+ `SmartHash::Loose` is non-strict upon construction:
108
+
109
+ ~~~
110
+ >> person = SmartHash::Loose[]
111
+ >> person.name
112
+ => nil
113
+ >> person.age
114
+ => nil
115
+ ~~~
116
+
117
+ Suppose you **know** you will use the `size` attribute and you don't want any interference with the `#size` method. Use attribute declaration:
118
+
119
+ ~~~
120
+ >> person = SmartHash[]
121
+ >> person.declare(:size)
122
+ >> person.size
123
+ KeyError: key not found: :size
124
+ >> person.size = "XL"
125
+ >> person.size
126
+ => "XL"
127
+ ~~~
128
+
129
+ Suppose you set an attribute and want to ensure that it's not overwritten. Use attribute protection:
130
+
131
+ ~~~
132
+ >> person = SmartHash[]
133
+ >> person.name = "John"
134
+ >> person.protect(:name)
135
+
136
+ >> person.name = "Bob"
137
+ ArgumentError: Attribute 'name' is protected
138
+ ~~~
139
+
140
+
141
+ Compatibility
142
+ -------------
143
+
144
+ Tested to run on:
145
+
146
+ * Ruby 1.9.2-p180, Linux, RVM
147
+
148
+ Compatibility issue reports will be greatly appreciated.
149
+
150
+
151
+ Copyright
152
+ ---------
153
+
154
+ Copyright © 2012 Alex Fortuna.
155
+
156
+ Licensed under the MIT License.
157
+
158
+
159
+ Feedback
160
+ --------
161
+
162
+ Send bug reports, suggestions and criticisms through [project's page on GitHub](http://github.com/dadooda/smart_hash).
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/smart_hash.rb ADDED
@@ -0,0 +1,191 @@
1
+ require "set"
2
+
3
+ # Get us `SmartHash::Loose`. It's usually `Dir[]` in other gems, but we've only got 1 file at the moment.
4
+ require File.expand_path("../smart_hash/loose", __FILE__)
5
+
6
+ # == A smarter alternative to OpenStruct
7
+ #
8
+ # Major features:
9
+ #
10
+ # * You can access attributes as methods or keys.
11
+ # * Attribute access is strict by default.
12
+ # * You can use <b>any</b> attribute names.
13
+ # * Descends from `Hash` and inherits its rich feature set.
14
+ #
15
+ # See {rubydoc documentation}[http://rubydoc.info/github/dadooda/smart_hash/master/frames] for basic usage examples.
16
+ class SmartHash < Hash
17
+ # Attribute name regexp without delimiters.
18
+ ATTR_REGEXP = /[a-zA-Z_]\w*/
19
+
20
+ # Attribute names that are forbidden.
21
+ # Forbidden attrs cannot be manupulated as such and are handled as methods only.
22
+ FORBIDDEN_ATTRS = [:default, :default_proc, :strict]
23
+
24
+ # Gem version.
25
+ VERSION = "0.1.0"
26
+
27
+ # See #declare.
28
+ attr_reader :declared_attrs
29
+
30
+ # See #protect.
31
+ attr_reader :protected_attrs
32
+
33
+ # Strict mode. Default is <tt>true</tt>.
34
+ #
35
+ # person = SmartHash[]
36
+ # person.invalid_stuff # KeyError: key not found: :invalid_stuff
37
+ #
38
+ # person.strict = false
39
+ # person.invalid_stuff # => nil
40
+ attr_accessor :strict
41
+
42
+ def initialize(*args)
43
+ super
44
+ _smart_hash_init
45
+ end
46
+
47
+ # Alternative constructor.
48
+ #
49
+ # person = SmartHash[]
50
+ def self.[](*args)
51
+ super.tap do |_|
52
+ _.instance_eval do
53
+ _smart_hash_init
54
+ end
55
+ end
56
+ end
57
+
58
+ # Declare attributes. By declaring the attributes you ensure that there's no
59
+ # interference from existing methods.
60
+ #
61
+ # person = SmartHash[]
62
+ # person.declare(:size)
63
+ # person.size # KeyError: key not found: :size
64
+ #
65
+ # person.size = "XL"
66
+ # person.size # => "XL"
67
+ #
68
+ # See also #undeclare.
69
+ def declare(*attrs)
70
+ raise ArgumentError, "No attrs specified" if attrs.empty?
71
+ attrs.each do |attr|
72
+ (v = attr).is_a?(klass = Symbol) or raise ArgumentError, "#{klass} expected, #{v.class} (#{v.inspect}) given"
73
+ attr.to_s.match /\A#{ATTR_REGEXP}\z/ or raise ArgumentError, "Incorrect attribute name '#{attr}'"
74
+ @declared_attrs << attr # `Set` is returned.
75
+ end
76
+ end
77
+
78
+ # Protect attributes from being assigned.
79
+ #
80
+ # person = SmartHash[]
81
+ # person.name = "John"
82
+ # person.protect(:name)
83
+ #
84
+ # person.name = "Bob" # ArgumentError: Attribute 'name' is protected
85
+ #
86
+ # See also #unprotect.
87
+ def protect(*attrs)
88
+ raise ArgumentError, "No attrs specified" if attrs.empty?
89
+ attrs.each do |attr|
90
+ (v = attr).is_a?(klass = Symbol) or raise ArgumentError, "#{klass} expected, #{v.class} (#{v.inspect}) given"
91
+ attr.to_s.match /\A#{ATTR_REGEXP}\z/ or raise ArgumentError, "Incorrect attribute name '#{attr}'"
92
+ @protected_attrs << attr
93
+ end
94
+ end
95
+
96
+ def undeclare(*attrs)
97
+ raise ArgumentError, "No attrs specified" if attrs.empty?
98
+ attrs.each do |attr|
99
+ @declared_attrs.delete(attr) # `Set` is returned.
100
+ end
101
+ end
102
+
103
+ def unprotect(*attrs)
104
+ raise ArgumentError, "No attrs specified" if attrs.empty?
105
+ attrs.each do |attr|
106
+ @protected_attrs.delete(attr)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ # Make private copies of methods we need.
113
+ [:fetch, :instance_eval].each do |method_name|
114
+ my_method_name = "_smart_hash_#{method_name}".to_sym
115
+ alias_method my_method_name, method_name
116
+ private my_method_name
117
+ end
118
+
119
+ # Common post-initialize routine.
120
+ def _smart_hash_init #:nodoc:
121
+ @declared_attrs = Set[]
122
+ @strict = true
123
+
124
+ # Protect only the bare minimum. Technically speaking, assigning ANYTHING that exists as a method is potentially dangerous
125
+ # or confusing. So it's fairly pointless to try to protect everything. If the person wants to screw everything up on purpose,
126
+ # he'll find a way to do it anyway.
127
+ @protected_attrs = Set[:inspect, :to_s]
128
+
129
+ # Suppress warnings.
130
+ vrb, $VERBOSE = $VERBOSE, nil
131
+
132
+ # Insert lookup routine for existing methods, such as <tt>size</tt>.
133
+ methods.map(&:to_s).each do |method_name|
134
+ # Install control routine on correct attribute access methods only.
135
+ # NOTE: Check longer REs first.
136
+ case method_name
137
+ when /\A(#{ATTR_REGEXP})=\z/
138
+ # Case "r.attr=".
139
+ attr = $1.to_sym
140
+ next if FORBIDDEN_ATTRS.include? attr
141
+ _smart_hash_instance_eval <<-EOT
142
+ def #{method_name}(value)
143
+ raise ArgumentError, "Attribute '#{attr}' is protected" if @protected_attrs.include? :#{attr}
144
+ self[:#{attr}] = value
145
+ end
146
+ EOT
147
+ when /\A#{ATTR_REGEXP}\z/
148
+ # Case "r.attr".
149
+ next if FORBIDDEN_ATTRS.include? attr
150
+ _smart_hash_instance_eval <<-EOT
151
+ def #{method_name}(*args)
152
+ if @declared_attrs.include?(:#{method_name}) or has_key?(:#{method_name})
153
+ if @strict
154
+ _smart_hash_fetch(:#{method_name})
155
+ else
156
+ self[:#{method_name}]
157
+ end
158
+ else
159
+ super
160
+ end
161
+ end
162
+ EOT
163
+ end # case
164
+ end # each
165
+
166
+ # Restore warnings.
167
+ $VERBOSE = vrb
168
+ end
169
+
170
+ def method_missing(method_name, *args)
171
+ # NOTE: No need to check for forbidden attrs here, since they exist as methods by definition.
172
+
173
+ case method_name
174
+ when /\A(.+)=\z/
175
+ # Case "r.attr=". Attribute assignment. Method name is pre-validated for us by Ruby.
176
+ attr = $1.to_sym
177
+ raise ArgumentError, "Attribute '#{attr}' is protected" if @protected_attrs.include? attr
178
+
179
+ self[attr] = args[0]
180
+ when /\A#{ATTR_REGEXP}\z/
181
+ # Case "r.attr".
182
+ if @strict
183
+ _smart_hash_fetch(method_name)
184
+ else
185
+ self[method_name]
186
+ end
187
+ else
188
+ super
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,24 @@
1
+ class SmartHash < Hash
2
+ # Non-strict SmartHash
3
+ #
4
+ # person = SmartHash::Loose[]
5
+ #
6
+ # is equivalent to:
7
+ #
8
+ # person = SmartHash[]
9
+ # person.strict = false
10
+ class Loose < ::SmartHash
11
+ # See SmartHash#initialize.
12
+ def initialize(*args)
13
+ super
14
+ @strict = false
15
+ end
16
+
17
+ # See SmartHash::[].
18
+ def self.[](*args)
19
+ super.tap do |_|
20
+ _.strict = false
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path("../lib/smart_hash", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "smart_hash"
5
+ s.version = SmartHash::VERSION
6
+ s.authors = ["Alex Fortuna"]
7
+ s.email = ["alex.r@askit.org"]
8
+ s.homepage = "http://github.com/dadooda/smart_hash"
9
+
10
+ # Copy these from class's description, adjust markup.
11
+ s.summary = %q{A smarter alternative to OpenStruct}
12
+ s.description = %q{A smarter alternative to OpenStruct}
13
+ # end of s.description=
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "yard"
22
+ end
@@ -0,0 +1,293 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe SmartHash do
4
+ before do
5
+ # Get key error exception sample. Should be `KeyError` for 1.9, `IndexError` for 1.8.
6
+ @key_error = {}.fetch(:kk) rescue $!.class
7
+ end
8
+
9
+ it "should generally work" do
10
+ r = described_class.new
11
+ r.should == {}
12
+
13
+ r = described_class[]
14
+ r.should == {}
15
+
16
+ r = described_class[:name => "John"]
17
+ r.should == {:name => "John"}
18
+
19
+ r = described_class[]
20
+ r[:name].should == nil
21
+ lambda {r.name}.should raise_error @key_error
22
+ r.name = "John"
23
+ r.should == {:name => "John"}
24
+
25
+ r = described_class[]
26
+ r[:young?] = true # This is allowed, we're `Hash`.
27
+ lambda {r.young?}.should raise_error NoMethodError # Because `young?` is not a valid attribute name.
28
+ r[:go!] = true
29
+ lambda {r.go!}.should raise_error NoMethodError # Same as above.
30
+
31
+ # Existing method shadowed by an attribute.
32
+ r = described_class[:name => "John"]
33
+ r.size.should == 1
34
+ (r.size = "XL").should == "XL"
35
+ r.size.should == "XL"
36
+
37
+ # Existing `attr=` method shadowed by an attribute.
38
+ # NOTE: `:something` and `:something=` are methods we've added in `Hash`, see spec helper.
39
+ r = described_class[]
40
+ r.method(:something=).should be_a Method
41
+ r.something = 99
42
+ r.something.should == 99
43
+ end # it "should generally work"
44
+
45
+ it "should allow to redefine `fetch`" do
46
+ r = described_class[]
47
+ r.fetch = 99
48
+ r.fetch.should == 99
49
+ r.name = "John"
50
+ r.name.should == "John"
51
+ r.should == {:fetch => 99, :name => "John"}
52
+ end
53
+
54
+ describe "ATTR_REGEXP" do
55
+ before :each do
56
+ re = described_class.const_get(:ATTR_REGEXP)
57
+ @access_regexp = /\A#{re}\z/
58
+ @assign_regexp = /\A#{re}=\z/
59
+ end
60
+
61
+ it "should generally work" do
62
+ # Errors first, OKs second.
63
+ @access_regexp.tap do |_|
64
+ "".should_not match _
65
+ "911".should_not match _
66
+ "911abc".should_not match _
67
+ "young?".should_not match _
68
+ "go!".should_not match _
69
+ end
70
+
71
+ @access_regexp.tap do |_|
72
+ "good_attr".should match _
73
+ "_".should match _
74
+ "__abc99__".should match _
75
+ end
76
+
77
+ @assign_regexp.tap do |_|
78
+ "good_attr=".should match _
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "attribute declaration" do
84
+ it "should generally work" do
85
+ r = described_class[:name => "John"]
86
+ r.declare(:size)
87
+ r.declared_attrs.should include :size
88
+ lambda {r.size}.should raise_error @key_error
89
+ r.undeclare(:size)
90
+ r.size.should == 1
91
+
92
+ # Direct modification.
93
+ r = described_class[:name => "John"]
94
+ r.declared_attrs << :size
95
+ lambda {r.size}.should raise_error @key_error
96
+ r.declared_attrs.clear
97
+ r.size.should == 1
98
+ end
99
+
100
+ describe "#declare" do
101
+ it "should not accept empty attrs list" do
102
+ r = described_class[]
103
+ lambda {r.declare}.should raise_error ArgumentError
104
+ end
105
+
106
+ it "should accept multiple attrs" do
107
+ r = described_class[]
108
+ r.declare(:count, :size)
109
+ r.declared_attrs.should include :count
110
+ r.declared_attrs.should include :size
111
+ end
112
+
113
+ it "should accept `Symbol` only" do
114
+ r = described_class[]
115
+ lambda {r.declare(1)}.should raise_error ArgumentError
116
+ lambda {r.declare("some_attr")}.should raise_error ArgumentError
117
+ lambda {r.declare(:some_attr)}.should_not raise_error
118
+ end
119
+
120
+ it "should validate attribute name" do
121
+ r = described_class[]
122
+ lambda {r.declare(:good_attr)}.should_not raise_error
123
+ lambda {r.declare(:bad_attr!)}.should raise_error ArgumentError
124
+ lambda {r.declare(:bad_attr?)}.should raise_error ArgumentError
125
+ end
126
+ end # describe "#declare"
127
+
128
+ describe "#undeclare" do
129
+ it "should not accept empty attrs list" do
130
+ r = described_class[]
131
+ lambda {r.undeclare}.should raise_error ArgumentError
132
+ end
133
+
134
+ it "should accept multiple attrs" do
135
+ r = described_class[:name => "John"]
136
+ r.declare(:count, :size)
137
+ lambda {r.count}.should raise_error @key_error
138
+ lambda {r.size}.should raise_error @key_error
139
+ r.undeclare(:count, :size)
140
+ r.count.should == 1
141
+ r.size.should == 1
142
+ end
143
+
144
+ it "should generally work" do
145
+ r = described_class[:name => "John"]
146
+ r.declare(:size)
147
+ lambda {r.size}.should raise_error @key_error
148
+ r.undeclare(:size)
149
+ r.size.should == 1
150
+ end
151
+
152
+ it "should not be strict" do
153
+ r = described_class[]
154
+ lambda {r.undeclare("young?")}.should_not raise_error
155
+ lambda {r.undeclare(5)}.should_not raise_error
156
+ lambda {r.undeclare([])}.should_not raise_error
157
+ end
158
+ end
159
+ end # describe "attribute declaration"
160
+
161
+ describe "attribute protection" do
162
+ it "should generally work" do
163
+ # General.
164
+ r = described_class[]
165
+ r.protect(:name)
166
+ lambda {r.name = "John"}.should raise_error ArgumentError
167
+
168
+ # Direct modification.
169
+ r = described_class[]
170
+ r.protected_attrs << :name
171
+ lambda {r.name = "John"}.should raise_error ArgumentError
172
+ r.protected_attrs.delete :name
173
+ lambda {r.name = "John"}.should_not raise_error ArgumentError
174
+
175
+ # Existing method.
176
+ r = described_class[:name => "John"]
177
+ r.protect(:size)
178
+ lambda {r.size = "XL"}.should raise_error ArgumentError
179
+ r.size.should == 1
180
+
181
+ # Protect/unprotect.
182
+ r = described_class[]
183
+ r.protect(:name)
184
+ lambda {r.name = "John"}.should raise_error ArgumentError
185
+ r.unprotect(:name)
186
+ r.name = "Johnny"
187
+ r.name.should == "Johnny"
188
+
189
+ # Existing "attr=".
190
+ r = described_class[]
191
+ r.something.should == "something"
192
+ r.protect(:something)
193
+ lambda {r.something = "other"}.should raise_error ArgumentError
194
+ r.unprotect(:something)
195
+ r.something = "other"
196
+ r.something.should == "other"
197
+ r.delete(:something)
198
+ r.something.should == "something"
199
+ end
200
+
201
+ it "should protect sensitive attrs by default" do
202
+ r = described_class[]
203
+ lambda {r.inspect = 99}.should raise_error ArgumentError
204
+ lambda {r.to_s = 99}.should raise_error ArgumentError
205
+ end
206
+
207
+ it "should allow to unprotect sensitive attrs if needed" do
208
+ r = described_class[]
209
+ r.unprotect(:inspect)
210
+ r.inspect = "top_secret"
211
+ r.inspect.should == "top_secret"
212
+
213
+ r = described_class[]
214
+ r.unprotect(:to_s)
215
+ r.to_s = "top_secret"
216
+ "#{r}".should == "top_secret"
217
+ end
218
+
219
+ describe "#protect" do
220
+ it "should not accept empty attrs list" do
221
+ r = described_class[]
222
+ lambda {r.protect}.should raise_error ArgumentError
223
+ end
224
+
225
+ it "should accept multiple attrs" do
226
+ r = described_class[]
227
+ r.protect(:count, :size)
228
+ r.protected_attrs.should include :count
229
+ r.protected_attrs.should include :size
230
+ end
231
+ end # describe "#protect"
232
+
233
+ describe "#unprotect" do
234
+ it "should not accept empty attrs list" do
235
+ r = described_class[]
236
+ lambda {r.unprotect}.should raise_error ArgumentError
237
+ end
238
+
239
+ it "should accept multiple attrs" do
240
+ r = described_class[]
241
+ r.protect(:count, :size)
242
+ r.protected_attrs.should include :count
243
+ r.protected_attrs.should include :size
244
+ r.unprotect(:count, :size)
245
+ r.protected_attrs.should_not include :count
246
+ r.protected_attrs.should_not include :size
247
+ end
248
+ end
249
+ end # describe "attribute declaration"
250
+
251
+ describe "defaults" do
252
+ it "should generally work" do
253
+ # Default, scalar.
254
+ r = described_class[]
255
+ r.default = 99
256
+ r.should == {} # Because `default` is a forbidden (non-attribute) name, but built-in method exists and it responds.
257
+ r[:anything].should == 99 # Because this is the default `Hash` behavior.
258
+ lambda {r.anything}.should raise_error @key_error # Because attribute access is more strict and default doesn't affect it.
259
+
260
+ # Default, proc.
261
+ r = described_class[]
262
+ r.default_proc = lambda {|h, k| "<#{k}>"}
263
+ r.should == {}
264
+ r[:anything].should == "<anything>"
265
+ r[:other].should == "<other>"
266
+ lambda {r.anything}.should raise_error @key_error
267
+
268
+ # Loose mode.
269
+ r = described_class[]
270
+ r.strict = false
271
+ r.anything.should == nil
272
+
273
+ # Loose mode with default.
274
+ r = described_class[]
275
+ r.strict = false
276
+ r.default = 99
277
+ r[:anything].should == 99
278
+ r.anything.should == 99
279
+ end
280
+ end # describe "defaults"
281
+ end # describe SmartHash do
282
+
283
+ describe SmartHash::Loose do
284
+ it "should generally work" do
285
+ r = described_class[]
286
+ r.anything.should == nil
287
+
288
+ r = described_class[]
289
+ r.default = 99
290
+ r[:anything].should == 99
291
+ r.anything.should == 99
292
+ end
293
+ end
@@ -0,0 +1,39 @@
1
+ # NOTE: I usually support `STANDALONE` mode in specs for Rails projects' components
2
+ # to be able to test them without loading the environment. This project does not
3
+ # depend on Rails *BUT* I still want a consistent RSpec file structure.
4
+ # If this is confusing, feel free to propose something better. :)
5
+
6
+ # No Rails, we're always standalone... and free! :)
7
+ STANDALONE = 1
8
+
9
+ if STANDALONE
10
+ # Provide root path object.
11
+ module Standalone
12
+ eval <<-EOT
13
+ def self.root
14
+ # This is an absolute path, it's perfectly safe to do a `+` and then `require`.
15
+ Pathname("#{File.expand_path('../..', __FILE__)}")
16
+ end
17
+ EOT
18
+ end
19
+
20
+ # Load stuff.
21
+ [
22
+ "lib/**/*.rb",
23
+ ].each do |fmask|
24
+ Dir[Standalone.root + fmask].each do |fn|
25
+ require fn
26
+ end
27
+ end
28
+ end # if STANDALONE
29
+
30
+ # Extend `Hash` with a non-forbidden `attr=` to test shadowing.
31
+ class ::Hash
32
+ def something
33
+ "something"
34
+ end
35
+
36
+ def something=(value)
37
+ "something="
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_hash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Fortuna
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-08 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &85732360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *85732360
25
+ - !ruby/object:Gem::Dependency
26
+ name: yard
27
+ requirement: &85732120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *85732120
36
+ description: A smarter alternative to OpenStruct
37
+ email:
38
+ - alex.r@askit.org
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - .gitignore
44
+ - .rspec
45
+ - Gemfile
46
+ - Gemfile.lock
47
+ - MIT-LICENSE
48
+ - README.md
49
+ - Rakefile
50
+ - lib/smart_hash.rb
51
+ - lib/smart_hash/loose.rb
52
+ - smart_hash.gemspec
53
+ - spec/smart_hash_spec.rb
54
+ - spec/spec_helper.rb
55
+ homepage: http://github.com/dadooda/smart_hash
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.10
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A smarter alternative to OpenStruct
79
+ test_files: []
80
+ has_rdoc: