deltacloud-core 0.0.1

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.
Files changed (110) hide show
  1. data/COPYING +502 -0
  2. data/Rakefile +108 -0
  3. data/bin/deltacloudd +88 -0
  4. data/config.ru +5 -0
  5. data/deltacloud.rb +14 -0
  6. data/lib/converters/xml_converter.rb +133 -0
  7. data/lib/deltacloud/base_driver.rb +19 -0
  8. data/lib/deltacloud/base_driver/base_driver.rb +189 -0
  9. data/lib/deltacloud/base_driver/features.rb +144 -0
  10. data/lib/deltacloud/drivers/ec2/ec2_driver.rb +318 -0
  11. data/lib/deltacloud/drivers/ec2/ec2_mock_driver.rb +170 -0
  12. data/lib/deltacloud/drivers/gogrid/gogrid_client.rb +45 -0
  13. data/lib/deltacloud/drivers/gogrid/gogrid_driver.rb +239 -0
  14. data/lib/deltacloud/drivers/mock/mock_driver.rb +275 -0
  15. data/lib/deltacloud/drivers/opennebula/cloud_client.rb +116 -0
  16. data/lib/deltacloud/drivers/opennebula/occi_client.rb +204 -0
  17. data/lib/deltacloud/drivers/opennebula/opennebula_driver.rb +241 -0
  18. data/lib/deltacloud/drivers/rackspace/rackspace_client.rb +129 -0
  19. data/lib/deltacloud/drivers/rackspace/rackspace_driver.rb +150 -0
  20. data/lib/deltacloud/drivers/rhevm/rhevm_driver.rb +254 -0
  21. data/lib/deltacloud/drivers/rimu/rimu_hosting_client.rb +87 -0
  22. data/lib/deltacloud/drivers/rimu/rimu_hosting_driver.rb +143 -0
  23. data/lib/deltacloud/hardware_profile.rb +131 -0
  24. data/lib/deltacloud/helpers.rb +5 -0
  25. data/lib/deltacloud/helpers/application_helper.rb +38 -0
  26. data/lib/deltacloud/helpers/conversion_helper.rb +39 -0
  27. data/lib/deltacloud/helpers/hardware_profiles_helper.rb +35 -0
  28. data/lib/deltacloud/models/base_model.rb +58 -0
  29. data/lib/deltacloud/models/image.rb +26 -0
  30. data/lib/deltacloud/models/instance.rb +37 -0
  31. data/lib/deltacloud/models/instance_profile.rb +47 -0
  32. data/lib/deltacloud/models/realm.rb +25 -0
  33. data/lib/deltacloud/models/storage_snapshot.rb +26 -0
  34. data/lib/deltacloud/models/storage_volume.rb +27 -0
  35. data/lib/deltacloud/state_machine.rb +84 -0
  36. data/lib/deltacloud/validation.rb +70 -0
  37. data/lib/drivers.rb +37 -0
  38. data/lib/sinatra/lazy_auth.rb +56 -0
  39. data/lib/sinatra/rabbit.rb +272 -0
  40. data/lib/sinatra/respond_to.rb +262 -0
  41. data/lib/sinatra/static_assets.rb +83 -0
  42. data/lib/sinatra/url_for.rb +44 -0
  43. data/public/favicon.ico +0 -0
  44. data/public/images/grid.png +0 -0
  45. data/public/images/logo-wide.png +0 -0
  46. data/public/images/rails.png +0 -0
  47. data/public/images/topbar-bg.png +0 -0
  48. data/public/javascripts/application.js +2 -0
  49. data/public/javascripts/controls.js +963 -0
  50. data/public/javascripts/dragdrop.js +973 -0
  51. data/public/javascripts/effects.js +1128 -0
  52. data/public/javascripts/prototype.js +4320 -0
  53. data/public/stylesheets/compiled/application.css +613 -0
  54. data/public/stylesheets/compiled/ie.css +31 -0
  55. data/public/stylesheets/compiled/print.css +27 -0
  56. data/public/stylesheets/compiled/screen.css +456 -0
  57. data/server.rb +340 -0
  58. data/tests/deltacloud_test.rb +60 -0
  59. data/tests/images_test.rb +94 -0
  60. data/tests/instances_test.rb +136 -0
  61. data/tests/realms_test.rb +56 -0
  62. data/tests/storage_snapshots_test.rb +48 -0
  63. data/tests/storage_volumes_test.rb +48 -0
  64. data/views/accounts/index.html.haml +11 -0
  65. data/views/accounts/show.html.haml +30 -0
  66. data/views/api/show.html.haml +15 -0
  67. data/views/api/show.xml.haml +5 -0
  68. data/views/docs/collection.html.haml +37 -0
  69. data/views/docs/collection.xml.haml +14 -0
  70. data/views/docs/index.html.haml +15 -0
  71. data/views/docs/index.xml.haml +5 -0
  72. data/views/docs/operation.html.haml +31 -0
  73. data/views/docs/operation.xml.haml +10 -0
  74. data/views/errors/auth_exception.html.haml +8 -0
  75. data/views/errors/auth_exception.xml.haml +2 -0
  76. data/views/errors/backend_error.html.haml +17 -0
  77. data/views/errors/backend_error.xml.haml +8 -0
  78. data/views/errors/validation_failure.html.haml +11 -0
  79. data/views/errors/validation_failure.xml.haml +7 -0
  80. data/views/hardware_profiles/index.html.haml +25 -0
  81. data/views/hardware_profiles/index.xml.haml +4 -0
  82. data/views/hardware_profiles/show.html.haml +19 -0
  83. data/views/hardware_profiles/show.xml.haml +17 -0
  84. data/views/images/index.html.haml +30 -0
  85. data/views/images/index.xml.haml +7 -0
  86. data/views/images/show.html.haml +21 -0
  87. data/views/images/show.xml.haml +5 -0
  88. data/views/instance_states/show.gv.erb +45 -0
  89. data/views/instance_states/show.html.haml +31 -0
  90. data/views/instance_states/show.xml.haml +8 -0
  91. data/views/instances/index.html.haml +29 -0
  92. data/views/instances/index.xml.haml +23 -0
  93. data/views/instances/new.html.haml +49 -0
  94. data/views/instances/show.html.haml +42 -0
  95. data/views/instances/show.xml.haml +28 -0
  96. data/views/layout.html.haml +23 -0
  97. data/views/realms/index.html.haml +29 -0
  98. data/views/realms/index.xml.haml +12 -0
  99. data/views/realms/show.html.haml +15 -0
  100. data/views/realms/show.xml.haml +10 -0
  101. data/views/root/index.html.haml +4 -0
  102. data/views/storage_snapshots/index.html.haml +20 -0
  103. data/views/storage_snapshots/index.xml.haml +11 -0
  104. data/views/storage_snapshots/show.html.haml +14 -0
  105. data/views/storage_snapshots/show.xml.haml +9 -0
  106. data/views/storage_volumes/index.html.haml +21 -0
  107. data/views/storage_volumes/index.xml.haml +13 -0
  108. data/views/storage_volumes/show.html.haml +20 -0
  109. data/views/storage_volumes/show.xml.haml +13 -0
  110. metadata +311 -0
@@ -0,0 +1,26 @@
1
+ #
2
+ # Copyright (C) 2009 Red Hat, Inc.
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+
18
+
19
+
20
+ class StorageSnapshot < BaseModel
21
+
22
+ attr_accessor :state
23
+ attr_accessor :storage_volume_id
24
+ attr_accessor :created
25
+
26
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Copyright (C) 2009 Red Hat, Inc.
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+
18
+
19
+ class StorageVolume < BaseModel
20
+
21
+ attr_accessor :created
22
+ attr_accessor :state
23
+ attr_accessor :capacity
24
+ attr_accessor :instance_id
25
+ attr_accessor :device
26
+
27
+ end
@@ -0,0 +1,84 @@
1
+
2
+ module Deltacloud
3
+ class StateMachine
4
+
5
+ attr_reader :states
6
+ def initialize(&block)
7
+ @states = []
8
+ instance_eval &block if block
9
+ end
10
+
11
+ def start()
12
+ state(:start)
13
+ end
14
+
15
+ def finish()
16
+ state(:finish)
17
+ end
18
+
19
+ def state(name)
20
+ state = @states.find{|e| e.name == name.to_sym}
21
+ if ( state.nil? )
22
+ state = State.new( self, name.to_sym )
23
+ @states << state
24
+ end
25
+ state
26
+ end
27
+
28
+ def method_missing(sym,*args)
29
+ return state( sym ) if ( args.empty? )
30
+ super( sym, *args )
31
+ end
32
+
33
+ class State
34
+
35
+ attr_reader :name
36
+ attr_reader :transitions
37
+
38
+ def initialize(machine, name)
39
+ @machine = machine
40
+ @name = name
41
+ @transitions = []
42
+ end
43
+
44
+ def to_s
45
+ self.name.to_s
46
+ end
47
+
48
+ def to(destination_name)
49
+ destination = @machine.state(destination_name)
50
+ transition = Transition.new( @machine, destination )
51
+ @transitions << transition
52
+ transition
53
+ end
54
+
55
+ end
56
+
57
+ class Transition
58
+
59
+ attr_reader :destination
60
+ attr_reader :action
61
+
62
+ def initialize(machine, destination)
63
+ @machine = machine
64
+ @destination = destination
65
+ @auto = false
66
+ @action = nil
67
+ end
68
+
69
+ def automatically
70
+ @auto = true
71
+ end
72
+
73
+ def automatically?
74
+ @auto
75
+ end
76
+
77
+ def on(action)
78
+ @action = action
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,70 @@
1
+ module Deltacloud::Validation
2
+
3
+ class Failure < StandardError
4
+ attr_reader :param
5
+ def initialize(param, msg='')
6
+ super(msg)
7
+ @param = param
8
+ end
9
+
10
+ def name
11
+ param.name
12
+ end
13
+ end
14
+
15
+ class Param
16
+ attr_reader :name, :klass, :type, :options, :description
17
+
18
+ def initialize(args)
19
+ @name = args[0]
20
+ @klass = args[1] || :string
21
+ @type = args[2] || :optional
22
+ @options = args[3] || []
23
+ @description = args[4] || ''
24
+ end
25
+
26
+ def required?
27
+ type.eql?(:required)
28
+ end
29
+
30
+ def optional?
31
+ type.eql?(:optional)
32
+ end
33
+ end
34
+
35
+ def param(*args)
36
+ raise DuplicateParamException if params[args[0]]
37
+ p = Param.new(args)
38
+ params[p.name] = p
39
+ end
40
+
41
+ def params
42
+ @params ||= {}
43
+ @params
44
+ end
45
+
46
+ # Add the parameters in hash +new+ to already existing parameters. If
47
+ # +new+ contains a parameter with an already existing name, the old
48
+ # definition is clobbered.
49
+ def add_params(new)
50
+ # We do not check for duplication on purpose: multiple calls
51
+ # to add_params should be cumulative
52
+ new.each { |p| @params[p.name] = p }
53
+ end
54
+
55
+ def each_param(&block)
56
+ params.each_value { |p| yield p }
57
+ end
58
+
59
+ def validate(values)
60
+ each_param do |p|
61
+ if p.required? and not values[p.name]
62
+ raise Failure.new(p, "Required parameter #{p.name} not found")
63
+ end
64
+ if values[p.name] and not p.options.empty? and
65
+ not p.options.include?(values[p.name])
66
+ raise Failure.new(p, "Parameter #{p.name} has value #{values[p.name]} which is not in #{p.options.join(", ")}")
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/drivers.rb ADDED
@@ -0,0 +1,37 @@
1
+ DRIVERS = {
2
+ :ec2 => { :name => "EC2" },
3
+ :rackspace => { :name => "Rackspace" },
4
+ :gogrid => { :name => "Gogrid" },
5
+ :rhevm => { :name => "RHEVM" },
6
+ :rimu => { :name => "Rimu", :class => "RimuHostingDriver"},
7
+ :opennebula => { :name => "Opennebula", :class => "OpennebulaDriver" },
8
+ :mock => { :name => "Mock" }
9
+ }
10
+
11
+ def driver_name
12
+ DRIVERS[DRIVER][:name]
13
+ end
14
+
15
+ def driver_class_name
16
+ basename = DRIVERS[DRIVER][:class] || "#{driver_name}Driver"
17
+ "Deltacloud::Drivers::#{driver_name}::#{basename}"
18
+ end
19
+
20
+ def driver_source_name
21
+ File.join("deltacloud", "drivers", "#{DRIVER}", "#{DRIVER}_driver.rb")
22
+ end
23
+
24
+ def driver_mock_source_name
25
+ return File.join('deltacloud', 'drivers', DRIVER.to_s, "#{DRIVER}_driver.rb") if driver_name.eql? 'Mock'
26
+ File.join('deltacloud', 'drivers', DRIVER, "#{DRIVER}_mock_driver.rb")
27
+ end
28
+
29
+ def driver
30
+ require driver_source_name
31
+
32
+ if Sinatra::Application.environment.eql? :test
33
+ require driver_mock_source_name
34
+ end
35
+
36
+ @driver ||= eval( driver_class_name ).new
37
+ end
@@ -0,0 +1,56 @@
1
+ require 'sinatra/base'
2
+
3
+ # Lazy Basic HTTP authentication. Authentication is only forced when the
4
+ # credentials are actually needed.
5
+ module Sinatra
6
+ module LazyAuth
7
+ class LazyCredentials
8
+ def initialize(app)
9
+ @app = app
10
+ @provided = false
11
+ end
12
+
13
+ def user
14
+ credentials!
15
+ @user
16
+ end
17
+
18
+ def password
19
+ credentials!
20
+ @password
21
+ end
22
+
23
+ def provided?
24
+ @provided
25
+ end
26
+
27
+ private
28
+ def credentials!
29
+ unless provided?
30
+ auth = Rack::Auth::Basic::Request.new(@app.request.env)
31
+ unless auth.provided? && auth.basic? && auth.credentials
32
+ @app.authorize!
33
+ end
34
+ @user = auth.credentials[0]
35
+ @password = auth.credentials[1]
36
+ @provided = true
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ def authorize!
43
+ r = "#{DRIVER}-deltacloud@#{HOSTNAME}"
44
+ response['WWW-Authenticate'] = %(Basic realm="#{r}")
45
+ throw(:halt, [401, "Not authorized\n"])
46
+ end
47
+
48
+ # Request the current user's credentials. Actual credentials are only
49
+ # requested when an attempt is made to get the user name or password
50
+ def credentials
51
+ LazyCredentials.new(self)
52
+ end
53
+ end
54
+
55
+ helpers LazyAuth
56
+ end
@@ -0,0 +1,272 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/url_for'
3
+ require 'deltacloud/validation'
4
+
5
+ module Sinatra
6
+
7
+ module Rabbit
8
+
9
+ class DuplicateParamException < Exception; end
10
+ class DuplicateOperationException < Exception; end
11
+ class DuplicateCollectionException < Exception; end
12
+
13
+ class Operation
14
+ attr_reader :name, :method
15
+
16
+ include ::Deltacloud::Validation
17
+
18
+ STANDARD = {
19
+ :index => { :method => :get, :member => false },
20
+ :show => { :method => :get, :member => true },
21
+ :create => { :method => :post, :member => false },
22
+ :update => { :method => :put, :member => true },
23
+ :destroy => { :method => :delete, :member => true }
24
+ }
25
+
26
+ def initialize(coll, name, opts, &block)
27
+ @name = name.to_sym
28
+ opts = STANDARD[@name].merge(opts) if standard?
29
+ @collection = coll
30
+ raise "No method for operation #{name}" unless opts[:method]
31
+ @method = opts[:method].to_sym
32
+ @member = opts[:member]
33
+ @description = ""
34
+ instance_eval(&block) if block_given?
35
+ generate_documentation
36
+ end
37
+
38
+ def standard?
39
+ STANDARD.keys.include?(name)
40
+ end
41
+
42
+ def description(text="")
43
+ return @description if text.blank?
44
+ @description = text
45
+ end
46
+
47
+ def generate_documentation
48
+ coll, oper = @collection, self
49
+ ::Sinatra::Application.get("/api/docs/#{@collection.name}/#{@name}") do
50
+ @collection, @operation = coll, oper
51
+ respond_to do |format|
52
+ format.html { haml :'docs/operation' }
53
+ format.xml { haml :'docs/operation' }
54
+ end
55
+ end
56
+ end
57
+
58
+ def control(&block)
59
+ op = self
60
+ @control = Proc.new do
61
+ op.validate(params)
62
+ instance_eval(&block)
63
+ end
64
+ end
65
+
66
+ def prefix
67
+ # FIXME: Make the /api prefix configurable
68
+ "/api"
69
+ end
70
+
71
+ def path(args = {})
72
+ l_prefix = args[:prefix] ? args[:prefix] : prefix
73
+ if @member
74
+ if standard?
75
+ "#{l_prefix}/#{@collection.name}/:id"
76
+ else
77
+ "#{l_prefix}/#{@collection.name}/:id/#{name}"
78
+ end
79
+ else
80
+ "#{l_prefix}/#{@collection.name}"
81
+ end
82
+ end
83
+
84
+ def generate
85
+ ::Sinatra::Application.send(@method, path, {}, &@control)
86
+ # Set up some Rails-like URL helpers
87
+ if name == :index
88
+ gen_route "#{@collection.name}_url"
89
+ elsif name == :show
90
+ gen_route "#{@collection.name.to_s.singularize}_url"
91
+ else
92
+ gen_route "#{name}_#{@collection.name.to_s.singularize}_url"
93
+ end
94
+ end
95
+
96
+ private
97
+ def gen_route(name)
98
+ route_url = path
99
+ if @member
100
+ ::Sinatra::Application.send(:define_method, name) do |id, *args|
101
+ url = query_url(route_url, args[0])
102
+ url_for url.gsub(/:id/, id.to_s), :full
103
+ end
104
+ else
105
+ ::Sinatra::Application.send(:define_method, name) do |*args|
106
+ url = query_url(route_url, args[0])
107
+ url_for url, :full
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ class Collection
114
+ attr_reader :name, :operations
115
+
116
+ def initialize(name, &block)
117
+ @name = name
118
+ @description = ""
119
+ @operations = {}
120
+ instance_eval(&block) if block_given?
121
+ generate_documentation
122
+ end
123
+
124
+ # Set/Return description for collection
125
+ # If first parameter is not present, full description will be
126
+ # returned.
127
+ def description(text='')
128
+ return @description if text.blank?
129
+ @description = text
130
+ end
131
+
132
+ def generate_documentation
133
+ coll, oper, features = self, @operations, driver.features(name)
134
+ ::Sinatra::Application.get("/api/docs/#{@name}") do
135
+ @collection, @operations, @features = coll, oper, features
136
+ respond_to do |format|
137
+ format.html { haml :'docs/collection' }
138
+ format.xml { haml :'docs/collection' }
139
+ end
140
+ end
141
+ end
142
+
143
+ # Add a new operation for this collection. For the standard REST
144
+ # operations :index, :show, :update, and :destroy, we already know
145
+ # what method to use and whether this is an operation on the URL for
146
+ # individual elements or for the whole collection.
147
+ #
148
+ # For non-standard operations, options must be passed:
149
+ # :method : one of the HTTP methods
150
+ # :member : whether this is an operation on the collection or an
151
+ # individual element (FIXME: custom operations on the
152
+ # collection will use a nonsensical URL) The URL for the
153
+ # operation is the element URL with the name of the operation
154
+ # appended
155
+ #
156
+ # This also defines a helper method like show_instance_url that returns
157
+ # the URL to this operation (in request context)
158
+ def operation(name, opts = {}, &block)
159
+ raise DuplicateOperationException if @operations[name]
160
+ @operations[name] = Operation.new(self, name, opts, &block)
161
+ end
162
+
163
+ def generate
164
+ operations.values.each { |op| op.generate }
165
+ app = ::Sinatra::Application
166
+ collname = name # Work around Ruby's weird scoping/capture
167
+ app.send(:define_method, "#{name.to_s.singularize}_url") do |id|
168
+ url_for "/api/#{collname}/#{id}", :full
169
+ end
170
+
171
+ if index_op = operations[:index]
172
+ app.send(:define_method, "#{name}_url") do
173
+ url_for index_op.path.gsub(/\/\?$/,''), :full
174
+ end
175
+ end
176
+ end
177
+
178
+ def add_feature_params(features)
179
+ features.each do |f|
180
+ f.operations.each do |fop|
181
+ if cop = operations[fop.name]
182
+ fop.params.each_key do |k|
183
+ if cop.params.has_key?(k)
184
+ raise DuplicateParamException, "Parameter '#{k}' for operation #{fop.name} defined by collection #{@name} and by feature #{f.name}"
185
+ else
186
+ cop.params[k] = fop.params[k]
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def collections
196
+ @collections ||= {}
197
+ end
198
+
199
+ # Create a new collection. NAME should be the pluralized name of the
200
+ # collection.
201
+ #
202
+ # Adds a helper method #{name}_url which returns the URL to the :index
203
+ # operation on this collection.
204
+ def collection(name, &block)
205
+ raise DuplicateCollectionException if collections[name]
206
+ collections[name] = Collection.new(name, &block)
207
+ collections[name].add_feature_params(driver.features(name))
208
+ collections[name].generate
209
+ end
210
+
211
+ # Generate a root route for API docs
212
+ get '/api/docs\/?' do
213
+ respond_to do |format|
214
+ format.html { haml :'docs/index' }
215
+ format.xml { haml :'docs/index' }
216
+ end
217
+ end
218
+
219
+ end
220
+
221
+ module RabbitHelper
222
+ def query_url(url, params)
223
+ return url if params.nil? || params.empty?
224
+ url + "?#{URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))}"
225
+ end
226
+
227
+ def entry_points
228
+ collections.values.inject([]) do |m, coll|
229
+ url = url_for coll.operations[:index].path, :full
230
+ m << [ coll.name, url ]
231
+ end
232
+ end
233
+ end
234
+
235
+ register Rabbit
236
+ helpers RabbitHelper
237
+ end
238
+
239
+ class String
240
+ # Rails defines this for a number of other classes, including Object
241
+ # see activesupport/lib/active_support/core_ext/object/blank.rb
242
+ def blank?
243
+ self !~ /\S/
244
+ end
245
+
246
+ # Title case.
247
+ #
248
+ # "this is a string".titlecase
249
+ # => "This Is A String"
250
+ #
251
+ # CREDIT: Eliazar Parra
252
+ # Copied from facets
253
+ def titlecase
254
+ gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
255
+ end
256
+
257
+ def pluralize
258
+ self + "s"
259
+ end
260
+
261
+ def singularize
262
+ self.gsub(/s$/, '')
263
+ end
264
+
265
+ def underscore
266
+ gsub(/::/, '/').
267
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
268
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
269
+ tr("-", "_").
270
+ downcase
271
+ end
272
+ end