extlib 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of extlib might be problematic. Click here for more details.

data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Sam Smoot.
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.txt CHANGED
@@ -1,3 +1,3 @@
1
1
  = extlib
2
2
 
3
- A support library for DataMapper and DataObjects.
3
+ A support library for DataMapper, DataObjects and Merb.
data/Rakefile CHANGED
@@ -2,31 +2,66 @@
2
2
  require 'pathname'
3
3
  require 'rubygems'
4
4
  require 'rake'
5
+ require "rake/clean"
6
+ require "rake/gempackagetask"
7
+ require "fileutils"
5
8
  require Pathname('spec/rake/spectask')
6
9
  require Pathname('lib/extlib/version')
7
10
 
8
11
  ROOT = Pathname(__FILE__).dirname.expand_path
9
12
 
13
+ ##############################################################################
14
+ # Package && release
15
+ ##############################################################################
16
+ RUBY_FORGE_PROJECT = "extlib"
17
+ PROJECT_URL = "http://extlib.rubyforge.org"
18
+ PROJECT_SUMMARY = "Support library for DataMapper and Merb."
19
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY
20
+
10
21
  AUTHOR = "Sam Smoot"
11
22
  EMAIL = "ssmoot@gmail.com"
12
- GEM_NAME = "extlib"
13
- GEM_VERSION = Extlib::VERSION
14
- GEM_DEPENDENCIES = [["english", ">=0.2.0"]]
15
- GEM_CLEAN = "*.gem", "**/.DS_Store"
16
- GEM_EXTRAS = { :has_rdoc => false }
17
23
 
18
- PROJECT_NAME = "extlib"
19
- PROJECT_URL = "http://extlib.rubyforge.org"
20
- PROJECT_DESCRIPTION = PROJECT_SUMMARY = "Support Library for DataMapper and DataObjects"
24
+ GEM_NAME = "extlib"
25
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
26
+ GEM_VERSION = Extlib::VERSION + PKG_BUILD
21
27
 
22
- require ROOT + 'tasks/hoe'
28
+ RELEASE_NAME = "REL #{GEM_VERSION}"
23
29
 
24
- task :default => 'extlib:spec'
25
- task :spec => 'extlib:spec'
30
+ require "lib/extlib/tasks/release"
31
+
32
+ spec = Gem::Specification.new do |s|
33
+ s.name = GEM_NAME
34
+ s.version = GEM_VERSION
35
+ s.platform = Gem::Platform::RUBY
36
+ s.author = AUTHOR
37
+ s.email = EMAIL
38
+ s.homepage = PROJECT_URL
39
+ s.summary = PROJECT_SUMMARY
40
+ s.description = PROJECT_DESCRIPTION
41
+ s.require_path = "lib"
42
+ s.files = ["LICENSE", "README.txt", "Rakefile"] + Dir["lib/**/*"]
43
+
44
+ # rdoc
45
+ s.has_rdoc = false
46
+ s.extra_rdoc_files = ["LICENSE", "README.txt"]
47
+
48
+ # Dependencies
49
+ s.add_dependency "english", ">=0.2.0"
50
+ end
51
+
52
+ Rake::GemPackageTask.new(spec) do |package|
53
+ package.gem_spec = spec
54
+ end
26
55
 
27
56
  desc 'Remove all package, docs and spec products'
28
57
  task :clobber_all => %w[ clobber_package clobber_doc extlib:clobber_spec ]
29
58
 
59
+ ##############################################################################
60
+ # Specs and continous integration
61
+ ##############################################################################
62
+ task :default => 'extlib:spec'
63
+ task :spec => 'extlib:spec'
64
+
30
65
  namespace :extlib do
31
66
  Spec::Rake::SpecTask.new(:spec) do |t|
32
67
  t.spec_opts << '--format' << 'specdoc' << '--colour'
@@ -44,6 +79,10 @@ namespace :extlib do
44
79
  end
45
80
  end
46
81
 
82
+
83
+ ##############################################################################
84
+ # Documentation
85
+ ##############################################################################
47
86
  desc "Generate documentation"
48
87
  task :doc do
49
88
  begin
@@ -58,6 +97,7 @@ end
58
97
  WINDOWS = (RUBY_PLATFORM =~ /win32|mingw|bccwin|cygwin/) rescue nil
59
98
  SUDO = WINDOWS ? '' : ('sudo' unless ENV['SUDOLESS'])
60
99
 
100
+
61
101
  desc "Install #{GEM_NAME}"
62
102
  task :install => :package do
63
103
  sh %{#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources}
@@ -143,3 +183,18 @@ namespace :ci do
143
183
  end
144
184
 
145
185
  task :ci => ["ci:spec", "ci:doc", "ci:saikuro", :install, :publish]
186
+
187
+ ##############################################################################
188
+ # Benchmarks
189
+ ##############################################################################
190
+
191
+ namespace :benchmark do
192
+ desc "Runs benchmarks"
193
+ task :run do
194
+ files = Dir["benchmarks/**/*.rb"]
195
+
196
+ files.each do |f|
197
+ system "ruby #{f}"
198
+ end
199
+ end
200
+ end
@@ -1,19 +1,32 @@
1
1
  require 'pathname'
2
2
  require 'rubygems'
3
3
 
4
+ __DIR__ = File.expand_path(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(__DIR__) unless $LOAD_PATH.include?(__DIR__)
6
+
4
7
  # for Pathname /
5
- require File.expand_path(File.join(File.dirname(__FILE__), 'extlib', 'pathname'))
8
+ require File.expand_path(File.join(__DIR__, 'extlib', 'pathname'))
6
9
 
7
10
  dir = Pathname(__FILE__).dirname.expand_path / 'extlib'
8
11
 
12
+ require dir / "class.rb"
13
+ require dir / "object"
14
+ require dir / "object_space"
15
+
16
+ require dir / "string"
17
+ require dir / "hash"
18
+ require dir / "mash"
19
+ require dir / "virtual_file"
20
+ require dir / "logger"
21
+ require dir / "time"
22
+
9
23
  require dir / 'assertions'
10
24
  require dir / 'blank'
11
25
  require dir / 'inflection'
12
26
  require dir / 'lazy_array'
13
- require dir / 'object'
14
27
  require dir / 'module'
15
28
  require dir / 'blank'
16
29
  require dir / 'pooling'
17
- require dir / 'string'
30
+ require dir / 'simple_set'
18
31
  require dir / 'struct'
19
32
  require dir / 'hook'
@@ -1,5 +1,10 @@
1
- # blank? methods for several different class types
2
1
  class Object
2
+ # @return <TrueClass, FalseClass>
3
+ #
4
+ # @example [].blank? #=> true
5
+ # @example [1].blank? #=> false
6
+ # @example [nil].blank? #=> false
7
+ #
3
8
  # Returns true if the object is nil or empty (if applicable)
4
9
  def blank?
5
10
  nil? || (respond_to?(:empty?) && empty?)
@@ -7,6 +12,8 @@ class Object
7
12
  end # class Object
8
13
 
9
14
  class Numeric
15
+ # @return <TrueClass, FalseClass>
16
+ #
10
17
  # Numerics can't be blank
11
18
  def blank?
12
19
  false
@@ -14,6 +21,8 @@ class Numeric
14
21
  end # class Numeric
15
22
 
16
23
  class NilClass
24
+ # @return <TrueClass, FalseClass>
25
+ #
17
26
  # Nils are always blank
18
27
  def blank?
19
28
  true
@@ -21,7 +30,9 @@ class NilClass
21
30
  end # class NilClass
22
31
 
23
32
  class TrueClass
24
- # True is not blank.
33
+ # @return <TrueClass, FalseClass>
34
+ #
35
+ # True is not blank.
25
36
  def blank?
26
37
  false
27
38
  end
@@ -35,6 +46,12 @@ class FalseClass
35
46
  end # class FalseClass
36
47
 
37
48
  class String
49
+ # @example "".blank? #=> true
50
+ # @example " ".blank? #=> true
51
+ # @example " hey ho ".blank? #=> false
52
+ #
53
+ # @return <TrueClass, FalseClass>
54
+ #
38
55
  # Strips out whitespace then tests if the string is empty.
39
56
  def blank?
40
57
  strip.empty?
@@ -0,0 +1,175 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
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.
21
+
22
+ # Allows attributes to be shared within an inheritance hierarchy, but where
23
+ # each descendant gets a copy of their parents' attributes, instead of just a
24
+ # pointer to the same. This means that the child can add elements to, for
25
+ # example, an array without those additions being shared with either their
26
+ # parent, siblings, or children, which is unlike the regular class-level
27
+ # attributes that are shared across the entire hierarchy.
28
+ class Class
29
+ # Defines class-level and instance-level attribute reader.
30
+ #
31
+ # @param *syms<Array> Array of attributes to define reader for.
32
+ # @return <Array[#to_s]> List of attributes that were made into cattr_readers
33
+ #
34
+ # @api public
35
+ #
36
+ # @todo Is this inconsistent in that it does not allow you to prevent
37
+ # an instance_reader via :instance_reader => false
38
+ def cattr_reader(*syms)
39
+ syms.flatten.each do |sym|
40
+ next if sym.is_a?(Hash)
41
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
+ unless defined? @@#{sym}
43
+ @@#{sym} = nil
44
+ end
45
+
46
+ def self.#{sym}
47
+ @@#{sym}
48
+ end
49
+
50
+ def #{sym}
51
+ @@#{sym}
52
+ end
53
+ RUBY
54
+ end
55
+ end
56
+
57
+ # Defines class-level (and optionally instance-level) attribute writer.
58
+ #
59
+ # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
60
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
61
+ # @return <Array[#to_s]> List of attributes that were made into cattr_writers
62
+ #
63
+ # @api public
64
+ def cattr_writer(*syms)
65
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
66
+ syms.flatten.each do |sym|
67
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
68
+ unless defined? @@#{sym}
69
+ @@#{sym} = nil
70
+ end
71
+
72
+ def self.#{sym}=(obj)
73
+ @@#{sym} = obj
74
+ end
75
+ RUBY
76
+
77
+ unless options[:instance_writer] == false
78
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
+ def #{sym}=(obj)
80
+ @@#{sym} = obj
81
+ end
82
+ RUBY
83
+ end
84
+ end
85
+ end
86
+
87
+ # Defines class-level (and optionally instance-level) attribute accessor.
88
+ #
89
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
90
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
91
+ # @return <Array[#to_s]> List of attributes that were made into accessors
92
+ #
93
+ # @api public
94
+ def cattr_accessor(*syms)
95
+ cattr_reader(*syms)
96
+ cattr_writer(*syms)
97
+ end
98
+
99
+ # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
100
+ # each subclass has a copy of parent's attribute.
101
+ #
102
+ # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
103
+ # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
104
+ #
105
+ # @api public
106
+ #
107
+ # @todo Do we want to block instance_reader via :instance_reader => false
108
+ # @todo It would be preferable that we do something with a Hash passed in
109
+ # (error out or do the same as other methods above) instead of silently
110
+ # moving on). In particular, this makes the return value of this function
111
+ # less useful.
112
+ def class_inheritable_reader(*ivars)
113
+ instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
114
+
115
+ ivars.each do |ivar|
116
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
117
+ def self.#{ivar}
118
+ return @#{ivar} if self == #{self} || defined?(@#{ivar})
119
+ ivar = superclass.#{ivar}
120
+ return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
121
+ @#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) ? ivar.dup : ivar
122
+ end
123
+ RUBY
124
+ unless instance_reader == false
125
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
126
+ def #{ivar}
127
+ self.class.#{ivar}
128
+ end
129
+ RUBY
130
+ end
131
+ end
132
+ end
133
+
134
+ # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
135
+ # each subclass has a copy of parent's attribute.
136
+ #
137
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
138
+ # define inheritable writer for.
139
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
140
+ # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
141
+ #
142
+ # @api public
143
+ #
144
+ # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
145
+ # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
146
+ def class_inheritable_writer(*ivars)
147
+ instance_writer = ivars.pop[:instance_writer] if ivars.last.is_a?(Hash)
148
+ ivars.each do |ivar|
149
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
150
+ def self.#{ivar}=(obj)
151
+ @#{ivar} = obj
152
+ end
153
+ RUBY
154
+ unless instance_writer == false
155
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
156
+ def #{ivar}=(obj) self.class.#{ivar} = obj end
157
+ RUBY
158
+ end
159
+ end
160
+ end
161
+
162
+ # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
163
+ # each subclass has a copy of parent's attribute.
164
+ #
165
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
166
+ # define inheritable accessor for.
167
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
168
+ # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
169
+ #
170
+ # @api public
171
+ def class_inheritable_accessor(*syms)
172
+ class_inheritable_reader(*syms)
173
+ class_inheritable_writer(*syms)
174
+ end
175
+ end
@@ -0,0 +1,410 @@
1
+ require 'base64'
2
+
3
+ class Hash
4
+ class << self
5
+ # Converts valid XML into a Ruby Hash structure.
6
+ #
7
+ # @param xml<String> A string representation of valid XML.
8
+ #
9
+ # @note Mixed content is treated as text and any tags in it are left unparsed
10
+ # @note Any attributes other than type on a node containing a text node will be
11
+ # discarded
12
+ #
13
+ # @details [Typecasting]
14
+ # Typecasting is performed on elements that have a +type+ attribute:
15
+ # integer::
16
+ # boolean:: Anything other than "true" evaluates to false.
17
+ # datetime::
18
+ # Returns a Time object. See Time documentation for valid Time strings.
19
+ # date::
20
+ # Returns a Date object. See Date documentation for valid Date strings.
21
+ #
22
+ # Keys are automatically converted to +snake_case+
23
+ #
24
+ # @example [Simple]
25
+ # <user gender='m'>
26
+ # <age type='integer'>35</age>
27
+ # <name>Home Simpson</name>
28
+ # <dob type='date'>1988-01-01</dob>
29
+ # <joined-at type='datetime'>2000-04-28 23:01</joined-at>
30
+ # <is-cool type='boolean'>true</is-cool>
31
+ # </user>
32
+ #
33
+ # evaluates to
34
+ #
35
+ # { "user" => {
36
+ # "gender" => "m",
37
+ # "age" => 35,
38
+ # "name" => "Home Simpson",
39
+ # "dob" => DateObject( 1998-01-01 ),
40
+ # "joined_at" => TimeObject( 2000-04-28 23:01),
41
+ # "is_cool" => true
42
+ # }
43
+ # }
44
+ #
45
+ # @example [Mixed Content]
46
+ # <story>
47
+ # A Quick <em>brown</em> Fox
48
+ # </story>
49
+ #
50
+ # evaluates to
51
+ #
52
+ # { "story" => "A Quick <em>brown</em> Fox" }
53
+ #
54
+ # @details [Attributes other than type on a node containing text]
55
+ # <story is-good='false'>
56
+ # A Quick <em>brown</em> Fox
57
+ # </story>
58
+ #
59
+ # evaluates to
60
+ #
61
+ # { "story" => "A Quick <em>brown</em> Fox" }
62
+ #
63
+ # <bicep unit='inches' type='integer'>60</bicep>
64
+ #
65
+ # evaluates with a typecast to an integer. But unit attribute is ignored.
66
+ #
67
+ # { "bicep" => 60 }
68
+ def from_xml( xml )
69
+ ToHashParser.from_xml(xml)
70
+ end
71
+ end
72
+
73
+ # This class has semantics of ActiveSupport's HashWithIndifferentAccess
74
+ # and we only have it so that people can write
75
+ # params[:key] instead of params['key'].
76
+ #
77
+ # @return <Mash> This hash as a Mash for string or symbol key access.
78
+ def to_mash
79
+ hash = Mash.new(self)
80
+ hash.default = default
81
+ hash
82
+ end
83
+
84
+ # @return <String> This hash as a query string
85
+ #
86
+ # @example
87
+ # { :name => "Bob",
88
+ # :address => {
89
+ # :street => '111 Ruby Ave.',
90
+ # :city => 'Ruby Central',
91
+ # :phones => ['111-111-1111', '222-222-2222']
92
+ # }
93
+ # }.to_params
94
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones]=111-111-1111222-222-2222&address[street]=111 Ruby Ave."
95
+ def to_params
96
+ params = ''
97
+ stack = []
98
+
99
+ each do |k, v|
100
+ if v.is_a?(Hash)
101
+ stack << [k,v]
102
+ else
103
+ params << "#{k}=#{v}&"
104
+ end
105
+ end
106
+
107
+ stack.each do |parent, hash|
108
+ hash.each do |k, v|
109
+ if v.is_a?(Hash)
110
+ stack << ["#{parent}[#{k}]", v]
111
+ else
112
+ params << "#{parent}[#{k}]=#{v}&"
113
+ end
114
+ end
115
+ end
116
+
117
+ params.chop! # trailing &
118
+ params
119
+ end
120
+
121
+ # @param *allowed<Array[(String, Symbol)]> The hash keys to include.
122
+ #
123
+ # @return <Hash> A new hash with only the selected keys.
124
+ #
125
+ # @example
126
+ # { :one => 1, :two => 2, :three => 3 }.only(:one)
127
+ # #=> { :one => 1 }
128
+ def only(*allowed)
129
+ hash = {}
130
+ allowed.each {|k| hash[k] = self[k] if self.has_key?(k) }
131
+ hash
132
+ end
133
+
134
+ # @param *rejected<Array[(String, Symbol)] The hash keys to exclude.
135
+ #
136
+ # @return <Hash> A new hash without the selected keys.
137
+ #
138
+ # @example
139
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
140
+ # #=> { :two => 2, :three => 3 }
141
+ def except(*rejected)
142
+ hash = self.dup
143
+ rejected.each {|k| hash.delete(k) }
144
+ hash
145
+ end
146
+
147
+ # @return <String> The hash as attributes for an XML tag.
148
+ #
149
+ # @example
150
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
151
+ # #=> 'one="1" two="TWO"'
152
+ def to_xml_attributes
153
+ map do |k,v|
154
+ %{#{k.to_s.camel_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
155
+ end.join(' ')
156
+ end
157
+
158
+ alias_method :to_html_attributes, :to_xml_attributes
159
+
160
+ # @param html_class<#to_s>
161
+ # The HTML class to add to the :class key. The html_class will be
162
+ # concatenated to any existing classes.
163
+ #
164
+ # @example hash[:class] #=> nil
165
+ # @example hash.add_html_class!(:selected)
166
+ # @example hash[:class] #=> "selected"
167
+ # @example hash.add_html_class!("class1 class2")
168
+ # @example hash[:class] #=> "selected class1 class2"
169
+ def add_html_class!(html_class)
170
+ if self[:class]
171
+ self[:class] = "#{self[:class]} #{html_class}"
172
+ else
173
+ self[:class] = html_class.to_s
174
+ end
175
+ end
176
+
177
+ # Converts all keys into string values. This is used during reloading to
178
+ # prevent problems when classes are no longer declared.
179
+ #
180
+ # @return <Array> An array of they hash's keys
181
+ #
182
+ # @example
183
+ # hash = { One => 1, Two => 2 }.proctect_keys!
184
+ # hash # => { "One" => 1, "Two" => 2 }
185
+ def protect_keys!
186
+ keys.each {|key| self[key.to_s] = delete(key) }
187
+ end
188
+
189
+ # Attempts to convert all string keys into Class keys. We run this after
190
+ # reloading to convert protected hashes back into usable hashes.
191
+ #
192
+ # @example
193
+ # # Provided that classes One and Two are declared in this scope:
194
+ # hash = { "One" => 1, "Two" => 2 }.unproctect_keys!
195
+ # hash # => { One => 1, Two => 2 }
196
+ def unprotect_keys!
197
+ keys.each do |key|
198
+ (self[Object.full_const_get(key)] = delete(key)) rescue nil
199
+ end
200
+ end
201
+
202
+ # Destructively and non-recursively convert each key to an uppercase string,
203
+ # deleting nil values along the way.
204
+ #
205
+ # @return <Hash> The newly environmentized hash.
206
+ #
207
+ # @example
208
+ # { :name => "Bob", :contact => { :email => "bob@bob.com" } }.environmentize_keys!
209
+ # #=> { "NAME" => "Bob", "CONTACT" => { :email => "bob@bob.com" } }
210
+ def environmentize_keys!
211
+ keys.each do |key|
212
+ val = delete(key)
213
+ next if val.nil?
214
+ self[key.to_s.upcase] = val
215
+ end
216
+ self
217
+ end
218
+ end
219
+
220
+ require 'rexml/parsers/streamparser'
221
+ require 'rexml/parsers/baseparser'
222
+ require 'rexml/light/node'
223
+
224
+ # This is a slighly modified version of the XMLUtilityNode from
225
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
226
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
227
+ # This represents the hard part of the work, all I did was change the
228
+ # underlying parser.
229
+ class REXMLUtilityNode
230
+ attr_accessor :name, :attributes, :children, :type
231
+ cattr_accessor :typecasts, :available_typecasts
232
+
233
+ self.typecasts = {}
234
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
235
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
236
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
237
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
238
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
239
+ self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
240
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
241
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
242
+ self.typecasts["symbol"] = lambda{|v| v.to_sym}
243
+ self.typecasts["string"] = lambda{|v| v.to_s}
244
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
245
+ self.typecasts["base64Binary"] = lambda{|v| Base64.decode64(v)}
246
+
247
+ self.available_typecasts = self.typecasts.keys
248
+
249
+ def initialize(name, attributes = {})
250
+ @name = name.tr("-", "_")
251
+ # leave the type alone if we don't know what it is
252
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
253
+
254
+ @nil_element = attributes.delete("nil") == "true"
255
+ @attributes = undasherize_keys(attributes)
256
+ @children = []
257
+ @text = false
258
+ end
259
+
260
+ def add_node(node)
261
+ @text = true if node.is_a? String
262
+ @children << node
263
+ end
264
+
265
+ def to_hash
266
+ if @type == "file"
267
+ f = StringIO.new(::Base64.decode64(@children.first || ""))
268
+ class << f
269
+ attr_accessor :original_filename, :content_type
270
+ end
271
+ f.original_filename = attributes['name'] || 'untitled'
272
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
273
+ return {name => f}
274
+ end
275
+
276
+ if @text
277
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
278
+ else
279
+ #change repeating groups into an array
280
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
281
+
282
+ out = nil
283
+ if @type == "array"
284
+ out = []
285
+ groups.each do |k, v|
286
+ if v.size == 1
287
+ out << v.first.to_hash.entries.first.last
288
+ else
289
+ out << v.map{|e| e.to_hash[k]}
290
+ end
291
+ end
292
+ out = out.flatten
293
+
294
+ else # If Hash
295
+ out = {}
296
+ groups.each do |k,v|
297
+ if v.size == 1
298
+ out.merge!(v.first)
299
+ else
300
+ out.merge!( k => v.map{|e| e.to_hash[k]})
301
+ end
302
+ end
303
+ out.merge! attributes unless attributes.empty?
304
+ out = out.empty? ? nil : out
305
+ end
306
+
307
+ if @type && out.nil?
308
+ { name => typecast_value(out) }
309
+ else
310
+ { name => out }
311
+ end
312
+ end
313
+ end
314
+
315
+ # Typecasts a value based upon its type. For instance, if
316
+ # +node+ has #type == "integer",
317
+ # {{[node.typecast_value("12") #=> 12]}}
318
+ #
319
+ # @param value<String> The value that is being typecast.
320
+ #
321
+ # @details [:type options]
322
+ # "integer"::
323
+ # converts +value+ to an integer with #to_i
324
+ # "boolean"::
325
+ # checks whether +value+, after removing spaces, is the literal
326
+ # "true"
327
+ # "datetime"::
328
+ # Parses +value+ using Time.parse, and returns a UTC Time
329
+ # "date"::
330
+ # Parses +value+ using Date.parse
331
+ #
332
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
333
+ # The result of typecasting +value+.
334
+ #
335
+ # @note
336
+ # If +self+ does not have a "type" key, or if it's not one of the
337
+ # options specified above, the raw +value+ will be returned.
338
+ def typecast_value(value)
339
+ return value unless @type
340
+ proc = self.class.typecasts[@type]
341
+ proc.nil? ? value : proc.call(value)
342
+ end
343
+
344
+ # Convert basic XML entities into their literal values.
345
+ #
346
+ # @param value<#gsub> An XML fragment.
347
+ #
348
+ # @return <#gsub> The XML fragment after converting entities.
349
+ def translate_xml_entities(value)
350
+ value.gsub(/&lt;/, "<").
351
+ gsub(/&gt;/, ">").
352
+ gsub(/&quot;/, '"').
353
+ gsub(/&apos;/, "'").
354
+ gsub(/&amp;/, "&")
355
+ end
356
+
357
+ # Take keys of the form foo-bar and convert them to foo_bar
358
+ def undasherize_keys(params)
359
+ params.keys.each do |key, value|
360
+ params[key.tr("-", "_")] = params.delete(key)
361
+ end
362
+ params
363
+ end
364
+
365
+ # Get the inner_html of the REXML node.
366
+ def inner_html
367
+ @children.join
368
+ end
369
+
370
+ # Converts the node into a readable HTML node.
371
+ #
372
+ # @return <String> The HTML node in text form.
373
+ def to_html
374
+ attributes.merge!(:type => @type ) if @type
375
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
376
+ end
377
+
378
+ # @alias #to_html #to_s
379
+ def to_s
380
+ to_html
381
+ end
382
+ end
383
+
384
+ class ToHashParser
385
+
386
+ def self.from_xml(xml)
387
+ stack = []
388
+ parser = REXML::Parsers::BaseParser.new(xml)
389
+
390
+ while true
391
+ event = parser.pull
392
+ case event[0]
393
+ when :end_document
394
+ break
395
+ when :end_doctype, :start_doctype
396
+ # do nothing
397
+ when :start_element
398
+ stack.push REXMLUtilityNode.new(event[1], event[2])
399
+ when :end_element
400
+ if stack.size > 1
401
+ temp = stack.pop
402
+ stack.last.add_node(temp)
403
+ end
404
+ when :text, :cdata
405
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0
406
+ end
407
+ end
408
+ stack.pop.to_hash
409
+ end
410
+ end