sinatra-rabbit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
@@ -0,0 +1,249 @@
1
+ # respond_to (The MIT License)
2
+
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
4
+ # and associated documentation files (the 'Software'), to deal in the Software without restriction,
5
+ # including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ # furnished to do so, subject to the following conditions:
8
+ #
9
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
10
+ # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
11
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
12
+ # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
13
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
14
+
15
+ require 'rack/accept'
16
+
17
+ puts "Loading repond_to"
18
+
19
+ module Sinatra
20
+
21
+ module RespondTo
22
+
23
+ class MissingTemplate < Sinatra::NotFound; end
24
+
25
+ # Define all MIME types you want to support here.
26
+ # This conversion table will be used for auto-negotiation
27
+ # with browser in sinatra when no 'format' parameter is specified.
28
+
29
+ SUPPORTED_ACCEPT_HEADERS = {
30
+ :xml => [
31
+ 'text/xml',
32
+ 'application/xml'
33
+ ],
34
+ :html => [
35
+ 'text/html',
36
+ 'application/xhtml+xml'
37
+ ],
38
+ :json => [
39
+ 'application/json'
40
+ ]
41
+ }
42
+
43
+ # We need to pass array of available response types to
44
+ # best_media_type method
45
+ def accept_to_array
46
+ SUPPORTED_ACCEPT_HEADERS.keys.collect do |key|
47
+ SUPPORTED_ACCEPT_HEADERS[key]
48
+ end.flatten
49
+ end
50
+
51
+ # Then, when we get best media type for response, we need
52
+ # to know which format to choose
53
+ def lookup_format_from_mime(mime)
54
+ SUPPORTED_ACCEPT_HEADERS.keys.each do |format|
55
+ return format if SUPPORTED_ACCEPT_HEADERS[format].include?(mime)
56
+ end
57
+ end
58
+
59
+ def self.registered(app)
60
+
61
+ app.helpers RespondTo::Helpers
62
+ use Rack::Accept
63
+
64
+ app.before do
65
+
66
+ # Skip development error image and static content
67
+ next if self.class.development? && request.path_info =~ %r{/__sinatra__/.*?.png}
68
+ next if options.static? && options.public? && (request.get? || request.head?) && static_file?(request.path_info)
69
+
70
+ # Remove extension from URI
71
+ # Extension will be available as a 'extension' method (extension=='txt')
72
+
73
+ extension request.path_info.match(/\.([^\.\/]+)$/).to_a.first
74
+
75
+ # If ?format= is present, ignore all Accept negotiations because
76
+ # we are not dealing with browser
77
+ if request.params.has_key? 'format'
78
+ format params['format'].to_sym
79
+ end
80
+
81
+ # Let's make a little exception here to handle
82
+ # /api/instance_states[.gv/.png] calls
83
+ if extension.eql?('gv')
84
+ format :gv
85
+ elsif extension.eql?('png')
86
+ format :png
87
+ end
88
+
89
+ # Get Rack::Accept::Response object and find best possible
90
+ # mime type to output.
91
+ # This negotiation works fine with latest rest-client gem:
92
+ #
93
+ # RestClient.get 'http://localhost:3001/api', {:accept => :json } =>
94
+ # 'application/json'
95
+ # RestClient.get 'http://localhost:3001/api', {:accept => :xml } =>
96
+ # 'application/xml'
97
+ #
98
+ # Also browsers like Firefox (3.6.x) and Chromium reporting
99
+ # 'application/xml+xhtml' which is recognized as :html reponse
100
+ # In browser you can force output using ?format=[format] parameter.
101
+
102
+ rack_accept = env['rack-accept.request']
103
+
104
+ if rack_accept.media_type.to_s.strip.eql?('Accept:')
105
+ format :xml
106
+ elsif is_chrome?
107
+ format :html
108
+ else
109
+ format lookup_format_from_mime(rack_accept.best_media_type(accept_to_array))
110
+ end
111
+
112
+ end
113
+
114
+ app.class_eval do
115
+
116
+ # Simple helper to detect Chrome based browsers
117
+ # which have screwed up they Accept headers.
118
+ # Set HTML as default output format here
119
+ def is_chrome?
120
+ true if env['HTTP_USER_AGENT'] =~ /Chrome/
121
+ end
122
+
123
+ # This code was copied from respond_to plugin
124
+ # http://github.com/cehoffman/sinatra-respond_to
125
+ # MIT License
126
+ alias :render_without_format :render
127
+ def render(*args, &block)
128
+ assumed_layout = args[1] == :layout
129
+ args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
130
+ render_without_format *args, &block
131
+ rescue Errno::ENOENT => e
132
+ raise MissingTemplate, "#{args[1]}.#{args[0]}" unless assumed_layout
133
+ raise e
134
+ end
135
+ private :render
136
+ end
137
+
138
+ # This code was copied from respond_to plugin
139
+ # http://github.com/cehoffman/sinatra-respond_to
140
+ app.configure :development do |dev|
141
+ dev.error MissingTemplate do
142
+ content_type :html, :charset => 'utf-8'
143
+ response.status = request.env['sinatra.error'].code
144
+
145
+ engine = request.env['sinatra.error'].message.split('.').last
146
+ engine = 'haml' unless ['haml', 'builder', 'erb'].include? engine
147
+
148
+ path = File.basename(request.path_info)
149
+ path = "root" if path.nil? || path.empty?
150
+
151
+ format = engine == 'builder' ? 'xml' : 'html'
152
+
153
+ layout = case engine
154
+ when 'haml' then "!!!\n%html\n %body= yield"
155
+ when 'erb' then "<html>\n <body>\n <%= yield %>\n </body>\n</html>"
156
+ end
157
+
158
+ layout = "<small>app.#{format}.#{engine}</small>\n<pre>#{escape_html(layout)}</pre>"
159
+
160
+ (<<-HTML).gsub(/^ {10}/, '')
161
+ <!DOCTYPE html>
162
+ <html>
163
+ <head>
164
+ <style type="text/css">
165
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
166
+ color:#888;margin:20px}
167
+ #c {margin:0 auto;width:500px;text-align:left;}
168
+ small {float:right;clear:both;}
169
+ pre {clear:both;text-align:left;font-size:70%;width:500px;margin:0 auto;}
170
+ </style>
171
+ </head>
172
+ <body>
173
+ <h2>Sinatra can't find #{request.env['sinatra.error'].message}</h2>
174
+ <img src='/__sinatra__/500.png'>
175
+ <pre>#{request.env['sinatra.error'].backtrace.join("\n")}</pre>
176
+ <div id="c">
177
+ <small>application.rb</small>
178
+ <pre>#{request.request_method.downcase} '#{request.path_info}' do\n respond_to do |wants|\n wants.#{format} { #{engine} :#{path} }\n end\nend</pre>
179
+ </div>
180
+ </body>
181
+ </html>
182
+ HTML
183
+ end
184
+
185
+ end
186
+ end
187
+
188
+ module Helpers
189
+
190
+ # This code was copied from respond_to plugin
191
+ # http://github.com/cehoffman/sinatra-respond_to
192
+ def self.included(klass)
193
+ klass.class_eval do
194
+ alias :content_type_without_save :content_type
195
+ def content_type(*args)
196
+ content_type_without_save *args
197
+ @_format = args.first.to_sym
198
+ response['Content-Type']
199
+ end
200
+ end
201
+ end
202
+
203
+ def static_file?(path)
204
+ public_dir = File.expand_path(options.public)
205
+ path = File.expand_path(File.join(public_dir, unescape(path)))
206
+
207
+ path[0, public_dir.length] == public_dir && File.file?(path)
208
+ end
209
+
210
+
211
+ # Extension holds trimmed extension. This is extra usefull
212
+ # when you want to build original URI (with extension)
213
+ # You can simply call "#{request.env['REQUEST_URI']}.#{extension}"
214
+ def extension(val=nil)
215
+ @_extension ||= val
216
+ @_extension
217
+ end
218
+
219
+ # This helper will holds current format. Helper should be
220
+ # accesible from all places in Sinatra
221
+ def format(val=nil)
222
+ @_format ||= val
223
+ @_format
224
+ end
225
+
226
+ def respond_to(&block)
227
+ wants = {}
228
+
229
+ def wants.method_missing(type, *args, &handler)
230
+ self[type] = handler
231
+ end
232
+
233
+ # Set proper content-type and encoding for
234
+ # text based formats
235
+ if [:xml, :gv, :html, :json].include?(format)
236
+ content_type format, :charset => 'utf-8'
237
+ end
238
+ yield wants
239
+ # Raise this error if requested format is not defined
240
+ # in respond_to { } block.
241
+ raise MissingTemplate if wants[format].nil?
242
+
243
+ wants[format].call
244
+ end
245
+
246
+ end
247
+
248
+ end
249
+ end
@@ -0,0 +1,53 @@
1
+ require 'uri'
2
+
3
+ module Sinatra
4
+ module UrlForHelper
5
+ # Construct a link to +url_fragment+, which should be given relative to
6
+ # the base of this Sinatra app. The mode should be either
7
+ # <code>:path_only</code>, which will generate an absolute path within
8
+ # the current domain (the default), or <code>:full</code>, which will
9
+ # include the site name and port number. (The latter is typically
10
+ # necessary for links in RSS feeds.) Example usage:
11
+ #
12
+ # url_for "/" # Returns "/myapp/"
13
+ # url_for "/foo" # Returns "/myapp/foo"
14
+ # url_for "/foo", :full # Returns "http://example.com/myapp/foo"
15
+ #--
16
+ # See README.rdoc for a list of some of the people who helped me clean
17
+ # up earlier versions of this code.
18
+ def url_for url_fragment, mode=:path_only
19
+ case mode
20
+ when :path_only
21
+ base = request.script_name
22
+ when :full
23
+ scheme = request.scheme
24
+ if (scheme == 'http' && request.port == 80 ||
25
+ scheme == 'https' && request.port == 443)
26
+ port = ""
27
+ else
28
+ port = ":#{request.port}"
29
+ end
30
+ request_host = HOSTNAME ? HOSTNAME : request.host
31
+ base = "#{scheme}://#{request_host}#{port}#{request.script_name}"
32
+ else
33
+ raise TypeError, "Unknown url_for mode #{mode}"
34
+ end
35
+ url_escape = URI.escape(url_fragment)
36
+ # Don't add the base fragment if url_for gets called more than once
37
+ # per url or the url_fragment passed in is an absolute url
38
+ if url_escape.match(/^#{base}/) or url_escape.match(/^http/)
39
+ url_escape
40
+ else
41
+ "#{base}#{url_escape}"
42
+ end
43
+ end
44
+
45
+ def root_url
46
+ url_for '/'
47
+ end
48
+ end
49
+
50
+
51
+
52
+ helpers UrlForHelper
53
+ end
@@ -0,0 +1,75 @@
1
+ module Sinatra
2
+
3
+ module Rabbit
4
+
5
+ module Validation
6
+ class Failure < StandardError
7
+ attr_reader :param
8
+ def initialize(param, msg='')
9
+ super(msg)
10
+ @param = param
11
+ end
12
+
13
+ def name
14
+ param.name
15
+ end
16
+ end
17
+
18
+ class Param
19
+ attr_reader :name, :klass, :type, :options, :description
20
+
21
+ def initialize(args)
22
+ @name = args[0]
23
+ @klass = args[1] || :string
24
+ @type = args[2] || :optional
25
+ @options = args[3] || []
26
+ @description = args[4] || ''
27
+ end
28
+
29
+ def required?
30
+ type.eql?(:required)
31
+ end
32
+
33
+ def optional?
34
+ type.eql?(:optional)
35
+ end
36
+ end
37
+
38
+ def param(*args)
39
+ raise RabbitDuplicateParamException if params[args[0]]
40
+ p = Param.new(args)
41
+ params[p.name] = p
42
+ end
43
+
44
+ def params
45
+ @params ||= {}
46
+ @params
47
+ end
48
+
49
+ # Add the parameters in hash +new+ to already existing parameters. If
50
+ # +new+ contains a parameter with an already existing name, the old
51
+ # definition is clobbered.
52
+ def add_params(new)
53
+ # We do not check for duplication on purpose: multiple calls
54
+ # to add_params should be cumulative
55
+ new.each { |p| @params[p.name] = p }
56
+ end
57
+
58
+ def each_param(&block)
59
+ params.each_value { |p| yield p }
60
+ end
61
+
62
+ def validate(values)
63
+ each_param do |p|
64
+ if p.required? and not values[p.name]
65
+ raise Failure.new(p, "Required parameter #{p.name} not found")
66
+ end
67
+ if values[p.name] and not p.options.empty? and
68
+ not p.options.include?(values[p.name])
69
+ raise Failure.new(p, "Parameter #{p.name} has value #{values[p.name]} which is not in #{p.options.join(", ")}")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,282 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/rabbit/url_for'
3
+ require 'sinatra/rabbit/respond_to'
4
+ require 'sinatra/rabbit/validation'
5
+
6
+ module Sinatra
7
+
8
+ module Rabbit
9
+
10
+ class RabbitDuplicateParamException < Exception; end
11
+ class RabbitDuplicateOperationException < Exception; end
12
+ class RabbitDuplicateCollectionException < Exception; end
13
+
14
+ def self.registered(app)
15
+ app.helpers Rabbit::Helpers
16
+ end
17
+
18
+ class Operation
19
+ attr_reader :name, :method
20
+
21
+ include Rabbit::Validation
22
+
23
+ STANDARD = {
24
+ :index => { :method => :get, :member => false },
25
+ :show => { :method => :get, :member => true },
26
+ :create => { :method => :post, :member => false },
27
+ :update => { :method => :put, :member => true },
28
+ :destroy => { :method => :delete, :member => true }
29
+ }
30
+
31
+ def initialize(coll, name, opts, &block)
32
+ @name = name.to_sym
33
+ opts = STANDARD[@name].merge(opts) if standard?
34
+ @collection = coll
35
+ raise "No method for operation #{name}" unless opts[:method]
36
+ @method = opts[:method].to_sym
37
+ @member = opts[:member]
38
+ @description = ""
39
+ instance_eval(&block) if block_given?
40
+ generate_documentation
41
+ end
42
+
43
+ def standard?
44
+ STANDARD.keys.include?(name)
45
+ end
46
+
47
+ def description(text="")
48
+ return @description if text.blank?
49
+ @description = text
50
+ end
51
+
52
+ def generate_documentation
53
+ coll, oper = @collection, self
54
+ ::Sinatra::Application.get("/api/docs/#{@collection.name}/#{@name}") do
55
+ @collection, @operation = coll, oper
56
+ respond_to do |format|
57
+ format.html { haml :'docs/operation' }
58
+ format.xml { haml :'docs/operation' }
59
+ end
60
+ end
61
+ end
62
+
63
+ def control(&block)
64
+ op = self
65
+ @control = Proc.new do
66
+ op.validate(params)
67
+ instance_eval(&block)
68
+ end
69
+ end
70
+
71
+ def prefix
72
+ # FIXME: Make the /api prefix configurable
73
+ "/api"
74
+ end
75
+
76
+ def path(args = {})
77
+ l_prefix = args[:prefix] ? args[:prefix] : prefix
78
+ if @member
79
+ if standard?
80
+ "#{l_prefix}/#{@collection.name}/:id"
81
+ else
82
+ "#{l_prefix}/#{@collection.name}/:id/#{name}"
83
+ end
84
+ else
85
+ "#{l_prefix}/#{@collection.name}"
86
+ end
87
+ end
88
+
89
+ def generate
90
+ ::Sinatra::Application.send(@method, path, {}, &@control)
91
+ # Set up some Rails-like URL helpers
92
+ if name == :index
93
+ gen_route "#{@collection.name}_url"
94
+ elsif name == :show
95
+ gen_route "#{@collection.name.to_s.singularize}_url"
96
+ else
97
+ gen_route "#{name}_#{@collection.name.to_s.singularize}_url"
98
+ end
99
+ end
100
+
101
+ private
102
+ def gen_route(name)
103
+ route_url = path
104
+ if @member
105
+ ::Sinatra::Application.send(:define_method, name) do |id, *args|
106
+ url = query_url(route_url, args[0])
107
+ url_for url.gsub(/:id/, id.to_s), :full
108
+ end
109
+ else
110
+ ::Sinatra::Application.send(:define_method, name) do |*args|
111
+ url = query_url(route_url, args[0])
112
+ url_for url, :full
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ class Collection
119
+ attr_reader :name, :operations
120
+
121
+ def initialize(name, &block)
122
+ @name = name
123
+ @description = ""
124
+ @operations = {}
125
+ instance_eval(&block) if block_given?
126
+ generate_documentation
127
+ end
128
+
129
+ # Set/Return description for collection
130
+ # If first parameter is not present, full description will be
131
+ # returned.
132
+ def description(text='')
133
+ return @description if text.blank?
134
+ @description = text
135
+ end
136
+
137
+ def generate_documentation
138
+ coll, oper, features = self, @operations, features(name)
139
+ ::Sinatra::Application.get("/api/docs/#{@name}") do
140
+ @collection, @operations, @features = coll, oper, features
141
+ respond_to do |format|
142
+ format.html { haml :'docs/collection' }
143
+ format.xml { haml :'docs/collection' }
144
+ end
145
+ end
146
+ end
147
+
148
+ # Add a new operation for this collection. For the standard REST
149
+ # operations :index, :show, :update, and :destroy, we already know
150
+ # what method to use and whether this is an operation on the URL for
151
+ # individual elements or for the whole collection.
152
+ #
153
+ # For non-standard operations, options must be passed:
154
+ # :method : one of the HTTP methods
155
+ # :member : whether this is an operation on the collection or an
156
+ # individual element (FIXME: custom operations on the
157
+ # collection will use a nonsensical URL) The URL for the
158
+ # operation is the element URL with the name of the operation
159
+ # appended
160
+ #
161
+ # This also defines a helper method like show_instance_url that returns
162
+ # the URL to this operation (in request context)
163
+ def operation(name, opts = {}, &block)
164
+ raise DuplicateOperationException if @operations[name]
165
+ @operations[name] = Operation.new(self, name, opts, &block)
166
+ end
167
+
168
+ def generate
169
+ operations.values.each { |op| op.generate }
170
+ app = ::Sinatra::Application
171
+ collname = name # Work around Ruby's weird scoping/capture
172
+ app.send(:define_method, "#{name.to_s.singularize}_url") do |id|
173
+ url_for "/api/#{collname}/#{id}", :full
174
+ end
175
+
176
+ if index_op = operations[:index]
177
+ app.send(:define_method, "#{name}_url") do
178
+ url_for index_op.path.gsub(/\/\?$/,''), :full
179
+ end
180
+ end
181
+ end
182
+
183
+ def add_feature_params(features)
184
+ features.each do |f|
185
+ f.operations.each do |fop|
186
+ if cop = operations[fop.name]
187
+ fop.params.each_key do |k|
188
+ if cop.params.has_key?(k)
189
+ raise DuplicateParamException, "Parameter '#{k}' for operation #{fop.name} defined by collection #{@name} and by feature #{f.name}"
190
+ else
191
+ cop.params[k] = fop.params[k]
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def collections
201
+ @collections ||= {}
202
+ end
203
+
204
+ # Create a new collection. NAME should be the pluralized name of the
205
+ # collection.
206
+ #
207
+ # Adds a helper method #{name}_url which returns the URL to the :index
208
+ # operation on this collection.
209
+ def collection(name, &block)
210
+ raise DuplicateCollectionException if collections[name]
211
+ return if collection_excluded?(name.to_sym)
212
+ collections[name] = Collection.new(name, &block)
213
+ collections[name].add_feature_params(features(name))
214
+ collections[name].generate
215
+ end
216
+
217
+ def collection_excluded?(name)
218
+ false
219
+ end
220
+
221
+ def features(collection_name)
222
+ []
223
+ end
224
+
225
+ # Generate a root route for API docs
226
+ get '/api/docs\/?' do
227
+ respond_to do |format|
228
+ format.html { haml :'docs/index' }
229
+ format.xml { haml :'docs/index' }
230
+ end
231
+ end
232
+
233
+ module Helpers
234
+ def query_url(url, params)
235
+ return url if params.nil? || params.empty?
236
+ url + "?#{URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))}"
237
+ end
238
+
239
+ def entry_points
240
+ collections.values.inject([]) do |m, coll|
241
+ url = url_for coll.operations[:index].path, :full
242
+ m << [ coll.name, url ]
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ class String
250
+ # Rails defines this for a number of other classes, including Object
251
+ # see activesupport/lib/active_support/core_ext/object/blank.rb
252
+ def blank?
253
+ self !~ /\S/
254
+ end
255
+
256
+ # Title case.
257
+ #
258
+ # "this is a string".titlecase
259
+ # => "This Is A String"
260
+ #
261
+ # CREDIT: Eliazar Parra
262
+ # Copied from facets
263
+ def titlecase
264
+ gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
265
+ end
266
+
267
+ def pluralize
268
+ self + "s"
269
+ end
270
+
271
+ def singularize
272
+ self.gsub(/s$/, '')
273
+ end
274
+
275
+ def underscore
276
+ gsub(/::/, '/').
277
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
278
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
279
+ tr("-", "_").
280
+ downcase
281
+ end
282
+ end
@@ -0,0 +1,49 @@
1
+ #
2
+ # Copyright (C) 2010 Red Hat, Inc.
3
+ #
4
+ # Licensed to the Apache Software Foundation (ASF) under one or more
5
+ # contributor license agreements. See the NOTICE file distributed with
6
+ # this work for additional information regarding copyright ownership. The
7
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance with the
9
+ # License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ # License for the specific language governing permissions and limitations
17
+ # under the License.
18
+
19
+ require 'rake'
20
+
21
+ Gem::Specification.new do |s|
22
+ s.author = 'Red Hat, Inc.'
23
+ s.homepage = "http://incubator.apache.org/deltacloud"
24
+ s.email = 'deltacloud-dev@incubator.apache.org'
25
+ s.name = 'sinatra-rabbit'
26
+
27
+ s.description = <<-EOF
28
+ Rabbit is a Sinatra extension which can help you writing
29
+ a simple REST API using easy to undestand DSL.
30
+ EOF
31
+
32
+ s.version = '0.0.1'
33
+ s.date = Time.now
34
+ s.summary = %q{Sinatra REST API DSL}
35
+ s.files = FileList[
36
+ 'lib/sinatra/*.rb',
37
+ 'lib/sinatra/rabbit/*.rb',
38
+ 'sinatra-rabbit.gemspec',
39
+ ].to_a
40
+
41
+ s.extra_rdoc_files = Dir["COPYING"]
42
+ s.required_ruby_version = '>= 1.8.1'
43
+
44
+ s.add_runtime_dependency 'sinatra', '>= 0.9.4'
45
+ s.add_runtime_dependency 'rack-accept', '>= 0.4.3'
46
+
47
+ s.add_dependency('haml', '>= 2.2.17')
48
+
49
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-rabbit
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Red Hat, Inc.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-01 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sinatra
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 51
30
+ segments:
31
+ - 0
32
+ - 9
33
+ - 4
34
+ version: 0.9.4
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rack-accept
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 9
46
+ segments:
47
+ - 0
48
+ - 4
49
+ - 3
50
+ version: 0.4.3
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: haml
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 37
62
+ segments:
63
+ - 2
64
+ - 2
65
+ - 17
66
+ version: 2.2.17
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ description: " Rabbit is a Sinatra extension which can help you writing\n a simple REST API using easy to undestand DSL.\n"
70
+ email: deltacloud-dev@incubator.apache.org
71
+ executables: []
72
+
73
+ extensions: []
74
+
75
+ extra_rdoc_files:
76
+ - COPYING
77
+ files:
78
+ - lib/sinatra/rabbit.rb
79
+ - lib/sinatra/rabbit/respond_to.rb
80
+ - lib/sinatra/rabbit/url_for.rb
81
+ - lib/sinatra/rabbit/validation.rb
82
+ - sinatra-rabbit.gemspec
83
+ - COPYING
84
+ has_rdoc: true
85
+ homepage: http://incubator.apache.org/deltacloud
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options: []
90
+
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 53
99
+ segments:
100
+ - 1
101
+ - 8
102
+ - 1
103
+ version: 1.8.1
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project:
116
+ rubygems_version: 1.3.7
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Sinatra REST API DSL
120
+ test_files: []
121
+