bbrowning-deltacloud-core 0.0.4-java
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 +176 -0
- data/Rakefile +99 -0
- data/bin/deltacloudd +120 -0
- data/config.ru +5 -0
- data/deltacloud.rb +18 -0
- data/lib/deltacloud/base_driver/base_driver.rb +229 -0
- data/lib/deltacloud/base_driver/features.rb +166 -0
- data/lib/deltacloud/base_driver/mock_driver.rb +40 -0
- data/lib/deltacloud/base_driver.rb +20 -0
- data/lib/deltacloud/drivers/ec2/ec2_driver.rb +410 -0
- data/lib/deltacloud/drivers/ec2/ec2_mock_driver.rb +170 -0
- data/lib/deltacloud/drivers/gogrid/gogrid_client.rb +50 -0
- data/lib/deltacloud/drivers/gogrid/gogrid_driver.rb +332 -0
- data/lib/deltacloud/drivers/gogrid/test.rb +13 -0
- data/lib/deltacloud/drivers/mock/data/images/img1.yml +3 -0
- data/lib/deltacloud/drivers/mock/data/images/img2.yml +3 -0
- data/lib/deltacloud/drivers/mock/data/images/img3.yml +3 -0
- data/lib/deltacloud/drivers/mock/data/instances/inst0.yml +16 -0
- data/lib/deltacloud/drivers/mock/data/instances/inst1.yml +9 -0
- data/lib/deltacloud/drivers/mock/data/instances/inst2.yml +9 -0
- data/lib/deltacloud/drivers/mock/data/storage_snapshots/snap1.yml +4 -0
- data/lib/deltacloud/drivers/mock/data/storage_snapshots/snap2.yml +4 -0
- data/lib/deltacloud/drivers/mock/data/storage_snapshots/snap3.yml +4 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol1.yml +6 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol2.yml +6 -0
- data/lib/deltacloud/drivers/mock/data/storage_volumes/vol3.yml +6 -0
- data/lib/deltacloud/drivers/mock/mock_driver.rb +277 -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 +130 -0
- data/lib/deltacloud/drivers/rackspace/rackspace_driver.rb +182 -0
- data/lib/deltacloud/drivers/rhevm/rhevm_driver.rb +255 -0
- data/lib/deltacloud/drivers/rimuhosting/rimuhosting_client.rb +85 -0
- data/lib/deltacloud/drivers/rimuhosting/rimuhosting_driver.rb +166 -0
- data/lib/deltacloud/drivers/terremark/terremark_driver.rb +286 -0
- data/lib/deltacloud/hardware_profile.rb +153 -0
- data/lib/deltacloud/helpers/application_helper.rb +115 -0
- data/lib/deltacloud/helpers/conversion_helper.rb +39 -0
- data/lib/deltacloud/helpers/hardware_profiles_helper.rb +35 -0
- data/lib/deltacloud/helpers.rb +5 -0
- data/lib/deltacloud/method_serializer.rb +85 -0
- data/lib/deltacloud/models/base_model.rb +59 -0
- data/lib/deltacloud/models/image.rb +27 -0
- data/lib/deltacloud/models/instance.rb +38 -0
- data/lib/deltacloud/models/instance_profile.rb +48 -0
- data/lib/deltacloud/models/key.rb +35 -0
- data/lib/deltacloud/models/realm.rb +26 -0
- data/lib/deltacloud/models/storage_snapshot.rb +27 -0
- data/lib/deltacloud/models/storage_volume.rb +28 -0
- data/lib/deltacloud/state_machine.rb +84 -0
- data/lib/deltacloud/validation.rb +70 -0
- data/lib/drivers.rb +50 -0
- data/lib/sinatra/accept_media_types.rb +128 -0
- data/lib/sinatra/lazy_auth.rb +56 -0
- data/lib/sinatra/rabbit.rb +273 -0
- data/lib/sinatra/respond_to.rb +272 -0
- data/lib/sinatra/static_assets.rb +83 -0
- data/lib/sinatra/url_for.rb +53 -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 +32 -0
- data/public/javascripts/jquery-1.4.2.min.js +154 -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 +354 -0
- data/support/fedora/deltacloudd +68 -0
- data/support/fedora/rubygem-deltacloud-core.spec +91 -0
- data/tests/api_test.rb +37 -0
- data/tests/hardware_profiles_test.rb +120 -0
- data/tests/images_test.rb +111 -0
- data/tests/instance_states_test.rb +52 -0
- data/tests/instances_test.rb +219 -0
- data/tests/realms_test.rb +78 -0
- data/tests/url_for_test.rb +50 -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 +19 -0
- data/views/errors/backend_error.xml.haml +8 -0
- data/views/errors/not_found.html.haml +6 -0
- data/views/errors/not_found.xml.haml +2 -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 +18 -0
- data/views/images/index.html.haml +30 -0
- data/views/images/index.xml.haml +8 -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 +30 -0
- data/views/instances/index.xml.haml +21 -0
- data/views/instances/new.html.haml +55 -0
- data/views/instances/show.html.haml +43 -0
- data/views/instances/show.xml.haml +49 -0
- data/views/keys/index.html.haml +26 -0
- data/views/keys/index.xml.haml +4 -0
- data/views/keys/new.html.haml +8 -0
- data/views/keys/show.html.haml +22 -0
- data/views/keys/show.xml.haml +20 -0
- data/views/layout.html.haml +26 -0
- data/views/realms/index.html.haml +29 -0
- data/views/realms/index.xml.haml +10 -0
- data/views/realms/show.html.haml +15 -0
- data/views/realms/show.xml.haml +9 -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 +9 -0
- data/views/storage_snapshots/show.html.haml +14 -0
- data/views/storage_snapshots/show.xml.haml +7 -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 +11 -0
- metadata +334 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
class Request
|
|
3
|
+
# The media types of the HTTP_ACCEPT header ordered according to their
|
|
4
|
+
# "quality" (preference level), without any media type parameters.
|
|
5
|
+
#
|
|
6
|
+
# ===== Examples
|
|
7
|
+
#
|
|
8
|
+
# env['HTTP_ACCEPT'] #=> 'application/xml;q=0.8,text/html,text/plain;q=0.9'
|
|
9
|
+
#
|
|
10
|
+
# req = Rack::Request.new(env)
|
|
11
|
+
# req.accept_media_types #=> ['text/html', 'text/plain', 'application/xml']
|
|
12
|
+
# req.accept_media_types.prefered #=> 'text/html'
|
|
13
|
+
#
|
|
14
|
+
# For more information, see:
|
|
15
|
+
# * Acept header: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
|
16
|
+
# * Quality values: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
|
|
17
|
+
#
|
|
18
|
+
# ===== Returns
|
|
19
|
+
# AcceptMediaTypes:: ordered list of accept header's media types
|
|
20
|
+
#
|
|
21
|
+
def accept_media_types
|
|
22
|
+
@accept_media_types ||= Rack::AcceptMediaTypes.new(@env['HTTP_ACCEPT'])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# AcceptMediaTypes is intended for wrapping env['HTTP_ACCEPT'].
|
|
27
|
+
#
|
|
28
|
+
# It allows ordering of its values (accepted media types) according to their
|
|
29
|
+
# "quality" (preference level).
|
|
30
|
+
#
|
|
31
|
+
# This wrapper is typically used to determine the request's prefered media
|
|
32
|
+
# type (see example below).
|
|
33
|
+
#
|
|
34
|
+
# ===== Examples
|
|
35
|
+
#
|
|
36
|
+
# env['HTTP_ACCEPT'] #=> 'application/xml;q=0.8,text/html,text/plain;q=0.9'
|
|
37
|
+
#
|
|
38
|
+
# types = Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT'])
|
|
39
|
+
# types #=> ['text/html', 'text/plain', 'application/xml']
|
|
40
|
+
# types.prefered #=> 'text/html'
|
|
41
|
+
#
|
|
42
|
+
# ===== Notes
|
|
43
|
+
#
|
|
44
|
+
# For simplicity, media type parameters are striped, as they are seldom used
|
|
45
|
+
# in practice. Users who need them are excepted to parse the Accept header
|
|
46
|
+
# manually.
|
|
47
|
+
#
|
|
48
|
+
# ===== References
|
|
49
|
+
#
|
|
50
|
+
# HTTP 1.1 Specs:
|
|
51
|
+
# * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
|
52
|
+
# * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
|
|
53
|
+
#
|
|
54
|
+
class AcceptMediaTypes < Array
|
|
55
|
+
|
|
56
|
+
#--
|
|
57
|
+
# NOTE
|
|
58
|
+
# Reason for special handling of nil accept header:
|
|
59
|
+
#
|
|
60
|
+
# "If no Accept header field is present, then it is assumed that the client
|
|
61
|
+
# accepts all media types."
|
|
62
|
+
#
|
|
63
|
+
def initialize(header)
|
|
64
|
+
if header.nil?
|
|
65
|
+
replace(['*/*'])
|
|
66
|
+
else
|
|
67
|
+
replace(order(header.gsub(/ /, '').split(/,/)))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The client's prefered media type.
|
|
72
|
+
def prefered
|
|
73
|
+
first
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Order media types by quality values, remove invalid types, and return media ranges.
|
|
79
|
+
#
|
|
80
|
+
def order(types) #:nodoc:
|
|
81
|
+
types.map {|type| AcceptMediaType.new(type) }.reverse.sort.reverse.select {|type| type.valid? }.map {|type| type.range }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
|
85
|
+
#
|
|
86
|
+
class AcceptMediaType #:nodoc:
|
|
87
|
+
include Comparable
|
|
88
|
+
|
|
89
|
+
# media-range = ( "*/*"
|
|
90
|
+
# | ( type "/" "*" )
|
|
91
|
+
# | ( type "/" subtype )
|
|
92
|
+
# ) *( ";" parameter )
|
|
93
|
+
attr_accessor :range
|
|
94
|
+
|
|
95
|
+
# qvalue = ( "0" [ "." 0*3DIGIT ] )
|
|
96
|
+
# | ( "1" [ "." 0*3("0") ] )
|
|
97
|
+
attr_accessor :quality
|
|
98
|
+
|
|
99
|
+
def initialize(type)
|
|
100
|
+
self.range, *params = type.split(';')
|
|
101
|
+
self.quality = extract_quality(params)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def <=>(type)
|
|
105
|
+
self.quality <=> type.quality
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# "A weight is normalized to a real number in the range 0 through 1,
|
|
109
|
+
# where 0 is the minimum and 1 the maximum value. If a parameter has a
|
|
110
|
+
# quality value of 0, then content with this parameter is `not
|
|
111
|
+
# acceptable' for the client."
|
|
112
|
+
#
|
|
113
|
+
def valid?
|
|
114
|
+
self.quality.between?(0.1, 1)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
# Extract value from 'q=FLOAT' parameter if present, otherwise assume 1
|
|
119
|
+
#
|
|
120
|
+
# "The default value is q=1."
|
|
121
|
+
#
|
|
122
|
+
def extract_quality(params)
|
|
123
|
+
q = params.detect {|p| p.match(/q=\d\.?\d{0,3}/) }
|
|
124
|
+
q ? q.split('=').last.to_f : 1.0
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
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,273 @@
|
|
|
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
|
+
return unless driver.has_collection?(name.to_sym)
|
|
207
|
+
collections[name] = Collection.new(name, &block)
|
|
208
|
+
collections[name].add_feature_params(driver.features(name))
|
|
209
|
+
collections[name].generate
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Generate a root route for API docs
|
|
213
|
+
get '/api/docs\/?' do
|
|
214
|
+
respond_to do |format|
|
|
215
|
+
format.html { haml :'docs/index' }
|
|
216
|
+
format.xml { haml :'docs/index' }
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
module RabbitHelper
|
|
223
|
+
def query_url(url, params)
|
|
224
|
+
return url if params.nil? || params.empty?
|
|
225
|
+
url + "?#{URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def entry_points
|
|
229
|
+
collections.values.inject([]) do |m, coll|
|
|
230
|
+
url = url_for coll.operations[:index].path, :full
|
|
231
|
+
m << [ coll.name, url ]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
register Rabbit
|
|
237
|
+
helpers RabbitHelper
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class String
|
|
241
|
+
# Rails defines this for a number of other classes, including Object
|
|
242
|
+
# see activesupport/lib/active_support/core_ext/object/blank.rb
|
|
243
|
+
def blank?
|
|
244
|
+
self !~ /\S/
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Title case.
|
|
248
|
+
#
|
|
249
|
+
# "this is a string".titlecase
|
|
250
|
+
# => "This Is A String"
|
|
251
|
+
#
|
|
252
|
+
# CREDIT: Eliazar Parra
|
|
253
|
+
# Copied from facets
|
|
254
|
+
def titlecase
|
|
255
|
+
gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def pluralize
|
|
259
|
+
self + "s"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def singularize
|
|
263
|
+
self.gsub(/s$/, '')
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def underscore
|
|
267
|
+
gsub(/::/, '/').
|
|
268
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
|
269
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
|
270
|
+
tr("-", "_").
|
|
271
|
+
downcase
|
|
272
|
+
end
|
|
273
|
+
end
|