grape 0.2.4 → 0.2.5

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.

@@ -1,3 +1,13 @@
1
+ 0.2.5 (01/10/2013)
2
+ ==================
3
+
4
+ * Added support for custom parsers via `parser`, in addition to built-in multipart, JSON and XML parsers - [@dblock](http://github.com/dblock).
5
+ * Removed `body_params`, data sent via a POST or PUT with a supported content-type is merged into `params` - [@dblock](http://github.com/dblock).
6
+ * Setting `format` will automatically remove other content-types by calling `content_type` - [@dblock](http://github.com/dblock).
7
+ * Setting `content_type` will prevent any input data other than the matching content-type or any Rack-supported form and parseable media types (`application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`) from being parsed - [@dblock](http://github.com/dblock).
8
+ * [#305](https://github.com/intridea/grape/issues/305): Fix: presenting arrays of objects via `represent` or when auto-detecting an `Entity` constant in the objects being presented - [@brandonweiss](https://github.com/brandonweiss).
9
+ * [#306](https://github.com/intridea/grape/issues/306): Added i18n support for validation error messages - [@niedhui](https://github.com/niedhui).
10
+
1
11
  0.2.4 (01/06/2013)
2
12
  ==================
3
13
 
data/Gemfile CHANGED
@@ -13,5 +13,4 @@ group :development, :test do
13
13
  gem 'rspec'
14
14
  gem 'rack-test', "~> 0.6.2", :require => "rack/test"
15
15
  gem 'github-markup'
16
- gem 'redcarpet'
17
16
  end
@@ -10,6 +10,11 @@ content negotiation, versioning and much more.
10
10
 
11
11
  [![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape)
12
12
 
13
+ ## Stable Release
14
+
15
+ You're reading the documentation for the next release of Grape, which should be 0.2.5.
16
+ The current stable release is [0.2.4](https://github.com/intridea/grape/blob/v0.2.4/README.markdown).
17
+
13
18
  ## Project Tracking
14
19
 
15
20
  * [Grape Google Group](http://groups.google.com/group/ruby-grape)
@@ -215,8 +220,8 @@ end
215
220
 
216
221
  ## Parameters
217
222
 
218
- Request parameters are available through the `params` hash object. This includes `GET` and `POST` parameters,
219
- along with any named parameters you specify in your route strings.
223
+ Request parameters are available through the `params` hash object. This includes `GET`, `POST`
224
+ and `PUT` parameters, along with any named parameters you specify in your route strings.
220
225
 
221
226
  ```ruby
222
227
  get :public_timeline do
@@ -224,7 +229,8 @@ get :public_timeline do
224
229
  end
225
230
  ```
226
231
 
227
- Parameters are also populated from the request body on POST and PUT for JSON and XML content-types.
232
+ Parameters are automatically populated from the request body on POST and PUT for form input, JSON and
233
+ XML content-types.
228
234
 
229
235
  The request:
230
236
 
@@ -638,7 +644,7 @@ end
638
644
 
639
645
  ## API Formats
640
646
 
641
- By default, Grape supports _XML_, _JSON_, and _TXT_ content-types. The default format is `:txt`.
647
+ By default, Grape supports _XML_, _JSON_, and _TXT_ content-types. The default format is `:txt`.
642
648
 
643
649
  Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API implementation.
644
650
 
@@ -646,21 +652,23 @@ Your API can declare which types to support by using `content_type`. Response fo
646
652
  is determined by the request's extension, an explicit `format` parameter in the query
647
653
  string, or `Accept` header.
648
654
 
649
- The following API will only respond to the JSON content-type. All other requests will
650
- fail with an HTTP 406 error code.
655
+ The following API will only respond to the JSON content-type and will not parse any other input than `application/json`,
656
+ 'application/x-www-form-urlencoded', 'multipart/form-data', 'multipart/related' and 'multipart/mixed'. All other requests
657
+ will fail with an HTTP 406 error code.
651
658
 
652
659
  ```ruby
653
660
  class Twitter::API < Grape::API
654
661
  format :json
655
- content_type :json, "application/json"
656
662
  end
657
663
  ```
658
664
 
659
- If you combine `format` with `rescue_from :all`, errors will be rendered using the same format. If you do not want this behavior, set the default error formatter with `default_error_formatter`.
665
+ If you combine `format` with `rescue_from :all`, errors will be rendered using the same format.
666
+ If you do not want this behavior, set the default error formatter with `default_error_formatter`.
660
667
 
661
668
  ```ruby
662
669
  class Twitter::API < Grape::API
663
670
  format :json
671
+ content_type :txt, "text/plain"
664
672
  default_error_formatter :txt
665
673
  end
666
674
  ```
@@ -696,7 +704,7 @@ Built-in formats are the following.
696
704
  * `:txt`: use object's `to_txt` when available, otherwise `to_s`
697
705
  * `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json`
698
706
 
699
- Use `default_format` to set the fallback format when the format could not be determined from the `Accept` header.
707
+ Use `default_format` to set the fallback format when the format could not be determined from the `Accept` header.
700
708
  See below for the order for choosing the API format.
701
709
 
702
710
  ```ruby
@@ -728,6 +736,42 @@ class API < Grape::API
728
736
  end
729
737
  ```
730
738
 
739
+ ## API Data Formats
740
+
741
+ Grape accepts and parses input data sent with the POST and PUT methods as described in the Parameters
742
+ section above. It also supports custom data formats. You must declare additional content-types via
743
+ `content_type` and optionally supply a parser via `parser` unless a parser is already available within
744
+ Grape to enable a custom format. Such a parser can be a function or a class.
745
+
746
+ Without a parser, data is available "as-is" and can be read with `env['rack.input'].read`.
747
+
748
+ The following example is a trivial parser that will assign any input with the "text/custom" content-type
749
+ to `:value`. The parameter will be available via `params[:value]` inside the API call.
750
+
751
+ ```ruby
752
+ module CustomParser
753
+ def self.call(object, env)
754
+ { :value => object.to_s }
755
+ end
756
+ end
757
+ ```
758
+
759
+ ```ruby
760
+ content_type :txt, "text/plain"
761
+ content_type :custom, "text/custom"
762
+ parser :custom, CustomParser
763
+
764
+ put "value" do
765
+ params[:value]
766
+ end
767
+ ```
768
+
769
+ You can invoke the above API as follows.
770
+
771
+ ```
772
+ curl -X PUT -d 'data' 'http://localhost:9292/value' -H Content-Type:text/custom -v
773
+ ```
774
+
731
775
  ## Reusable Responses with Entities
732
776
 
733
777
  Entities are a reusable means for converting Ruby objects to API responses.
@@ -817,10 +861,10 @@ module API
817
861
  desc 'Statuses index', {
818
862
  :object_fields => API::Entities::Status.documentation
819
863
  }
820
- get '/statues' do
864
+ get '/statuses' do
821
865
  statuses = Status.all
822
866
  type = current_user.admin? ? :full : :default
823
- present statues, with: API::Entities::Status, :type => type
867
+ present statuses, with: API::Entities::Status, :type => type
824
868
  end
825
869
  end
826
870
  end
@@ -927,9 +971,9 @@ class MyAPI < Grape::API
927
971
  end
928
972
  ```
929
973
 
930
- The current endpoint responding to the request is `self` within the API block
931
- or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties,
932
- such as `source` which gives you access to the original code block of the API
974
+ The current endpoint responding to the request is `self` within the API block
975
+ or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties,
976
+ such as `source` which gives you access to the original code block of the API
933
977
  implementation. This can be particularly useful for building a logger middleware.
934
978
 
935
979
  ```ruby
@@ -956,7 +1000,7 @@ For instance when you're API needs to get part of an URL, for instance:
956
1000
 
957
1001
  ```ruby
958
1002
  class TwitterAPI < Grape::API
959
- namespace :statues do
1003
+ namespace :statuses do
960
1004
  get '/(*:status)', :anchor => false do
961
1005
 
962
1006
  end
@@ -3,6 +3,7 @@ require 'rack/auth/basic'
3
3
  require 'rack/auth/digest/md5'
4
4
  require 'logger'
5
5
  require 'grape/util/deep_merge'
6
+ require 'grape/util/content_types'
6
7
 
7
8
  module Grape
8
9
  # The API class is the primary entry point for
@@ -129,7 +130,12 @@ module Grape
129
130
  def format(new_format = nil)
130
131
  if new_format
131
132
  set(:format, new_format.to_sym)
133
+ # define the default error formatters
132
134
  set(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {}))
135
+ # define a single mime type
136
+ mime_type = content_types[new_format.to_sym]
137
+ raise "missing mime type for #{new_format}" unless mime_type
138
+ settings.imbue(:content_types, new_format.to_sym => mime_type)
133
139
  else
134
140
  settings[:format]
135
141
  end
@@ -140,6 +146,11 @@ module Grape
140
146
  settings.imbue(:formatters, content_type.to_sym => new_formatter)
141
147
  end
142
148
 
149
+ # Specify a custom parser for a content-type.
150
+ def parser(content_type, new_parser)
151
+ settings.imbue(:parsers, content_type.to_sym => new_parser)
152
+ end
153
+
143
154
  # Specify a default error formatter.
144
155
  def default_error_formatter(new_formatter = nil)
145
156
  new_formatter ? set(:default_error_formatter, new_formatter) : settings[:default_error_formatter]
@@ -155,6 +166,11 @@ module Grape
155
166
  settings.imbue(:content_types, key.to_sym => val)
156
167
  end
157
168
 
169
+ # All available content types.
170
+ def content_types
171
+ Grape::ContentTypes.content_types_for(settings[:content_types])
172
+ end
173
+
158
174
  # Specify the default status code for errors.
159
175
  def default_error_status(new_status = nil)
160
176
  new_status ? set(:default_error_status, new_status) : settings[:default_error_status]
@@ -283,7 +299,6 @@ module Grape
283
299
  if app.respond_to?(:inherit_settings)
284
300
  app.inherit_settings(settings.clone)
285
301
  end
286
-
287
302
  endpoints << Grape::Endpoint.new(settings.clone,
288
303
  :method => :any,
289
304
  :path => path,
@@ -159,8 +159,7 @@ module Grape
159
159
  def params
160
160
  @params ||= Hashie::Mash.new.
161
161
  deep_merge(request.params).
162
- deep_merge(env['rack.routing_args'] || {}).
163
- deep_merge(self.body_params)
162
+ deep_merge(env['rack.routing_args'] || {})
164
163
  end
165
164
 
166
165
  # A filtering method that will return a hash
@@ -185,23 +184,6 @@ module Grape
185
184
  }
186
185
  end
187
186
 
188
- # Pull out request body params if the content type matches and we're on a POST or PUT
189
- def body_params
190
- if ['POST', 'PUT'].include?(request.request_method.to_s.upcase) && request.content_length.to_i > 0
191
- return @body_params ||=
192
- case env['CONTENT_TYPE']
193
- when 'application/json'
194
- MultiJson.load(request.body.read)
195
- when 'application/xml'
196
- MultiXml.parse(request.body.read)
197
- else
198
- {}
199
- end
200
- end
201
-
202
- {}
203
- end
204
-
205
187
  # The API version as specified in the URL.
206
188
  def version; env['api.version'] end
207
189
 
@@ -314,11 +296,14 @@ module Grape
314
296
  def present(object, options = {})
315
297
  entity_class = options.delete(:with)
316
298
 
317
- object.class.ancestors.each do |potential|
299
+ # auto-detect the entity from the first object in the collection
300
+ object_instance = object.is_a?(Array) ? object.first : object
301
+
302
+ object_instance.class.ancestors.each do |potential|
318
303
  entity_class ||= (settings[:representations] || {})[potential]
319
304
  end
320
305
 
321
- entity_class ||= object.class.const_get(:Entity) if object.class.const_defined?(:Entity)
306
+ entity_class ||= object_instance.class.const_get(:Entity) if object_instance.class.const_defined?(:Entity)
322
307
 
323
308
  root = options.delete(:root)
324
309
 
@@ -410,7 +395,8 @@ module Grape
410
395
  :format => settings[:format],
411
396
  :default_format => settings[:default_format] || :txt,
412
397
  :content_types => settings[:content_types],
413
- :formatters => settings[:formatters]
398
+ :formatters => settings[:formatters],
399
+ :parsers => settings[:parsers]
414
400
 
415
401
  aggregate_setting(:middleware).each do |m|
416
402
  m = m.dup
@@ -0,0 +1,7 @@
1
+ en:
2
+ grape:
3
+ errors:
4
+ messages:
5
+ coerce: 'invalid parameter: %{attribute}'
6
+ presence: 'missing parameter: %{attribute}'
7
+ regexp: 'invalid parameter: %{attribute}'
@@ -1,21 +1,11 @@
1
- require 'active_support/ordered_hash'
2
1
  require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'grape/util/content_types'
3
3
  require 'multi_json'
4
4
  require 'multi_xml'
5
5
 
6
6
  module Grape
7
7
  module Middleware
8
8
  class Base
9
- # Content types are listed in order of preference.
10
- CONTENT_TYPES = ActiveSupport::OrderedHash[
11
- :xml, 'application/xml',
12
- :serializable_hash, 'application/json',
13
- :json, 'application/json',
14
- :atom, 'application/atom+xml',
15
- :rss, 'application/rss+xml',
16
- :txt, 'text/plain',
17
- ]
18
-
19
9
  attr_reader :app, :env, :options
20
10
 
21
11
  # @param [Rack Application] app The standard argument for a Rack middleware.
@@ -61,7 +51,7 @@ module Grape
61
51
  end
62
52
 
63
53
  def content_types
64
- options[:content_types] || CONTENT_TYPES
54
+ ContentTypes.content_types_for(options[:content_types])
65
55
  end
66
56
 
67
57
  def content_type
@@ -17,71 +17,99 @@ module Grape
17
17
  end
18
18
 
19
19
  def before
20
- fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
21
- if content_type_for(fmt)
22
- if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0
23
- parser = Grape::Parser::Base.parser_for fmt, options
24
- unless parser.nil?
20
+ negotiate_content_type
21
+ read_body_input
22
+ end
23
+
24
+ def after
25
+ status, headers, bodies = *@app_response
26
+ formatter = Grape::Formatter::Base.formatter_for env['api.format'], options
27
+ bodymap = bodies.collect do |body|
28
+ formatter.call body, env
29
+ end
30
+ headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
31
+ Rack::Response.new(bodymap, status, headers).to_a
32
+ end
33
+
34
+ private
35
+
36
+ def read_body_input
37
+ request_method = request.request_method.to_s.upcase
38
+ if [ 'POST', 'PUT' ].include?(request_method) && (! request.form_data?) && (request.content_length.to_i > 0)
39
+ if env['rack.input'] && (body = env['rack.input'].read).strip.length > 0
25
40
  begin
26
- body = parser.call body, env
27
- env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
28
- env['rack.request.form_input'] = env['rack.input']
29
- rescue
30
- # It's possible that it's just regular POST content -- just back off
41
+ fmt = mime_types[format_from_content_type]
42
+ if content_type_for(fmt)
43
+ parser = Grape::Parser::Base.parser_for fmt, options
44
+ unless parser.nil?
45
+ begin
46
+ body = parser.call body, env
47
+ env['rack.request.form_hash'] = env['rack.request.form_hash'] ? env['rack.request.form_hash'].merge(body) : body
48
+ env['rack.request.form_input'] = env['rack.input']
49
+ rescue
50
+ # It's possible that it's just regular POST content -- just back off
51
+ end
52
+ end
53
+ else
54
+ throw :error, :status => 406, :message => 'The requested content-type is not supported.'
55
+ end
56
+ ensure
57
+ env['rack.input'].rewind
31
58
  end
32
59
  end
33
- env['rack.input'].rewind
34
60
  end
35
- env['api.format'] = fmt
36
- else
37
- throw :error, :status => 406, :message => 'The requested format is not supported.'
38
61
  end
39
- end
40
62
 
41
- def format_from_extension
42
- parts = request.path.split('.')
63
+ def negotiate_content_type
64
+ fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
65
+ if content_type_for(fmt)
66
+ env['api.format'] = fmt
67
+ else
68
+ throw :error, :status => 406, :message => 'The requested format is not supported.'
69
+ end
70
+ end
43
71
 
44
- if parts.size > 1
45
- extension = parts.last
72
+ def format_from_content_type
73
+ fmt = env['CONTENT_TYPE']
46
74
  # avoid symbol memory leak on an unknown format
47
- return extension.to_sym if content_type_for(extension)
75
+ return fmt.to_sym if content_type_for(fmt)
76
+ fmt
48
77
  end
49
- nil
50
- end
51
78
 
52
- def format_from_params
53
- fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
54
- # avoid symbol memory leak on an unknown format
55
- return fmt.to_sym if content_type_for(fmt)
56
- fmt
57
- end
79
+ def format_from_extension
80
+ parts = request.path.split('.')
58
81
 
59
- def format_from_header
60
- mime_array.each do |t|
61
- if mime_types.key?(t)
62
- return mime_types[t]
82
+ if parts.size > 1
83
+ extension = parts.last
84
+ # avoid symbol memory leak on an unknown format
85
+ return extension.to_sym if content_type_for(extension)
63
86
  end
87
+ nil
64
88
  end
65
- nil
66
- end
67
89
 
68
- def mime_array
69
- accept = headers['accept'] or return []
90
+ def format_from_params
91
+ fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
92
+ # avoid symbol memory leak on an unknown format
93
+ return fmt.to_sym if content_type_for(fmt)
94
+ fmt
95
+ end
70
96
 
71
- accept.gsub(/\b/,'').scan(%r((\w+/[\w+.-]+)(?:(?:;[^,]*?)?;\s*q=([\d.]+))?)).sort_by { |_, q| -q.to_f }.map {|mime, _|
72
- mime.sub(%r(vnd\.[^+]+\+), '')
73
- }
74
- end
97
+ def format_from_header
98
+ mime_array.each do |t|
99
+ if mime_types.key?(t)
100
+ return mime_types[t]
101
+ end
102
+ end
103
+ nil
104
+ end
75
105
 
76
- def after
77
- status, headers, bodies = *@app_response
78
- formatter = Grape::Formatter::Base.formatter_for env['api.format'], options
79
- bodymap = bodies.collect do |body|
80
- formatter.call body, env
106
+ def mime_array
107
+ accept = headers['accept'] or return []
108
+
109
+ accept.gsub(/\b/,'').scan(%r((\w+/[\w+.-]+)(?:(?:;[^,]*?)?;\s*q=([\d.]+))?)).sort_by { |_, q| -q.to_f }.map {|mime, _|
110
+ mime.sub(%r(vnd\.[^+]+\+), '')
111
+ }
81
112
  end
82
- headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
83
- Rack::Response.new(bodymap, status, headers).to_a
84
- end
85
113
 
86
114
  end
87
115
  end
@@ -0,0 +1,19 @@
1
+ require 'active_support/ordered_hash'
2
+
3
+ module Grape
4
+ module ContentTypes
5
+ # Content types are listed in order of preference.
6
+ CONTENT_TYPES = ActiveSupport::OrderedHash[
7
+ :xml, 'application/xml',
8
+ :serializable_hash, 'application/json',
9
+ :json, 'application/json',
10
+ :atom, 'application/atom+xml',
11
+ :rss, 'application/rss+xml',
12
+ :txt, 'text/plain',
13
+ ]
14
+
15
+ def self.content_types_for(from_settings)
16
+ from_settings || Grape::ContentTypes::CONTENT_TYPES
17
+ end
18
+ end
19
+ end
@@ -1,12 +1,14 @@
1
1
  require 'virtus'
2
+ require 'i18n'
2
3
 
4
+ I18n.load_path << File.expand_path('../locale/en.yml', __FILE__)
3
5
  module Grape
4
-
6
+
5
7
  module Validations
6
-
8
+
7
9
  ##
8
10
  # All validators must inherit from this class.
9
- #
11
+ #
10
12
  class Validator
11
13
  attr_reader :attrs
12
14
 
@@ -30,6 +32,11 @@ module Grape
30
32
  end
31
33
  end
32
34
 
35
+ def i18n_message(type, attribute)
36
+ i18n_attr = I18n.t("grape.errors.attributes.#{attribute}", :default => attribute.to_s)
37
+ I18n.t("grape.errors.messages.#{type}", :attribute => i18n_attr)
38
+ end
39
+
33
40
  private
34
41
 
35
42
  def self.convert_to_short_name(klass)
@@ -60,17 +67,17 @@ module Grape
60
67
  Validations::register_validator(short_name, klass)
61
68
  end
62
69
  end
63
-
70
+
64
71
  class << self
65
72
  attr_accessor :validators
66
73
  end
67
-
74
+
68
75
  self.validators = {}
69
-
76
+
70
77
  def self.register_validator(short_name, klass)
71
78
  validators[short_name] = klass
72
79
  end
73
-
80
+
74
81
  class ParamsScope
75
82
  attr_accessor :element, :parent
76
83
 
@@ -80,17 +87,17 @@ module Grape
80
87
  @api = api
81
88
  instance_eval(&block)
82
89
  end
83
-
90
+
84
91
  def requires(*attrs)
85
92
  validations = {:presence => true}
86
93
  if attrs.last.is_a?(Hash)
87
94
  validations.merge!(attrs.pop)
88
95
  end
89
-
96
+
90
97
  push_declared_params(attrs)
91
98
  validates(attrs, validations)
92
99
  end
93
-
100
+
94
101
  def optional(*attrs)
95
102
  validations = {}
96
103
  if attrs.last.is_a?(Hash)
@@ -119,16 +126,16 @@ module Grape
119
126
  private
120
127
  def validates(attrs, validations)
121
128
  doc_attrs = { :required => validations.keys.include?(:presence) }
122
-
129
+
123
130
  # special case (type = coerce)
124
131
  if validations[:type]
125
132
  validations[:coerce] = validations.delete(:type)
126
133
  end
127
-
134
+
128
135
  if coerce_type = validations[:coerce]
129
136
  doc_attrs[:type] = coerce_type.to_s
130
137
  end
131
-
138
+
132
139
  if desc = validations.delete(:desc)
133
140
  doc_attrs[:desc] = desc
134
141
  end
@@ -169,18 +176,18 @@ module Grape
169
176
  @api.settings[:declared_params] += attrs
170
177
  end
171
178
  end
172
-
179
+
173
180
  # This module is mixed into the API Class.
174
181
  module ClassMethods
175
182
  def reset_validations!
176
183
  settings.peek[:declared_params] = []
177
184
  settings.peek[:validations] = []
178
185
  end
179
-
186
+
180
187
  def params(&block)
181
188
  ParamsScope.new(self, nil, nil, &block)
182
189
  end
183
-
190
+
184
191
  def document_attribute(names, opts)
185
192
  @last_description ||= {}
186
193
  @last_description[:params] ||= {}
@@ -189,9 +196,9 @@ module Grape
189
196
  @last_description[:params][name[:full_name].to_s].merge!(opts)
190
197
  end
191
198
  end
192
-
199
+
193
200
  end
194
-
201
+
195
202
  end
196
203
  end
197
204
 
@@ -3,28 +3,28 @@ module Grape
3
3
  class API
4
4
  Boolean = Virtus::Attribute::Boolean
5
5
  end
6
-
6
+
7
7
  module Validations
8
-
8
+
9
9
  class CoerceValidator < SingleOptionValidator
10
10
  def validate_param!(attr_name, params)
11
11
  new_value = coerce_value(@option, params[attr_name])
12
12
  if valid_type?(new_value)
13
13
  params[attr_name] = new_value
14
14
  else
15
- raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
15
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => i18n_message(:coerce, attr_name)
16
16
  end
17
17
  end
18
-
18
+
19
19
  class InvalidValue; end
20
20
  private
21
-
21
+
22
22
  def _valid_array_type?(type, values)
23
23
  values.all? do |val|
24
24
  _valid_single_type?(type, val)
25
25
  end
26
26
  end
27
-
27
+
28
28
  def _valid_single_type?(klass, val)
29
29
  # allow nil, to ignore when a parameter is absent
30
30
  return true if val.nil?
@@ -36,7 +36,7 @@ module Grape
36
36
  val.is_a?(klass)
37
37
  end
38
38
  end
39
-
39
+
40
40
  def valid_type?(val)
41
41
  if @option.is_a?(Array)
42
42
  _valid_array_type?(@option[0], val)
@@ -44,18 +44,18 @@ module Grape
44
44
  _valid_single_type?(@option, val)
45
45
  end
46
46
  end
47
-
47
+
48
48
  def coerce_value(type, val)
49
49
  converter = Virtus::Attribute.build(:a, type)
50
50
  converter.coerce(val)
51
-
51
+
52
52
  # not the prettiest but some invalid coercion can currently trigger
53
53
  # errors in Virtus (see coerce_spec.rb)
54
54
  rescue
55
55
  InvalidValue.new
56
56
  end
57
-
57
+
58
58
  end
59
-
59
+
60
60
  end
61
61
  end
@@ -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 Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "missing parameter: #{attr_name}"
6
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => i18n_message(:presence, attr_name)
7
7
  end
8
8
  end
9
9
  end
@@ -1,10 +1,10 @@
1
1
  module Grape
2
2
  module Validations
3
-
3
+
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 Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}"
7
+ raise Grape::Exceptions::ValidationError, :status => 400, :param => attr_name, :message => i18n_message(:regexp, attr_name)
8
8
  end
9
9
  end
10
10
  end
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.2.4'
2
+ VERSION = '0.2.5'
3
3
  end
@@ -1078,6 +1078,44 @@ describe Grape::API do
1078
1078
  end
1079
1079
  end
1080
1080
 
1081
+ describe '.parser' do
1082
+ context 'lambda parser' do
1083
+ before :each do
1084
+ subject.content_type :txt, "text/plain"
1085
+ subject.content_type :custom, "text/custom"
1086
+ subject.parser :custom, lambda { |object, env| { object.to_sym => object.to_s.reverse } }
1087
+ subject.put :simple do
1088
+ params[:simple]
1089
+ end
1090
+ end
1091
+ it 'uses parser' do
1092
+ put '/simple', "simple", "CONTENT_TYPE" => "text/custom"
1093
+ last_response.status.should == 200
1094
+ last_response.body.should eql "elpmis"
1095
+ end
1096
+ end
1097
+ context 'custom parser class' do
1098
+ module CustomParser
1099
+ def self.call(object, env)
1100
+ { object.to_sym => object.to_s.reverse }
1101
+ end
1102
+ end
1103
+ before :each do
1104
+ subject.content_type :txt, "text/plain"
1105
+ subject.content_type :custom, "text/custom"
1106
+ subject.parser :custom, CustomParser
1107
+ subject.put :simple do
1108
+ params[:simple]
1109
+ end
1110
+ end
1111
+ it 'uses custom parser' do
1112
+ put '/simple', "simple", "CONTENT_TYPE" => "text/custom"
1113
+ last_response.status.should == 200
1114
+ last_response.body.should eql "elpmis"
1115
+ end
1116
+ end
1117
+ end
1118
+
1081
1119
  describe '.default_error_status' do
1082
1120
  it 'allows setting default_error_status' do
1083
1121
  subject.rescue_from :all
@@ -1270,10 +1308,10 @@ describe Grape::API do
1270
1308
  subject.routes.map { |route|
1271
1309
  { :description => route.route_description, :params => route.route_params }
1272
1310
  }.should eq [
1273
- { :description => "method",
1274
- :params => {
1275
- "ns_param" => { :required => true, :desc => "namespace parameter" },
1276
- "method_param" => { :required => false, :desc => "method parameter" }
1311
+ { :description => "method",
1312
+ :params => {
1313
+ "ns_param" => { :required => true, :desc => "namespace parameter" },
1314
+ "method_param" => { :required => false, :desc => "method parameter" }
1277
1315
  }
1278
1316
  }
1279
1317
  ]
@@ -1301,13 +1339,13 @@ describe Grape::API do
1301
1339
  subject.routes.map { |route|
1302
1340
  { :description => route.route_description, :params => route.route_params }
1303
1341
  }.should eq [
1304
- { :description => "method",
1305
- :params => {
1306
- "ns_param" => { :required => true, :desc => "ns param 2" },
1307
- "ns1_param" => { :required => true, :desc => "ns1 param" },
1308
- "ns2_param" => { :required => true, :desc => "ns2 param" },
1309
- "method_param" => { :required => false, :desc => "method param" }
1310
- }
1342
+ { :description => "method",
1343
+ :params => {
1344
+ "ns_param" => { :required => true, :desc => "ns param 2" },
1345
+ "ns1_param" => { :required => true, :desc => "ns1 param" },
1346
+ "ns2_param" => { :required => true, :desc => "ns2 param" },
1347
+ "method_param" => { :required => false, :desc => "method param" }
1348
+ }
1311
1349
  }
1312
1350
  ]
1313
1351
  end
@@ -1325,12 +1363,12 @@ describe Grape::API do
1325
1363
  end
1326
1364
  subject.get "method" do ; end
1327
1365
 
1328
- subject.routes.map { |route|
1366
+ subject.routes.map { |route|
1329
1367
  route.route_params
1330
1368
  }.should eq [{
1331
1369
  "group1[param1]" => { :required => false, :desc => "group1 param1 desc" },
1332
1370
  "group1[param2]" => { :required => true, :desc => "group1 param2 desc" },
1333
- "group2[param1]" => { :required => false, :desc => "group2 param1 desc" },
1371
+ "group2[param1]" => { :required => false, :desc => "group2 param1 desc" },
1334
1372
  "group2[param2]" => { :required => true, :desc => "group2 param2 desc" }
1335
1373
  }]
1336
1374
  end
@@ -1346,11 +1384,11 @@ describe Grape::API do
1346
1384
  subject.routes.map { |route|
1347
1385
  { :description => route.route_description, :params => route.route_params }
1348
1386
  }.should eq [
1349
- { :description => "nesting",
1350
- :params => {
1351
- "root_param" => { :required => true, :desc => "root param" },
1352
- "nested[nested_param]" => { :required => true, :desc => "nested param" }
1353
- }
1387
+ { :description => "nesting",
1388
+ :params => {
1389
+ "root_param" => { :required => true, :desc => "root param" },
1390
+ "nested[nested_param]" => { :required => true, :desc => "nested param" }
1391
+ }
1354
1392
  }
1355
1393
  ]
1356
1394
  end
@@ -1550,6 +1588,7 @@ describe Grape::API do
1550
1588
  context ':txt' do
1551
1589
  before(:each) do
1552
1590
  subject.format :txt
1591
+ subject.content_type :json, "application/json"
1553
1592
  subject.get '/meaning_of_life' do
1554
1593
  { :meaning_of_life => 42 }
1555
1594
  end
@@ -1567,9 +1606,30 @@ describe Grape::API do
1567
1606
  last_response.body.should == { :meaning_of_life => 42 }.to_s
1568
1607
  end
1569
1608
  end
1609
+ context ':txt only' do
1610
+ before(:each) do
1611
+ subject.format :txt
1612
+ subject.get '/meaning_of_life' do
1613
+ { :meaning_of_life => 42 }
1614
+ end
1615
+ end
1616
+ it 'forces txt without an extension' do
1617
+ get '/meaning_of_life'
1618
+ last_response.body.should == { :meaning_of_life => 42 }.to_s
1619
+ end
1620
+ it 'forces txt with the wrong extension' do
1621
+ get '/meaning_of_life.json'
1622
+ last_response.body.should == { :meaning_of_life => 42 }.to_s
1623
+ end
1624
+ it 'forces txt from a non-accepting header' do
1625
+ get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'application/json' }
1626
+ last_response.body.should == { :meaning_of_life => 42 }.to_s
1627
+ end
1628
+ end
1570
1629
  context ':json' do
1571
1630
  before(:each) do
1572
1631
  subject.format :json
1632
+ subject.content_type :txt, "text/plain"
1573
1633
  subject.get '/meaning_of_life' do
1574
1634
  { :meaning_of_life => 42 }
1575
1635
  end
@@ -7,11 +7,11 @@ describe Grape::Endpoint do
7
7
  describe '#initialize' do
8
8
  it 'takes a settings stack, options, and a block' do
9
9
  p = Proc.new {}
10
- expect {
10
+ expect {
11
11
  Grape::Endpoint.new(Grape::Util::HashStack.new, {
12
12
  :path => '/',
13
13
  :method => :get
14
- }, &p)
14
+ }, &p)
15
15
  }.not_to raise_error
16
16
  end
17
17
  end
@@ -246,7 +246,6 @@ describe Grape::Endpoint do
246
246
  subject.post '/request_body' do
247
247
  params[:user]
248
248
  end
249
-
250
249
  subject.put '/request_body' do
251
250
  params[:user]
252
251
  end
@@ -274,18 +273,24 @@ describe Grape::Endpoint do
274
273
 
275
274
  it 'does not include parameters not defined by the body' do
276
275
  subject.post '/omitted_params' do
277
- body_params[:version].should == nil
276
+ params[:version].should == nil
277
+ params[:user].should == 'Bob'
278
278
  end
279
- post '/omitted_params', MultiJson.dump(:user => 'Blah'), {'CONTENT_TYPE' => 'application/json'}
279
+ post '/omitted_params', MultiJson.dump(:user => 'Bob'), {'CONTENT_TYPE' => 'application/json'}
280
280
  end
281
+ end
281
282
 
282
- it 'should return an equivalent hash on subsequenst calls' do
283
- subject.post '/two_times' do
284
- body_params.should == body_params
285
- end
286
- post '/two_times', MultiJson.dump(:user => 'Bobby T.'), {'CONTENT_TYPE' => 'application/json'}
283
+ it "responds with a 406 for an unsupported content-type" do
284
+ subject.format :json
285
+ # subject.content_type :json, "application/json"
286
+ subject.put '/request_body' do
287
+ params[:user]
287
288
  end
289
+ put '/request_body', '<user>Bobby T.</user>', {'CONTENT_TYPE' => 'application/xml'}
290
+ last_response.status.should == 406
291
+ last_response.body.should == '{"error":"The requested content-type is not supported."}'
288
292
  end
293
+
289
294
  end
290
295
 
291
296
  describe '#error!' do
@@ -437,6 +442,20 @@ describe Grape::Endpoint do
437
442
  last_response.body.should == 'Hiya'
438
443
  end
439
444
 
445
+ it 'pulls a representation from the class options if the presented object is a collection of objects' do
446
+ entity = Class.new(Grape::Entity)
447
+ entity.stub!(:represent).and_return("Hiya")
448
+
449
+ class TestObject; end
450
+
451
+ subject.represent TestObject, :with => entity
452
+ subject.get '/example' do
453
+ present [TestObject.new]
454
+ end
455
+ get '/example'
456
+ last_response.body.should == "Hiya"
457
+ end
458
+
440
459
  it 'pulls a representation from the class ancestor if it exists' do
441
460
  entity = Class.new(Grape::Entity)
442
461
  entity.stub!(:represent).and_return("Hiya")
@@ -465,6 +484,20 @@ describe Grape::Endpoint do
465
484
  last_response.body.should == 'Auto-detect!'
466
485
  end
467
486
 
487
+ it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
488
+ some_model = Class.new
489
+ entity = Class.new(Grape::Entity)
490
+ entity.stub!(:represent).and_return("Auto-detect!")
491
+
492
+ some_model.const_set :Entity, entity
493
+
494
+ subject.get '/example' do
495
+ present [some_model.new]
496
+ end
497
+ get '/example'
498
+ last_response.body.should == 'Auto-detect!'
499
+ end
500
+
468
501
  it 'adds a root key to the output if one is given' do
469
502
  subject.get '/example' do
470
503
  present({:abc => 'def'}, :root => :root)
@@ -606,5 +639,5 @@ describe Grape::Endpoint do
606
639
  last_response.body.should == "http://example.org/api/v1/url"
607
640
  end
608
641
  end
609
-
642
+
610
643
  end
@@ -149,16 +149,37 @@ describe Grape::Middleware::Formatter do
149
149
 
150
150
  context 'Input' do
151
151
  it 'parses the body from a POST/PUT and put the contents into rack.request.form_hash' do
152
- subject.call({'PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json', 'rack.input' => StringIO.new('{"is_boolean":true,"string":"thing"}')})
152
+ io = StringIO.new('{"is_boolean":true,"string":"thing"}')
153
+ subject.call({
154
+ 'PATH_INFO' => '/info',
155
+ 'REQUEST_METHOD' => 'POST',
156
+ 'CONTENT_TYPE' => 'application/json',
157
+ 'rack.input' => io,
158
+ 'CONTENT_LENGTH' => io.length
159
+ })
153
160
  subject.env['rack.request.form_hash']['is_boolean'].should be_true
154
161
  subject.env['rack.request.form_hash']['string'].should == 'thing'
155
162
  end
156
163
  it 'parses the body from an xml POST/PUT and put the contents into rack.request.from_hash' do
157
- subject.call({'PATH_INFO' => '/info.xml', 'HTTP_ACCEPT' => 'application/xml', 'rack.input' => StringIO.new('<thing><name>Test</name></thing>')})
164
+ io = StringIO.new('<thing><name>Test</name></thing>')
165
+ subject.call({
166
+ 'PATH_INFO' => '/info.xml',
167
+ 'REQUEST_METHOD' => 'POST',
168
+ 'CONTENT_TYPE' => 'application/xml',
169
+ 'rack.input' => io,
170
+ 'CONTENT_LENGTH' => io.length
171
+ })
158
172
  subject.env['rack.request.form_hash']['thing']['name'].should == 'Test'
159
173
  end
160
174
  it 'is able to fail gracefully if the body is regular POST content' do
161
- subject.call({'PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json', 'rack.input' => StringIO.new('name=Other+Test+Thing')})
175
+ io = StringIO.new('name=Other+Test+Thing')
176
+ subject.call({
177
+ 'PATH_INFO' => '/info',
178
+ 'REQUEST_METHOD' => 'POST',
179
+ 'CONTENT_TYPE' => 'application/json',
180
+ 'rack.input' => io,
181
+ 'CONTENT_LENGTH' => io.length
182
+ })
162
183
  subject.env['rack.request.form_hash'].should be_nil
163
184
  end
164
185
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
3
 
3
4
  describe Grape::Validations::CoerceValidator do
@@ -5,11 +6,23 @@ describe Grape::Validations::CoerceValidator do
5
6
  def app; subject end
6
7
 
7
8
  describe 'coerce' do
9
+ it "i18n error on malformed input" do
10
+ I18n.load_path << File.expand_path('../zh-CN.yml',__FILE__)
11
+ I18n.locale = :'zh-CN'
12
+ subject.params { requires :age, :type => Integer }
13
+ subject.get '/single' do 'int works'; end
14
+
15
+ get '/single', :age => '43a'
16
+ last_response.status.should == 400
17
+ last_response.body.should == '年龄格式不正确'
18
+ I18n.locale = :en
19
+
20
+ end
8
21
  it 'error on malformed input' do
9
22
  subject.params { requires :int, :type => Integer }
10
23
  subject.get '/single' do 'int works'; end
11
24
 
12
- get '/single', :int => '43a'
25
+ get '/single', :int => '43a'
13
26
  last_response.status.should == 400
14
27
  last_response.body.should == 'invalid parameter: int'
15
28
 
@@ -89,7 +102,7 @@ describe Grape::Validations::CoerceValidator do
89
102
  get '/bool', { :bool => 1 }
90
103
  last_response.status.should == 200
91
104
  last_response.body.should == 'TrueClass'
92
-
105
+
93
106
  get '/bool', { :bool => 0 }
94
107
  last_response.status.should == 200
95
108
  last_response.body.should == 'FalseClass'
@@ -64,15 +64,21 @@ describe Grape::Validations::PresenceValidator do
64
64
  end
65
65
 
66
66
  it 'validates id' do
67
- post('/')
67
+ post '/'
68
68
  last_response.status.should == 400
69
69
  last_response.body.should == '{"error":"missing parameter: id"}'
70
70
 
71
- post('/', {}, 'rack.input' => StringIO.new('{"id" : "a56b"}'))
71
+ io = StringIO.new('{"id" : "a56b"}')
72
+ post '/', {}, 'rack.input' => io,
73
+ 'CONTENT_TYPE' => 'application/json',
74
+ 'CONTENT_LENGTH' => io.length
72
75
  last_response.body.should == '{"error":"invalid parameter: id"}'
73
76
  last_response.status.should == 400
74
77
 
75
- post('/', {}, 'rack.input' => StringIO.new('{"id" : 56}'))
78
+ io = StringIO.new('{"id" : 56}')
79
+ post '/', {}, 'rack.input' => io,
80
+ 'CONTENT_TYPE' => 'application/json',
81
+ 'CONTENT_LENGTH' => io.length
76
82
  last_response.body.should == '{"ret":56}'
77
83
  last_response.status.should == 201
78
84
  end
@@ -0,0 +1,9 @@
1
+ zh-CN:
2
+ grape:
3
+ errors:
4
+ attributes:
5
+ age: 年龄
6
+ messages:
7
+ coerce: '%{attribute}格式不正确'
8
+ presence: '请填写{attribute}'
9
+ regexp: '%{attribute}格式不正确'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-06 00:00:00.000000000 Z
12
+ date: 2013-01-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -285,6 +285,7 @@ files:
285
285
  - lib/grape/formatter/serializable_hash.rb
286
286
  - lib/grape/formatter/txt.rb
287
287
  - lib/grape/formatter/xml.rb
288
+ - lib/grape/locale/en.yml
288
289
  - lib/grape/middleware/auth/basic.rb
289
290
  - lib/grape/middleware/auth/digest.rb
290
291
  - lib/grape/middleware/auth/oauth2.rb
@@ -300,6 +301,7 @@ files:
300
301
  - lib/grape/parser/json.rb
301
302
  - lib/grape/parser/xml.rb
302
303
  - lib/grape/route.rb
304
+ - lib/grape/util/content_types.rb
303
305
  - lib/grape/util/deep_merge.rb
304
306
  - lib/grape/util/hash_stack.rb
305
307
  - lib/grape/validations.rb
@@ -325,6 +327,7 @@ files:
325
327
  - spec/grape/validations/coerce_spec.rb
326
328
  - spec/grape/validations/presence_spec.rb
327
329
  - spec/grape/validations/regexp_spec.rb
330
+ - spec/grape/validations/zh-CN.yml
328
331
  - spec/grape/validations_spec.rb
329
332
  - spec/shared/versioning_examples.rb
330
333
  - spec/spec_helper.rb
@@ -345,7 +348,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
345
348
  version: '0'
346
349
  segments:
347
350
  - 0
348
- hash: -130218097
351
+ hash: 3430639806354577699
349
352
  required_rubygems_version: !ruby/object:Gem::Requirement
350
353
  none: false
351
354
  requirements:
@@ -354,12 +357,36 @@ required_rubygems_version: !ruby/object:Gem::Requirement
354
357
  version: '0'
355
358
  segments:
356
359
  - 0
357
- hash: -130218097
360
+ hash: 3430639806354577699
358
361
  requirements: []
359
362
  rubyforge_project: grape
360
363
  rubygems_version: 1.8.24
361
364
  signing_key:
362
365
  specification_version: 3
363
366
  summary: A simple Ruby framework for building REST-like APIs.
364
- test_files: []
367
+ test_files:
368
+ - spec/grape/api_spec.rb
369
+ - spec/grape/endpoint_spec.rb
370
+ - spec/grape/entity_spec.rb
371
+ - spec/grape/middleware/auth/basic_spec.rb
372
+ - spec/grape/middleware/auth/digest_spec.rb
373
+ - spec/grape/middleware/auth/oauth2_spec.rb
374
+ - spec/grape/middleware/base_spec.rb
375
+ - spec/grape/middleware/error_spec.rb
376
+ - spec/grape/middleware/exception_spec.rb
377
+ - spec/grape/middleware/formatter_spec.rb
378
+ - spec/grape/middleware/versioner/header_spec.rb
379
+ - spec/grape/middleware/versioner/param_spec.rb
380
+ - spec/grape/middleware/versioner/path_spec.rb
381
+ - spec/grape/middleware/versioner_spec.rb
382
+ - spec/grape/util/hash_stack_spec.rb
383
+ - spec/grape/validations/coerce_spec.rb
384
+ - spec/grape/validations/presence_spec.rb
385
+ - spec/grape/validations/regexp_spec.rb
386
+ - spec/grape/validations/zh-CN.yml
387
+ - spec/grape/validations_spec.rb
388
+ - spec/shared/versioning_examples.rb
389
+ - spec/spec_helper.rb
390
+ - spec/support/basic_auth_encode_helpers.rb
391
+ - spec/support/versioned_helpers.rb
365
392
  has_rdoc: