hash_object 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 +9 -0
- data/.rspec +1 -0
- data/.rvm +3 -0
- data/Gemfile +20 -0
- data/Rakefile +35 -0
- data/hash_object.gemspec +27 -0
- data/lib/VERSION.yml +6 -0
- data/lib/hash_object.rb +243 -0
- data/spec/hash_object_spec.rb +118 -0
- data/spec/spec_helpers.rb +7 -0
- metadata +75 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-c -f p
|
data/.rvm
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in hash_object.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem "choosy" # for tasks
|
8
|
+
gem "yard" # for docs
|
9
|
+
end
|
10
|
+
|
11
|
+
group :test do
|
12
|
+
gem "rspec"
|
13
|
+
gem "autotest"
|
14
|
+
gem "ZenTest"
|
15
|
+
gem "autotest-notification"
|
16
|
+
if `uname -a` =~ /^Darwin/
|
17
|
+
gem "autotest-fsevent"
|
18
|
+
gem "autotest-growl"
|
19
|
+
end
|
20
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../spec", __FILE__)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'choosy/rake'
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
desc "Run the RSpec tests"
|
12
|
+
RSpec::Core::RakeTask.new(:spec)
|
13
|
+
|
14
|
+
desc "Cleans the gem files up."
|
15
|
+
task :clean => ['gem:clean']
|
16
|
+
|
17
|
+
desc "Show the documentation"
|
18
|
+
task :doc => ['doc:yardoc']
|
19
|
+
namespace :doc do
|
20
|
+
desc "Build the documentation"
|
21
|
+
task :gen do
|
22
|
+
sh "yardoc"
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Open the docs in a browser"
|
26
|
+
task :view => :gen do
|
27
|
+
sh "open doc/_index.html"
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Cleans up the doc directory"
|
31
|
+
task :clean do
|
32
|
+
sh "rm -rf doc"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
data/hash_object.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
VERSION = begin
|
5
|
+
require 'choosy/version'
|
6
|
+
Choosy::Version.load_from_lib
|
7
|
+
rescue LoadError
|
8
|
+
'0'
|
9
|
+
end
|
10
|
+
|
11
|
+
Gem::Specification.new do |s|
|
12
|
+
s.name = "hash_object"
|
13
|
+
s.version = VERSION
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.authors = ["Gabe McArthur"]
|
16
|
+
s.email = ["madeonamac@gmail.com"]
|
17
|
+
s.homepage = "http://github.com/gabemc/hash_object"
|
18
|
+
s.summary = %q{A stupid meta tool for mapping existing hash objects into real objects, for convenience.}
|
19
|
+
s.description = %q{A stupid meta tool for mapping existing hash objects into real objects, for convenience.}
|
20
|
+
|
21
|
+
s.rubyforge_project = "hash-object"
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
data/lib/VERSION.yml
ADDED
data/lib/hash_object.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
|
2
|
+
# This is a helper module that makes it quite easy to define the
|
3
|
+
# Hash -> reified object mapping.
|
4
|
+
module HashObject
|
5
|
+
# Adds the class methods that are implemented on the included class.
|
6
|
+
def self.included(base)
|
7
|
+
base.instance_variable_set("@_elements", {})
|
8
|
+
base.instance_variable_set("@_strict", true)
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
# We need to convert strange elements into boolean values.
|
13
|
+
# This is a convenience class for that purpose.
|
14
|
+
class BooleanConverter
|
15
|
+
# Parses the current element into a boolean value.
|
16
|
+
#
|
17
|
+
# @param [Object] element The element to parse.
|
18
|
+
# @return [Boolean] The parsed element
|
19
|
+
def self.parse(element)
|
20
|
+
if element == 'false' || element == 0
|
21
|
+
false
|
22
|
+
else
|
23
|
+
!!element
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# An error thrown if a mapping is somehow incorrect.
|
29
|
+
class ConfigurationError < Exception; end
|
30
|
+
|
31
|
+
# An internal object that records the state of the mapping between
|
32
|
+
# individual keys in a hash object and the actual methods that need
|
33
|
+
# to be created.
|
34
|
+
class Element
|
35
|
+
# @return [Symbol] sym The name of the method
|
36
|
+
attr_reader :sym
|
37
|
+
# @return [Symbol] sym The name that is to be parsed from the Hash, if it is not the symbol name.
|
38
|
+
attr_reader :name
|
39
|
+
|
40
|
+
# Creates an element mapping.
|
41
|
+
#
|
42
|
+
# @param [Symbol] sym The symbol that defines the method name (and possibly the element string)
|
43
|
+
# @param [Hash] options The initialization options
|
44
|
+
# @option options [Boolean] :required Whether this element is required. Default is true.
|
45
|
+
# @option options [Object, Proc] :default The default value for the element, if not seen.
|
46
|
+
# @option options [Class] :type The type of the element to parse it into.
|
47
|
+
# @option options [Object] :builder If this is a complex object that needs to be constructed, you can pass in a builder object
|
48
|
+
# to do the object initialization, circumventing the standard policy.
|
49
|
+
# @option options [String] :name If you want to map a regular hash key string into a symbol.
|
50
|
+
def initialize(sym, options)
|
51
|
+
@sym = sym
|
52
|
+
@required = options[:required] != false
|
53
|
+
@default = options[:default]
|
54
|
+
@type = options[:type]
|
55
|
+
@single = options[:single]
|
56
|
+
@builder = options[:builder]
|
57
|
+
@name = options[:name]
|
58
|
+
|
59
|
+
if @type
|
60
|
+
raise ConfigurationError, "'#{sym}' requires a type: #{@type}" unless @type.is_a?(Class)
|
61
|
+
if !@type.respond_to?(:parse)
|
62
|
+
raise ConfigurationError, "'#{sym}' attribute requires type '#{@type.name}' to implement 'parse'"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets the value of the newly created element, either parsing the value,
|
68
|
+
# setting the default, or using the builder.
|
69
|
+
#
|
70
|
+
# @param [Object] obj The object being altered.
|
71
|
+
# @param [Object] value The value being set on the object.
|
72
|
+
# @return [Object] The value that is set on the object being created.
|
73
|
+
def set(obj, value)
|
74
|
+
if @type
|
75
|
+
if @single
|
76
|
+
value = @type.parse(value)
|
77
|
+
else
|
78
|
+
value = value.map{|e| @type.parse(e)}
|
79
|
+
end
|
80
|
+
elsif @builder
|
81
|
+
if @single
|
82
|
+
value = @builder.call(value)
|
83
|
+
else
|
84
|
+
value = value.map{|e| @builder.call(e)}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
obj.send("#{@sym}=".to_sym, value)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Sets the default value of the object.
|
91
|
+
#
|
92
|
+
# @param [Object] obj The object being altered.
|
93
|
+
# @return [nil]
|
94
|
+
# @raise [ConfigurationError] If the element is required.
|
95
|
+
def set_default(obj)
|
96
|
+
if @required
|
97
|
+
raise ConfigurationError, "The '#{@sym}' attribute is required for '#{obj.class.name}'"
|
98
|
+
else
|
99
|
+
obj.send("#{@sym}=".to_sym, default_value)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# An abstraction around the default value of the object, whether
|
104
|
+
# it is a reified object or a Proc that will generate the
|
105
|
+
# default value.
|
106
|
+
#
|
107
|
+
# @return [Object] The default object
|
108
|
+
def default_value
|
109
|
+
if @default.is_a?(Proc)
|
110
|
+
@default.call
|
111
|
+
else
|
112
|
+
@default
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Include the given class methods that will be used to create
|
118
|
+
# associated element mappings.
|
119
|
+
module ClassMethods
|
120
|
+
|
121
|
+
# Whether we will strictly enforce the mapping -- i.e., will we
|
122
|
+
# fail if there are elements in the hash that we don't understand.
|
123
|
+
# The default is false.
|
124
|
+
#
|
125
|
+
# @param [Boolean] bool Whether to make this mapping strict.
|
126
|
+
# @return [nil]
|
127
|
+
def strict(bool)
|
128
|
+
@_strict = !!bool
|
129
|
+
end
|
130
|
+
|
131
|
+
# Creates a new element mapping with the given name. This mapping
|
132
|
+
# can be highly customized by the options passed in. In fact, the
|
133
|
+
# other mapping methods on this class (see #boolean #has_many) simply
|
134
|
+
# delegate to this method.
|
135
|
+
#
|
136
|
+
# @param [Symbol] sym The name of the new property for this object.
|
137
|
+
# @param [Hash] options The options that alter how the element is mapped.
|
138
|
+
# @option options [Boolean] :reader Whether we support only an attr_writer.
|
139
|
+
# This is a bit weird, as it essentially hides the element from outside
|
140
|
+
# objects, but it can be useful when going for information hiding, since
|
141
|
+
# the '@sym' name is still visible inside the object.
|
142
|
+
# @option options [Boolean] :single Whether we map only a single element.
|
143
|
+
# Otherwise, we map many elements (see #has_many).
|
144
|
+
# @option options [Symbol] :qname The "question" type name for the method.
|
145
|
+
# Since all of the method definitions create a 'element?' method in addition
|
146
|
+
# to the standard 'element' methods, to see if the property is set,
|
147
|
+
# you can customize the name of the question mark method here. Leave off
|
148
|
+
# the '?' at the end, though.
|
149
|
+
# @option options [Boolean] :required Whether this element is required. Default is true.
|
150
|
+
# @option options [Object, Proc] :default The default value for the element, if not seen.
|
151
|
+
# @option options [String, Symbol] :name The actual key in the hash that
|
152
|
+
# we are mapping to, if it is not the actual 'sym' that we passed in.
|
153
|
+
# @option options [Proc] :builder A builder proc that will do the actual parsing,
|
154
|
+
# circumventing the standard #parse method.
|
155
|
+
# @option options [Class] :type The type that will be used to parse this element.
|
156
|
+
# @return [nil]
|
157
|
+
def element(sym, options={})
|
158
|
+
if options[:reader] == false
|
159
|
+
attr_writer sym
|
160
|
+
else
|
161
|
+
attr_accessor sym
|
162
|
+
end
|
163
|
+
if options[:single].nil?
|
164
|
+
options[:single] ||= true
|
165
|
+
end
|
166
|
+
if options[:single]
|
167
|
+
self.class_eval <<EOF, __FILE__, __LINE__
|
168
|
+
def #{options[:qname] || sym}?
|
169
|
+
!!@#{sym}
|
170
|
+
end
|
171
|
+
EOF
|
172
|
+
else
|
173
|
+
self.class_eval <<EOF, __FILE__, __LINE__
|
174
|
+
def #{options[:qname] || sym}?
|
175
|
+
if @#{sym}.nil?
|
176
|
+
false
|
177
|
+
else
|
178
|
+
!@#{sym}.empty?
|
179
|
+
end
|
180
|
+
end
|
181
|
+
EOF
|
182
|
+
end
|
183
|
+
|
184
|
+
elem = Element.new(sym, options)
|
185
|
+
@_elements[sym.to_s] = elem
|
186
|
+
if options[:name]
|
187
|
+
@_elements[options[:name]] = elem
|
188
|
+
end
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
# Maps an element to a boolean value using the BooleanConverter.
|
193
|
+
#
|
194
|
+
# @param [Symbol] sym The name of the boolean property
|
195
|
+
# @param [Hash] options The configuration options (see #element)
|
196
|
+
# @return [nil]
|
197
|
+
def boolean(sym, options={})
|
198
|
+
element(sym, {:type => BooleanConverter, :single => true, :reader => false}.merge(options))
|
199
|
+
end
|
200
|
+
|
201
|
+
# Maps an array of child elements into a property.
|
202
|
+
#
|
203
|
+
# @param [Symbol] sym The name of the many property mappings
|
204
|
+
# @param [Hash] options The configuration options (see #element)
|
205
|
+
def has_many(sym, options={})
|
206
|
+
element(sym, {:single => false}.merge(options))
|
207
|
+
end
|
208
|
+
|
209
|
+
# The parse method is what actually unpacks the hash object into
|
210
|
+
# a reified object. It does require that the object be a hash. If
|
211
|
+
# you have nested mappings, this method will be called when the
|
212
|
+
# child hash objects are parsed into reified objects. The only
|
213
|
+
# time that isn't the case is when you have a builder that handles
|
214
|
+
# that object construction for you.
|
215
|
+
#
|
216
|
+
# @param [Hash] The hash that we are going to unpack.
|
217
|
+
# @return [Object] The object that got built.
|
218
|
+
def parse(hash)
|
219
|
+
raise ArgumentError, "Requires a hash to read in" unless hash.is_a?(Hash)
|
220
|
+
obj = new
|
221
|
+
|
222
|
+
# Exclude from checking elements we've already matched
|
223
|
+
matching_names = []
|
224
|
+
|
225
|
+
hash.each do |key, value|
|
226
|
+
if elem = @_elements[key]
|
227
|
+
elem.set(obj, value)
|
228
|
+
matching_names << elem.sym.to_s if elem.name == key
|
229
|
+
elsif @_strict
|
230
|
+
raise ConfigurationError, "Unsupported attribute '#{key}: #{value}' for #{self.name}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
@_elements.each do |key, elem|
|
235
|
+
next if hash.has_key?(key)
|
236
|
+
next if matching_names.include?(key)
|
237
|
+
elem.set_default(obj)
|
238
|
+
end
|
239
|
+
|
240
|
+
obj
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helpers'
|
2
|
+
require 'hash_object'
|
3
|
+
|
4
|
+
describe HashObject do
|
5
|
+
class D
|
6
|
+
include HashObject
|
7
|
+
element :original_name, :name => 'originalName'
|
8
|
+
end
|
9
|
+
|
10
|
+
class C
|
11
|
+
include HashObject
|
12
|
+
element :name
|
13
|
+
end
|
14
|
+
|
15
|
+
class B
|
16
|
+
include HashObject
|
17
|
+
element :address
|
18
|
+
strict false
|
19
|
+
end
|
20
|
+
|
21
|
+
class A
|
22
|
+
include HashObject
|
23
|
+
element :name
|
24
|
+
has_many :aliases, :required => false, :default => lambda(){Array.new}
|
25
|
+
boolean :default, :required => false, :default => false
|
26
|
+
has_many :b, :required => false, :type => B
|
27
|
+
has_many :c, :required => false, :builder => lambda {|x|"_#{x}_"}
|
28
|
+
end
|
29
|
+
|
30
|
+
context "for configured objects" do
|
31
|
+
before :each do
|
32
|
+
@a = A.new
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should create new methods for 'once'" do
|
36
|
+
@a.should respond_to(:name)
|
37
|
+
@a.should respond_to(:name=)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should create new methods for 'has_many'" do
|
41
|
+
@a.should respond_to(:aliases)
|
42
|
+
@a.should respond_to(:aliases=)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should create new methods for booleans" do
|
46
|
+
@a.should respond_to(:default?)
|
47
|
+
@a.should respond_to(:default=)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should be able to suppress the creation of the reader" do
|
51
|
+
@a.should_not respond_to(:default)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should fail when an object doesn't implement 'parse'" do
|
55
|
+
attempting {
|
56
|
+
class X
|
57
|
+
include HashObject
|
58
|
+
element :here, :type => String
|
59
|
+
end
|
60
|
+
}.should raise_error(HashObject::ConfigurationError, /requires type/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "for parsed objects" do
|
65
|
+
it "should fail when an element isn't supported" do
|
66
|
+
attempting {
|
67
|
+
A.parse({'noattr' => 'foo'})
|
68
|
+
}.should raise_error(HashObject::ConfigurationError, /noattr/)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not fail when the object being parsed is not strict" do
|
72
|
+
attempting {
|
73
|
+
B.parse({'address' => 'this', 'not-an-element' => 'goes here'})
|
74
|
+
}.should_not raise_error
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should set the required elements" do
|
78
|
+
a = A.parse({'name' => 'bob'})
|
79
|
+
a.name.should eql('bob')
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should set the default values if not set" do
|
83
|
+
a = A.parse({'name' => 'bob'})
|
84
|
+
a.aliases.should eql([])
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should fail when the name isn't there" do
|
88
|
+
attempting {
|
89
|
+
A.parse({})
|
90
|
+
}.should raise_error(HashObject::ConfigurationError, /'name' attribute is required for/)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should set collection values" do
|
94
|
+
a = A.parse({'name' => 'bob', 'aliases' => ['this', 'that']})
|
95
|
+
a.aliases.should eql(['this', 'that'])
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should handle nested types" do
|
99
|
+
a = A.parse({'name' => 'bob', 'b' => [{'address' => 'someplace'}]})
|
100
|
+
a.b[0].address.should eql('someplace')
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should return false when querried about empty sublists" do
|
104
|
+
a = A.parse({'name' => 'bob'})
|
105
|
+
a.aliases?.should be_false
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should create objects using builders, when necessary" do
|
109
|
+
a = A.parse({'name' => 'bob', 'c' => ['x', 'y']})
|
110
|
+
a.c.should eql(['_x_', '_y_'])
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should be able to map an original name to a new name" do
|
114
|
+
d = D.parse({'originalName' => 'orin'})
|
115
|
+
d.original_name.should eql('orin')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hash_object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Gabe McArthur
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-21 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: A stupid meta tool for mapping existing hash objects into real objects, for convenience.
|
22
|
+
email:
|
23
|
+
- madeonamac@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- .rspec
|
33
|
+
- .rvm
|
34
|
+
- Gemfile
|
35
|
+
- Rakefile
|
36
|
+
- hash_object.gemspec
|
37
|
+
- lib/VERSION.yml
|
38
|
+
- lib/hash_object.rb
|
39
|
+
- spec/hash_object_spec.rb
|
40
|
+
- spec/spec_helpers.rb
|
41
|
+
homepage: http://github.com/gabemc/hash_object
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project: hash-object
|
70
|
+
rubygems_version: 1.8.2
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: A stupid meta tool for mapping existing hash objects into real objects, for convenience.
|
74
|
+
test_files: []
|
75
|
+
|