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.
- data/COPYING +502 -0
- data/Rakefile +108 -0
- data/bin/deltacloudd +88 -0
- data/config.ru +5 -0
- data/deltacloud.rb +14 -0
- data/lib/converters/xml_converter.rb +133 -0
- data/lib/deltacloud/base_driver.rb +19 -0
- data/lib/deltacloud/base_driver/base_driver.rb +189 -0
- data/lib/deltacloud/base_driver/features.rb +144 -0
- data/lib/deltacloud/drivers/ec2/ec2_driver.rb +318 -0
- data/lib/deltacloud/drivers/ec2/ec2_mock_driver.rb +170 -0
- data/lib/deltacloud/drivers/gogrid/gogrid_client.rb +45 -0
- data/lib/deltacloud/drivers/gogrid/gogrid_driver.rb +239 -0
- data/lib/deltacloud/drivers/mock/mock_driver.rb +275 -0
- data/lib/deltacloud/drivers/opennebula/cloud_client.rb +116 -0
- data/lib/deltacloud/drivers/opennebula/occi_client.rb +204 -0
- data/lib/deltacloud/drivers/opennebula/opennebula_driver.rb +241 -0
- data/lib/deltacloud/drivers/rackspace/rackspace_client.rb +129 -0
- data/lib/deltacloud/drivers/rackspace/rackspace_driver.rb +150 -0
- data/lib/deltacloud/drivers/rhevm/rhevm_driver.rb +254 -0
- data/lib/deltacloud/drivers/rimu/rimu_hosting_client.rb +87 -0
- data/lib/deltacloud/drivers/rimu/rimu_hosting_driver.rb +143 -0
- data/lib/deltacloud/hardware_profile.rb +131 -0
- data/lib/deltacloud/helpers.rb +5 -0
- data/lib/deltacloud/helpers/application_helper.rb +38 -0
- data/lib/deltacloud/helpers/conversion_helper.rb +39 -0
- data/lib/deltacloud/helpers/hardware_profiles_helper.rb +35 -0
- data/lib/deltacloud/models/base_model.rb +58 -0
- data/lib/deltacloud/models/image.rb +26 -0
- data/lib/deltacloud/models/instance.rb +37 -0
- data/lib/deltacloud/models/instance_profile.rb +47 -0
- data/lib/deltacloud/models/realm.rb +25 -0
- data/lib/deltacloud/models/storage_snapshot.rb +26 -0
- data/lib/deltacloud/models/storage_volume.rb +27 -0
- data/lib/deltacloud/state_machine.rb +84 -0
- data/lib/deltacloud/validation.rb +70 -0
- data/lib/drivers.rb +37 -0
- data/lib/sinatra/lazy_auth.rb +56 -0
- data/lib/sinatra/rabbit.rb +272 -0
- data/lib/sinatra/respond_to.rb +262 -0
- data/lib/sinatra/static_assets.rb +83 -0
- data/lib/sinatra/url_for.rb +44 -0
- data/public/favicon.ico +0 -0
- data/public/images/grid.png +0 -0
- data/public/images/logo-wide.png +0 -0
- data/public/images/rails.png +0 -0
- data/public/images/topbar-bg.png +0 -0
- data/public/javascripts/application.js +2 -0
- data/public/javascripts/controls.js +963 -0
- data/public/javascripts/dragdrop.js +973 -0
- data/public/javascripts/effects.js +1128 -0
- data/public/javascripts/prototype.js +4320 -0
- data/public/stylesheets/compiled/application.css +613 -0
- data/public/stylesheets/compiled/ie.css +31 -0
- data/public/stylesheets/compiled/print.css +27 -0
- data/public/stylesheets/compiled/screen.css +456 -0
- data/server.rb +340 -0
- data/tests/deltacloud_test.rb +60 -0
- data/tests/images_test.rb +94 -0
- data/tests/instances_test.rb +136 -0
- data/tests/realms_test.rb +56 -0
- data/tests/storage_snapshots_test.rb +48 -0
- data/tests/storage_volumes_test.rb +48 -0
- data/views/accounts/index.html.haml +11 -0
- data/views/accounts/show.html.haml +30 -0
- data/views/api/show.html.haml +15 -0
- data/views/api/show.xml.haml +5 -0
- data/views/docs/collection.html.haml +37 -0
- data/views/docs/collection.xml.haml +14 -0
- data/views/docs/index.html.haml +15 -0
- data/views/docs/index.xml.haml +5 -0
- data/views/docs/operation.html.haml +31 -0
- data/views/docs/operation.xml.haml +10 -0
- data/views/errors/auth_exception.html.haml +8 -0
- data/views/errors/auth_exception.xml.haml +2 -0
- data/views/errors/backend_error.html.haml +17 -0
- data/views/errors/backend_error.xml.haml +8 -0
- data/views/errors/validation_failure.html.haml +11 -0
- data/views/errors/validation_failure.xml.haml +7 -0
- data/views/hardware_profiles/index.html.haml +25 -0
- data/views/hardware_profiles/index.xml.haml +4 -0
- data/views/hardware_profiles/show.html.haml +19 -0
- data/views/hardware_profiles/show.xml.haml +17 -0
- data/views/images/index.html.haml +30 -0
- data/views/images/index.xml.haml +7 -0
- data/views/images/show.html.haml +21 -0
- data/views/images/show.xml.haml +5 -0
- data/views/instance_states/show.gv.erb +45 -0
- data/views/instance_states/show.html.haml +31 -0
- data/views/instance_states/show.xml.haml +8 -0
- data/views/instances/index.html.haml +29 -0
- data/views/instances/index.xml.haml +23 -0
- data/views/instances/new.html.haml +49 -0
- data/views/instances/show.html.haml +42 -0
- data/views/instances/show.xml.haml +28 -0
- data/views/layout.html.haml +23 -0
- data/views/realms/index.html.haml +29 -0
- data/views/realms/index.xml.haml +12 -0
- data/views/realms/show.html.haml +15 -0
- data/views/realms/show.xml.haml +10 -0
- data/views/root/index.html.haml +4 -0
- data/views/storage_snapshots/index.html.haml +20 -0
- data/views/storage_snapshots/index.xml.haml +11 -0
- data/views/storage_snapshots/show.html.haml +14 -0
- data/views/storage_snapshots/show.xml.haml +9 -0
- data/views/storage_volumes/index.html.haml +21 -0
- data/views/storage_volumes/index.xml.haml +13 -0
- data/views/storage_volumes/show.html.haml +20 -0
- data/views/storage_volumes/show.xml.haml +13 -0
- 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
|