grape 0.2.2 → 0.2.3

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.

Files changed (40) hide show
  1. data/CHANGELOG.markdown +17 -3
  2. data/Gemfile +1 -1
  3. data/README.markdown +269 -171
  4. data/grape.gemspec +1 -0
  5. data/lib/grape.rb +43 -22
  6. data/lib/grape/api.rb +7 -4
  7. data/lib/grape/endpoint.rb +48 -9
  8. data/lib/grape/entity.rb +1 -1
  9. data/lib/grape/error_formatter/base.rb +32 -0
  10. data/lib/grape/error_formatter/json.rb +17 -0
  11. data/lib/grape/error_formatter/txt.rb +18 -0
  12. data/lib/grape/error_formatter/xml.rb +17 -0
  13. data/lib/grape/exceptions/validation_error.rb +9 -5
  14. data/lib/grape/formatter/base.rb +33 -0
  15. data/lib/grape/formatter/json.rb +15 -0
  16. data/lib/grape/formatter/serializable_hash.rb +35 -0
  17. data/lib/grape/formatter/txt.rb +13 -0
  18. data/lib/grape/formatter/xml.rb +13 -0
  19. data/lib/grape/middleware/base.rb +18 -103
  20. data/lib/grape/middleware/error.rb +16 -32
  21. data/lib/grape/middleware/formatter.rb +8 -9
  22. data/lib/grape/middleware/versioner.rb +3 -2
  23. data/lib/grape/middleware/versioner/header.rb +1 -1
  24. data/lib/grape/parser/base.rb +31 -0
  25. data/lib/grape/parser/json.rb +13 -0
  26. data/lib/grape/parser/xml.rb +13 -0
  27. data/lib/grape/validations.rb +1 -1
  28. data/lib/grape/validations/coerce.rb +1 -1
  29. data/lib/grape/validations/presence.rb +1 -1
  30. data/lib/grape/validations/regexp.rb +1 -1
  31. data/lib/grape/version.rb +1 -1
  32. data/spec/grape/api_spec.rb +183 -9
  33. data/spec/grape/endpoint_spec.rb +27 -1
  34. data/spec/grape/entity_spec.rb +21 -21
  35. data/spec/grape/middleware/base_spec.rb +15 -15
  36. data/spec/grape/middleware/exception_spec.rb +38 -16
  37. data/spec/grape/middleware/formatter_spec.rb +6 -40
  38. data/spec/grape/validations/presence_spec.rb +20 -20
  39. data/spec/spec_helper.rb +1 -1
  40. metadata +132 -58
@@ -5,6 +5,16 @@ require 'multi_xml'
5
5
  module Grape
6
6
  module Middleware
7
7
  class Base
8
+ # Content types are listed in order of preference.
9
+ CONTENT_TYPES = ActiveSupport::OrderedHash[
10
+ :xml, 'application/xml',
11
+ :serializable_hash, 'application/json',
12
+ :json, 'application/json',
13
+ :atom, 'application/atom+xml',
14
+ :rss, 'application/rss+xml',
15
+ :txt, 'text/plain',
16
+ ]
17
+
8
18
  attr_reader :app, :env, :options
9
19
 
10
20
  # @param [Rack Application] app The standard argument for a Rack middleware.
@@ -43,111 +53,16 @@ module Grape
43
53
  Rack::Response.new(@app_response)
44
54
  end
45
55
 
56
+ def content_types
57
+ options[:content_types] || CONTENT_TYPES
58
+ end
46
59
 
47
- module Formats
48
- # Content types are listed in order of preference.
49
- CONTENT_TYPES = ActiveSupport::OrderedHash[
50
- :xml, 'application/xml',
51
- :json, 'application/json',
52
- :atom, 'application/atom+xml',
53
- :rss, 'application/rss+xml',
54
- :txt, 'text/plain',
55
- ]
56
- FORMATTERS = {
57
- :json => :encode_json,
58
- :txt => :encode_txt,
59
- :xml => :encode_xml
60
- }
61
- PARSERS = {
62
- :json => :decode_json,
63
- :xml => :decode_xml
64
- }
65
-
66
- def formatters
67
- FORMATTERS.merge(options[:formatters] || {})
68
- end
69
-
70
- def parsers
71
- PARSERS.merge(options[:parsers] || {})
72
- end
73
-
74
- def content_types
75
- CONTENT_TYPES.merge(options[:content_types] || {})
76
- end
77
-
78
- def content_type
79
- content_types[options[:format]] || 'text/html'
80
- end
81
-
82
- def mime_types
83
- content_types.invert
84
- end
85
-
86
- def formatter_for(api_format)
87
- spec = formatters[api_format]
88
- case spec
89
- when nil
90
- lambda { |obj| obj }
91
- when Symbol
92
- method(spec)
93
- else
94
- spec
95
- end
96
- end
97
-
98
- def parser_for(api_format)
99
- spec = parsers[api_format]
100
- case spec
101
- when nil
102
- nil
103
- when Symbol
104
- method(spec)
105
- else
106
- spec
107
- end
108
- end
109
-
110
- def decode_json(object)
111
- MultiJson.load(object)
112
- end
113
-
114
- def serializable?(object)
115
- object.respond_to?(:serializable_hash) ||
116
- object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) ||
117
- object.kind_of?(Hash)
118
- end
119
-
120
- def serialize(object)
121
- if object.respond_to? :serializable_hash
122
- object.serializable_hash
123
- elsif object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false)
124
- object.map {|o| o.serializable_hash }
125
- elsif object.kind_of?(Hash)
126
- object.inject({}) { |h,(k,v)| h[k] = serialize(v); h }
127
- else
128
- object
129
- end
130
- end
131
-
132
- def encode_json(object)
133
- return object if object.is_a?(String)
134
- return MultiJson.dump(serialize(object)) if serializable?(object)
135
- return object.to_json if object.respond_to?(:to_json)
136
-
137
- MultiJson.dump(object)
138
- end
139
-
140
- def encode_txt(object)
141
- object.respond_to?(:to_txt) ? object.to_txt : object.to_s
142
- end
143
-
144
- def decode_xml(object)
145
- MultiXml.parse(object)
146
- end
60
+ def content_type
61
+ content_types[env['api.format'] || options[:format]] || 'text/html'
62
+ end
147
63
 
148
- def encode_xml(object)
149
- object.respond_to?(:to_xml) ? object.to_xml : object.to_s
150
- end
64
+ def mime_types
65
+ content_types.invert
151
66
  end
152
67
 
153
68
  end
@@ -4,44 +4,27 @@ require 'multi_json'
4
4
  module Grape
5
5
  module Middleware
6
6
  class Error < Base
7
- include Formats
8
7
 
9
8
  def default_options
10
- {
9
+ {
11
10
  :default_status => 403, # default status returned on error
12
11
  :default_message => "",
13
12
  :format => :txt,
14
13
  :formatters => {},
15
- :rescue_all => false, # true to rescue all exceptions
14
+ :error_formatters => {},
15
+ :rescue_all => false, # true to rescue all exceptions
16
16
  :rescue_options => { :backtrace => false }, # true to display backtrace
17
17
  :rescue_handlers => {}, # rescue handler blocks
18
18
  :rescued_errors => []
19
19
  }
20
20
  end
21
21
 
22
- def encode_json(message, backtrace)
23
- result = message.is_a?(Hash) ? message : { :error => message }
24
- if (options[:rescue_options] || {})[:backtrace] && backtrace && ! backtrace.empty?
25
- result = result.merge({ :backtrace => backtrace })
26
- end
27
- MultiJson.dump(result)
28
- end
29
-
30
- def encode_txt(message, backtrace)
31
- result = message.is_a?(Hash) ? MultiJson.dump(message) : message
32
- if (options[:rescue_options] || {})[:backtrace] && backtrace && ! backtrace.empty?
33
- result += "\r\n "
34
- result += backtrace.join("\r\n ")
35
- end
36
- result
37
- end
38
-
39
22
  def call!(env)
40
23
  @env = env
41
-
24
+
42
25
  begin
43
- error_response(catch(:error){
44
- return @app.call(@env)
26
+ error_response(catch(:error){
27
+ return @app.call(@env)
45
28
  })
46
29
  rescue Exception => e
47
30
  is_rescuable = rescuable?(e.class)
@@ -58,30 +41,31 @@ module Grape
58
41
  def rescuable?(klass)
59
42
  options[:rescue_all] || (options[:rescued_errors] || []).include?(klass)
60
43
  end
61
-
44
+
62
45
  def handle_error(e)
63
46
  error_response({ :message => e.message, :backtrace => e.backtrace })
64
47
  end
65
-
48
+
66
49
  def error_response(error = {})
67
50
  status = error[:status] || options[:default_status]
68
51
  message = error[:message] || options[:default_message]
69
52
  headers = {'Content-Type' => content_type}
70
53
  headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
71
54
  backtrace = error[:backtrace] || []
72
- rack_response(format_message(message, backtrace, status), status, headers)
55
+ rack_response(format_message(message, backtrace), status, headers)
73
56
  end
74
57
 
75
58
  def rack_response(message, status = options[:default_status], headers = { 'Content-Type' => content_type })
76
59
  Rack::Response.new([ message ], status, headers).finish
77
60
  end
78
-
79
- def format_message(message, backtrace, status)
80
- formatter = formatter_for(options[:format])
81
- throw :error, :status => 406, :message => "The requested format #{options[:format]} is not supported." unless formatter
82
- formatter.call(message, backtrace)
61
+
62
+ def format_message(message, backtrace)
63
+ format = env['api.format'] || options[:format]
64
+ formatter = Grape::ErrorFormatter::Base.formatter_for(format, options)
65
+ throw :error, :status => 406, :message => "The requested format #{format} is not supported." unless formatter
66
+ formatter.call(message, backtrace, options, env)
83
67
  end
84
-
68
+
85
69
  end
86
70
  end
87
71
  end
@@ -3,13 +3,11 @@ require 'grape/middleware/base'
3
3
  module Grape
4
4
  module Middleware
5
5
  class Formatter < Base
6
- include Formats
7
6
 
8
7
  def default_options
9
8
  {
10
9
  :default_format => :txt,
11
10
  :formatters => {},
12
- :content_types => {},
13
11
  :parsers => {}
14
12
  }
15
13
  end
@@ -22,10 +20,10 @@ module Grape
22
20
  fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
23
21
  if content_types.key?(fmt)
24
22
  if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0
25
- parser = parser_for fmt
23
+ parser = Grape::Parser::Base.parser_for fmt, options
26
24
  unless parser.nil?
27
25
  begin
28
- body = parser.call(body)
26
+ body = parser.call body, env
29
27
  env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
30
28
  env['rack.request.form_input'] = env['rack.input']
31
29
  rescue
@@ -42,10 +40,10 @@ module Grape
42
40
 
43
41
  def format_from_extension
44
42
  parts = request.path.split('.')
45
- extension = parts.last.to_sym
46
43
 
47
- if parts.size > 1 && content_types.key?(extension)
48
- return extension
44
+ if parts.size > 1
45
+ extension = parts.last.to_sym
46
+ return extension if content_types.key?(extension)
49
47
  end
50
48
  nil
51
49
  end
@@ -74,13 +72,14 @@ module Grape
74
72
 
75
73
  def after
76
74
  status, headers, bodies = *@app_response
77
- formatter = formatter_for env['api.format']
75
+ formatter = Grape::Formatter::Base.formatter_for env['api.format'], options
78
76
  bodymap = bodies.collect do |body|
79
- formatter.call(body)
77
+ formatter.call body, env
80
78
  end
81
79
  headers['Content-Type'] = content_types[env['api.format']] unless headers['Content-Type']
82
80
  Rack::Response.new(bodymap, status, headers).to_a
83
81
  end
82
+
84
83
  end
85
84
  end
86
85
  end
@@ -3,6 +3,7 @@
3
3
  #
4
4
  # :header - version from HTTP Accept header.
5
5
  # :path - version from uri. e.g. /v1/resource
6
+ # :param - version from uri query string, e.g. /v1/resource?apiver=v1
6
7
  #
7
8
  # See individual classes for details.
8
9
  module Grape
@@ -10,7 +11,7 @@ module Grape
10
11
  module Versioner
11
12
  extend self
12
13
 
13
- # @param strategy [Symbol] :path or :header
14
+ # @param strategy [Symbol] :path, :header or :param
14
15
  # @return a middleware class based on strategy
15
16
  def using(strategy)
16
17
  case strategy
@@ -26,4 +27,4 @@ module Grape
26
27
  end
27
28
  end
28
29
  end
29
- end
30
+ end
@@ -25,7 +25,6 @@ module Grape
25
25
  #
26
26
  # @throws [RuntimeError] if Accept header is invalid
27
27
  class Header < Base
28
- include Formats
29
28
 
30
29
  def before
31
30
  header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
@@ -112,6 +111,7 @@ module Grape
112
111
  type, subtype = Rack::Accept::Header.parse_media_type media_type
113
112
  subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
114
113
  end
114
+
115
115
  end
116
116
  end
117
117
  end
@@ -0,0 +1,31 @@
1
+ module Grape
2
+ module Parser
3
+ module Base
4
+
5
+ class << self
6
+
7
+ PARSERS = {
8
+ :json => Grape::Parser::Json,
9
+ :xml => Grape::Parser::Xml
10
+ }
11
+
12
+ def parsers(options)
13
+ PARSERS.merge(options[:parsers] || {})
14
+ end
15
+
16
+ def parser_for(api_format, options = {})
17
+ spec = parsers(options)[api_format]
18
+ case spec
19
+ when nil
20
+ nil
21
+ when Symbol
22
+ method(spec)
23
+ else
24
+ spec
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Grape
2
+ module Parser
3
+ module Json
4
+ class << self
5
+
6
+ def call(object, env)
7
+ MultiJson.load(object)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Grape
2
+ module Parser
3
+ module Xml
4
+ class << self
5
+
6
+ def call(object, env)
7
+ MultiXml.parse(object)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -102,7 +102,7 @@ module Grape
102
102
  end
103
103
 
104
104
  def group(element, &block)
105
- scope = ParamsScope.new(@api, element, self, &block)
105
+ ParamsScope.new(@api, element, self, &block)
106
106
  end
107
107
 
108
108
  def params(params)
@@ -12,7 +12,7 @@ module Grape
12
12
  if valid_type?(new_value)
13
13
  params[attr_name] = new_value
14
14
  else
15
- raise ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
15
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
16
16
  end
17
17
  end
18
18
 
@@ -3,7 +3,7 @@ module Grape
3
3
  class PresenceValidator < Validator
4
4
  def validate_param!(attr_name, params)
5
5
  unless params.has_key?(attr_name)
6
- raise ValidationError, :status => 400, :param => attr_name, :message => "missing parameter: #{attr_name}"
6
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "missing parameter: #{attr_name}"
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  class RegexpValidator < SingleOptionValidator
5
5
  def validate_param!(attr_name, params)
6
6
  if params[attr_name] && !( params[attr_name].to_s =~ @option )
7
- raise ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
7
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
8
8
  end
9
9
  end
10
10
  end
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.2.2'
2
+ VERSION = '0.2.3'
3
3
  end
@@ -189,6 +189,53 @@ describe Grape::API do
189
189
  last_response.body.should eql 'Created a Vote'
190
190
  end
191
191
 
192
+ describe "root routes should work with" do
193
+ before do
194
+ def subject.enable_root_route!
195
+ self.get("/") {"root"}
196
+ end
197
+ end
198
+
199
+ after do
200
+ last_response.body.should eql 'root'
201
+ end
202
+
203
+ describe "path versioned APIs" do
204
+ before do
205
+ subject.version 'v1', :using => :path
206
+ subject.enable_root_route!
207
+ end
208
+
209
+ it "without a format" do
210
+ versioned_get "/", "v1", :using => :path
211
+ end
212
+
213
+ it "with a format" do
214
+ get "/v1/.json"
215
+ end
216
+ end
217
+
218
+ it "header versioned APIs" do
219
+ subject.version 'v1', :using => :header, :vendor => 'test'
220
+ subject.enable_root_route!
221
+
222
+ versioned_get "/", "v1", :using => :header
223
+ end
224
+
225
+ it "param versioned APIs" do
226
+ subject.version 'v1', :using => :param
227
+ subject.enable_root_route!
228
+
229
+ versioned_get "/", "v1", :using => :param
230
+ end
231
+
232
+ it "unversioned APIs" do
233
+ subject.enable_root_route!
234
+
235
+ get "/"
236
+ end
237
+ end
238
+
192
239
  it 'should allow for multiple paths' do
193
240
  subject.get(["/abc", "/def"]) do
194
241
  "foo"
@@ -409,11 +456,18 @@ describe Grape::API do
409
456
  end
410
457
 
411
458
  it 'should set content type for error' do
412
- subject.error_format :json
459
+ subject.format :json
413
460
  subject.get('/error') { error!('error in json', 500) }
414
461
  get '/error.json'
415
462
  last_response.headers['Content-Type'].should eql 'application/json'
416
463
  end
464
+
465
+ it 'should set content type for xml' do
466
+ subject.format :xml
467
+ subject.get('/error') { error!('error in xml', 500) }
468
+ get '/error.xml'
469
+ last_response.headers['Content-Type'].should eql 'application/xml'
470
+ end
417
471
  end
418
472
 
419
473
  context 'custom middleware' do
@@ -821,10 +875,10 @@ describe Grape::API do
821
875
  end
822
876
  end
823
877
 
824
- describe ".error_format" do
878
+ describe ".format for error" do
825
879
  it 'should rescue all errors and return :txt' do
826
880
  subject.rescue_from :all
827
- subject.error_format :txt
881
+ subject.format :txt
828
882
  subject.get '/exception' do
829
883
  raise "rain!"
830
884
  end
@@ -832,9 +886,9 @@ describe Grape::API do
832
886
  last_response.body.should eql "rain!"
833
887
  end
834
888
 
835
- it 'should rescue all errros and return :txt with backtrace' do
889
+ it 'should rescue all errors and return :txt with backtrace' do
836
890
  subject.rescue_from :all, :backtrace => true
837
- subject.error_format :txt
891
+ subject.format :txt
838
892
  subject.get '/exception' do
839
893
  raise "rain!"
840
894
  end
@@ -842,9 +896,28 @@ describe Grape::API do
842
896
  last_response.body.start_with?("rain!\r\n").should be_true
843
897
  end
844
898
 
899
+ context "class" do
900
+ before :each do
901
+ class CustomErrorFormatter
902
+ def self.call(message, backtrace, options, env)
903
+ "message: #{message} @backtrace"
904
+ end
905
+ end
906
+ end
907
+ it 'should return a custom error format' do
908
+ subject.rescue_from :all, :backtrace => true
909
+ subject.error_formatter :txt, CustomErrorFormatter
910
+ subject.get '/exception' do
911
+ raise "rain!"
912
+ end
913
+ get '/exception'
914
+ last_response.body.should == "message: rain! @backtrace"
915
+ end
916
+ end
917
+
845
918
  it 'should rescue all errors and return :json' do
846
919
  subject.rescue_from :all
847
- subject.error_format :json
920
+ subject.format :json
848
921
  subject.get '/exception' do
849
922
  raise "rain!"
850
923
  end
@@ -853,7 +926,7 @@ describe Grape::API do
853
926
  end
854
927
  it 'should rescue all errors and return :json with backtrace' do
855
928
  subject.rescue_from :all, :backtrace => true
856
- subject.error_format :json
929
+ subject.format :json
857
930
  subject.get '/exception' do
858
931
  raise "rain!"
859
932
  end
@@ -863,7 +936,7 @@ describe Grape::API do
863
936
  json["backtrace"].length.should > 0
864
937
  end
865
938
  it 'should rescue error! and return txt' do
866
- subject.error_format :txt
939
+ subject.format :txt
867
940
  subject.get '/error' do
868
941
  error!("Access Denied", 401)
869
942
  end
@@ -871,7 +944,7 @@ describe Grape::API do
871
944
  last_response.body.should eql "Access Denied"
872
945
  end
873
946
  it 'should rescue error! and return json' do
874
- subject.error_format :json
947
+ subject.format :json
875
948
  subject.get '/error' do
876
949
  error!("Access Denied", 401)
877
950
  end
@@ -897,6 +970,76 @@ describe Grape::API do
897
970
  get '/content'
898
971
  last_response.content_type.should == "text/javascript"
899
972
  end
973
+ it "removes existing content types" do
974
+ subject.content_type :xls, "application/vnd.ms-excel"
975
+ subject.get :excel do
976
+ "some binary content"
977
+ end
978
+ get '/excel.json'
979
+ last_response.status.should == 406
980
+ last_response.body.should == "The requested format is not supported."
981
+ end
982
+ end
983
+
984
+ describe ".formatter" do
985
+ context "multiple formatters" do
986
+ before :each do
987
+ subject.formatter :json, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some]}\"}" }
988
+ subject.formatter :txt, lambda { |object, env| "custom_formatter: #{object[:some]}" }
989
+ subject.get :simple do
990
+ {:some => 'hash'}
991
+ end
992
+ end
993
+ it 'sets one formatter' do
994
+ get '/simple.json'
995
+ last_response.body.should eql '{"custom_formatter":"hash"}'
996
+ end
997
+ it 'sets another formatter' do
998
+ get '/simple.txt'
999
+ last_response.body.should eql 'custom_formatter: hash'
1000
+ end
1001
+ end
1002
+ context "custom formatter" do
1003
+ before :each do
1004
+ subject.content_type :json, 'application/json'
1005
+ subject.content_type :custom, 'application/custom'
1006
+ subject.formatter :custom, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some]}\"}" }
1007
+ subject.get :simple do
1008
+ {:some => 'hash'}
1009
+ end
1010
+ end
1011
+ it 'uses json' do
1012
+ get '/simple.json'
1013
+ last_response.body.should eql '{"some":"hash"}'
1014
+ end
1015
+ it 'uses custom formatter' do
1016
+ get '/simple.custom', { 'HTTP_ACCEPT' => 'application/custom' }
1017
+ last_response.body.should eql '{"custom_formatter":"hash"}'
1018
+ end
1019
+ end
1020
+ context "custom formatter class" do
1021
+ module CustomFormatter
1022
+ def self.call(object, env)
1023
+ "{\"custom_formatter\":\"#{object[:some]}\"}"
1024
+ end
1025
+ end
1026
+ before :each do
1027
+ subject.content_type :json, 'application/json'
1028
+ subject.content_type :custom, 'application/custom'
1029
+ subject.formatter :custom, CustomFormatter
1030
+ subject.get :simple do
1031
+ {:some => 'hash'}
1032
+ end
1033
+ end
1034
+ it 'uses json' do
1035
+ get '/simple.json'
1036
+ last_response.body.should eql '{"some":"hash"}'
1037
+ end
1038
+ it 'uses custom formatter' do
1039
+ get '/simple.custom', { 'HTTP_ACCEPT' => 'application/custom' }
1040
+ last_response.body.should eql '{"custom_formatter":"hash"}'
1041
+ end
1042
+ end
900
1043
  end
901
1044
 
902
1045
  describe ".default_error_status" do
@@ -1337,5 +1480,36 @@ describe Grape::API do
1337
1480
  last_response.body.should == { :meaning_of_life => 42 }.to_json
1338
1481
  end
1339
1482
  end
1483
+ context ":serializable_hash" do
1484
+ before(:each) do
1485
+ class SimpleExample
1486
+ def serializable_hash
1487
+ {:abc => 'def'}
1488
+ end
1489
+ end
1490
+ subject.format :serializable_hash
1491
+ end
1492
+ it "instance" do
1493
+ subject.get '/example' do
1494
+ SimpleExample.new
1495
+ end
1496
+ get '/example'
1497
+ last_response.body.should == '{"abc":"def"}'
1498
+ end
1499
+ it "root" do
1500
+ subject.get '/example' do
1501
+ { "root" => SimpleExample.new }
1502
+ end
1503
+ get '/example'
1504
+ last_response.body.should == '{"root":{"abc":"def"}}'
1505
+ end
1506
+ it "array" do
1507
+ subject.get '/examples' do
1508
+ [ SimpleExample.new, SimpleExample.new ]
1509
+ end
1510
+ get '/examples'
1511
+ last_response.body.should == '[{"abc":"def"},{"abc":"def"}]'
1512
+ end
1513
+ end
1340
1514
  end
1341
1515
  end