smart_hash 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +26 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/Rakefile +1 -0
- data/lib/smart_hash.rb +191 -0
- data/lib/smart_hash/loose.rb +24 -0
- data/smart_hash.gemspec +22 -0
- data/spec/smart_hash_spec.rb +293 -0
- data/spec/spec_helper.rb +39 -0
- metadata +80 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
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
|
data/smart_hash.gemspec
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|