diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 634050c85..81d101abb 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -7,6 +7,8 @@ class API # Class methods that we want to call on the API rather than on the API object NON_OVERRIDABLE = %i[call call! configuration compile! inherited recognize_path].freeze + Helpers = Grape::DSL::Helpers::BaseHelper + class Boolean def self.build(val) return nil if val != true && val != false @@ -15,10 +17,6 @@ def self.build(val) end end - class Instance - Boolean = Grape::API::Boolean - end - class << self extend Forwardable attr_accessor :base_instance, :instances diff --git a/lib/grape/api/helpers.rb b/lib/grape/api/helpers.rb deleted file mode 100644 index 00da38f86..000000000 --- a/lib/grape/api/helpers.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Grape - class API - module Helpers - include Grape::DSL::Helpers::BaseHelper - end - end -end diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 190b647e4..b691df692 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -5,8 +5,18 @@ class API # The API Instance class, is the engine behind Grape::API. Each class that inherits # from this will represent a different API instance class Instance + extend Grape::DSL::Settings + extend Grape::DSL::Desc + extend Grape::DSL::Validations + extend Grape::DSL::Callbacks + extend Grape::DSL::Logger + extend Grape::DSL::Middleware + extend Grape::DSL::RequestResponse + extend Grape::DSL::Routing + extend Grape::DSL::Helpers extend Grape::Middleware::Auth::DSL - include Grape::DSL::API + + Boolean = Grape::API::Boolean class << self attr_reader :instance, :base @@ -128,7 +138,7 @@ def evaluate_as_instance_with_configuration(block, lazy: false) def inherited(subclass) super subclass.reset! - subclass.logger = logger.clone + subclass.logger logger.clone end def inherit_settings(other_settings) diff --git a/lib/grape/dsl/api.rb b/lib/grape/dsl/api.rb deleted file mode 100644 index 263617cbe..000000000 --- a/lib/grape/dsl/api.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Grape - module DSL - module API - extend ActiveSupport::Concern - - include Grape::DSL::Validations - include Grape::DSL::Callbacks - include Grape::DSL::Configuration - include Grape::DSL::Helpers - include Grape::DSL::Middleware - include Grape::DSL::RequestResponse - include Grape::DSL::Routing - end - end -end diff --git a/lib/grape/dsl/callbacks.rb b/lib/grape/dsl/callbacks.rb index 43d4b18dc..5f3b3913a 100644 --- a/lib/grape/dsl/callbacks.rb +++ b/lib/grape/dsl/callbacks.rb @@ -2,66 +2,16 @@ module Grape module DSL - # Blocks can be executed before or after every API call, using `before`, `after`, - # `before_validation` and `after_validation`. - # - # Before and after callbacks execute in the following order: - # - # 1. `before` - # 2. `before_validation` - # 3. _validations_ - # 4. `after_validation` - # 5. _the API call_ - # 6. `after` - # - # Steps 4, 5 and 6 only happen if validation succeeds. module Callbacks - extend ActiveSupport::Concern + # before: execute the given block before validation, coercion, or any endpoint + # before_validation: execute the given block after `before`, but prior to validation or coercion + # after_validation: execute the given block after validations and coercions, but before any endpoint code + # after: execute the given block after the endpoint code has run except in unsuccessful + # finally: execute the given block after the endpoint code even if unsuccessful - include Grape::DSL::Configuration - - module ClassMethods - # Execute the given block before validation, coercion, or any endpoint - # code is executed. - def before(&block) - namespace_stackable(:befores, block) - end - - # Execute the given block after `before`, but prior to validation or - # coercion. - def before_validation(&block) - namespace_stackable(:before_validations, block) - end - - # Execute the given block after validations and coercions, but before - # any endpoint code. - def after_validation(&block) - namespace_stackable(:after_validations, block) - end - - # Execute the given block after the endpoint code has run. - def after(&block) - namespace_stackable(:afters, block) - end - - # Allows you to specify a something that will always be executed after a call - # API call. Unlike the `after` block, this code will run even on - # unsuccesful requests. - # @example - # class ExampleAPI < Grape::API - # before do - # ApiLogger.start - # end - # finally do - # ApiLogger.close - # end - # end - # - # This will make sure that the ApiLogger is opened and closed around every - # request - # @param ensured_block [Proc] The block to be executed after every api_call - def finally(&block) - namespace_stackable(:finallies, block) + %w[before before_validation after_validation after finally].each do |callback_method| + define_method callback_method.to_sym do |&block| + namespace_stackable(callback_method.pluralize.to_sym, block) end end end diff --git a/lib/grape/dsl/configuration.rb b/lib/grape/dsl/configuration.rb deleted file mode 100644 index 6abf75968..000000000 --- a/lib/grape/dsl/configuration.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Grape - module DSL - module Configuration - extend ActiveSupport::Concern - - module ClassMethods - include Grape::DSL::Settings - include Grape::DSL::Logger - include Grape::DSL::Desc - end - end - end -end diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index 2d3155026..954820bdd 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -3,8 +3,6 @@ module Grape module DSL module Desc - include Grape::DSL::Settings - ROUTE_ATTRIBUTES = %i[ body_name consumes diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index 875fee511..4338ff1ac 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -3,81 +3,76 @@ module Grape module DSL module Helpers - extend ActiveSupport::Concern - include Grape::DSL::Configuration - - module ClassMethods - # Add helper methods that will be accessible from any - # endpoint within this namespace (and child namespaces). - # - # When called without a block, all known helpers within this scope - # are included. - # - # @param [Array] new_modules optional array of modules to include - # @param [Block] block optional block of methods to include - # - # @example Define some helpers. - # - # class ExampleAPI < Grape::API - # helpers do - # def current_user - # User.find_by_id(params[:token]) - # end - # end - # end - # - # @example Include many modules - # - # class ExampleAPI < Grape::API - # helpers Authentication, Mailer, OtherModule - # end - # - def helpers(*new_modules, &block) - include_new_modules(new_modules) - include_block(block) - include_all_in_scope if !block && new_modules.empty? - end + # Add helper methods that will be accessible from any + # endpoint within this namespace (and child namespaces). + # + # When called without a block, all known helpers within this scope + # are included. + # + # @param [Array] new_modules optional array of modules to include + # @param [Block] block optional block of methods to include + # + # @example Define some helpers. + # + # class ExampleAPI < Grape::API + # helpers do + # def current_user + # User.find_by_id(params[:token]) + # end + # end + # end + # + # @example Include many modules + # + # class ExampleAPI < Grape::API + # helpers Authentication, Mailer, OtherModule + # end + # + def helpers(*new_modules, &block) + include_new_modules(new_modules) + include_block(block) + include_all_in_scope if !block && new_modules.empty? + end - protected + protected - def include_new_modules(modules) - return if modules.empty? + def include_new_modules(modules) + return if modules.empty? - modules.each { |mod| make_inclusion(mod) } - end + modules.each { |mod| make_inclusion(mod) } + end - def include_block(block) - return unless block + def include_block(block) + return unless block - Module.new.tap do |mod| - make_inclusion(mod) { mod.class_eval(&block) } - end + Module.new.tap do |mod| + make_inclusion(mod) { mod.class_eval(&block) } end + end - def make_inclusion(mod, &block) - define_boolean_in_mod(mod) - inject_api_helpers_to_mod(mod, &block) - namespace_stackable(:helpers, mod) - end + def make_inclusion(mod, &block) + define_boolean_in_mod(mod) + inject_api_helpers_to_mod(mod, &block) + namespace_stackable(:helpers, mod) + end - def include_all_in_scope - Module.new.tap do |mod| - namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include } - change! - end + def include_all_in_scope + Module.new.tap do |mod| + namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include } + change! end + end - def define_boolean_in_mod(mod) - return if defined? mod::Boolean + def define_boolean_in_mod(mod) + return if defined? mod::Boolean - mod.const_set(:Boolean, Grape::API::Boolean) - end + mod.const_set(:Boolean, Grape::API::Boolean) + end - def inject_api_helpers_to_mod(mod, &block) - mod.extend(BaseHelper) unless mod.is_a?(BaseHelper) - yield if block - mod.api_changed(self) - end + def inject_api_helpers_to_mod(mod, &block) + mod.extend(BaseHelper) unless mod.is_a?(BaseHelper) + yield if block + mod.api_changed(self) end # This module extends user defined helpers diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 2d7568afa..4072011fd 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -3,10 +3,6 @@ module Grape module DSL module InsideRoute - extend ActiveSupport::Concern - include Grape::DSL::Settings - include Grape::DSL::Headers - # Denotes a situation where a DSL method has been invoked in a # filter which it should not yet be available in class MethodNotYetAvailable < StandardError; end diff --git a/lib/grape/dsl/logger.rb b/lib/grape/dsl/logger.rb index 516cdcd7d..c0b38b6f1 100644 --- a/lib/grape/dsl/logger.rb +++ b/lib/grape/dsl/logger.rb @@ -3,10 +3,6 @@ module Grape module DSL module Logger - include Grape::DSL::Settings - - attr_writer :logger - # Set or retrive the configured logger. If none was configured, this # method will create a new one, logging to stdout. # @param logger [Object] the new logger to use diff --git a/lib/grape/dsl/middleware.rb b/lib/grape/dsl/middleware.rb index 07e6f9fe7..c745b0c69 100644 --- a/lib/grape/dsl/middleware.rb +++ b/lib/grape/dsl/middleware.rb @@ -3,51 +3,33 @@ module Grape module DSL module Middleware - extend ActiveSupport::Concern - - include Grape::DSL::Configuration - - module ClassMethods - # Apply a custom middleware to the API. Applies - # to the current namespace and any children, but - # not parents. - # - # @param middleware_class [Class] The class of the middleware you'd like - # to inject. - def use(middleware_class, *args, &block) - arr = [:use, middleware_class, *args] - arr << block if block - - namespace_stackable(:middleware, arr) - end - - def insert(*args, &block) - arr = [:insert, *args] - arr << block if block - - namespace_stackable(:middleware, arr) - end - - def insert_before(*args, &block) - arr = [:insert_before, *args] - arr << block if block - - namespace_stackable(:middleware, arr) - end + # Apply a custom middleware to the API. Applies + # to the current namespace and any children, but + # not parents. + # + # @param middleware_class [Class] The class of the middleware you'd like + # to inject. + def use(middleware_class, *args, &block) + arr = [:use, middleware_class, *args] + arr << block if block + + namespace_stackable(:middleware, arr) + end - def insert_after(*args, &block) - arr = [:insert_after, *args] + %i[insert insert_before insert_after].each do |method_name| + define_method method_name do |*args, &block| + arr = [method_name, *args] arr << block if block namespace_stackable(:middleware, arr) end + end - # Retrieve an array of the middleware classes - # and arguments that are currently applied to the - # application. - def middleware - namespace_stackable(:middleware) || [] - end + # Retrieve an array of the middleware classes + # and arguments that are currently applied to the + # application. + def middleware + namespace_stackable(:middleware) || [] end end end diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 6002aff66..e877e030a 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -6,8 +6,6 @@ module DSL # and describe the parameters accepted by an endpoint, or all endpoints # within a namespace. module Parameters - extend ActiveSupport::Concern - # Set the module used to build the request.params. # # @param build_with the ParamBuilder module to use when building request.params diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 75bf8d703..91523d83a 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -3,168 +3,162 @@ module Grape module DSL module RequestResponse - extend ActiveSupport::Concern + # Specify the default format for the API's serializers. + # May be `:json` or `:txt` (default). + def default_format(new_format = nil) + namespace_inheritable(:default_format, new_format&.to_sym) + end - include Grape::DSL::Configuration + # Specify the format for the API's serializers. + # May be `:json`, `:xml`, `:txt`, etc. + def format(new_format = nil) + return namespace_inheritable(:format) unless new_format - module ClassMethods - # Specify the default format for the API's serializers. - # May be `:json` or `:txt` (default). - def default_format(new_format = nil) - namespace_inheritable(:default_format, new_format&.to_sym) - end + symbolic_new_format = new_format.to_sym + namespace_inheritable(:format, symbolic_new_format) + namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(symbolic_new_format)) - # Specify the format for the API's serializers. - # May be `:json`, `:xml`, `:txt`, etc. - def format(new_format = nil) - return namespace_inheritable(:format) unless new_format + content_type = content_types[symbolic_new_format] + raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type - symbolic_new_format = new_format.to_sym - namespace_inheritable(:format, symbolic_new_format) - namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(symbolic_new_format)) + namespace_stackable(:content_types, symbolic_new_format => content_type) + end - content_type = content_types[symbolic_new_format] - raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type + # Specify a custom formatter for a content-type. + def formatter(content_type, new_formatter) + namespace_stackable(:formatters, content_type.to_sym => new_formatter) + end - namespace_stackable(:content_types, symbolic_new_format => content_type) - end + # Specify a custom parser for a content-type. + def parser(content_type, new_parser) + namespace_stackable(:parsers, content_type.to_sym => new_parser) + end - # Specify a custom formatter for a content-type. - def formatter(content_type, new_formatter) - namespace_stackable(:formatters, content_type.to_sym => new_formatter) - end + # Specify a default error formatter. + def default_error_formatter(new_formatter_name = nil) + return namespace_inheritable(:default_error_formatter) unless new_formatter_name - # Specify a custom parser for a content-type. - def parser(content_type, new_parser) - namespace_stackable(:parsers, content_type.to_sym => new_parser) - end + new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) + namespace_inheritable(:default_error_formatter, new_formatter) + end - # Specify a default error formatter. - def default_error_formatter(new_formatter_name = nil) - return namespace_inheritable(:default_error_formatter) unless new_formatter_name + def error_formatter(format, options) + formatter = if options.is_a?(Hash) && options.key?(:with) + options[:with] + else + options + end - new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) - namespace_inheritable(:default_error_formatter, new_formatter) - end + namespace_stackable(:error_formatters, format.to_sym => formatter) + end - def error_formatter(format, options) - formatter = if options.is_a?(Hash) && options.key?(:with) - options[:with] - else - options - end + # Specify additional content-types, e.g.: + # content_type :xls, 'application/vnd.ms-excel' + def content_type(key, val) + namespace_stackable(:content_types, key.to_sym => val) + end - namespace_stackable(:error_formatters, format.to_sym => formatter) - end + # All available content types. + def content_types + c_types = namespace_stackable_with_hash(:content_types) + Grape::ContentTypes.content_types_for c_types + end - # Specify additional content-types, e.g.: - # content_type :xls, 'application/vnd.ms-excel' - def content_type(key, val) - namespace_stackable(:content_types, key.to_sym => val) - end + # Specify the default status code for errors. + def default_error_status(new_status = nil) + namespace_inheritable(:default_error_status, new_status) + end - # All available content types. - def content_types - c_types = namespace_stackable_with_hash(:content_types) - Grape::ContentTypes.content_types_for c_types + # Allows you to rescue certain exceptions that occur to return + # a grape error rather than raising all the way to the + # server level. + # + # @example Rescue from custom exceptions + # class ExampleAPI < Grape::API + # class CustomError < StandardError; end + # + # rescue_from CustomError + # end + # + # @overload rescue_from(*exception_classes, **options) + # @param [Array] exception_classes A list of classes that you want to rescue, or + # the symbol :all to rescue from all exceptions. + # @param [Block] block Execution block to handle the given exception. + # @param [Hash] options Options for the rescue usage. + # @option options [Boolean] :backtrace Include a backtrace in the rescue response. + # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes + # @param [Proc] handler Execution proc to handle the given exception as an + # alternative to passing a block. + def rescue_from(*args, &block) + if args.last.is_a?(Proc) + handler = args.pop + elsif block + handler = block end - # Specify the default status code for errors. - def default_error_status(new_status = nil) - namespace_inheritable(:default_error_status, new_status) + options = args.extract_options! + raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with) + + handler ||= extract_with(options) + + if args.include?(:all) + namespace_inheritable(:rescue_all, true) + namespace_inheritable(:all_rescue_handler, handler) + elsif args.include?(:grape_exceptions) + namespace_inheritable(:rescue_all, true) + namespace_inheritable(:rescue_grape_exceptions, true) + namespace_inheritable(:grape_exceptions_rescue_handler, handler) + else + handler_type = + case options[:rescue_subclasses] + when nil, true + :rescue_handlers + else + :base_only_rescue_handlers + end + + namespace_reverse_stackable(handler_type, args.to_h { |arg| [arg, handler] }) end - # Allows you to rescue certain exceptions that occur to return - # a grape error rather than raising all the way to the - # server level. - # - # @example Rescue from custom exceptions - # class ExampleAPI < Grape::API - # class CustomError < StandardError; end - # - # rescue_from CustomError - # end - # - # @overload rescue_from(*exception_classes, **options) - # @param [Array] exception_classes A list of classes that you want to rescue, or - # the symbol :all to rescue from all exceptions. - # @param [Block] block Execution block to handle the given exception. - # @param [Hash] options Options for the rescue usage. - # @option options [Boolean] :backtrace Include a backtrace in the rescue response. - # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes - # @param [Proc] handler Execution proc to handle the given exception as an - # alternative to passing a block. - def rescue_from(*args, &block) - if args.last.is_a?(Proc) - handler = args.pop - elsif block - handler = block - end - - options = args.extract_options! - raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with) - - handler ||= extract_with(options) - - if args.include?(:all) - namespace_inheritable(:rescue_all, true) - namespace_inheritable(:all_rescue_handler, handler) - elsif args.include?(:grape_exceptions) - namespace_inheritable(:rescue_all, true) - namespace_inheritable(:rescue_grape_exceptions, true) - namespace_inheritable(:grape_exceptions_rescue_handler, handler) - else - handler_type = - case options[:rescue_subclasses] - when nil, true - :rescue_handlers - else - :base_only_rescue_handlers - end - - namespace_reverse_stackable(handler_type, args.to_h { |arg| [arg, handler] }) - end - - namespace_stackable(:rescue_options, options) - end + namespace_stackable(:rescue_options, options) + end - # Allows you to specify a default representation entity for a - # class. This allows you to map your models to their respective - # entities once and then simply call `present` with the model. - # - # @example - # class ExampleAPI < Grape::API - # represent User, with: Entity::User - # - # get '/me' do - # present current_user # with: Entity::User is assumed - # end - # end - # - # Note that Grape will automatically go up the class ancestry to - # try to find a representing entity, so if you, for example, define - # an entity to represent `Object` then all presented objects will - # bubble up and utilize the entity provided on that `represent` call. - # - # @param model_class [Class] The model class that will be represented. - # @option options [Class] :with The entity class that will represent the model. - def represent(model_class, options) - raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class) - - namespace_stackable(:representations, model_class => options[:with]) - end + # Allows you to specify a default representation entity for a + # class. This allows you to map your models to their respective + # entities once and then simply call `present` with the model. + # + # @example + # class ExampleAPI < Grape::API + # represent User, with: Entity::User + # + # get '/me' do + # present current_user # with: Entity::User is assumed + # end + # end + # + # Note that Grape will automatically go up the class ancestry to + # try to find a representing entity, so if you, for example, define + # an entity to represent `Object` then all presented objects will + # bubble up and utilize the entity provided on that `represent` call. + # + # @param model_class [Class] The model class that will be represented. + # @option options [Class] :with The entity class that will represent the model. + def represent(model_class, options) + raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class) + + namespace_stackable(:representations, model_class => options[:with]) + end - private + private - def extract_with(options) - return unless options.key?(:with) + def extract_with(options) + return unless options.key?(:with) - with_option = options.delete(:with) - return with_option if with_option.instance_of?(Proc) - return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String) + with_option = options.delete(:with) + return with_option if with_option.instance_of?(Proc) + return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String) - raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc" - end + raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc" end end end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 4db89e5d3..62c360670 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -3,245 +3,240 @@ module Grape module DSL module Routing - extend ActiveSupport::Concern - include Grape::DSL::Configuration - - module ClassMethods - attr_reader :endpoints - - # Specify an API version. - # - # @example API with legacy support. - # class MyAPI < Grape::API - # version 'v2' - # - # get '/main' do - # {some: 'data'} - # end - # - # version 'v1' do - # get '/main' do - # {legacy: 'data'} - # end - # end - # end - # - def version(*args, &block) - if args.any? - options = args.extract_options! - options = options.reverse_merge(using: :path) - requested_versions = args.flatten.map(&:to_s) - - raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) - - @versions = versions | requested_versions - - if block - within_namespace do - namespace_inheritable(:version, requested_versions) - namespace_inheritable(:version_options, options) - - instance_eval(&block) - end - else + attr_reader :endpoints + + # Specify an API version. + # + # @example API with legacy support. + # class MyAPI < Grape::API + # version 'v2' + # + # get '/main' do + # {some: 'data'} + # end + # + # version 'v1' do + # get '/main' do + # {legacy: 'data'} + # end + # end + # end + # + def version(*args, &block) + if args.any? + options = args.extract_options! + options = options.reverse_merge(using: :path) + requested_versions = args.flatten.map(&:to_s) + + raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) + + @versions = versions | requested_versions + + if block + within_namespace do namespace_inheritable(:version, requested_versions) namespace_inheritable(:version_options, options) + + instance_eval(&block) end + else + namespace_inheritable(:version, requested_versions) + namespace_inheritable(:version_options, options) end - - @versions.last if instance_variable_defined?(:@versions) && @versions end - # Define a root URL prefix for your entire API. - def prefix(prefix = nil) - namespace_inheritable(:root_prefix, prefix&.to_s) - end + @versions.last if instance_variable_defined?(:@versions) && @versions + end - # Create a scope without affecting the URL. - # - # @param _name [Symbol] Purely placebo, just allows to name the scope to - # make the code more readable. - def scope(_name = nil, &block) - within_namespace do - nest(block) - end - end + # Define a root URL prefix for your entire API. + def prefix(prefix = nil) + namespace_inheritable(:root_prefix, prefix&.to_s) + end - def build_with(build_with) - namespace_inheritable(:build_params_with, build_with) + # Create a scope without affecting the URL. + # + # @param _name [Symbol] Purely placebo, just allows to name the scope to + # make the code more readable. + def scope(_name = nil, &block) + within_namespace do + nest(block) end + end - # Do not route HEAD requests to GET requests automatically. - def do_not_route_head! - namespace_inheritable(:do_not_route_head, true) - end + def build_with(build_with) + namespace_inheritable(:build_params_with, build_with) + end - # Do not automatically route OPTIONS. - def do_not_route_options! - namespace_inheritable(:do_not_route_options, true) - end + # Do not route HEAD requests to GET requests automatically. + def do_not_route_head! + namespace_inheritable(:do_not_route_head, true) + end - def lint! - namespace_inheritable(:lint, true) - end + # Do not automatically route OPTIONS. + def do_not_route_options! + namespace_inheritable(:do_not_route_options, true) + end - def do_not_document! - namespace_inheritable(:do_not_document, true) - end + def lint! + namespace_inheritable(:lint, true) + end - def mount(mounts, *opts) - mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) - mounts.each_pair do |app, path| - if app.respond_to?(:mount_instance) - opts_with = opts.any? ? opts.first[:with] : {} - mount({ app.mount_instance(configuration: opts_with) => path }, *opts) - next - end - in_setting = inheritable_setting + def do_not_document! + namespace_inheritable(:do_not_document, true) + end - if app.respond_to?(:inheritable_setting, true) - mount_path = Grape::Router.normalize_path(path) - app.top_level_setting.namespace_stackable[:mount_path] = mount_path + def mount(mounts, *opts) + mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) + mounts.each_pair do |app, path| + if app.respond_to?(:mount_instance) + opts_with = opts.any? ? opts.first[:with] : {} + mount({ app.mount_instance(configuration: opts_with) => path }, *opts) + next + end + in_setting = inheritable_setting - app.inherit_settings(inheritable_setting) + if app.respond_to?(:inheritable_setting, true) + mount_path = Grape::Router.normalize_path(path) + app.top_level_setting.namespace_stackable[:mount_path] = mount_path - in_setting = app.top_level_setting + app.inherit_settings(inheritable_setting) - app.change! - change! - end + in_setting = app.top_level_setting - # When trying to mount multiple times the same endpoint, remove the previous ones - # from the list of endpoints if refresh_already_mounted parameter is true - refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false - if refresh_already_mounted && !endpoints.empty? - endpoints.delete_if do |endpoint| - endpoint.options[:app].to_s == app.to_s - end - end + app.change! + change! + end - endpoints << Grape::Endpoint.new( - in_setting, - method: :any, - path: path, - app: app, - route_options: { anchor: false }, - forward_match: !app.respond_to?(:inheritable_setting), - for: self - ) + # When trying to mount multiple times the same endpoint, remove the previous ones + # from the list of endpoints if refresh_already_mounted parameter is true + refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false + if refresh_already_mounted && !endpoints.empty? + endpoints.delete_if do |endpoint| + endpoint.options[:app].to_s == app.to_s + end end - end - # Defines a route that will be recognized - # by the Grape API. - # - # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted. - # @param paths [String] One or more strings representing the URL segment(s) for this route. - # - # @example Defining a basic route. - # class MyAPI < Grape::API - # route(:any, '/hello') do - # {hello: 'world'} - # end - # end - def route(methods, paths = ['/'], route_options = {}, &block) - methods = '*' if methods == :any - endpoint_options = { - method: methods, - path: paths, - for: self, - route_options: { - params: namespace_stackable_with_hash(:params) || {} - }.deep_merge(route_setting(:description) || {}).deep_merge(route_options || {}) - } - - new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block) - endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) } - - route_end - reset_validations! + endpoints << Grape::Endpoint.new( + in_setting, + method: :any, + path: path, + app: app, + route_options: { anchor: false }, + forward_match: !app.respond_to?(:inheritable_setting), + for: self + ) end + end - Grape::HTTP_SUPPORTED_METHODS.each do |supported_method| - define_method supported_method.downcase do |*args, &block| - options = args.extract_options! - paths = args.first || ['/'] - route(supported_method, paths, options, &block) - end + # Defines a route that will be recognized + # by the Grape API. + # + # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted. + # @param paths [String] One or more strings representing the URL segment(s) for this route. + # + # @example Defining a basic route. + # class MyAPI < Grape::API + # route(:any, '/hello') do + # {hello: 'world'} + # end + # end + def route(methods, paths = ['/'], route_options = {}, &block) + methods = '*' if methods == :any + endpoint_options = { + method: methods, + path: paths, + for: self, + route_options: { + params: namespace_stackable_with_hash(:params) || {} + }.deep_merge(route_setting(:description) || {}).deep_merge(route_options || {}) + } + + new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block) + endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) } + + route_end + reset_validations! + end + + Grape::HTTP_SUPPORTED_METHODS.each do |supported_method| + define_method supported_method.downcase do |*args, &block| + options = args.extract_options! + paths = args.first || ['/'] + route(supported_method, paths, options, &block) end + end - # Declare a "namespace", which prefixes all subordinate routes with its - # name. Any endpoints within a namespace, group, resource or segment, - # etc., will share their parent context as well as any configuration - # done in the namespace context. - # - # @example - # - # namespace :foo do - # get 'bar' do - # # defines the endpoint: GET /foo/bar - # end - # end - def namespace(space = nil, options = {}, &block) - return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block - - within_namespace do - nest(block) do - namespace_stackable(:namespace, Namespace.new(space, options)) if space - end + # Declare a "namespace", which prefixes all subordinate routes with its + # name. Any endpoints within a namespace, group, resource or segment, + # etc., will share their parent context as well as any configuration + # done in the namespace context. + # + # @example + # + # namespace :foo do + # get 'bar' do + # # defines the endpoint: GET /foo/bar + # end + # end + def namespace(space = nil, options = {}, &block) + return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block + + within_namespace do + nest(block) do + namespace_stackable(:namespace, Namespace.new(space, options)) if space end end + end - alias group namespace - alias resource namespace - alias resources namespace - alias segment namespace + alias group namespace + alias resource namespace + alias resources namespace + alias segment namespace - # An array of API routes. - def routes - @routes ||= prepare_routes - end + # An array of API routes. + def routes + @routes ||= prepare_routes + end - # Remove all defined routes. - def reset_routes! - endpoints.each(&:reset_routes!) - @routes = nil - end + # Remove all defined routes. + def reset_routes! + endpoints.each(&:reset_routes!) + @routes = nil + end - def reset_endpoints! - @endpoints = [] - end + def reset_endpoints! + @endpoints = [] + end - # This method allows you to quickly define a parameter route segment - # in your API. - # - # @param param [Symbol] The name of the parameter you wish to declare. - # @option options [Regexp] You may supply a regular expression that the declared parameter must meet. - def route_param(param, options = {}, &block) - options = options.dup + # This method allows you to quickly define a parameter route segment + # in your API. + # + # @param param [Symbol] The name of the parameter you wish to declare. + # @option options [Regexp] You may supply a regular expression that the declared parameter must meet. + def route_param(param, options = {}, &block) + options = options.dup - options[:requirements] = { - param.to_sym => options[:requirements] - } if options[:requirements].is_a?(Regexp) + options[:requirements] = { + param.to_sym => options[:requirements] + } if options[:requirements].is_a?(Regexp) - Grape::Validations::ParamsScope.new(api: self) do - requires param, type: options[:type] - end if options.key?(:type) + Grape::Validations::ParamsScope.new(api: self) do + requires param, type: options[:type] + end if options.key?(:type) - namespace(":#{param}", options, &block) - end + namespace(":#{param}", options, &block) + end - # @return array of defined versions - def versions - @versions ||= [] - end + # @return array of defined versions + def versions + @versions ||= [] + end - private + private - def refresh_mounted_api(mounts, *opts) - opts << { refresh_already_mounted: true } - mount(mounts, *opts) - end + def refresh_mounted_api(mounts, *opts) + opts << { refresh_already_mounted: true } + mount(mounts, *opts) end end end diff --git a/lib/grape/dsl/settings.rb b/lib/grape/dsl/settings.rb index 5c145a467..bbd29d1a7 100644 --- a/lib/grape/dsl/settings.rb +++ b/lib/grape/dsl/settings.rb @@ -7,13 +7,21 @@ module DSL # matter where they're defined, and inheritable settings which apply only # in the current scope and scopes nested under it. module Settings - extend ActiveSupport::Concern + extend Forwardable attr_writer :inheritable_setting, :top_level_setting + def_delegators :inheritable_setting, :route_end + # Fetch our top-level settings, which apply to all endpoints in the API. def top_level_setting - @top_level_setting ||= build_top_level_setting + @top_level_setting ||= Grape::Util::InheritableSetting.new.tap do |setting| + # Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to + # +inheritable_setting+, however, it doesn't contain any user-defined settings. + # Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+ + # in the chain for every endpoint. + setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance + end end # Fetch our current inheritable settings, which are inherited by @@ -42,46 +50,31 @@ def get_or_set(type, key, value) end end - # @param key [Symbol] - # @param value [Object] - # @return (see #get_or_set) - def global_setting(key, value = nil) - get_or_set :global, key, value - end - - # @param key [Symbol] - def unset_global_setting(key) - unset :global, key - end - - # (see #global_setting) - def route_setting(key, value = nil) - get_or_set :route, key, value - end - - # (see #unset_global_setting) - def unset_route_setting(key) - unset :route, key - end + # defines the following methods: + # - namespace_inheritable + # - namespace_stackable - # (see #global_setting) - def namespace_setting(key, value = nil) - get_or_set :namespace, key, value + %i[namespace_inheritable namespace_stackable].each do |method_name| + define_method method_name do |key, value = nil| + get_or_set method_name, key, value + end end - # (see #unset_global_setting) - def unset_namespace_setting(key) - unset :namespace, key + def unset_namespace_stackable(*keys) + keys.each do |key| + unset :namespace_stackable, key + end end - # (see #global_setting) - def namespace_inheritable(key, value = nil) - get_or_set :namespace_inheritable, key, value - end + # defines the following methods: + # - global_setting + # - route_setting + # - namespace_setting - # (see #unset_global_setting) - def unset_namespace_inheritable(key) - unset :namespace_inheritable, key + %i[global route namespace].each do |method_name| + define_method :"#{method_name}_setting" do |key, value = nil| + get_or_set method_name, key, value + end end # @param key [Symbol] @@ -89,11 +82,6 @@ def namespace_inheritable_to_nil(key) inheritable_setting.namespace_inheritable[key] = nil end - # (see #global_setting) - def namespace_stackable(key, value = nil) - get_or_set :namespace_stackable, key, value - end - def namespace_reverse_stackable(key, value = nil) get_or_set :namespace_reverse_stackable, key, value end @@ -114,21 +102,6 @@ def namespace_reverse_stackable_with_hash(key) end end - # (see #unset_global_setting) - def unset_namespace_stackable(key) - unset :namespace_stackable, key - end - - # (see #global_setting) - def api_class_setting(key, value = nil) - get_or_set :api_class, key, value - end - - # (see #unset_global_setting) - def unset_api_class_setting(key) - unset :api_class, key - end - # Fork our inheritable settings to a new instance, copied from our # parent's, but separate so we won't modify it. Every call to this # method should have an answering call to #namespace_end. @@ -142,12 +115,6 @@ def namespace_end @inheritable_setting = inheritable_setting.parent end - # Stop defining settings for the current route and clear them for the - # next, within a namespace. - def route_end - inheritable_setting.route_end - end - # Execute the block within a context where our inheritable settings are forked # to a new copy (see #namespace_start). def within_namespace(&block) @@ -160,20 +127,6 @@ def within_namespace(&block) result end - - private - - # Builds the current class :inheritable_setting. If available, it inherits from - # the superclass's :inheritable_setting. - def build_top_level_setting - Grape::Util::InheritableSetting.new.tap do |setting| - # Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to - # +inheritable_setting+, however, it doesn't contain any user-defined settings. - # Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+ - # in the chain for every endpoint. - setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance - end - end end end end diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index 81d71bfeb..6beb8f37a 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -3,54 +3,46 @@ module Grape module DSL module Validations - extend ActiveSupport::Concern - - include Grape::DSL::Configuration - - module ClassMethods - # Clears all defined parameters and validations. The main purpose of it is to clean up - # settings, so next endpoint won't interfere with previous one. - # - # params do - # # params for the endpoint below this block - # end - # post '/current' do - # # whatever - # end - # - # # somewhere between them the reset_validations! method gets called - # - # params do - # # params for the endpoint below this block - # end - # post '/next' do - # # whatever - # end - def reset_validations! - unset_namespace_stackable :declared_params - unset_namespace_stackable :validations - unset_namespace_stackable :params - end + # Clears all defined parameters and validations. The main purpose of it is to clean up + # settings, so next endpoint won't interfere with previous one. + # + # params do + # # params for the endpoint below this block + # end + # post '/current' do + # # whatever + # end + # + # # somewhere between them the reset_validations! method gets called + # + # params do + # # params for the endpoint below this block + # end + # post '/next' do + # # whatever + # end + def reset_validations! + unset_namespace_stackable :declared_params, :params, :validations + end - # Opens a root-level ParamsScope, defining parameter coercions and - # validations for the endpoint. - # @yield instance context of the new scope - def params(&block) - Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) - end + # Opens a root-level ParamsScope, defining parameter coercions and + # validations for the endpoint. + # @yield instance context of the new scope + def params(&block) + Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) + end - # Declare the contract to be used for the endpoint's parameters. - # @param contract [Class | Dry::Schema::Processor] - # The contract or schema to be used for validation. Optional. - # @yield a block yielding a new instance of Dry::Schema::Params - # subclass, allowing to define the schema inline. When the - # +contract+ parameter is a schema, it will be used as a parent. Optional. - def contract(contract = nil, &block) - raise ArgumentError, 'Either contract or block must be provided' unless contract || block - raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema) + # Declare the contract to be used for the endpoint's parameters. + # @param contract [Class | Dry::Schema::Processor] + # The contract or schema to be used for validation. Optional. + # @yield a block yielding a new instance of Dry::Schema::Params + # subclass, allowing to define the schema inline. When the + # +contract+ parameter is a schema, it will be used as a parent. Optional. + def contract(contract = nil, &block) + raise ArgumentError, 'Either contract or block must be provided' unless contract || block + raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema) - Grape::Validations::ContractScope.new(self, contract, &block) - end + Grape::Validations::ContractScope.new(self, contract, &block) end end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index e3bd6b61f..9280a2147 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -8,6 +8,7 @@ module Grape class Endpoint extend Forwardable include Grape::DSL::Settings + include Grape::DSL::Headers include Grape::DSL::InsideRoute attr_accessor :block, :source, :options diff --git a/lib/grape/types/invalid_value.rb b/lib/grape/types/invalid_value.rb deleted file mode 100644 index ae356daa1..000000000 --- a/lib/grape/types/invalid_value.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# only exists to make it shorter for external use -module Grape - module Types - InvalidValue = Class.new(Grape::Validations::Types::InvalidValue) - end -end diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index 7fc444fe2..74192b1a1 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -5,7 +5,7 @@ let(:dummy_class) do Class.new do - include Grape::DSL::Callbacks + extend Grape::DSL::Callbacks end end diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index aa2aa4c33..884163b7b 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -6,6 +6,30 @@ let(:dummy_class) do Class.new do extend Grape::DSL::Desc + + def self.namespace_setting(key, value = nil) + if value + namespace_setting_hash[key] = value + else + namespace_setting_hash[key] + end + end + + def self.route_setting(key, value = nil) + if value + route_setting_hash[key] = value + else + route_setting_hash[key] + end + end + + def self.namespace_setting_hash + @namespace_setting_hash ||= {} + end + + def self.route_setting_hash + @route_setting_hash ||= {} + end end end diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index 1c4d539ee..a1b07bd5c 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -5,7 +5,7 @@ let(:dummy_class) do Class.new do - include Grape::DSL::Helpers + extend Grape::DSL::Helpers def self.mods namespace_stackable(:helpers) @@ -14,6 +14,20 @@ def self.mods def self.first_mod mods.first end + + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end + + def self.namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 5e5b8b5d0..d0edd697d 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -12,7 +12,53 @@ def initialize @env = {} @header = {} - @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } + @new_settings = { namespace_inheritable: namespace_inheritable_hash, namespace_stackable: namespace_stackable_hash } + end + + def namespace_inheritable(key, value = nil) + if value + namespace_inheritable_hash[key] = value + else + namespace_inheritable_hash[key] + end + end + + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end + + def namespace_stackable_with_hash(key, value = nil) + if value + namespace_stackable_with_hash_hash[key] = value + else + namespace_stackable_with_hash_hash[key] + end + end + + def header(key = nil, val = nil) + if key + val ? header[key] = val : header.delete(key) + else + @header ||= Grape::Util::Header.new + end + end + + private + + def namespace_inheritable_hash + @namespace_inheritable_hash ||= {} + end + + def namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new { [] } + end + + def namespace_stackable_with_hash_hash + @namespace_stackable_with_hash_hash ||= {} end end end diff --git a/spec/grape/dsl/logger_spec.rb b/spec/grape/dsl/logger_spec.rb index ae1bab567..3b135c48e 100644 --- a/spec/grape/dsl/logger_spec.rb +++ b/spec/grape/dsl/logger_spec.rb @@ -1,24 +1,46 @@ # frozen_string_literal: true describe Grape::DSL::Logger do - subject { Class.new(dummy_logger) } - let(:dummy_logger) do Class.new do extend Grape::DSL::Logger + def self.global_setting(key, value = nil) + if value + global_setting_hash[key] = value + else + global_setting_hash[key] + end + end + + def self.global_setting_hash + @global_setting_hash ||= {} + end end end - let(:logger) { instance_double(Logger) } - describe '.logger' do - it 'sets a logger' do - subject.logger logger - expect(subject.logger).to eq logger + context 'when setting a logger' do + subject { dummy_logger.logger :my_logger } + + it { is_expected.to eq(:my_logger) } end - it 'returns a logger' do - expect(subject.logger(logger)).to eq logger + context 'when retrieving logger' do + context 'when never been set' do + subject { dummy_logger.logger } + + before { allow(Logger).to receive(:new).with($stdout).and_return(:stdout_logger) } + + it { is_expected.to eq(:stdout_logger) } + end + + context 'when already set' do + subject { dummy_logger.logger } + + before { dummy_logger.logger :my_logger } + + it { is_expected.to eq(:my_logger) } + end end end end diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index 1b363697a..776910d5e 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -5,7 +5,21 @@ let(:dummy_class) do Class.new do - include Grape::DSL::Middleware + extend Grape::DSL::Middleware + + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end + + def self.namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end end end diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 24bedc65b..b232315e5 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -5,14 +5,36 @@ let(:dummy_class) do Class.new do - include Grape::DSL::RequestResponse + extend Grape::DSL::RequestResponse - def self.set(key, value) - settings[key.to_sym] = value + def self.namespace_stackable_with_hash(_key) + {} end - def self.imbue(key, value) - settings.imbue(key, value) + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end + + def self.namespace_inheritable(key, value = nil) + if value + namespace_inheritable_hash[key] = value + else + namespace_inheritable_hash[key] + end + end + + def self.namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end + + def self.namespace_inheritable_hash + @namespace_inheritable_hash ||= {} end end end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 1b54ba913..27952bdce 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -5,7 +5,63 @@ let(:dummy_class) do Class.new do - include Grape::DSL::Routing + extend Grape::DSL::Routing + + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end + + def self.namespace_stackable_with_hash(key, value = nil) + if value + namespace_stackable_with_hash_hash[key] = value + else + namespace_stackable_with_hash_hash[key] + end + end + + def self.namespace_inheritable(key, value = nil) + if value + namespace_inheritable_hash[key] = value + else + namespace_inheritable_hash[key] + end + end + + def self.route_setting(key, value = nil) + if value + route_setting_hash[key] = value + else + route_setting_hash[key] + end + end + + def self.inheritable_setting + @inheritable_setting ||= Grape::Util::InheritableSetting.new + end + + def self.namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end + + def self.namespace_stackable_with_hash_hash + @namespace_stackable_with_hash_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end + + def self.namespace_inheritable_hash + @namespace_inheritable_hash ||= {} + end + + def self.route_setting_hash + @route_setting_hash ||= {} + end end end diff --git a/spec/grape/dsl/settings_spec.rb b/spec/grape/dsl/settings_spec.rb index e4b20fa36..550195472 100644 --- a/spec/grape/dsl/settings_spec.rb +++ b/spec/grape/dsl/settings_spec.rb @@ -40,13 +40,6 @@ def reset_validations!; end end end - describe '#unset_global_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:global, :dummy) - subject.unset_global_setting(:dummy) - end - end - describe '#route_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:route, :dummy, 1) @@ -63,13 +56,6 @@ def reset_validations!; end end end - describe '#unset_route_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:route, :dummy) - subject.unset_route_setting(:dummy) - end - end - describe '#namespace_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace, :dummy, 1) @@ -106,13 +92,6 @@ def reset_validations!; end end end - describe '#unset_namespace_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:namespace, :dummy) - subject.unset_namespace_setting(:dummy) - end - end - describe '#namespace_inheritable' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace_inheritable, :dummy, 1) @@ -139,13 +118,6 @@ def reset_validations!; end end end - describe '#unset_namespace_inheritable' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:namespace_inheritable, :dummy) - subject.unset_namespace_inheritable(:dummy) - end - end - describe '#namespace_stackable' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace_stackable, :dummy, 1) @@ -179,20 +151,6 @@ def reset_validations!; end end end - describe '#api_class_setting' do - it 'delegates to get_or_set' do - expect(subject).to receive(:get_or_set).with(:api_class, :dummy, 1) - subject.api_class_setting(:dummy, 1) - end - end - - describe '#unset_api_class_setting' do - it 'delegates to unset' do - expect(subject).to receive(:unset).with(:api_class, :dummy) - subject.unset_api_class_setting(:dummy) - end - end - describe '#within_namespace' do it 'calls start and end for a namespace' do expect(subject).to receive :namespace_start diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb new file mode 100644 index 000000000..93a55191d --- /dev/null +++ b/spec/grape/dsl/validations_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +describe Grape::DSL::Validations do + subject { dummy_class } + + let(:dummy_class) do + Class.new do + extend Grape::DSL::Validations + def self.unset_namespace_stackable(*_keys); end + end + end + + describe '.reset_validations' do + subject { dummy_class.reset_validations! } + + it 'calls unset_namespace_stackable properly' do + expect(dummy_class).to receive(:unset_namespace_stackable).with(:declared_params, :params, :validations) + subject + end + end + + describe '.params' do + subject { dummy_class.params { :my_block } } + + it 'creates a proper Grape::Validations::ParamsScope' do + expect(Grape::Validations::ParamsScope).to receive(:new).with(api: dummy_class, type: Hash) do |_func, &block| + expect(block.call).to eq(:my_block) + end.and_return(:param_scope) + expect(subject).to eq(:param_scope) + end + end + + describe '.contract' do + context 'when contract is nil and blockless' do + it 'raises an ArgumentError' do + expect { dummy_class.contract }.to raise_error(ArgumentError, 'Either contract or block must be provided') + end + end + + context 'when contract is nil and but a block is provided' do + it 'returns a proper rape::Validations::ContractScope' do + expect(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, nil) do |_func, &block| + expect(block.call).to eq(:my_block) + end.and_return(:my_contract_scope) + + expect(dummy_class.contract { :my_block }).to eq(:my_contract_scope) + end + end + + context 'when contract is present and blockless' do + subject { dummy_class.contract(:my_contract) } + + before do + allow(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, :my_contract).and_return(:my_contract_scope) + end + + it { is_expected.to eq(:my_contract_scope) } + end + + context 'when contract and block are provided' do + context 'when contract does not respond to schema' do + let(:my_contract) { Class.new } + + it 'returns a proper rape::Validations::ContractScope' do + expect(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, my_contract) do |_func, &block| + expect(block.call).to eq(:my_block) + end.and_return(:my_contract_scope) + + expect(dummy_class.contract(my_contract.new) { :my_block }).to eq(:my_contract_scope) + end + end + + context 'when contract responds to schema' do + let(:my_contract) do + Class.new do + def schema; end + end + end + + it 'raises an ArgumentError' do + expect { dummy_class.contract(my_contract.new) { :my_block } }.to raise_error(ArgumentError, 'Cannot inherit from contract, only schema') + end + end + end + end +end diff --git a/spec/grape/validations/validators/coerce_validator_spec.rb b/spec/grape/validations/validators/coerce_validator_spec.rb index 81cd4b511..decca5870 100644 --- a/spec/grape/validations/validators/coerce_validator_spec.rb +++ b/spec/grape/validations/validators/coerce_validator_spec.rb @@ -249,7 +249,7 @@ def self.parsed?(value) let(:custom_type) do Class.new do def self.parse(_val) - Grape::Types::InvalidValue.new('must be unique') + Grape::Validations::Types::InvalidValue.new('must be unique') end end end diff --git a/spec/integration/dry_validation/dry_validation_spec.rb b/spec/integration/dry_validation/dry_validation_spec.rb index d7b2f8efa..b24a39c8c 100644 --- a/spec/integration/dry_validation/dry_validation_spec.rb +++ b/spec/integration/dry_validation/dry_validation_spec.rb @@ -6,47 +6,21 @@ let(:app) do Class.new do - include Grape::DSL::Validations - end - end - - describe '.reset_validations!' do - before do - subject.namespace_stackable :declared_params, ['dummy'] - subject.namespace_stackable :validations, ['dummy'] - subject.namespace_stackable :params, ['dummy'] - subject.route_setting :description, description: 'lol', params: ['dummy'] - subject.reset_validations! - end - - after do - subject.unset_route_setting :description - end - - it 'resets declared params' do - expect(subject.namespace_stackable(:declared_params)).to be_empty - end - - it 'resets validations' do - expect(subject.namespace_stackable(:validations)).to be_empty - end + extend Grape::DSL::Validations - it 'resets params' do - expect(subject.namespace_stackable(:params)).to be_empty - end - - it 'does not reset documentation description' do - expect(subject.route_setting(:description)[:description]).to eq 'lol' - end - end - - describe '.params' do - it 'returns a ParamsScope' do - expect(subject.params).to be_a Grape::Validations::ParamsScope - end + def self.namespace_stackable(key, value = nil) + if value + namespace_stackable_hash[key] << value + else + namespace_stackable_hash[key] + end + end - it 'evaluates block' do - expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' + def self.namespace_stackable_hash + @namespace_stackable_hash ||= Hash.new do |hash, key| + hash[key] = [] + end + end end end