Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic registration #2516

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx).
* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx).
* [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx).
* [#7](https://github.com/ericproulx/grape/pull/7): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
9 changes: 0 additions & 9 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,6 @@ def call(...)
instance_for_rack.call(...)
end

# Alleviates problems with autoloading by tring to search for the constant
def const_missing(*args)
if base_instance.const_defined?(*args)
base_instance.const_get(*args)
else
super
end
end

# The remountable class can have a configuration hash to provide some dynamic class-level variables.
# For instance, a description could be done using: `desc configuration[:description]` if it may vary
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
Expand Down
16 changes: 4 additions & 12 deletions lib/grape/error_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,14 @@

module Grape
module ErrorFormatter
module_function
extend Grape::Util::Registry

DEFAULTS = {
serializable_hash: Grape::ErrorFormatter::Json,
json: Grape::ErrorFormatter::Json,
jsonapi: Grape::ErrorFormatter::Json,
txt: Grape::ErrorFormatter::Txt,
xml: Grape::ErrorFormatter::Xml
}.freeze
module_function

def formatter_for(format, error_formatters = nil, default_error_formatter = nil)
select_formatter(error_formatters, format) || default_error_formatter || DEFAULTS[:txt]
end
return error_formatters[format] if error_formatters&.key?(format)

def select_formatter(error_formatters, format)
error_formatters&.key?(format) ? error_formatters[format] : DEFAULTS[format]
registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt
end
end
end
74 changes: 53 additions & 21 deletions lib/grape/error_formatter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,68 @@

module Grape
module ErrorFormatter
module Base
def present(message, env)
present_options = {}
presented_message = message
if presented_message.is_a?(Hash)
presented_message = presented_message.dup
present_options[:with] = presented_message.delete(:with)
class Base
class << self
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace)
merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception)

wrapped_message = wrap_message(present(message, env))
if wrapped_message.is_a?(Hash)
wrapped_message[:backtrace] = backtrace if merge_backtrace
wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception
end

format_structured_message(wrapped_message)
end

presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
def present(message, env)
present_options = {}
presented_message = message
if presented_message.is_a?(Hash)
presented_message = presented_message.dup
present_options[:with] = presented_message.delete(:with)
end

presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)

unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
# env['api.endpoint'].route does not work when the error occurs within a middleware
# the Endpoint does not have a valid env at this moment
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []

found_code = http_codes.find do |http_code|
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
end if env[Grape::Env::API_ENDPOINT].request

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

found_code = http_codes.find do |http_code|
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
end if env[Grape::Env::API_ENDPOINT].request
if presenter
embeds = { env: env }
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
presented_message = presenter.represent(presented_message, embeds).serializable_hash
end

presenter = found_code[2] if found_code
presented_message
end

if presenter
embeds = { env: env }
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
presented_message = presenter.represent(presented_message, embeds).serializable_hash
def wrap_message(message)
return message if message.is_a?(Hash)

{ message: message }
end

def format_structured_message(_structured_message)
raise NotImplementedError
end

presented_message
def inherited(klass)
super
return if klass.name.blank?

ErrorFormatter.register(klass.name.demodulize.underscore, klass)
end
end
end
end
Expand Down
31 changes: 7 additions & 24 deletions lib/grape/error_formatter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,26 @@

module Grape
module ErrorFormatter
module Json
extend Base

class Json < Base
class << self
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
result = wrap_message(present(message, env))

result = merge_rescue_options(result, backtrace, options, original_exception) if result.is_a?(Hash)

::Grape::Json.dump(result)
def format_structured_message(structured_message)
::Grape::Json.dump(structured_message)
end

private

def wrap_message(message)
if message.is_a?(Hash)
message
elsif message.is_a?(Exceptions::ValidationErrors)
message.as_json
else
{ error: ensure_utf8(message) }
end
return message if message.is_a?(Hash)
return message.as_json if message.is_a?(Exceptions::ValidationErrors)

{ error: ensure_utf8(message) }
end

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

message.encode('UTF-8', invalid: :replace, undef: :replace)
end

def merge_rescue_options(result, backtrace, options, original_exception)
rescue_options = options[:rescue_options] || {}
result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception

result
end
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions lib/grape/error_formatter/jsonapi.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Grape
module ErrorFormatter
class Jsonapi < Json; end
end
end
7 changes: 7 additions & 0 deletions lib/grape/error_formatter/serializable_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Grape
module ErrorFormatter
class SerializableHash < Json; end
end
end
33 changes: 13 additions & 20 deletions lib/grape/error_formatter/txt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@

module Grape
module ErrorFormatter
module Txt
extend Base

class << self
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
message = present(message, env)

result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message
Array.wrap(result).tap do |final_result|
rescue_options = options[:rescue_options] || {}
if rescue_options[:backtrace] && backtrace.present?
final_result << 'backtrace:'
final_result.concat(backtrace)
end
if rescue_options[:original_exception] && original_exception
final_result << 'original exception:'
final_result << original_exception.inspect
end
end.join("\r\n ")
end
class Txt < Base
def self.format_structured_message(structured_message)
message = structured_message[:message] || Grape::Json.dump(structured_message)
Array.wrap(message).tap do |final_message|
if structured_message.key?(:backtrace)
final_message << 'backtrace:'
final_message.concat(structured_message[:backtrace])
end
if structured_message.key?(:original_exception)
final_message << 'original exception:'
final_message << structured_message[:original_exception]
end
end.join("\r\n ")
end
end
end
Expand Down
16 changes: 3 additions & 13 deletions lib/grape/error_formatter/xml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,9 @@

module Grape
module ErrorFormatter
module Xml
extend Base

class << self
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
message = present(message, env)

result = message.is_a?(Hash) ? message : { message: message }
rescue_options = options[:rescue_options] || {}
result = result.merge(backtrace: backtrace) if rescue_options[:backtrace] && backtrace && !backtrace.empty?
result = result.merge(original_exception: original_exception.inspect) if rescue_options[:original_exception] && original_exception
result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s
end
class Xml < Base
def self.format_structured_message(structured_message)
structured_message.respond_to?(:to_xml) ? structured_message.to_xml(root: :error) : structured_message.to_s
end
end
end
Expand Down
16 changes: 4 additions & 12 deletions lib/grape/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,16 @@

module Grape
module Formatter
module_function
extend Grape::Util::Registry

DEFAULTS = {
json: Grape::Formatter::Json,
jsonapi: Grape::Formatter::Json,
serializable_hash: Grape::Formatter::SerializableHash,
txt: Grape::Formatter::Txt,
xml: Grape::Formatter::Xml
}.freeze
module_function

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

def formatter_for(api_format, formatters)
select_formatter(formatters, api_format) || DEFAULT_LAMBDA_FORMATTER
end
return formatters[api_format] if formatters&.key?(api_format)

def select_formatter(formatters, api_format)
formatters&.key?(api_format) ? formatters[api_format] : DEFAULTS[api_format]
registry[api_format] || DEFAULT_LAMBDA_FORMATTER
end
end
end
18 changes: 18 additions & 0 deletions lib/grape/formatter/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Grape
module Formatter
class Base
def self.call(_object, _env)
raise NotImplementedError
end

def self.inherited(klass)
super
return if klass.name.blank?

Formatter.register(klass.name.demodulize.underscore, klass)
end
end
end
end
10 changes: 4 additions & 6 deletions lib/grape/formatter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

module Grape
module Formatter
module Json
class << self
def call(object, _env)
return object.to_json if object.respond_to?(:to_json)
class Json < Base
def self.call(object, _env)
return object.to_json if object.respond_to?(:to_json)

::Grape::Json.dump(object)
end
::Grape::Json.dump(object)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/formatter/serializable_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Grape
module Formatter
module SerializableHash
class SerializableHash < Base
class << self
def call(object, _env)
return object if object.is_a?(String)
Expand Down
8 changes: 3 additions & 5 deletions lib/grape/formatter/txt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

module Grape
module Formatter
module Txt
class << self
def call(object, _env)
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
end
class Txt < Base
def self.call(object, _env)
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
end
end
end
Expand Down
10 changes: 4 additions & 6 deletions lib/grape/formatter/xml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

module Grape
module Formatter
module Xml
class << self
def call(object, _env)
return object.to_xml if object.respond_to?(:to_xml)
class Xml < Base
def self.call(object, _env)
return object.to_xml if object.respond_to?(:to_xml)

raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
end
raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
end
end
end
Expand Down
8 changes: 5 additions & 3 deletions lib/grape/middleware/versioner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
module Grape
module Middleware
module Versioner
extend Grape::Util::Registry

module_function

# @param strategy [Symbol] :path, :header, :accept_version_header or :param
# @return a middleware class based on strategy
def using(strategy)
Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}")
rescue NameError
raise Grape::Exceptions::InvalidVersionerOption, strategy
raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy)

registry[strategy]
end
end
end
Expand Down
Loading
Loading