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 ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ vendor/
6
+ *.swp
7
+ .DS_Store
8
+ .yardoc/
9
+ doc/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -c -f p
data/.rvm ADDED
@@ -0,0 +1,3 @@
1
+ rvm use 1.8.7
2
+ rvm gemset create hash_object
3
+ rvm use 1.8.7@hash_object
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
+
@@ -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
@@ -0,0 +1,6 @@
1
+ ---
2
+ date: 21/06/2011
3
+ version:
4
+ tiny: 0
5
+ major: 0
6
+ minor: 1
@@ -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
@@ -0,0 +1,7 @@
1
+ def attempting(&block)
2
+ lambda &block
3
+ end
4
+
5
+ def attempting_to(&block)
6
+ lambda &block
7
+ 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
+