grape 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
1
  --color
2
- --format=nested
3
- --fail-fast
2
+ --format=nested
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
1
3
  gem 'rack'
2
4
  gem 'rack-mount', '~> 0.6.13'
3
5
  gem 'rack-jsonp'
@@ -9,6 +11,7 @@ gem 'multi_xml'
9
11
  group :development do
10
12
  gem 'rake'
11
13
  gem 'jeweler'
14
+ # gem 'yard'
12
15
  end
13
16
 
14
17
  group :test do
@@ -1,4 +1,5 @@
1
1
  GEM
2
+ remote: http://rubygems.org/
2
3
  specs:
3
4
  builder (2.1.2)
4
5
  cucumber (0.8.5)
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Michael Bleigh
1
+ Copyright (c) 2010 Michael Bleigh and Intridea, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -0,0 +1,75 @@
1
+ # Grape
2
+
3
+ Grape is a REST-like API micro-framework for Ruby. It is built to complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily provide APIs. It has built-in support for common conventions such as multiple formats, subdomain/prefix restriction, and versioning.
4
+
5
+ ## Installation
6
+
7
+ Grape is available as a gem, to install it just install the gem:
8
+
9
+ gem install grape
10
+
11
+ ## Basic Usage
12
+
13
+ Grape APIs are Rack applications that are created by subclassing `Grape::API`. Below is a simple example showing some of the more common features of Grape in the context of recreating parts of the Twitter API.
14
+
15
+ class Twitter::API < Grape::Base
16
+ version '1'
17
+
18
+ helpers do
19
+ def current_user
20
+ @current_user ||= User.authorize!(env)
21
+ end
22
+
23
+ def authenticate!
24
+ error!('401 Unauthorized', 401) unless current_user
25
+ end
26
+ end
27
+
28
+ resource :statuses do
29
+ get :public_timeline do
30
+ Tweet.limit(20)
31
+ end
32
+
33
+ get :home_timeline do
34
+ authenticate!
35
+ current_user.home_timeline
36
+ end
37
+
38
+ get '/show/:id' do
39
+ Tweet.find(params[:id])
40
+ end
41
+
42
+ post :update do
43
+ authenticate!
44
+ Tweet.create(
45
+ :user => current_user,
46
+ :text => params[:status]
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ This would create a Rack application that could be used like so (in a Rackup file):
53
+
54
+ use Twitter::API
55
+
56
+ And would respond to the following routes:
57
+
58
+ GET /1/statuses/public_timeline(.json)
59
+ GET /1/statuses/home_timeline(.json)
60
+ GET /1/statuses/show/:id(.json)
61
+ POST /1/statuses/update(.json)
62
+
63
+ Serialization takes place automatically. For more detailed usage information, please visit the [Grape Wiki](http://github.com/intridea/grape/wiki).
64
+
65
+ ## Note on Patches/Pull Requests
66
+
67
+ * Fork the project.
68
+ * Make your feature addition or bug fix.
69
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
70
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
71
+ * Send me a pull request. Bonus points for topic branches.
72
+
73
+ ## Copyright
74
+
75
+ Copyright (c) 2010 Michael Bleigh and Intridea, Inc. See LICENSE for details.
data/Rakefile CHANGED
@@ -3,6 +3,10 @@ require 'bundler'
3
3
 
4
4
  Bundler.setup :default, :test, :development
5
5
 
6
+ def version
7
+ @version ||= open('VERSION').read.trim
8
+ end
9
+
6
10
  begin
7
11
  require 'jeweler'
8
12
  Jeweler::Tasks.new do |gem|
@@ -33,12 +37,34 @@ end
33
37
  task :spec => :check_dependencies
34
38
  task :default => :spec
35
39
 
36
- require 'rake/rdoctask'
37
- Rake::RDocTask.new do |rdoc|
38
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
39
-
40
- rdoc.rdoc_dir = 'rdoc'
41
- rdoc.title = "grape #{version}"
42
- rdoc.rdoc_files.include('README*')
43
- rdoc.rdoc_files.include('lib/**/*.rb')
44
- end
40
+ begin
41
+ require 'yard'
42
+ YARD_OPTS = ['-m', 'markdown', '-M', 'maruku']
43
+ DOC_FILES = ['lib/**/*.rb', 'README.markdown']
44
+
45
+ YARD::Rake::YardocTask.new(:doc) do |t|
46
+ t.files = DOC_FILES
47
+ t.options = YARD_OPTS
48
+ end
49
+
50
+ namespace :doc do
51
+ YARD::Rake::YardocTask.new(:pages) do |t|
52
+ t.files = DOC_FILES
53
+ t.options = YARD_OPTS + ['-o', '../grape.doc']
54
+ end
55
+
56
+ namespace :pages do
57
+ desc 'Generate and publish YARD docs to GitHub pages.'
58
+ task :publish => ['doc:pages'] do
59
+ Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
60
+ system("git add .")
61
+ system("git add -u")
62
+ system("git commit -m 'Generating docs for version #{version}.'")
63
+ system("git push origin gh-pages")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ rescue LoadError
69
+ puts "You need to install YARD."
70
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -5,16 +5,16 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{grape}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Michael Bleigh"]
12
- s.date = %q{2010-11-13}
12
+ s.date = %q{2010-11-14}
13
13
  s.description = %q{A Ruby framework for rapid API development with great conventions.}
14
14
  s.email = %q{michael@intridea.com}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE",
17
- "README.rdoc"
17
+ "README.markdown"
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  "Gemfile",
25
25
  "Gemfile.lock",
26
26
  "LICENSE",
27
- "README.rdoc",
27
+ "README.markdown",
28
28
  "Rakefile",
29
29
  "VERSION",
30
30
  "autotest/discover.rb",
@@ -1,13 +1,19 @@
1
1
  require 'rack/mount'
2
2
  require 'rack/auth/basic'
3
+ require 'logger'
3
4
 
4
5
  module Grape
6
+ # The API class is the primary entry point for
7
+ # creating Grape APIs. Users should subclass this
8
+ # class in order to build an API.
5
9
  class API
6
- module Helpers; end
7
-
8
10
  class << self
9
11
  attr_reader :route_set
10
12
 
13
+ def logger
14
+ @logger ||= Logger.new($STDOUT)
15
+ end
16
+
11
17
  def reset!
12
18
  @settings = [{}]
13
19
  @route_set = Rack::Mount::RouteSet.new
@@ -15,7 +21,7 @@ module Grape
15
21
  end
16
22
 
17
23
  def call(env)
18
- puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
24
+ logger.info "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
19
25
  route_set.freeze.call(env)
20
26
  end
21
27
 
@@ -30,6 +36,11 @@ module Grape
30
36
  @settings
31
37
  end
32
38
 
39
+ # Set a configuration value for this
40
+ # namespace.
41
+ #
42
+ # @param key [Symbol] The key of the configuration variable.
43
+ # @param value [Object] The value to which to set the configuration variable.
33
44
  def set(key, value)
34
45
  @settings.last[key.to_sym] = value
35
46
  end
@@ -48,6 +59,9 @@ module Grape
48
59
  new_versions.any? ? nest(block){ set(:version, new_versions) } : settings[:version]
49
60
  end
50
61
 
62
+ # Specify the default format for the API's
63
+ # serializers. Currently only `:json` is
64
+ # supported.
51
65
  def default_format(new_format = nil)
52
66
  new_format ? set(:default_format, new_format.to_sym) : settings[:default_format]
53
67
  end
@@ -76,6 +90,8 @@ module Grape
76
90
  end
77
91
  end
78
92
 
93
+ # Add an authentication type to the API. Currently
94
+ # only `:http_basic` is supported.
79
95
  def auth(type = nil, options = {}, &block)
80
96
  if type
81
97
  set(:auth, {:type => type.to_sym, :proc => block}.merge(options))
@@ -93,47 +109,34 @@ module Grape
93
109
  auth :http_basic, options, &block
94
110
  end
95
111
 
96
- def route_set
97
- @route_set ||= Rack::Mount::RouteSet.new
98
- end
99
-
100
- def compile_path(path)
101
- parts = []
102
- parts << prefix if prefix
103
- parts << ':version' if version
104
- parts << namespace if namespace
105
- parts << path
106
- Rack::Mount::Utils.normalize_path(parts.join('/'))
107
- end
108
-
109
- def route(method, path_info, &block)
110
- route_set.add_route(build_endpoint(&block),
111
- :path_info => Rack::Mount::Strexp.compile(compile_path(path_info)),
112
- :request_method => method
113
- )
114
- end
115
-
116
- def build_endpoint(&block)
117
-
118
- b = Rack::Builder.new
119
- b.use Grape::Middleware::Error
120
- b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
121
- b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
122
- b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
123
- b.use Grape::Middleware::Formatter, :default_format => default_format || :json
124
-
125
- endpoint = Grape::Endpoint.new(&block)
126
- endpoint.send :extend, helpers
127
- b.run endpoint
112
+ # Defines a route that will be recognized
113
+ # by the Grape API.
114
+ #
115
+ # @param methods [HTTP Verb(s)] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
116
+ # @param paths [String(s)] One or more strings representing the URL segment(s) for this route.
117
+ # @param block [Proc] The code to be executed
118
+ def route(methods, paths, &block)
119
+ methods = Array(methods)
120
+ paths = ['/'] if paths == []
121
+ paths = Array(paths)
122
+ endpoint = build_endpoint(&block)
128
123
 
129
- b.to_app
124
+ methods.each do |method|
125
+ paths.each do |path|
126
+ path = Rack::Mount::Strexp.compile(compile_path(path))
127
+ route_set.add_route(endpoint,
128
+ :path_info => path,
129
+ :request_method => (method.to_s.upcase unless method == :any)
130
+ )
131
+ end
132
+ end
130
133
  end
131
134
 
132
- def get(path_info = '', &block); route('GET', path_info, &block) end
133
- def post(path_info = '', &block); route('POST', path_info, &block) end
134
- def put(path_info = '', &block); route('PUT', path_info, &block) end
135
- def head(path_info = '', &block); route('HEAD', path_info, &block) end
136
- def delete(path_info = '', &block); route('DELETE', path_info, &block) end
135
+ def get(*paths, &block); route('GET', paths, &block) end
136
+ def post(*paths, &block); route('POST', paths, &block) end
137
+ def put(*paths, &block); route('PUT', paths, &block) end
138
+ def head(*paths, &block); route('HEAD', paths, &block) end
139
+ def delete(*paths, &block); route('DELETE', paths, &block) end
137
140
 
138
141
  def namespace(space = nil, &block)
139
142
  if space || block_given?
@@ -145,6 +148,19 @@ module Grape
145
148
  end
146
149
  end
147
150
 
151
+ alias_method :group, :namespace
152
+ alias_method :resource, :namespace
153
+ alias_method :resources, :namespace
154
+
155
+ # Create a scope without affecting the URL.
156
+ #
157
+ # @param name [Symbol] Purely placebo, just allows to to name the scope to make the code more readable.
158
+ def scope(name = nil, &block)
159
+ nest(block)
160
+ end
161
+
162
+ protected
163
+
148
164
  # Execute first the provided block, then each of the
149
165
  # block passed in. Allows for simple 'before' setups
150
166
  # of settings stack pushes.
@@ -152,7 +168,7 @@ module Grape
152
168
  blocks.reject!{|b| b.nil?}
153
169
  if blocks.any?
154
170
  settings_stack << {}
155
- instance_eval &block
171
+ instance_eval &block if block_given?
156
172
  blocks.each{|b| instance_eval &b}
157
173
  settings_stack.pop
158
174
  else
@@ -160,13 +176,37 @@ module Grape
160
176
  end
161
177
  end
162
178
 
163
- alias_method :group, :namespace
164
- alias_method :resource, :namespace
165
- alias_method :resources, :namespace
179
+ def build_endpoint(&block)
180
+ b = Rack::Builder.new
181
+ b.use Grape::Middleware::Error
182
+ b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
183
+ b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
184
+ b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
185
+ b.use Grape::Middleware::Formatter, :default_format => default_format || :json
186
+
187
+ endpoint = Grape::Endpoint.generate(&block)
188
+ endpoint.send :include, helpers
189
+ b.run endpoint
190
+
191
+ b.to_app
192
+ end
166
193
 
167
194
  def inherited(subclass)
168
195
  subclass.reset!
169
196
  end
197
+
198
+ def route_set
199
+ @route_set ||= Rack::Mount::RouteSet.new
200
+ end
201
+
202
+ def compile_path(path)
203
+ parts = []
204
+ parts << prefix if prefix
205
+ parts << ':version' if version
206
+ parts << namespace if namespace
207
+ parts << path
208
+ Rack::Mount::Utils.normalize_path(parts.join('/'))
209
+ end
170
210
  end
171
211
 
172
212
  reset!
@@ -7,12 +7,26 @@ module Grape
7
7
  # on the instance level of this class may be called
8
8
  # from inside a `get`, `post`, etc. block.
9
9
  class Endpoint
10
- def initialize(&block)
11
- @block = block
10
+ def self.generate(&block)
11
+ c = Class.new(Grape::Endpoint)
12
+ c.class_eval do
13
+ @block = block
14
+ end
15
+ c
16
+ end
17
+
18
+ class << self
19
+ attr_accessor :block
20
+ end
21
+
22
+ def self.call(env)
23
+ new.call(env)
12
24
  end
13
25
 
14
26
  attr_reader :env, :request
15
27
 
28
+ # The parameters passed into the request as
29
+ # well as parsed from URL segments.
16
30
  def params
17
31
  @params ||= request.params.merge(env['rack.routing_args'] || {}).inject({}) do |h,(k,v)|
18
32
  h[k.to_s] = v
@@ -21,13 +35,21 @@ module Grape
21
35
  end
22
36
  end
23
37
 
38
+ # The API version as specified in the URL.
24
39
  def version; env['api.version'] end
25
40
 
41
+ # End the request and display an error to the
42
+ # end user with the specified message.
43
+ #
44
+ # @param message [String] The message to display.
45
+ # @param status [Integer] the HTTP Status Code. Defaults to 403.
26
46
  def error!(message, status=403)
27
47
  throw :error, :message => message, :status => status
28
48
  end
29
49
 
30
50
  # Set or retrieve the HTTP status code.
51
+ #
52
+ # @param status [Integer] The HTTP Status Code to return for this request.
31
53
  def status(status = nil)
32
54
  if status
33
55
  @status = status
@@ -54,10 +76,10 @@ module Grape
54
76
 
55
77
  def call(env)
56
78
  @env = env
57
- @request = Rack::Request.new(@env)
58
79
  @header = {}
80
+ @request = Rack::Request.new(@env)
59
81
 
60
- response_text = instance_eval &@block
82
+ response_text = instance_eval &self.class.block
61
83
 
62
84
  [status, header, [response_text]]
63
85
  end
@@ -99,7 +99,7 @@ module Grape
99
99
  end
100
100
 
101
101
  def encode_txt(object)
102
- body.respond_to?(:to_txt) ? body.to_txt : body.to_s
102
+ object.respond_to?(:to_txt) ? object.to_txt : object.to_s
103
103
  end
104
104
  end
105
105
  end
@@ -119,7 +119,7 @@ describe Grape::API do
119
119
  version 'v2'
120
120
  compile_path('hello').should == '/:version/hello'
121
121
  end
122
- subject.compile_path('hello').should == '/hello'
122
+ subject.send(:compile_path, 'hello').should == '/hello'
123
123
  end
124
124
 
125
125
  %w(group resource resources).each do |als|
@@ -149,6 +149,39 @@ describe Grape::API do
149
149
  last_response.body.should == 'Created a Vote'
150
150
  end
151
151
 
152
+ it 'should allow for multiple paths' do
153
+ subject.get("/abc", "/def") do
154
+ "foo"
155
+ end
156
+
157
+ get '/abc'
158
+ last_response.body.should == 'foo'
159
+ get '/def'
160
+ last_response.body.should == 'foo'
161
+ end
162
+
163
+ it 'should allow for multiple verbs' do
164
+ subject.route([:get, :post], '/abc') do
165
+ "hiya"
166
+ end
167
+
168
+ get '/abc'
169
+ last_response.body.should == 'hiya'
170
+ post '/abc'
171
+ last_response.body.should == 'hiya'
172
+ end
173
+
174
+ it 'should allow for :any as a verb' do
175
+ subject.route(:any, '/abc') do
176
+ "lol"
177
+ end
178
+
179
+ %w(get post put delete).each do |m|
180
+ send(m, '/abc')
181
+ last_response.body.should == 'lol'
182
+ end
183
+ end
184
+
152
185
  verbs = %w(post get head delete put)
153
186
  verbs.each do |verb|
154
187
  it "should allow and properly constrain a #{verb.upcase} method" do
@@ -280,4 +313,31 @@ describe Grape::API do
280
313
  lambda{get '/howdy'}.should_not raise_error
281
314
  end
282
315
  end
316
+
317
+ describe '.scope' do
318
+ it 'should scope the various settings' do
319
+ subject.version 'v2'
320
+
321
+ subject.scope :legacy do
322
+ version 'v1'
323
+
324
+ get '/abc' do
325
+ version
326
+ end
327
+ end
328
+
329
+ subject.get '/def' do
330
+ version
331
+ end
332
+
333
+ get '/v2/abc'
334
+ last_response.status.should == 404
335
+ get '/v1/abc'
336
+ last_response.status.should == 200
337
+ get '/v1/def'
338
+ last_response.status.should == 404
339
+ get '/v2/def'
340
+ last_response.status.should == 200
341
+ end
342
+ end
283
343
  end
@@ -72,4 +72,33 @@ describe Grape::Endpoint do
72
72
  last_response.body.should == "Unauthorized."
73
73
  end
74
74
  end
75
+
76
+ it 'should not persist params between calls' do
77
+ subject.post('/new') do
78
+ params[:text]
79
+ end
80
+
81
+ post '/new', :text => 'abc'
82
+ last_response.body.should == 'abc'
83
+
84
+ post '/new', :text => 'def'
85
+ last_response.body.should == 'def'
86
+ end
87
+
88
+ it 'should reset all instance variables (except block) between calls' do
89
+ subject.helpers do
90
+ def memoized
91
+ @memoized ||= params[:howdy]
92
+ end
93
+ end
94
+
95
+ subject.get('/hello') do
96
+ memoized
97
+ end
98
+
99
+ get '/hello?howdy=hey'
100
+ last_response.body.should == 'hey'
101
+ get '/hello?howdy=yo'
102
+ last_response.body.should == 'yo'
103
+ end
75
104
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Michael Bleigh
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-13 00:00:00 -06:00
18
+ date: 2010-11-14 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -140,7 +140,7 @@ extensions: []
140
140
 
141
141
  extra_rdoc_files:
142
142
  - LICENSE
143
- - README.rdoc
143
+ - README.markdown
144
144
  files:
145
145
  - .document
146
146
  - .gitignore
@@ -149,7 +149,7 @@ files:
149
149
  - Gemfile
150
150
  - Gemfile.lock
151
151
  - LICENSE
152
- - README.rdoc
152
+ - README.markdown
153
153
  - Rakefile
154
154
  - VERSION
155
155
  - autotest/discover.rb
@@ -1,52 +0,0 @@
1
- = UNDER CONSTRUCTION. DO NOT USE
2
-
3
- = Grape
4
-
5
- Grape is a REST-like API micro-framework for Ruby. It is built to complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily provide APIs. It has built-in support for common conventions such as multiple formats, subdomain/prefix restriction, and versioning.
6
-
7
- class Twitter::API < Grape::Base
8
- subdomain 'api'
9
- version '1'
10
- formats :xml, :json
11
- authorization :oauth, User
12
-
13
- resource :statuses do
14
- group :timelines do
15
- formats :rss, :atom
16
-
17
- get :public_timeline do
18
- optional :trim_user, Boolean
19
- optional :include_entities, Boolean
20
-
21
- Tweet.limit(20)
22
- end
23
-
24
- get :home_timeline do
25
- authorized
26
-
27
- user.home_timeline
28
- end
29
- end
30
- end
31
- end
32
-
33
- # Rack endpoint
34
- Twitter::API.statuses.timelines.get(:public_timeline)
35
-
36
- class Twitter::API::User < Grape::Resource::ActiveRecord
37
- represents ::User
38
-
39
- property :status, lambda{|u| u.latest_status}, Twitter::API::Status
40
- end
41
-
42
- == Note on Patches/Pull Requests
43
-
44
- * Fork the project.
45
- * Make your feature addition or bug fix.
46
- * Add tests for it. This is important so I don't break it in a future version unintentionally.
47
- * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
48
- * Send me a pull request. Bonus points for topic branches.
49
-
50
- == Copyright
51
-
52
- Copyright (c) 2010 Michael Bleigh and Intridea, Inc. See LICENSE for details.