Skip to content

Commit 7ec3e6d

Browse files
ericproulxdblock
andauthored
Dynamic registration (#2516)
* use `to_enum` instead of including `Enumerable` to attributes_iterator.rb and validation_errors.rb * Remove unused build_coercer.rb * Remove const_missing in api.rb * Add Grape::Util::Registry Add deregister module in spec * Add Grape::Parser::Base and use Grape::Util::Registry * Add Grape::Formatter::Base and use Grape::Util::Registry * Add Grape::Middleware::Versioner::Base and use Grape::Util::Registry * Add Grape::ErrorFormatter::Base and use Grape::Util::Registry * Add Grape::Util::Registry to Grape::Validations ContractScope validator has been moved to validations/validators and renamed properly * Add `deregister in `before(:all)`` * Add `deregister` to Grape::Validations only * Use `prepend` * Fix Ruby 2.7 Fix rubocop * Refactor collection_coercer_for Refactor Grape::Validations::Types cache_key * Add CHANGELOG.md * Revert coercer_cache changes. Will do it another time * Revert enumerable change * Refactor registry * Update CHANGELOG.md Co-authored-by: Daniel (dB.) Doubrovkine <dblock@dblock.org> --------- Co-authored-by: Daniel (dB.) Doubrovkine <dblock@dblock.org>
1 parent 30b3a43 commit 7ec3e6d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+421
-424
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx).
1010
* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx).
1111
* [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx).
12+
* [#2516](https://github.com/ruby-grape/grape/pull/2516): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx).
1213
* Your contribution here.
1314

1415
#### Fixes

lib/grape/api.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,6 @@ def call(...)
7878
instance_for_rack.call(...)
7979
end
8080

81-
# Alleviates problems with autoloading by tring to search for the constant
82-
def const_missing(*args)
83-
if base_instance.const_defined?(*args)
84-
base_instance.const_get(*args)
85-
else
86-
super
87-
end
88-
end
89-
9081
# The remountable class can have a configuration hash to provide some dynamic class-level variables.
9182
# For instance, a description could be done using: `desc configuration[:description]` if it may vary
9283
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration

lib/grape/error_formatter.rb

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,14 @@
22

33
module Grape
44
module ErrorFormatter
5-
module_function
5+
extend Grape::Util::Registry
66

7-
DEFAULTS = {
8-
serializable_hash: Grape::ErrorFormatter::Json,
9-
json: Grape::ErrorFormatter::Json,
10-
jsonapi: Grape::ErrorFormatter::Json,
11-
txt: Grape::ErrorFormatter::Txt,
12-
xml: Grape::ErrorFormatter::Xml
13-
}.freeze
7+
module_function
148

159
def formatter_for(format, error_formatters = nil, default_error_formatter = nil)
16-
select_formatter(error_formatters, format) || default_error_formatter || DEFAULTS[:txt]
17-
end
10+
return error_formatters[format] if error_formatters&.key?(format)
1811

19-
def select_formatter(error_formatters, format)
20-
error_formatters&.key?(format) ? error_formatters[format] : DEFAULTS[format]
12+
registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt
2113
end
2214
end
2315
end

lib/grape/error_formatter/base.rb

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,66 @@
22

33
module Grape
44
module ErrorFormatter
5-
module Base
6-
def present(message, env)
7-
present_options = {}
8-
presented_message = message
9-
if presented_message.is_a?(Hash)
10-
presented_message = presented_message.dup
11-
present_options[:with] = presented_message.delete(:with)
5+
class Base
6+
class << self
7+
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
8+
merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace)
9+
merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception)
10+
11+
wrapped_message = wrap_message(present(message, env))
12+
if wrapped_message.is_a?(Hash)
13+
wrapped_message[:backtrace] = backtrace if merge_backtrace
14+
wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception
15+
end
16+
17+
format_structured_message(wrapped_message)
1218
end
1319

14-
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
20+
def present(message, env)
21+
present_options = {}
22+
presented_message = message
23+
if presented_message.is_a?(Hash)
24+
presented_message = presented_message.dup
25+
present_options[:with] = presented_message.delete(:with)
26+
end
27+
28+
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
29+
30+
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
31+
# env['api.endpoint'].route does not work when the error occurs within a middleware
32+
# the Endpoint does not have a valid env at this moment
33+
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
34+
35+
found_code = http_codes.find do |http_code|
36+
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
37+
end if env[Grape::Env::API_ENDPOINT].request
1538

16-
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
17-
# env['api.endpoint'].route does not work when the error occurs within a middleware
18-
# the Endpoint does not have a valid env at this moment
19-
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
39+
presenter = found_code[2] if found_code
40+
end
2041

21-
found_code = http_codes.find do |http_code|
22-
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
23-
end if env[Grape::Env::API_ENDPOINT].request
42+
if presenter
43+
embeds = { env: env }
44+
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
45+
presented_message = presenter.represent(presented_message, embeds).serializable_hash
46+
end
2447

25-
presenter = found_code[2] if found_code
48+
presented_message
2649
end
2750

28-
if presenter
29-
embeds = { env: env }
30-
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
31-
presented_message = presenter.represent(presented_message, embeds).serializable_hash
51+
def wrap_message(message)
52+
return message if message.is_a?(Hash)
53+
54+
{ message: message }
55+
end
56+
57+
def format_structured_message(_structured_message)
58+
raise NotImplementedError
3259
end
3360

34-
presented_message
61+
def inherited(klass)
62+
super
63+
ErrorFormatter.register(klass)
64+
end
3565
end
3666
end
3767
end

lib/grape/error_formatter/json.rb

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,26 @@
22

33
module Grape
44
module ErrorFormatter
5-
module Json
6-
extend Base
7-
5+
class Json < Base
86
class << self
9-
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
10-
result = wrap_message(present(message, env))
11-
12-
result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash)
13-
14-
::Grape::Json.dump(result)
7+
def format_structured_message(structured_message)
8+
::Grape::Json.dump(structured_message)
159
end
1610

1711
private
1812

1913
def wrap_message(message)
20-
if message.is_a?(Hash)
21-
message
22-
elsif message.is_a?(Exceptions::ValidationErrors)
23-
message.as_json
24-
else
25-
{ error: ensure_utf8(message) }
26-
end
14+
return message if message.is_a?(Hash)
15+
return message.as_json if message.is_a?(Exceptions::ValidationErrors)
16+
17+
{ error: ensure_utf8(message) }
2718
end
2819

2920
def ensure_utf8(message)
3021
return message unless message.respond_to? :encode
3122

3223
message.encode('UTF-8', invalid: :replace, undef: :replace)
3324
end
34-
35-
def merge_rescue_options(result, backtrace, options, original_exception)
36-
rescue_options = options[:rescue_options] || {}
37-
result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
38-
result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception
39-
40-
result
41-
end
4225
end
4326
end
4427
end

lib/grape/error_formatter/jsonapi.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module ErrorFormatter
5+
class Jsonapi < Json; end
6+
end
7+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module ErrorFormatter
5+
class SerializableHash < Json; end
6+
end
7+
end

lib/grape/error_formatter/txt.rb

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@
22

33
module Grape
44
module ErrorFormatter
5-
module Txt
6-
extend Base
7-
8-
class << self
9-
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
10-
message = present(message, env)
11-
12-
result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message
13-
Array.wrap(result).tap do |final_result|
14-
rescue_options = options[:rescue_options] || {}
15-
if rescue_options[:backtrace] && backtrace.present?
16-
final_result << 'backtrace:'
17-
final_result.concat(backtrace)
18-
end
19-
if rescue_options[:original_exception] && original_exception
20-
final_result << 'original exception:'
21-
final_result << original_exception.inspect
22-
end
23-
end.join("\r\n ")
24-
end
5+
class Txt < Base
6+
def self.format_structured_message(structured_message)
7+
message = structured_message[:message] || Grape::Json.dump(structured_message)
8+
Array.wrap(message).tap do |final_message|
9+
if structured_message.key?(:backtrace)
10+
final_message << 'backtrace:'
11+
final_message.concat(structured_message[:backtrace])
12+
end
13+
if structured_message.key?(:original_exception)
14+
final_message << 'original exception:'
15+
final_message << structured_message[:original_exception]
16+
end
17+
end.join("\r\n ")
2518
end
2619
end
2720
end

lib/grape/error_formatter/xml.rb

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,9 @@
22

33
module Grape
44
module ErrorFormatter
5-
module Xml
6-
extend Base
7-
8-
class << self
9-
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
10-
message = present(message, env)
11-
12-
result = message.is_a?(Hash) ? message : { message: message }
13-
rescue_options = options[:rescue_options] || {}
14-
result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
15-
result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception
16-
result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s
17-
end
5+
class Xml < Base
6+
def self.format_structured_message(structured_message)
7+
structured_message.respond_to?(:to_xml) ? structured_message.to_xml(root: :error) : structured_message.to_s
188
end
199
end
2010
end

lib/grape/formatter.rb

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,16 @@
22

33
module Grape
44
module Formatter
5-
module_function
5+
extend Grape::Util::Registry
66

7-
DEFAULTS = {
8-
json: Grape::Formatter::Json,
9-
jsonapi: Grape::Formatter::Json,
10-
serializable_hash: Grape::Formatter::SerializableHash,
11-
txt: Grape::Formatter::Txt,
12-
xml: Grape::Formatter::Xml
13-
}.freeze
7+
module_function
148

159
DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj }
1610

1711
def formatter_for(api_format, formatters)
18-
select_formatter(formatters, api_format) || DEFAULT_LAMBDA_FORMATTER
19-
end
12+
return formatters[api_format] if formatters&.key?(api_format)
2013

21-
def select_formatter(formatters, api_format)
22-
formatters&.key?(api_format) ? formatters[api_format] : DEFAULTS[api_format]
14+
registry[api_format] || DEFAULT_LAMBDA_FORMATTER
2315
end
2416
end
2517
end

lib/grape/formatter/base.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module Formatter
5+
class Base
6+
def self.call(_object, _env)
7+
raise NotImplementedError
8+
end
9+
10+
def self.inherited(klass)
11+
super
12+
Formatter.register(klass)
13+
end
14+
end
15+
end
16+
end

lib/grape/formatter/json.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
module Grape
44
module Formatter
5-
module Json
6-
class << self
7-
def call(object, _env)
8-
return object.to_json if object.respond_to?(:to_json)
5+
class Json < Base
6+
def self.call(object, _env)
7+
return object.to_json if object.respond_to?(:to_json)
98

10-
::Grape::Json.dump(object)
11-
end
9+
::Grape::Json.dump(object)
1210
end
1311
end
1412
end

lib/grape/formatter/serializable_hash.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module Grape
44
module Formatter
5-
module SerializableHash
5+
class SerializableHash < Base
66
class << self
77
def call(object, _env)
88
return object if object.is_a?(String)

lib/grape/formatter/txt.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22

33
module Grape
44
module Formatter
5-
module Txt
6-
class << self
7-
def call(object, _env)
8-
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
9-
end
5+
class Txt < Base
6+
def self.call(object, _env)
7+
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
108
end
119
end
1210
end

lib/grape/formatter/xml.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
module Grape
44
module Formatter
5-
module Xml
6-
class << self
7-
def call(object, _env)
8-
return object.to_xml if object.respond_to?(:to_xml)
5+
class Xml < Base
6+
def self.call(object, _env)
7+
return object.to_xml if object.respond_to?(:to_xml)
98

10-
raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
11-
end
9+
raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
1210
end
1311
end
1412
end

lib/grape/middleware/versioner.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
module Grape
1212
module Middleware
1313
module Versioner
14+
extend Grape::Util::Registry
15+
1416
module_function
1517

1618
# @param strategy [Symbol] :path, :header, :accept_version_header or :param
1719
# @return a middleware class based on strategy
1820
def using(strategy)
19-
Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}")
20-
rescue NameError
21-
raise Grape::Exceptions::InvalidVersionerOption, strategy
21+
raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy)
22+
23+
registry[strategy]
2224
end
2325
end
2426
end

0 commit comments

Comments
 (0)