grape 0.3.2 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ 0.4.0 (3/17/2013)
2
+ =================
3
+
4
+ * [#356](https://github.com/intridea/grape/pull/356): Fix: presenting collections other than `Array` (eg. `ActiveRecord::Relation`) - [@zimbatm](https://github.com/zimbatm).
5
+ * [#352](https://github.com/intridea/grape/pull/352): Fix: using `Rack::JSONP` with `Grape::Entity` responses - [@deckchair](https://github.com/deckchair).
6
+ * [#347](https://github.com/intridea/grape/issues/347): Grape will accept any valid JSON as PUT or POST, including strings, symbols and arrays - [@qqshfox](https://github.com/qqshfox), [@dblock](https://github.com/dblock).
7
+ * [#347](https://github.com/intridea/grape/issues/347): JSON format APIs always return valid JSON, eg. strings are now returned as `"string"` and no longer `string` - [@dblock](https://github.com/dblock).
8
+ * Raw body input from POST and PUT requests (`env['rack.input'].read`) is now available in `api.request.input` - [@dblock](https://github.com/dblock).
9
+ * Parsed body input from POST and PUT requests is now available in `api.request.body` - [@dblock](https://github.com/dblock).
10
+ * [#343](https://github.com/intridea/grape/pull/343): Fix: return `Content-Type: text/plain` with error 405 - [@gustavosaume](https://github.com/gustavosaume), [@wyattisimo](https://github.com/wyattisimo).
11
+ * [#357](https://github.com/intridea/grape/pull/357): Grape now requires Rack 1.3.0 or newer - [@jhecking](https://github.com/jhecking).
12
+ * [#320](https://github.com/intridea/grape/issues/320): API `namespace` now supports `requirements` - [@niedhui](https://github.com/niedhui).
13
+ * [#353](https://github.com/intridea/grape/issues/353): Revert to standard Ruby logger formatter, `require active_support/all` if you want old behavior - [@rhunter](https://github.com/rhunter), [@dblock](https://github.com/dblock).
14
+ * Fix: `undefined method `call' for nil:NilClass` for an API method implementation without a block, now returns an empty string - [@dblock](https://github.com/dblock).
15
+
1
16
  0.3.2 (2/28/2013)
2
17
  =================
3
18
 
data/Gemfile CHANGED
@@ -14,4 +14,5 @@ group :development, :test do
14
14
  gem 'rack-test', "~> 0.6.2", :require => "rack/test"
15
15
  gem 'github-markup'
16
16
  gem 'cookiejar'
17
+ gem 'rack-contrib'
17
18
  end
data/README.md CHANGED
@@ -8,11 +8,11 @@ providing a simple DSL to easily develop RESTful APIs. It has built-in support
8
8
  for common conventions, including multiple formats, subdomain/prefix restriction,
9
9
  content negotiation, versioning and much more.
10
10
 
11
- [![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape)
11
+ [![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape) [![Code Climate](https://codeclimate.com/github/intridea/grape.png)](https://codeclimate.com/github/intridea/grape)
12
12
 
13
13
  ## Stable Release
14
14
 
15
- You're reading the documentation for Grape, release 0.3.2.
15
+ You're reading the documentation for the stable release 0.4.0.
16
16
 
17
17
  ## Project Tracking
18
18
 
@@ -363,12 +363,12 @@ end
363
363
 
364
364
  ### Validation Errors
365
365
 
366
- When validation and coercion errors occur an exception of type `Grape::Exceptions::ValidationError` is raised.
366
+ When validation and coercion errors occur an exception of type `Grape::Exceptions::Validation` is raised.
367
367
  If the exception goes uncaught it will respond with a status of 400 and an error message.
368
- You can rescue a `Grape::Exceptions::ValidationError` and respond with a custom response.
368
+ You can rescue a `Grape::Exceptions::Validation` and respond with a custom response.
369
369
 
370
370
  ```ruby
371
- rescue_from Grape::Exceptions::ValidationError do |e|
371
+ rescue_from Grape::Exceptions::Validation do |e|
372
372
  Rack::Response.new({
373
373
  'status' => e.status,
374
374
  'message' => e.message,
@@ -402,12 +402,20 @@ header "X-Robots-Tag", "noindex"
402
402
  ## Routes
403
403
 
404
404
  Optionally, you can define requirements for your named route parameters using regular
405
- expressions. The route will match only if all requirements are met.
405
+ expressions on namespace or endpoint. The route will match only if all requirements are met.
406
406
 
407
407
  ```ruby
408
408
  get ':id', :requirements => { :id => /[0-9]*/ } do
409
409
  Status.find(params[:id])
410
410
  end
411
+
412
+ namespace :outer, :requirements => { :id => /[0-9]*/ } do
413
+ get :id do
414
+ end
415
+
416
+ get ":id/edit" do
417
+ end
418
+ end
411
419
  ```
412
420
 
413
421
  ## Helpers
@@ -721,9 +729,8 @@ By default, Grape supports _XML_, _JSON_, and _TXT_ content-types. The default f
721
729
 
722
730
  Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API implementation.
723
731
 
724
- Your API can declare which types to support by using `content_type`. Response format
725
- is determined by the request's extension, an explicit `format` parameter in the query
726
- string, or `Accept` header.
732
+ Your API can declare which types to support by using `content_type`. Response format is determined by the
733
+ request's extension, an explicit `format` parameter in the query string, or `Accept` header.
727
734
 
728
735
  The following API will only respond to the JSON content-type and will not parse any other input than `application/json`,
729
736
  `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests
@@ -795,6 +802,45 @@ The order for choosing the format is the following.
795
802
  * Use the default format, if specified by the `default_format` option.
796
803
  * Default to `:txt`.
797
804
 
805
+ ### JSONP
806
+
807
+ Grape suports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the
808
+ [rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`.
809
+
810
+ ```ruby
811
+ require 'rack/contrib'
812
+
813
+ class API < Grape::API
814
+ use Rack::JSONP
815
+ format :json
816
+ get '/' do
817
+ 'Hello World'
818
+ end
819
+ end
820
+ ```
821
+
822
+ ### CORS
823
+
824
+ Grape supports CORS via [Rack::CORS](https://github.com/cyu/rack-cors), part of the
825
+ [rack-cors](https://github.com/cyu/rack-cors) gem. Add `rack-cors` to your `Gemfile`.
826
+
827
+ ```ruby
828
+ require 'rack/cors'
829
+
830
+ class API < Grape::API
831
+ use Rack::Cors do
832
+ allow do
833
+ origins '*'
834
+ resource '*', :headers => :any, :methods => :get
835
+ end
836
+ end
837
+ format :json
838
+ get '/' do
839
+ 'Hello World'
840
+ end
841
+ end
842
+ ```
843
+
798
844
  ## Content-type
799
845
 
800
846
  Content-type is set by the formatter. You can override the content-type of the response at runtime
@@ -816,7 +862,8 @@ section above. It also supports custom data formats. You must declare additional
816
862
  `content_type` and optionally supply a parser via `parser` unless a parser is already available within
817
863
  Grape to enable a custom format. Such a parser can be a function or a class.
818
864
 
819
- Without a parser, data is available "as-is" and can be read with `env['rack.input'].read`.
865
+ With a parser, parsed data is available "as-is" in `env['api.request.body']`.
866
+ Without a parser, data is available "as-is" and in `env['api.request.input']`.
820
867
 
821
868
  The following example is a trivial parser that will assign any input with the "text/custom" content-type
822
869
  to `:value`. The parameter will be available via `params[:value]` inside the API call.
@@ -1153,4 +1200,4 @@ MIT License. See LICENSE for details.
1153
1200
 
1154
1201
  ## Copyright
1155
1202
 
1156
- Copyright (c) 2010-2012 Michael Bleigh, and Intridea, Inc.
1203
+ Copyright (c) 2010-2013 Michael Bleigh, and Intridea, Inc.
data/grape.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "grape"
16
16
 
17
- s.add_runtime_dependency 'rack'
17
+ s.add_runtime_dependency 'rack', '>= 1.3.0'
18
18
  s.add_runtime_dependency 'rack-mount'
19
19
  s.add_runtime_dependency 'rack-accept'
20
20
  s.add_runtime_dependency 'activesupport'
data/lib/grape.rb CHANGED
@@ -6,7 +6,8 @@ require 'rack/accept'
6
6
  require 'rack/auth/basic'
7
7
  require 'rack/auth/digest/md5'
8
8
  require 'hashie'
9
- require 'active_support/all'
9
+ require 'active_support/core_ext/hash/indifferent_access'
10
+ require 'active_support/ordered_hash'
10
11
  require 'grape/util/deep_merge'
11
12
  require 'grape/util/content_types'
12
13
  require 'multi_json'
@@ -20,6 +21,7 @@ module Grape
20
21
  autoload :API, 'grape/api'
21
22
  autoload :Endpoint, 'grape/endpoint'
22
23
  autoload :Route, 'grape/route'
24
+ autoload :Namespace, 'grape/namespace'
23
25
  autoload :Cookies, 'grape/cookies'
24
26
  autoload :Validations, 'grape/validations'
25
27
 
data/lib/grape/api.rb CHANGED
@@ -356,17 +356,17 @@ module Grape
356
356
  def options(paths = ['/'], options = {}, &block); route('OPTIONS', paths, options, &block) end
357
357
  def patch(paths = ['/'], options = {}, &block); route('PATCH', paths, options, &block) end
358
358
 
359
- def namespace(space = nil, &block)
359
+ def namespace(space = nil, options = {}, &block)
360
360
  if space || block_given?
361
361
  previous_namespace_description = @namespace_description
362
362
  @namespace_description = (@namespace_description || {}).deep_merge(@last_description || {})
363
363
  @last_description = nil
364
364
  nest(block) do
365
- set(:namespace, space.to_s) if space
365
+ set(:namespace, Namespace.new(space, options)) if space
366
366
  end
367
367
  @namespace_description = previous_namespace_description
368
368
  else
369
- Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
369
+ Namespace.joined_space_path(settings)
370
370
  end
371
371
  end
372
372
 
@@ -507,7 +507,7 @@ module Grape
507
507
  not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - methods
508
508
  not_allowed_methods << "OPTIONS" if self.class.settings[:do_not_route_options]
509
509
  not_allowed_methods.each do |bad_method|
510
- @route_set.add_route( proc { [405, { 'Allow' => allow_header }, []]}, {
510
+ @route_set.add_route( proc { [405, { 'Allow' => allow_header, 'Content-Type' => 'text/plain' }, []]}, {
511
511
  :path_info => path_info,
512
512
  :request_method => bad_method
513
513
  })
@@ -35,9 +35,9 @@ module Grape
35
35
  def initialize(settings, options = {}, &block)
36
36
  @settings = settings
37
37
  if block_given?
38
- method_name = [
38
+ method_name = [
39
39
  options[:method],
40
- settings.gather(:namespace).join("/"),
40
+ Namespace.joined_space(settings),
41
41
  settings.gather(:mount_path).join("/"),
42
42
  Array(options[:path]).join("/")
43
43
  ].join(" ")
@@ -88,7 +88,11 @@ module Grape
88
88
  anchor = options[:route_options][:anchor]
89
89
  anchor = anchor.nil? ? true : anchor
90
90
 
91
- requirements = options[:route_options][:requirements] || {}
91
+ endpoint_requirements = options[:route_options][:requirements] || {}
92
+ all_requirements = (settings.gather(:namespace).map(&:requirements) << endpoint_requirements)
93
+ requirements = all_requirements.reduce({}) do |base_requirements, single_requirements|
94
+ base_requirements.merge!(single_requirements)
95
+ end
92
96
 
93
97
  path = compile_path(prepared_path, anchor && !options[:app], requirements)
94
98
  regex = Rack::Mount::RegexpWithNamedGroups.new(path)
@@ -137,7 +141,7 @@ module Grape
137
141
  end
138
142
 
139
143
  def namespace
140
- Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
144
+ @namespace ||= Namespace.joined_space_path(settings)
141
145
  end
142
146
 
143
147
  def compile_path(prepared_path, anchor = true, requirements = {})
@@ -316,7 +320,7 @@ module Grape
316
320
  entity_class = options.delete(:with)
317
321
 
318
322
  # auto-detect the entity from the first object in the collection
319
- object_instance = object.is_a?(Array) ? object.first : object
323
+ object_instance = object.respond_to?(:first) ? object.first : object
320
324
 
321
325
  object_instance.class.ancestors.each do |potential|
322
326
  entity_class ||= (settings[:representations] || {})[potential]
@@ -379,7 +383,7 @@ module Grape
379
383
 
380
384
  run_filters after_validations
381
385
 
382
- response_text = @block.call(self)
386
+ response_text = @block ? @block.call(self) : nil
383
387
  run_filters afters
384
388
  cookies.write(header)
385
389
 
@@ -399,6 +403,16 @@ module Grape
399
403
  :rescue_options => settings[:rescue_options],
400
404
  :rescue_handlers => merged_setting(:rescue_handlers)
401
405
 
406
+ aggregate_setting(:middleware).each do |m|
407
+ m = m.dup
408
+ block = m.pop if m.last.is_a?(Proc)
409
+ if block
410
+ b.use *m, &block
411
+ else
412
+ b.use *m
413
+ end
414
+ end
415
+
402
416
  b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
403
417
  b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
404
418
 
@@ -417,16 +431,6 @@ module Grape
417
431
  :formatters => settings[:formatters],
418
432
  :parsers => settings[:parsers]
419
433
 
420
- aggregate_setting(:middleware).each do |m|
421
- m = m.dup
422
- block = m.pop if m.last.is_a?(Proc)
423
- if block
424
- b.use *m, &block
425
- else
426
- b.use *m
427
- end
428
- end
429
-
430
434
  b
431
435
  end
432
436
 
@@ -4,7 +4,6 @@ module Grape
4
4
  class << self
5
5
 
6
6
  def call(object, env)
7
- return object if object.is_a?(String)
8
7
  return object.to_json if object.respond_to?(:to_json)
9
8
  MultiJson.dump(object)
10
9
  end
@@ -7,7 +7,7 @@ en:
7
7
  regexp: 'invalid parameter: %{attribute}'
8
8
  missing_vendor_option:
9
9
  problem: 'missing :vendor option.'
10
- summary: 'when version using header, you must specify :verdor option. '
10
+ summary: 'when version using header, you must specify :vendor option. '
11
11
  resolution: "eg: version 'v1', :using => :header, :vendor => 'twitter'"
12
12
  missing_mime_type:
13
13
  problem: 'missing mime type for %{new_format}'
@@ -37,29 +37,39 @@ module Grape
37
37
 
38
38
  private
39
39
 
40
+ # store read input in env['api.request.input']
40
41
  def read_body_input
41
42
  if (request.post? || request.put? || request.patch?) && (! request.form_data?) && (! request.parseable_data?) && (request.content_length.to_i > 0)
42
- if env['rack.input'] && (body = env['rack.input'].read).length > 0
43
- begin
44
- fmt = mime_types[request.media_type] if request.media_type
45
- if content_type_for(fmt)
46
- parser = Grape::Parser::Base.parser_for fmt, options
47
- unless parser.nil?
48
- begin
49
- body = parser.call body, env
50
- env['rack.request.form_hash'] = env['rack.request.form_hash'] ? env['rack.request.form_hash'].merge(body) : body
51
- env['rack.request.form_input'] = env['rack.input']
52
- rescue Exception => e
53
- throw :error, :status => 400, :message => e.message
54
- end
43
+ if env['rack.input'] && (body = (env['api.request.input'] = env['rack.input'].read)).length > 0
44
+ read_rack_input body
45
+ end
46
+ end
47
+ end
48
+
49
+ # store parsed input in env['api.request.body']
50
+ def read_rack_input(body)
51
+ begin
52
+ fmt = mime_types[request.media_type] if request.media_type
53
+ if content_type_for(fmt)
54
+ parser = Grape::Parser::Base.parser_for fmt, options
55
+ if parser
56
+ begin
57
+ body = (env['api.request.body'] = parser.call(body, env))
58
+ if body.is_a?(Hash)
59
+ env['rack.request.form_hash'] = env['rack.request.form_hash'] ?
60
+ env['rack.request.form_hash'].merge(body) :
61
+ body
55
62
  end
56
- else
57
- throw :error, :status => 406, :message => "The requested content-type '#{request.media_type}' is not supported."
63
+ env['rack.request.form_input'] = env['rack.input']
64
+ rescue Exception => e
65
+ throw :error, :status => 400, :message => e.message
58
66
  end
59
- ensure
60
- env['rack.input'].rewind
61
67
  end
68
+ else
69
+ throw :error, :status => 406, :message => "The requested content-type '#{request.media_type}' is not supported."
62
70
  end
71
+ ensure
72
+ env['rack.input'].rewind
63
73
  end
64
74
  end
65
75
 
@@ -0,0 +1,24 @@
1
+ module Grape
2
+ class Namespace
3
+ attr_reader :space, :options
4
+
5
+ # options:
6
+ # requirements: a hash
7
+ def initialize(space, options = {})
8
+ @space, @options = space.to_s, options
9
+ end
10
+
11
+ def requirements
12
+ options[:requirements] || {}
13
+ end
14
+
15
+ def self.joined_space(settings)
16
+ settings.gather(:namespace).map(&:space).join("/")
17
+ end
18
+
19
+ def self.joined_space_path(settings)
20
+ Rack::Mount::Utils.normalize_path(joined_space(settings))
21
+ end
22
+
23
+ end
24
+ end
data/lib/grape/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -57,6 +57,7 @@ describe Grape::API do
57
57
  end
58
58
  end
59
59
  end
60
+
60
61
  describe '.version using param' do
61
62
  it_should_behave_like 'versioning' do
62
63
  let(:macro_options) do
@@ -93,9 +94,7 @@ describe Grape::API do
93
94
  # end
94
95
  end
95
96
 
96
- it 'routes if any media type is allowed' do
97
-
98
- end
97
+ # pending 'routes if any media type is allowed'
99
98
  end
100
99
 
101
100
  describe '.represent' do
@@ -108,7 +107,6 @@ describe Grape::API do
108
107
  subject.represent Object, :with => klass
109
108
  subject.settings[:representations][Object].should == klass
110
109
  end
111
-
112
110
  end
113
111
 
114
112
  describe '.namespace' do
@@ -189,7 +187,6 @@ describe Grape::API do
189
187
  get do
190
188
  "Votes"
191
189
  end
192
-
193
190
  post do
194
191
  "Created a Vote"
195
192
  end
@@ -201,15 +198,22 @@ describe Grape::API do
201
198
  last_response.body.should eql 'Created a Vote'
202
199
  end
203
200
 
201
+ it 'handles empty calls' do
202
+ subject.get "/"
203
+ get "/"
204
+ last_response.body.should eql ""
205
+ end
206
+
204
207
  describe 'root routes should work with' do
205
208
  before do
209
+ subject.format :txt
206
210
  def subject.enable_root_route!
207
- self.get("/") {"root"}
211
+ self.get("/") { "root" }
208
212
  end
209
213
  end
210
214
 
211
215
  after do
212
- last_response.body.should eql 'root'
216
+ last_response.body.should eql "root"
213
217
  end
214
218
 
215
219
  describe 'path versioned APIs' do
@@ -314,6 +318,31 @@ describe Grape::API do
314
318
  last_response.body.should eql 'hiya'
315
319
  end
316
320
 
321
+ [ :put, :post ].each do |verb|
322
+ context verb do
323
+ [ 'string', :symbol, 1, -1.1, {}, [], true, false, nil ].each do |object|
324
+ it "allows a(n) #{object.class} json object in params" do
325
+ subject.format :json
326
+ subject.send(verb) do
327
+ env['api.request.body']
328
+ end
329
+ send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json' }
330
+ last_response.status.should == (verb == :post ? 201 : 200)
331
+ last_response.body.should eql MultiJson.dump(object)
332
+ end
333
+ it "stores input in api.request.input" do
334
+ subject.format :json
335
+ subject.send(verb) do
336
+ env['api.request.input']
337
+ end
338
+ send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json' }
339
+ last_response.status.should == (verb == :post ? 201 : 200)
340
+ last_response.body.should eql MultiJson.dump(object).to_json
341
+ end
342
+ end
343
+ end
344
+ end
345
+
317
346
  it 'allows for multipart paths' do
318
347
 
319
348
  subject.route([:get, :post], '/:id/first') do
@@ -395,6 +424,17 @@ describe Grape::API do
395
424
  last_response.headers['Allow'].should eql 'OPTIONS, GET, POST, HEAD'
396
425
  end
397
426
 
427
+ specify '405 responses includes an Content-Type header' do
428
+ subject.get 'example' do
429
+ "example"
430
+ end
431
+ subject.post 'example' do
432
+ "example"
433
+ end
434
+ put '/example'
435
+ last_response.headers['Content-Type'].should eql 'text/plain'
436
+ end
437
+
398
438
  it 'adds an OPTIONS route that returns a 204 and an Allow header' do
399
439
  subject.get 'example' do
400
440
  "example"
@@ -631,6 +671,20 @@ describe Grape::API do
631
671
  last_response.body.should == 'true'
632
672
  end
633
673
  end
674
+
675
+ it 'mounts behind error middleware' do
676
+ m = Class.new(Grape::Middleware::Base) do
677
+ def before
678
+ throw :error, :message => "Caught in the Net", :status => 400
679
+ end
680
+ end
681
+ subject.use m
682
+ subject.get "/" do
683
+ end
684
+ get "/"
685
+ last_response.status.should == 400
686
+ last_response.body.should == "Caught in the Net"
687
+ end
634
688
  end
635
689
  end
636
690
  describe '.basic' do
@@ -685,6 +739,13 @@ describe Grape::API do
685
739
  mylogger.should_receive(:info).exactly(1).times
686
740
  subject.logger.info "this will be logged"
687
741
  end
742
+
743
+ it "defaults to a standard logger log format" do
744
+ t = Time.at(100)
745
+ Time.stub(:now).and_return(t)
746
+ STDOUT.should_receive(:write).with("I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : this will be logged\n")
747
+ subject.logger.info "this will be logged"
748
+ end
688
749
  end
689
750
 
690
751
  describe '.helpers' do