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 +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:
|