hash_object 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+