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
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency 'multi_xml'
24
24
  s.add_runtime_dependency 'hashie', '~> 1.2'
25
25
  s.add_runtime_dependency 'virtus'
26
+ s.add_runtime_dependency 'builder'
26
27
 
27
28
  s.add_development_dependency 'rake'
28
29
  s.add_development_dependency 'maruku'
@@ -2,42 +2,63 @@ require 'rack'
2
2
  require 'rack/builder'
3
3
 
4
4
  module Grape
5
- autoload :API, 'grape/api'
6
- autoload :Endpoint, 'grape/endpoint'
7
- autoload :MiddlewareStack, 'grape/middleware_stack'
8
- autoload :Client, 'grape/client'
9
- autoload :Route, 'grape/route'
10
- autoload :Entity, 'grape/entity'
11
- autoload :Cookies, 'grape/cookies'
12
- autoload :Validations, 'grape/validations'
5
+ autoload :API, 'grape/api'
6
+ autoload :Endpoint, 'grape/endpoint'
7
+ autoload :MiddlewareStack, 'grape/middleware_stack'
8
+ autoload :Client, 'grape/client'
9
+ autoload :Route, 'grape/route'
10
+ autoload :Entity, 'grape/entity'
11
+ autoload :Cookies, 'grape/cookies'
12
+ autoload :Validations, 'grape/validations'
13
13
 
14
14
  module Exceptions
15
- autoload :Base, 'grape/exceptions/base'
15
+ autoload :Base, 'grape/exceptions/base'
16
+ autoload :ValidationError, 'grape/exceptions/validation_error'
17
+ end
18
+
19
+ module ErrorFormatter
20
+ autoload :Base, 'grape/error_formatter/base'
21
+ autoload :Json, 'grape/error_formatter/json'
22
+ autoload :Txt, 'grape/error_formatter/txt'
23
+ autoload :Xml, 'grape/error_formatter/xml'
24
+ end
25
+
26
+ module Formatter
27
+ autoload :Base, 'grape/formatter/base'
28
+ autoload :Json, 'grape/formatter/json'
29
+ autoload :SerializableHash, 'grape/formatter/serializable_hash'
30
+ autoload :Txt, 'grape/formatter/txt'
31
+ autoload :Xml, 'grape/formatter/xml'
32
+ end
33
+
34
+ module Parser
35
+ autoload :Base, 'grape/parser/base'
36
+ autoload :Json, 'grape/parser/json'
37
+ autoload :Xml, 'grape/parser/xml'
16
38
  end
17
- autoload :ValidationError, 'grape/exceptions/validation_error'
18
39
 
19
40
  module Middleware
20
- autoload :Base, 'grape/middleware/base'
21
- autoload :Prefixer, 'grape/middleware/prefixer'
22
- autoload :Versioner, 'grape/middleware/versioner'
23
- autoload :Formatter, 'grape/middleware/formatter'
24
- autoload :Error, 'grape/middleware/error'
41
+ autoload :Base, 'grape/middleware/base'
42
+ autoload :Prefixer, 'grape/middleware/prefixer'
43
+ autoload :Versioner, 'grape/middleware/versioner'
44
+ autoload :Formatter, 'grape/middleware/formatter'
45
+ autoload :Error, 'grape/middleware/error'
25
46
 
26
47
  module Auth
27
- autoload :OAuth2, 'grape/middleware/auth/oauth2'
28
- autoload :Basic, 'grape/middleware/auth/basic'
29
- autoload :Digest, 'grape/middleware/auth/digest'
48
+ autoload :OAuth2, 'grape/middleware/auth/oauth2'
49
+ autoload :Basic, 'grape/middleware/auth/basic'
50
+ autoload :Digest, 'grape/middleware/auth/digest'
30
51
  end
31
52
 
32
53
  module Versioner
33
- autoload :Path, 'grape/middleware/versioner/path'
34
- autoload :Header, 'grape/middleware/versioner/header'
35
- autoload :Param, 'grape/middleware/versioner/param'
54
+ autoload :Path, 'grape/middleware/versioner/path'
55
+ autoload :Header, 'grape/middleware/versioner/header'
56
+ autoload :Param, 'grape/middleware/versioner/param'
36
57
  end
37
58
  end
38
59
 
39
60
  module Util
40
- autoload :HashStack, 'grape/util/hash_stack'
61
+ autoload :HashStack, 'grape/util/hash_stack'
41
62
  end
42
63
  end
43
64
 
@@ -130,10 +130,13 @@ module Grape
130
130
  new_format ? set(:format, new_format.to_sym) : settings[:format]
131
131
  end
132
132
 
133
- # Specify the format for error messages.
134
- # May be `:json` or `:txt` (default).
135
- def error_format(new_format = nil)
136
- new_format ? set(:error_format, new_format.to_sym) : settings[:error_format]
133
+ # Specify a custom formatter for a content-type.
134
+ def formatter(content_type, new_formatter)
135
+ settings.imbue(:formatters, content_type.to_sym => new_formatter)
136
+ end
137
+
138
+ def error_formatter(format, new_formatter)
139
+ settings.imbue(:error_formatters, format.to_sym => new_formatter)
137
140
  end
138
141
 
139
142
  # Specify additional content-types, e.g.:
@@ -11,9 +11,37 @@ module Grape
11
11
  attr_accessor :block, :options, :settings
12
12
  attr_reader :env, :request
13
13
 
14
+ class << self
15
+ # @api private
16
+ #
17
+ # Create an UnboundMethod that is appropriate for executing an endpoint
18
+ # route.
19
+ #
20
+ # The unbound method allows explicit calls to +return+ without raising a
21
+ # +LocalJumpError+. The method will be removed, but a +Proc+ reference to
22
+ # it will be returned. The returned +Proc+ expects a single argument: the
23
+ # instance of +Endpoint+ to bind to the method during the call.
24
+ #
25
+ # @param [String, Symbol] method_name
26
+ # @return [Proc]
27
+ # @raise [NameError] an instance method with the same name already exists
28
+ def generate_api_method(method_name, &block)
29
+ if instance_methods.include?(method_name.to_sym) || instance_methods.include?(method_name.to_s)
30
+ raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name")
31
+ end
32
+ define_method(method_name, &block)
33
+ method = instance_method(method_name)
34
+ remove_method(method_name)
35
+ proc { |endpoint_instance| method.bind(endpoint_instance).call }
36
+ end
37
+ end
38
+
14
39
  def initialize(settings, options = {}, &block)
15
40
  @settings = settings
16
- @block = block
41
+ if block_given?
42
+ method_name = "#{options[:method]} #{settings.gather(:namespace).join( "/")} #{Array(options[:path]).join("/")}"
43
+ @block = self.class.generate_api_method(method_name, &block)
44
+ end
17
45
  @options = options
18
46
 
19
47
  raise ArgumentError, "Must specify :path option." unless options.key?(:path)
@@ -82,10 +110,20 @@ module Grape
82
110
  def prepare_path(path)
83
111
  parts = []
84
112
  parts << settings[:root_prefix] if settings[:root_prefix]
85
- parts << ':version' if settings[:version] && settings[:version_options][:using] == :path
86
- parts << namespace.to_s if namespace
87
- parts << path.to_s if path && '/' != path
88
- Rack::Mount::Utils.normalize_path(parts.join('/') + '(.:format)')
113
+
114
+ uses_path_versioning = settings[:version] && settings[:version_options][:using] == :path
115
+ namespace_is_empty = namespace && (namespace.to_s =~ /^\s*$/ || namespace.to_s == '/')
116
+ path_is_empty = path && (path.to_s =~ /^\s*$/ || path.to_s == '/')
117
+
118
+ parts << ':version' if uses_path_versioning
119
+ if !uses_path_versioning || (!namespace_is_empty || !path_is_empty)
120
+ parts << namespace.to_s if namespace
121
+ parts << path.to_s if path && '/' != path
122
+ format_suffix = '(.:format)'
123
+ else
124
+ format_suffix = '(/.:format)'
125
+ end
126
+ Rack::Mount::Utils.normalize_path(parts.join('/') + format_suffix)
89
127
  end
90
128
 
91
129
  def namespace
@@ -135,7 +173,7 @@ module Grape
135
173
  unless settings[:declared_params]
136
174
  raise ArgumentError, "Tried to filter for declared parameters but none exist."
137
175
  end
138
-
176
+
139
177
  settings[:declared_params].inject({}){|h,k|
140
178
  output_key = options[:stringify] ? k.to_s : k.to_sym
141
179
  if params.key?(output_key) || options[:include_missing]
@@ -324,7 +362,7 @@ module Grape
324
362
 
325
363
  run_filters after_validations
326
364
 
327
- response_text = instance_eval &self.block
365
+ response_text = @block.call(self)
328
366
  run_filters afters
329
367
  cookies.write(header)
330
368
 
@@ -339,7 +377,7 @@ module Grape
339
377
  :default_status => settings[:default_error_status] || 403,
340
378
  :rescue_all => settings[:rescue_all],
341
379
  :rescued_errors => aggregate_setting(:rescued_errors),
342
- :format => settings[:error_format] || :txt,
380
+ :error_formatters => settings[:error_formatters],
343
381
  :rescue_options => settings[:rescue_options],
344
382
  :rescue_handlers => merged_setting(:rescue_handlers)
345
383
 
@@ -357,7 +395,8 @@ module Grape
357
395
  b.use Grape::Middleware::Formatter,
358
396
  :format => settings[:format],
359
397
  :default_format => settings[:default_format] || :txt,
360
- :content_types => settings[:content_types]
398
+ :content_types => settings[:content_types],
399
+ :formatters => settings[:formatters]
361
400
 
362
401
  aggregate_setting(:middleware).each do |m|
363
402
  m = m.dup
@@ -306,7 +306,7 @@ module Grape
306
306
  # hash for the given data model and is used as the basis for serialization to
307
307
  # JSON and other formats.
308
308
  #
309
- # @param options [Hash] Any options you pass in here will be known to the entity
309
+ # @param runtime_options [Hash] Any options you pass in here will be known to the entity
310
310
  # representation, this is where you can trigger things from conditional options
311
311
  # etc.
312
312
  def serializable_hash(runtime_options = {})
@@ -0,0 +1,32 @@
1
+ module Grape
2
+ module ErrorFormatter
3
+ module Base
4
+
5
+ class << self
6
+
7
+ FORMATTERS = {
8
+ :json => Grape::ErrorFormatter::Json,
9
+ :txt => Grape::ErrorFormatter::Txt,
10
+ :xml => Grape::ErrorFormatter::Xml
11
+ }
12
+
13
+ def formatters(options)
14
+ FORMATTERS.merge(options[:error_formatters] || {})
15
+ end
16
+
17
+ def formatter_for(api_format, options = {})
18
+ spec = formatters(options)[api_format]
19
+ case spec
20
+ when nil
21
+ lambda { |message, backtrace, options| message }
22
+ when Symbol
23
+ method(spec)
24
+ else
25
+ spec
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module Grape
2
+ module ErrorFormatter
3
+ module Json
4
+ class << self
5
+
6
+ def call(message, backtrace, options = {}, env = nil)
7
+ result = message.is_a?(Hash) ? message : { :error => message }
8
+ if (options[:rescue_options] || {})[:backtrace] && backtrace && ! backtrace.empty?
9
+ result = result.merge({ :backtrace => backtrace })
10
+ end
11
+ MultiJson.dump(result)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Grape
2
+ module ErrorFormatter
3
+ module Txt
4
+ class << self
5
+
6
+ def call(message, backtrace, options = {}, env = nil)
7
+ result = message.is_a?(Hash) ? MultiJson.dump(message) : message
8
+ if (options[:rescue_options] || {})[:backtrace] && backtrace && ! backtrace.empty?
9
+ result += "\r\n "
10
+ result += backtrace.join("\r\n ")
11
+ end
12
+ result
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module Grape
2
+ module ErrorFormatter
3
+ module Xml
4
+ class << self
5
+
6
+ def call(message, backtrace, options = {}, env = nil)
7
+ result = message.is_a?(Hash) ? message : { :message => message }
8
+ if (options[:rescue_options] || {})[:backtrace] && backtrace && ! backtrace.empty?
9
+ result = result.merge({ :backtrace => backtrace })
10
+ end
11
+ result.respond_to?(:to_xml) ? result.to_xml(:root => :error) : result.to_s
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,10 +1,14 @@
1
1
  require 'grape/exceptions/base'
2
2
 
3
- class ValidationError < Grape::Exceptions::Base
4
- attr_accessor :param
3
+ module Grape
4
+ module Exceptions
5
+ class ValidationError < Grape::Exceptions::Base
6
+ attr_accessor :param
5
7
 
6
- def initialize(args = {})
7
- @param = args[:param].to_s if args.has_key? :param
8
- super
8
+ def initialize(args = {})
9
+ @param = args[:param].to_s if args.has_key? :param
10
+ super
11
+ end
12
+ end
9
13
  end
10
14
  end
@@ -0,0 +1,33 @@
1
+ module Grape
2
+ module Formatter
3
+ module Base
4
+
5
+ class << self
6
+
7
+ FORMATTERS = {
8
+ :json => Grape::Formatter::Json,
9
+ :serializable_hash => Grape::Formatter::SerializableHash,
10
+ :txt => Grape::Formatter::Txt,
11
+ :xml => Grape::Formatter::Xml
12
+ }
13
+
14
+ def formatters(options)
15
+ FORMATTERS.merge(options[:formatters] || {})
16
+ end
17
+
18
+ def formatter_for(api_format, options = {})
19
+ spec = formatters(options)[api_format]
20
+ case spec
21
+ when nil
22
+ lambda { |obj, env| obj }
23
+ when Symbol
24
+ method(spec)
25
+ else
26
+ spec
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ module Grape
2
+ module Formatter
3
+ module Json
4
+ class << self
5
+
6
+ def call(object, env)
7
+ return object if object.is_a?(String)
8
+ return object.to_json if object.respond_to?(:to_json)
9
+ MultiJson.dump(object)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Grape
2
+ module Formatter
3
+ module SerializableHash
4
+ class << self
5
+
6
+ def call(object, env)
7
+ return object if object.is_a?(String)
8
+ return MultiJson.dump(serialize(object)) if serializable?(object)
9
+ return object.to_json if object.respond_to?(:to_json)
10
+ MultiJson.dump(object)
11
+ end
12
+
13
+ private
14
+
15
+ def serializable?(object)
16
+ object.respond_to?(:serializable_hash) ||
17
+ object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) ||
18
+ object.kind_of?(Hash)
19
+ end
20
+
21
+ def serialize(object)
22
+ if object.respond_to? :serializable_hash
23
+ object.serializable_hash
24
+ elsif object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false)
25
+ object.map {|o| o.serializable_hash }
26
+ elsif object.kind_of?(Hash)
27
+ object.inject({}) { |h,(k,v)| h[k] = serialize(v); h }
28
+ else
29
+ object
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ module Grape
2
+ module Formatter
3
+ module Txt
4
+ class << self
5
+
6
+ def call(object, env)
7
+ object.respond_to?(:to_txt) ? object.to_txt : object.to_s
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Grape
2
+ module Formatter
3
+ module Xml
4
+ class << self
5
+
6
+ def call(object, env)
7
+ object.respond_to?(:to_xml) ? object.to_xml : object.to_s
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end