Skip to content

Commit

Permalink
Dynamic registration (ruby-grape#2516)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

---------

Co-authored-by: Daniel (dB.) Doubrovkine <[email protected]>
  • Loading branch information
ericproulx and dblock authored Dec 27, 2024
1 parent 30b3a43 commit 7ec3e6d
Show file tree
Hide file tree
Showing 42 changed files with 421 additions and 424 deletions.
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).
* [#2516](https://github.com/ruby-grape/grape/pull/2516): 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
72 changes: 51 additions & 21 deletions lib/grape/error_formatter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,66 @@

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
ErrorFormatter.register(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
16 changes: 16 additions & 0 deletions lib/grape/formatter/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

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

def self.inherited(klass)
super
Formatter.register(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

0 comments on commit 7ec3e6d

Please sign in to comment.