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