smart_hash 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.
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: