From a34ff4426caab9f8e0f6ed8f9feec90079e77f0d Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Tue, 1 Nov 2022 16:17:53 +1100 Subject: [PATCH] Update API docs for 2.0.0 (#1229) --- .rubocop.yml | 2 + .yardopts | 4 + Gemfile | 1 + lib/hanami.rb | 148 ++++- lib/hanami/app.rb | 74 +-- lib/hanami/assets/app_config.rb | 19 +- lib/hanami/assets/config.rb | 11 +- lib/hanami/config.rb | 318 +++++++++-- lib/hanami/config/actions.rb | 90 ++- .../config/actions/content_security_policy.rb | 2 +- lib/hanami/config/actions/cookies.rb | 27 + lib/hanami/config/logger.rb | 107 +++- lib/hanami/config/views.rb | 8 +- lib/hanami/constants.rb | 4 + lib/hanami/errors.rb | 17 + lib/hanami/extensions/action.rb | 45 +- lib/hanami/extensions/view.rb | 10 +- lib/hanami/extensions/view/context.rb | 5 +- lib/hanami/providers/inflector.rb | 6 + lib/hanami/providers/logger.rb | 8 + lib/hanami/providers/rack.rb | 12 + lib/hanami/providers/routes.rb | 18 +- lib/hanami/routes.rb | 15 +- lib/hanami/settings.rb | 136 +++-- lib/hanami/settings/env_store.rb | 2 +- lib/hanami/slice.rb | 515 +++++++++++++++++- lib/hanami/slice/router.rb | 37 +- lib/hanami/slice/routing/middleware/stack.rb | 39 +- lib/hanami/slice/routing/resolver.rb | 5 + lib/hanami/slice/view_name_inferrer.rb | 2 +- lib/hanami/web/rack_logger.rb | 12 + .../actions/content_security_policy_spec.rb | 14 +- .../config/actions/default_values_spec.rb | 2 +- spec/unit/hanami/config/logger_spec.rb | 24 +- 34 files changed, 1496 insertions(+), 243 deletions(-) create mode 100644 .yardopts diff --git a/.rubocop.yml b/.rubocop.yml index 705027c1..48329779 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,8 @@ Style/LambdaCall: Enabled: false Style/StabbyLambdaParentheses: Enabled: false +Style/StringLiteralsInInterpolation: + Enabled: false Style/TrailingCommaInArguments: Enabled: false Style/TrailingCommaInArrayLiteral: diff --git a/.yardopts b/.yardopts new file mode 100644 index 00000000..df71a9ec --- /dev/null +++ b/.yardopts @@ -0,0 +1,4 @@ +--markup=markdown + +--plugin junk +--junk-log-ignore UnknownError diff --git a/Gemfile b/Gemfile index ba2c934e..b8eaf9cc 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gemspec unless ENV["CI"] gem "byebug", platforms: :mri gem "yard" + gem "yard-junk" end gem "hanami-utils", github: "hanami/utils", branch: "main" diff --git a/lib/hanami.rb b/lib/hanami.rb index 79d6464b..161096e6 100644 --- a/lib/hanami.rb +++ b/lib/hanami.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "hanami/constants" + # A complete web framework for Ruby # # @since 0.1.0 @@ -13,7 +15,7 @@ module Hanami # # Raises an exception if the app file cannot be found. # - # @return [Hanami::App] the loaded app class + # @return [app] the loaded app class # # @api public # @since 2.0.0 @@ -34,6 +36,52 @@ module Hanami end end + # Returns the Hamami app class. + # + # To ensure your Hanami app is loaded, run {.setup} (or `require "hanami/setup"`) first. + # + # @return [Hanami::App] the app class + # + # @raise [AppLoadError] if the app has not been loaded + # + # @see .setup + # + # @api public + # @since 2.0.0 + def self.app + @_mutex.synchronize do + unless defined?(@_app) + raise AppLoadError, + "Hanami.app is not yet configured. " \ + "You may need to `require \"hanami/setup\"` to load your config/app.rb file." + end + + @_app + end + end + + # Returns true if the Hanami app class has been loaded. + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 + def self.app? + instance_variable_defined?(:@_app) + end + + # @api private + # @since 2.0.0 + def self.app=(klass) + @_mutex.synchronize do + if instance_variable_defined?(:@_app) + raise AppLoadError, "Hanami.app is already configured." + end + + @_app = klass unless klass.name.nil? + end + end + # Finds and returns the absolute path for the Hanami app file (`config/app.rb`). # # Searches within the given directory, then searches upwards through parent directories until the @@ -57,59 +105,94 @@ module Hanami end end - APP_PATH = "config/app.rb" - private_constant :APP_PATH - - def self.app - @_mutex.synchronize do - unless defined?(@_app) - raise AppLoadError, - "Hanami.app is not yet configured. " \ - "You may need to `require \"hanami/setup\"` to load your config/app.rb file." - end - - @_app - end - end - - def self.app? - instance_variable_defined?(:@_app) - end - - def self.app=(klass) - @_mutex.synchronize do - if instance_variable_defined?(:@_app) - raise AppLoadError, "Hanami.app is already configured." - end - - @_app = klass unless klass.name.nil? - end - end - + # Returns the Hanami app environment as loaded from the `HANAMI_ENV` environment variable. + # + # @example + # Hanami.env # => :development + # + # @return [Symbol] the environment name + # + # @api public + # @since 2.0.0 def self.env ENV.fetch("HANAMI_ENV") { ENV.fetch("RACK_ENV", "development") }.to_sym end + # Returns true if {.env} matches any of the given names + # + # @example + # Hanami.env # => :development + # Hanami.env?(:development, :test) # => true + # + # @param names [Array] the environment names to check + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 def self.env?(*names) names.map(&:to_sym).include?(env) end + # Returns the app's logger. + # + # Direct global access to the logger via this method is not recommended. Instead, consider + # accessing the logger via the app or slice container, in most cases as an dependency using the + # `Deps` mixin. + # + # @example + # # app/my_component.rb + # + # module MyApp + # class MyComponent + # include Deps["logger"] + # + # def some_method + # logger.info("hello") + # end + # end + # end + # + # @return [Hanami::Logger] + # + # @api public + # @since 1.0.0 def self.logger app[:logger] end + # Prepares the Hanami app. + # + # @see App::ClassMethods#prepare + # + # @api public + # @since 2.0.0 def self.prepare app.prepare end + # Boots the Hanami app. + # + # @see App::ClassMethods#boot + # + # @api public + # @since 2.0.0 def self.boot app.boot end + # Shuts down the Hanami app. + # + # @see App::ClassMethods#shutdown + # + # @api public + # @since 2.0.0 def self.shutdown app.shutdown end + # @api private + # @since 2.0.0 def self.bundled?(gem_name) @_mutex.synchronize do @_bundled[gem_name] ||= begin @@ -120,6 +203,11 @@ module Hanami end end + # Returns an array of bundler group names to be eagerly loaded by hanami-cli and other CLI + # extensions. + # + # @api private + # @since 2.0.0 def self.bundler_groups [:plugins] end diff --git a/lib/hanami/app.rb b/lib/hanami/app.rb index a45acc5d..f1800d1f 100644 --- a/lib/hanami/app.rb +++ b/lib/hanami/app.rb @@ -6,12 +6,11 @@ require_relative "slice" require_relative "slice_name" module Hanami - # The Hanami app is a singular slice tasked with managing the core components of - # the app and coordinating overall app boot. + # The Hanami app is a singular slice tasked with managing the core components of the app and + # coordinating overall app boot. # - # For smaller apps, the app may be the only slice present, whereas larger apps - # may consist of many slices, with the app reserved for holding a small number - # of shared components only. + # For smaller apps, the app may be the only slice present, whereas larger apps may consist of many + # slices, with the app reserved for holding a small number of shared components only. # # @see Slice # @@ -20,6 +19,8 @@ module Hanami class App < Slice @_mutex = Mutex.new + # @api private + # @since 2.0.0 def self.inherited(subclass) super @@ -31,9 +32,9 @@ module Hanami subclass.class_eval do @config = Hanami::Config.new(app_name: slice_name, env: Hanami.env) - # Prepare the load path (based on the default root of `Dir.pwd`) as early as - # possible, so you can make a `require` inside the body of an `App` subclass, - # which may be useful for certain kinds of app configuration. + # Prepare the load path (based on the default root of `Dir.pwd`) as early as possible, so + # you can make a `require` inside the body of an `App` subclass, which may be useful for + # certain kinds of app configuration. prepare_load_path load_dotenv @@ -43,29 +44,41 @@ module Hanami # App class interface module ClassMethods + # Returns the app's config. + # + # @return [Hanami::Config] + # + # @api public + # @since 2.0.0 attr_reader :config + # Returns the app's {SliceName}. + # + # @return [Hanami::SliceName] + # + # @see Slice::ClassMethods#slice_name + # + # @api public + # @since 2.0.0 def app_name slice_name end - # Prepares the $LOAD_PATH based on the app's configured root, prepending the `lib/` - # directory if it exists. If the lib directory is already added, this will do - # nothing. + # Prepares the $LOAD_PATH based on the app's configured root, prepending the `lib/` directory + # if it exists. If the lib directory is already added, this will do nothing. # - # In ordinary circumstances, you should never have to call this method: this method - # is called immediately upon subclassing {Hanami::App}, as a convenicence to put - # lib/ (under the default root of `Dir.pwd`) on the load path automatically. This is - # helpful if you need to require files inside the subclass body for performing - # certain app configuration steps. + # In ordinary circumstances, you should never have to call this method: this method is called + # immediately upon subclassing {Hanami::App}, as a convenicence to put lib/ (under the default + # root of `Dir.pwd`) on the load path automatically. This is helpful if you need to require + # files inside the subclass body for performing certain app configuration steps. # - # If you change your app's `config.root` and you need to require files from its - # `lib/` directory within your {App} subclass body, you should call - # {.prepare_load_path} explicitly after setting the new root. + # If you change your app's `config.root` and you need to require files from its `lib/` + # directory within your {App} subclass body, you should call {.prepare_load_path} explicitly + # after setting the new root. # - # Otherwise, this method is called again as part of the app {.prepare} step, so if - # you've changed your app's root and do _not_ need to require files within your {App} - # subclass body, then you don't need to call this method. + # Otherwise, this method is called again as part of the app {.prepare} step, so if you've + # changed your app's root and do _not_ need to require files within your {App} subclass body, + # then you don't need to call this method. # # @example # module MyApp @@ -124,12 +137,12 @@ module Hanami # Make app-wide notifications available as early as possible container.use(:notifications) - # Ensure all basic slice preparation is complete before we make adjustments below - # (which rely on the basic prepare steps having already run) + # Ensure all basic slice preparation is complete before we make adjustments below (which + # rely on the basic prepare steps having already run) super - # Run specific prepare steps for the app slice. Note also that some - # standard steps have been skipped via the empty method overrides below. + # Run specific prepare steps for the app slice. Note also that some standard steps have been + # skipped via the empty method overrides below. prepare_app_component_dirs prepare_app_providers end @@ -150,8 +163,8 @@ module Hanami end end - # When auto-registering components in app/, ignore files in `app/lib/` (these will - # be auto-registered as above), as well as the configured no_auto_register_paths + # When auto-registering components in app/, ignore files in `app/lib/` (these will be + # auto-registered as above), as well as the configured no_auto_register_paths no_auto_register_paths = ([LIB_DIR] + config.no_auto_register_paths) .map { |path| path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}" @@ -183,9 +196,8 @@ module Hanami end def prepare_autoloader - # Component dirs are automatically pushed to the autoloader by dry-system's - # zeitwerk plugin. This method adds other dirs that are not otherwise configured - # as component dirs. + # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin. + # This method adds other dirs that are not otherwise configured as component dirs. # Autoload classes from `lib/[app_namespace]/` if root.join(LIB_DIR, app_name.name).directory? diff --git a/lib/hanami/assets/app_config.rb b/lib/hanami/assets/app_config.rb index afae06f0..e0537ed4 100644 --- a/lib/hanami/assets/app_config.rb +++ b/lib/hanami/assets/app_config.rb @@ -4,21 +4,21 @@ require "dry/configurable" require_relative "config" module Hanami + # @api private module Assets - # @since 2.0.0 - # @api public + # App config for assets. + # + # This is NOT RELEASED as of 2.0.0. + # + # @api private class AppConfig include Dry::Configurable - # @since 2.0.0 - # @api private attr_reader :base_config protected :base_config setting :server_url, default: "http://localhost:8080" - # @since 2.0.0 - # @api private def initialize(*) super @@ -30,8 +30,6 @@ module Hanami @base_config = source.base_config.dup end - # @since 2.0.0 - # @api private def finalize! end @@ -39,7 +37,6 @@ module Hanami # # @return [Set] # - # @since 2.0.0 # @api private def settings base_config.settings + self.class.settings @@ -47,8 +44,6 @@ module Hanami private - # @since 2.0.0 - # @api private def method_missing(name, *args, &block) if config.respond_to?(name) config.public_send(name, *args, &block) @@ -59,8 +54,6 @@ module Hanami end end - # @since 2.0.0 - # @api private def respond_to_missing?(name, _incude_all = false) config.respond_to?(name) || base_config.respond_to?(name) || super end diff --git a/lib/hanami/assets/config.rb b/lib/hanami/assets/config.rb index 35cd22ba..21f89a21 100644 --- a/lib/hanami/assets/config.rb +++ b/lib/hanami/assets/config.rb @@ -4,8 +4,11 @@ require "dry/configurable" module Hanami module Assets - # @since 2.0.0 - # @api public + # App config for assets. + # + # This is NOT RELEASED as of 2.0.0. + # + # @api private class Config include Dry::Configurable @@ -34,8 +37,6 @@ module Hanami private - # @since 2.0.0 - # @api private def method_missing(name, *args, &block) if config.respond_to?(name) config.public_send(name, *args, &block) @@ -44,8 +45,6 @@ module Hanami end end - # @since 2.0.0 - # @api private def respond_to_missing?(name, _incude_all = false) config.respond_to?(name) || super end diff --git a/lib/hanami/config.rb b/lib/hanami/config.rb index 713ab156..1aa90c0b 100644 --- a/lib/hanami/config.rb +++ b/lib/hanami/config.rb @@ -14,20 +14,94 @@ require_relative "settings/env_store" require_relative "slice/routing/middleware/stack" module Hanami - # Hanami app configuration + # Hanami app config # # @since 2.0.0 class Config + # @api private + DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new } + private_constant :DEFAULT_ENVIRONMENTS + include Dry::Configurable + # @!attribute [rw] root + # Sets the root for the app or slice. + # + # For the app, this defaults to `Dir.pwd`. For slices detected in `slices/` `config/slices/`, + # this defaults to `slices/[slice_name]/`. + # + # Accepts a string path and will return a `Pathname`. + # + # @return [Pathname] + # + # @api public + # @since 2.0.0 setting :root, constructor: ->(path) { Pathname(path) if path } - setting :no_auto_register_paths, default: %w[entities] - + # @!attribute [rw] inflector + # Sets the app's inflector. + # + # This expects a `Dry::Inflector` (or compatible) inflector instance. + # + # To configure custom inflection rules without having to assign a whole inflector, see + # {#inflections}. + # + # @return [Dry::Inflector] + # + # @see #inflections + # + # @api public + # @since 2.0.0 setting :inflector, default: Dry::Inflector.new + # @!attribute [rw] settings_store + # Sets the store used to retrieve {Hanami::Settings} values. + # + # Defaults to an instance of {Hanami::Settings::EnvStore}. + # + # @return [#fetch] + # + # @see Hanami::Settings + # @see Hanami::Settings::EnvStore#fetch + # + # @api public + # @since 2.0.0 setting :settings_store, default: Hanami::Settings::EnvStore.new + # @!attribute [rw] slices + # Sets the slices to load when the app is preared or booted. + # + # Defaults to `nil`, which will load all slices. Set this to an array of slice names to load + # only those slices. + # + # This attribute is also populated from the `HANAMI_SLICES` environment variable. + # + # @example + # config.slices = ["admin", "search"] + # + # @example + # ENV["HANAMI_SLICES"] # => "admin,search" + # config.slices # => ["admin", "search"] + # + # @return [Array, nil] + # + # @api public + # @since 2.0.0 + setting :slices + + # @!attribute [rw] shared_app_component_keys + # Sets the keys for the components to be imported from the app into all other slices. + # + # You should append items to this array, since the default shared components are essential for + # slices to operate within the app. + # + # @example + # config.shared_app_component_keys += ["shared_component_a", "shared_component_b"] + # + # @return [Array] + # + # @api public + # @since 2.0.0 setting :shared_app_component_keys, default: %w[ inflector logger @@ -37,56 +111,119 @@ module Hanami settings ] - setting :slices - - setting :base_url, default: "http://0.0.0.0:2300", constructor: ->(url) { URI(url) } + # @!attribute [rw] no_auto_register_paths + # Sets the paths to skip from container auto-registration. + # + # Defaults to `["entities"]`. + # + # @return [Array] array of relative paths + # + # @api public + # @since 2.0.0 + setting :no_auto_register_paths, default: %w[entities] + # TODO: Remove this; we have `config.actions.sessions` instead + # + # @api private setting :sessions, default: :null, constructor: ->(*args) { Sessions.new(*args) } - setting :logger, cloneable: true - - DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new } - private_constant :DEFAULT_ENVIRONMENTS - - # @return [Symbol] The name of the application + # @!attribute [rw] base_url + # Sets the base URL for app's web server. # - # @api public + # This is passed to the {Slice::ClassMethods#router router} and used for generating links. + # + # Defaults to `"http://0.0.0.0:2300"`. String values passed are turned into `URI` instances. + # + # @return [URI] + # + # @see Slice::ClassMethods#router + # + # @api public + # @since 2.0.0 + setting :base_url, default: "http://0.0.0.0:2300", constructor: ->(url) { URI(url) } + + # Returns the app or slice's {Hanami::SliceName slice_name}. + # + # This is useful for default config values that depend on this name. + # + # @return [Hanami::SliceName] + # + # @api private + # @since 2.0.0 attr_reader :app_name - # @return [String] The current environment + # Returns the app's environment. # - # @api public + # @example + # config.env # => :development + # + # @return [Symbol] + # + # @api private + # @since 2.0.0 attr_reader :env - # @return [Hanami::Config::Actions] + # Returns the app's actions config, or a null config if hanami-controller is not bundled. + # + # @example When hanami-controller is bundled + # config.actions.default_request_format # => :html + # + # @example When hanami-controller is not bundled + # config.actions.default_request_format # => NoMethodError + # + # @return [Hanami::Config::Actions, Hanami::Config::NullConfig] # # @api public + # @since 2.0.0 attr_reader :actions - # @return [Hanami::Slice::Routing::Middleware::Stack] + # Returns the app's middleware stack, or nil if hanami-router is not bundled. + # + # Use this to configure middleware that should apply to all routes. + # + # @example + # config.middleware.use :body_parser, :json + # config.middleware.use MyCustomMiddleware + # + # @return [Hanami::Slice::Routing::Middleware::Stack, nil] # # @api public + # @since 2.0.0 attr_reader :middleware # @api private + # @since 2.0.0 alias_method :middleware_stack, :middleware - # @return [Hanami::Config::Router] + # Returns the app's router config, or a null config if hanami-router is not bundled. + # + # @example When hanami-router is bundled + # config.router.resolver # => Hanami::Slice::Routing::Resolver + # + # @example When hanami-router is not bundled + # config.router.resolver # => NoMethodError + # + # @return [Hanami::Config::Router, Hanami::Config::NullConfig] # # @api public + # @since 2.0.0 attr_reader :router - # @return [Hanami::Config::Views] + # Returns the app's views config, or a null config if hanami-view is not bundled. # - # @api public + # This is NOT RELEASED as of 2.0.0. + # + # @api private attr_reader :views - # @return [Hanami::Assets::AppConfiguration] + # Returns the app's assets config. # - # @api public + # This is NOT RELEASED as of 2.0.0. + # + # @api private attr_reader :assets - # @return [Concurrent::Hash] A hash of default environments + # @return [Concurrent::Hash] a hash of default environments # # @api private attr_reader :environments @@ -103,7 +240,7 @@ module Hanami self.root = Dir.pwd load_from_env - config.logger = Config::Logger.new(env: env, app_name: app_name) + @logger = Config::Logger.new(env: env, app_name: app_name) # TODO: Make assets config dependent require "hanami/assets/app_config" @@ -128,31 +265,6 @@ module Hanami yield self if block_given? end - # Apply config for the given environment - # - # @param env [String] the environment name - # - # @return [Hanami::Config] - # - # @api public - def environment(env_name, &block) - environments[env_name] << block - apply_env_config - - self - end - - # Configure application's inflections - # - # @see https://dry-rb.org/gems/dry-inflector - # - # @return [Dry::Inflector] - # - # @api public - def inflections(&block) - self.inflector = Dry::Inflector.new(&block) - end - # @api private def initialize_copy(source) super @@ -168,7 +280,13 @@ module Hanami end @views = source.views.dup end + private :initialize_copy + # Finalizes the config. + # + # This is called when the app or slice is prepared. After this, no further changes to config can + # be made. + # # @api private def finalize! apply_env_config @@ -183,16 +301,114 @@ module Hanami super end - # Set a default global logger instance + # Applies config for a given app environment. + # + # The given block will be evaluated in the context of `self` via `instance_eval`. + # + # @example + # config.environment(:test) do + # config.logger.level = :info + # end + # + # @param env_name [Symbol] the environment name + # + # @return [Hanami::Config] + # + # @see Hanami.env # # @api public + # @since 2.0.0 + def environment(env_name, &block) + environments[env_name] << block + apply_env_config + + self + end + + # Configures the app's custom inflections. + # + # You should call this one time only. Subsequent calls will override previously configured + # inflections. + # + # @example + # config.inflections do |inflections| + # inflections.acronym "WNBA" + # end + # + # @see https://dry-rb.org/gems/dry-inflector + # + # @return [Dry::Inflector] the configured inflector + # + # @api public + # @since 2.0.0 + def inflections(&block) + self.inflector = Dry::Inflector.new(&block) + end + + # Disabling this to permit distinct documentation for `#logger` vs `#logger=` + # + # rubocop:disable Style/TrivialAccessors + + # Returns the logger config. + # + # Use this to configure various options for the default `Hanami::Logger` logger instance. + # + # @example + # config.logger.level = :debug + # + # @return [Hanami::Config::Logger] + # + # @see Hanami::Config::Logger + # + # @api public + # @since 2.0.0 + def logger + @logger + end + + # Sets the app's logger instance. + # + # This entirely replaces the default `Hanami::Logger` instance that would have been + # + # @see #logger_instance + # + # @api public + # @since 2.0.0 def logger=(logger_instance) @logger_instance = logger_instance end - # Return configured logger instance + # rubocop:enable Style/TrivialAccessors + + # Returns the configured logger instance. + # + # Unless you've replaced the logger with {#logger=}, this returns an `Hanami::Logger` configured + # with the options configured through {#logger}. + # + # This configured logger is registered in all app and slice containers as `"logger"`. For + # typical usage, you should access the logger via this component, not directly from config. + # + # @example Accessing the logger component + # Hanami.app["logger"] # => # + # + # @example Injecting the logger as a dependency + # module MyApp + # class MyClass + # include Deps["logger"] + # + # def my_method + # logger.info("hello") + # end + # end + # end + # + # @return [Hanami::Logger] + # + # @see #logger + # @see Hanami::Config::Logger # # @api public + # @since 2.0.0 def logger_instance @logger_instance || logger.instance end diff --git a/lib/hanami/config/actions.rb b/lib/hanami/config/actions.rb index f63c25e1..bd1eae12 100644 --- a/lib/hanami/config/actions.rb +++ b/lib/hanami/config/actions.rb @@ -10,24 +10,99 @@ module Hanami class Config # Hanami actions config # + # This exposes all the settings from the standalone `Hanami::Action` class, pre-configured with + # sensible defaults for actions within a full Hanami app. It also provides additional settings + # for further integration of actions with other full stack app components. + # # @since 2.0.0 + # @api public class Actions include Dry::Configurable + # @!attribute [rw] cookies + # Sets or returns a hash of cookie options for actions. + # + # The hash is wrapped by {Hanami::Config::Actions::Cookies}, which also provides an + # `enabled?` method, returning true in the case of any options provided. + # + # @example + # config.actions.cookies = {max_age: 300} + # + # @return [Hanami::Config::Actions::Cookies] + # + # @api public + # @since 2.0.0 setting :cookies, default: {}, constructor: -> options { Cookies.new(options) } + + # @!attribute [rw] sessions + # Sets or returns the session store (and its options) for actions. + # + # The given values are taken as an argument list to be passed to {Config::Sessions#initialize}. + # + # The configured session store is used when setting up the app or slice + # {Slice::ClassMethods#router router}. + # + # @example + # config.sessions = :cookie, {secret: "xyz"} + # + # @return [Config::Sessions] + # + # @see Config::Sessions + # @see Slice::ClassMethods#router + # + # @api public + # @since 2.0.0 setting :sessions, constructor: proc { |storage, *options| Sessions.new(storage, *options) } + + # @!attribute [rw] csrf_protection + # Sets or returns whether CSRF protection should be enabled for action classes. + # + # Defaults to true if {#sessions} is enabled. You can override this by explicitly setting a + # true or false value. + # + # When true, this will include `Hanami::Action::CSRFProtection` in all action classes. + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 setting :csrf_protection - setting :name_inference_base, default: "actions" - setting :view_context_identifier, default: "views.context" - setting :view_name_inferrer, default: Slice::ViewNameInferrer - setting :view_name_inference_base, default: "views" - + # Returns the Content Security Policy config for actions. + # + # The resulting policy is set as a default `"Content-Security-Policy"` response header. + # + # @return [Hanami::Config::Actions::ContentSecurityPolicy] + # + # @api public + # @since 2.0.0 attr_accessor :content_security_policy + # The following settings are for view and assets integration with actions, and are NOT + # publicly released as of 2.0.0. We'll make full documentation available when these become + # public in a subsequent release. + + # @!attribute [rw] name_inference_base + # @api private + setting :name_inference_base, default: "actions" + + # @!attribute [rw] view_context_identifier + # @api private + setting :view_context_identifier, default: "views.context" + + # @!attribute [rw] view_name_inferrer + # @api private + setting :view_name_inferrer, default: Slice::ViewNameInferrer + + # @!attribute [rw] view_name_inference_base + # @api private + setting :view_name_inference_base, default: "views" + + # @api private attr_reader :base_config protected :base_config + # @api private def initialize(*, **options) super() @@ -42,19 +117,22 @@ module Hanami configure_defaults end + # @api private def initialize_copy(source) super @base_config = source.base_config.dup @content_security_policy = source.content_security_policy.dup end + private :initialize_copy + # @api private def finalize! # A nil value for `csrf_protection` means it has not been explicitly configured # (neither true nor false), so we can default it to whether sessions are enabled self.csrf_protection = sessions.enabled? if csrf_protection.nil? if content_security_policy - default_headers["Content-Security-Policy"] = content_security_policy.to_str + default_headers["Content-Security-Policy"] = content_security_policy.to_s end end diff --git a/lib/hanami/config/actions/content_security_policy.rb b/lib/hanami/config/actions/content_security_policy.rb index a6625250..4445663c 100644 --- a/lib/hanami/config/actions/content_security_policy.rb +++ b/lib/hanami/config/actions/content_security_policy.rb @@ -99,7 +99,7 @@ module Hanami # @since 2.0.0 # @api private - def to_str + def to_s @policy.map do |key, value| "#{dasherize(key)} #{value}" end.join(";\n") diff --git a/lib/hanami/config/actions/cookies.rb b/lib/hanami/config/actions/cookies.rb index 04addf62..4bfccbb6 100644 --- a/lib/hanami/config/actions/cookies.rb +++ b/lib/hanami/config/actions/cookies.rb @@ -9,18 +9,45 @@ module Hanami # actions, and adds the `enabled?` method to allow app base action to determine whether to # include the `Action::Cookies` module. # + # @api public # @since 2.0.0 class Cookies + # Returns the cookie options. + # + # @return [Hash] + # + # @api public + # @since 2.0.0 attr_reader :options + # Returns a new `Cookies`. + # + # You should not need to initialize this class directly. Instead use + # {Hanami::Config::Actions#cookies}. + # + # @api private + # @since 2.0.0 def initialize(options) @options = options end + # Returns true if any cookie options have been provided. + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 def enabled? !options.nil? end + # Returns the cookie options. + # + # If no options have been provided, returns an empty hash. + # + # @return [Hash] + # + # @api public def to_h options.to_h end diff --git a/lib/hanami/config/logger.rb b/lib/hanami/config/logger.rb index f7c49882..e8386c80 100644 --- a/lib/hanami/config/logger.rb +++ b/lib/hanami/config/logger.rb @@ -7,28 +7,114 @@ module Hanami class Config # Hanami logger config # + # @api public # @since 2.0.0 class Logger include Dry::Configurable + # @return [Hanami::SliceName] + # + # @api private + # @since 2.0.o attr_reader :app_name - protected :config - + # @!attribute [rw] level + # Sets or returns the logger level. + # + # Defaults to `:info` for the production environment and `:debug` for all others. + # + # @return [Symbol] + # + # @api public + # @since 2.0.0 setting :level + # @!attribute [rw] stream + # Sets or returns the logger's stream. + # + # This can be a file path or an `IO`-like object for the logger to write to. + # + # Defaults to `"log/test.log"` for the test environment and `$stdout` for all others. + # + # @return [String, #write] + # + # @api public + # @since 2.0.0 setting :stream + # @!attribute [rw] formatter + # Sets or returns the logger's formatter. + # + # This may be a name that matches a formatter registered with `Hanami::Logger`, which + # includes `:default` and `:json`. + # + # This may also be an instance of Ruby's built-in `::Logger::Formatter` or any compatible + # object. + # + # Defaults to `:json` for the production environment, and `nil` for all others. A `nil` + # value will result in a plain `::Logger::Formatter` instance. + # + # @return [Symbol, ::Logger::Formatter] + # + # @api public + # @since 2.0.0 setting :formatter - setting :colors + # @!attribute [rw] colors + # Sets or returns whether log lines should be colorized. + # + # Defaults to `false`. + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 + setting :colors, default: false + # @!attribute [rw] filters + # Sets or returns an array of attribute names to filter from logs. + # + # Defaults to `["_csrf", "password", "password_confirmation"]`. If you want to preserve + # these defaults, append to this array rather than reassigning it. + # + # @return [Array] + # + # @api public + # @since 2.0.0 setting :filters, default: %w[_csrf password password_confirmation].freeze - setting :options, default: [], constructor: ->(value) { Array(value).flatten }, cloneable: true - + # @!attribute [rw] logger_class + # Sets or returns the class to use for the logger. + # + # This should be compatible with the arguments passed to the logger class' `.new` method in + # {#instance}. + # + # Defaults to `Hanami::Logger`. + # + # @api public + # @since 2.0.0 setting :logger_class, default: Hanami::Logger + # @!attribute [rw] options + # Sets or returns an array of positional arguments to pass to the {logger_class} when + # initializing the logger. + # + # Defaults to `[]` + # + # @return [Array] + # + # @api public + # @since 2.0.0 + setting :options, default: [], constructor: ->(value) { Array(value).flatten } + + # Returns a new `Logger` config. + # + # You should not need to initialize this directly, instead use {Hanami::Config#logger}. + # + # @param env [Symbol] the Hanami env + # @param app_name [Hanami::SliceName] + # + # @api private def initialize(env:, app_name:) @app_name = app_name @@ -50,13 +136,14 @@ module Hanami when :production :json end - - config.colors = case env - when :production, :test - false - end end + # Returns a new instance of the logger. + # + # @return [logger_class] + # + # @api public + # @since 2.0.0 def instance logger_class.new( app_name.name, diff --git a/lib/hanami/config/views.rb b/lib/hanami/config/views.rb index 850e86c3..e80cc894 100644 --- a/lib/hanami/config/views.rb +++ b/lib/hanami/config/views.rb @@ -7,7 +7,9 @@ module Hanami class Config # Hanami views config # - # @since 2.0.0 + # This is NOT RELEASED as of 2.0.0. + # + # @api private class Views include Dry::Configurable @@ -16,6 +18,7 @@ module Hanami attr_reader :base_config protected :base_config + # @api private def initialize(*) super @@ -24,10 +27,12 @@ module Hanami configure_defaults end + # @api private def initialize_copy(source) super @base_config = source.base_config.dup end + private :initialize_copy # Returns the list of available settings # @@ -39,6 +44,7 @@ module Hanami self.class.settings + View.settings - NON_FORWARDABLE_METHODS end + # @api private def finalize! return self if frozen? diff --git a/lib/hanami/constants.rb b/lib/hanami/constants.rb index dcc158f4..e3f7704f 100644 --- a/lib/hanami/constants.rb +++ b/lib/hanami/constants.rb @@ -13,6 +13,10 @@ module Hanami PATH_DELIMITER = "/" private_constant :PATH_DELIMITER + # @api private + APP_PATH = "config/app.rb" + private_constant :APP_PATH + # @api private CONFIG_DIR = "config" private_constant :CONFIG_DIR diff --git a/lib/hanami/errors.rb b/lib/hanami/errors.rb index 140dcbdf..08be85a2 100644 --- a/lib/hanami/errors.rb +++ b/lib/hanami/errors.rb @@ -1,18 +1,35 @@ # frozen_string_literal: true module Hanami + # Base class for all Hanami errors. + # + # @api public # @since 2.0.0 Error = Class.new(StandardError) + # Error raised when {Hanami::App} fails to load. + # + # @api public # @since 2.0.0 AppLoadError = Class.new(Error) + # Error raised when an {Hanami::Slice} fails to load. + # + # @api public # @since 2.0.0 SliceLoadError = Class.new(Error) + # Error raised when an individual component fails to load. + # + # @api public # @since 2.0.0 ComponentLoadError = Class.new(Error) + # Error raised when unsupported middleware configuration is given. + # + # @see Hanami::Slice::Routing::Middleware::Stack#use + # + # @api public # @since 2.0.0 UnsupportedMiddlewareSpecError = Class.new(Error) end diff --git a/lib/hanami/extensions/action.rb b/lib/hanami/extensions/action.rb index 19242311..b1a3bda2 100644 --- a/lib/hanami/extensions/action.rb +++ b/lib/hanami/extensions/action.rb @@ -5,14 +5,17 @@ require_relative "../slice_configurable" require_relative "action/slice_configured_action" module Hanami + # @api private module Extensions - # Extended behavior for actions intended for use within an Hanami app. + # Integrated behavior for `Hanami::Action` classes within Hanami apps. # - # @see Hanami::Action + # @see InstanceMethods + # @see https://github.com/hanami/controller # # @api public # @since 2.0.0 module Action + # @api private def self.included(action_class) super @@ -21,15 +24,45 @@ module Hanami action_class.prepend(InstanceMethods) end + # Class methods for app-integrated actions. + # + # @since 2.0.0 module ClassMethods + # @api private def configure_for_slice(slice) extend SliceConfiguredAction.new(slice) end end + # Instance methods for app-integrated actions. + # + # @since 2.0.0 module InstanceMethods - attr_reader :view, :view_context, :routes + # @api private + attr_reader :view + # @api private + attr_reader :view_context + + # Returns the app or slice's {Hanami::Slice::RoutesHelper RoutesHelper} for use within + # action instance methods. + # + # @return [Hanami::Slice::RoutesHelper] + # + # @api public + # @since 2.0.0 + attr_reader :routes + + # @overload def initialize(routes: nil, **kwargs) + # Returns a new `Hanami::Action` with app components injected as dependencies. + # + # These dependencies are injected automatically so that a call to `.new` (with no + # arguments) returns a fully integrated action. + # + # @param routes [Hanami::Slice::RoutesHelper] + # + # @api public + # @since 2.0.0 def initialize(view: nil, view_context: nil, routes: nil, **kwargs) @view = view @view_context = view_context @@ -40,27 +73,31 @@ module Hanami private + # @api private def build_response(**options) options = options.merge(view_options: method(:view_options)) super(**options) end + # @api private def finish(req, res, halted) res.render(view, **req.params) if !halted && auto_render?(res) super end + # @api private def view_options(req, res) {context: view_context&.with(**view_context_options(req, res))}.compact end + # @api private def view_context_options(req, res) {request: req, response: res} end # Returns true if a view should automatically be rendered onto the response body. # - # This may be overridden to enable/disable automatic rendering. + # This may be overridden to enable or disable automatic rendering. # # @param res [Hanami::Action::Response] # diff --git a/lib/hanami/extensions/view.rb b/lib/hanami/extensions/view.rb index 671370ad..64149c54 100644 --- a/lib/hanami/extensions/view.rb +++ b/lib/hanami/extensions/view.rb @@ -5,14 +5,17 @@ require_relative "../slice_configurable" require_relative "view/slice_configured_view" module Hanami + # @api private module Extensions - # Extended behavior for actions intended for use within an Hanami app. + # Integrated behavior for `Hanami::View` classes within Hanami apps. + # + # This is NOT RELEASED as of 2.0.0. # # @see Hanami::View # - # @api public - # @since 2.0.0 + # @api private module View + # @api private def self.included(view_class) super @@ -20,6 +23,7 @@ module Hanami view_class.extend(ClassMethods) end + # @api private module ClassMethods # @api private def configure_for_slice(slice) diff --git a/lib/hanami/extensions/view/context.rb b/lib/hanami/extensions/view/context.rb index e789c0d8..7125e4a8 100644 --- a/lib/hanami/extensions/view/context.rb +++ b/lib/hanami/extensions/view/context.rb @@ -11,8 +11,9 @@ module Hanami module View # View context for views in Hanami apps. # - # @api public - # @since 2.0.0 + # This is NOT RELEASED as of 2.0.0. + # + # @api private module Context def self.included(context_class) super diff --git a/lib/hanami/providers/inflector.rb b/lib/hanami/providers/inflector.rb index e4974d72..85c819d8 100644 --- a/lib/hanami/providers/inflector.rb +++ b/lib/hanami/providers/inflector.rb @@ -1,8 +1,14 @@ # frozen_string_literal: true module Hanami + # @api private module Providers + # Provider source to register inflector component in Hanami slices. + # + # @api private + # @since 2.0.0 class Inflector < Dry::System::Provider::Source + # @api private def start register :inflector, Hanami.app.inflector end diff --git a/lib/hanami/providers/logger.rb b/lib/hanami/providers/logger.rb index 70b26352..a2f21a64 100644 --- a/lib/hanami/providers/logger.rb +++ b/lib/hanami/providers/logger.rb @@ -1,8 +1,16 @@ # frozen_string_literal: true module Hanami + # @api private module Providers + # Provider source to register logger component in Hanami slices. + # + # @see Hanami::Config#logger + # + # @api private + # @since 2.0.0 class Logger < Dry::System::Provider::Source + # @api private def start register :logger, Hanami.app.config.logger_instance end diff --git a/lib/hanami/providers/rack.rb b/lib/hanami/providers/rack.rb index f58ebcc2..c293d0a8 100644 --- a/lib/hanami/providers/rack.rb +++ b/lib/hanami/providers/rack.rb @@ -1,8 +1,19 @@ # frozen_string_literal: true module Hanami + # @api private module Providers + # Provider source to register Rack integration components in Hanami slices. + # + # @see Hanami::Providers::Logger + # @see Hanami::Web::RackLogger + # @see https://github.com/rack/rack + # @see https://dry-rb.org/gems/dry-monitor/ + # + # @api private + # @since 2.0.0 class Rack < Dry::System::Provider::Source + # @api private def prepare require "dry/monitor" require "hanami/web/rack_logger" @@ -10,6 +21,7 @@ module Hanami Dry::Monitor.load_extensions(:rack) end + # @api private def start target.start :logger diff --git a/lib/hanami/providers/routes.rb b/lib/hanami/providers/routes.rb index 46ad8342..78e34c52 100644 --- a/lib/hanami/providers/routes.rb +++ b/lib/hanami/providers/routes.rb @@ -1,27 +1,37 @@ # frozen_string_literal: true module Hanami + # @api private module Providers + # Provider source to register routes helper component in Hanami slices. + # + # @see Hanami::Slice::RoutesHelper + # + # @api private + # @since 2.0.0 class Routes < Dry::System::Provider::Source + # @api private def self.for_slice(slice) Class.new(self) do |klass| klass.instance_variable_set(:@slice, slice) end end + # @api private def self.slice @slice || Hanami.app end + # @api private def prepare require "hanami/slice/routes_helper" end + # @api private def start - # Register a lazy instance of RoutesHelper to ensure we don't load prematurely - # load the router during the process of booting. This ensures the router's - # resolver can run strict action key checks once when it runs on a fully booted - # slice. + # Register a lazy instance of RoutesHelper to ensure we don't load prematurely load the + # router during the process of booting. This ensures the router's resolver can run strict + # action key checks once when it runs on a fully booted slice. register :routes do Hanami::Slice::RoutesHelper.new(self.class.slice.router) end diff --git a/lib/hanami/routes.rb b/lib/hanami/routes.rb index 3f740ffd..65013b56 100644 --- a/lib/hanami/routes.rb +++ b/lib/hanami/routes.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "constants" +require_relative "errors" require_relative "slice/router" module Hanami @@ -26,8 +27,13 @@ module Hanami # @see Hanami::Slice::Router # @since 2.0.0 class Routes + # Error raised when no action could be found in an app or slice container for the key given in a + # routes file. + # + # @api public # @since 2.0.0 - class MissingActionError < Error + class MissingActionError < Hanami::Error + # @api private def initialize(action_key, slice) action_path = action_key.gsub(CONTAINER_KEY_DELIMITER, PATH_DELIMITER) action_constant = slice.inflector.camelize( @@ -43,8 +49,13 @@ module Hanami end end + # Error raised when a given routes endpoint does not implement the `#call` interface required + # for Rack. + # + # @api public # @since 2.0.0 - class NotCallableEndpointError < Error + class NotCallableEndpointError < Hanami::Error + # @api private def initialize(endpoint) super("#{endpoint.inspect} is not compatible with Rack. Please make sure it implements #call.") end diff --git a/lib/hanami/settings.rb b/lib/hanami/settings.rb index c147db52..bc0adb82 100644 --- a/lib/hanami/settings.rb +++ b/lib/hanami/settings.rb @@ -2,42 +2,94 @@ require "dry/core" require "dry/configurable" +require_relative "errors" module Hanami - # App settings + # Provides user-defined settings for an Hanami app or slice. # - # Users are expected to inherit from this class to define their app settings. + # Define your own settings by inheriting from this class in `config/settings.rb` within an app or + # slice. Your settings will be loaded from matching ENV vars (with upper-cased names) and made + # registered as a component as part of the Hanami app {Hanami::Slice::ClassMethods#prepare + # prepare} step. + # + # The settings instance is registered in your app and slice containers as a `"settings"` + # component. You can use the `Deps` mixin to inject this dependency and make settings available to + # your other components as required. # # @example # # config/settings.rb # # frozen_string_literal: true # - # require "hanami/settings" - # require "my_app/types" - # # module MyApp # class Settings < Hanami::Settings - # setting :database_url - # setting :feature_flag, default: false, constructor: Types::Params::Bool + # Secret = Types::String.constrained(min_size: 20) + # + # setting :database_url, constructor: Types::String + # setting :session_secret, constructor: Secret + # setting :some_flag, default: false, constructor: Types::Params::Bool # end # end # - # Settings are defined with [dry-configurable](https://dry-rb.org/gems/dry-configurable/), so you - # can take a look there to see the supported syntax. + # Settings are defined with [dry-configurable][dry-c]'s `setting` method. You may likely want to + # provide `default:` and `constructor:` options for your settings. # - # Users work with an instance of this class made available within the `settings` key in the - # container. The instance gets its settings populated from a configurable store, which defaults to - # {Hanami::Settings::EnvStore}. + # If you have [dry-types][dry-t] bundled, then a nested `Types` module will be available for type + # checking your setting values. Pass type objects to the setting `constructor:` options to ensure + # their values meet your type expectations. You can use dry-types' default type objects or define + # your own. # - # A different store can be set through the `settings_store` Hanami configuration option. All it - # needs to do is implementing a `#fetch` method with the same signature as `Hash#fetch`. + # When the settings are initialized, all type errors will be collected and presented together for + # correction. Settings are loaded early, as part of the Hanami app's + # {Hanami::Slice::ClassMethods#prepare prepare} step, to ensure that the app boots only when valid + # settings are present. + # + # Setting values are loaded from a configurable store, which defaults to + # {Hanami::Settings::EnvStore}, which fetches the values from equivalent upper-cased keys in + # `ENV`. You can configue an alternative store via {Hanami::Config#settings_store}. Setting stores + # must implement a `#fetch` method with the same signature as `Hash#fetch`. + # + # [dry-c]: https://dry-rb.org/gems/dry-configurable/ + # [dry-t]: https://dry-rb.org/gems/dry-types/ # # @see Hanami::Settings::DotenvStore # # @api public # @since 2.0.0 class Settings + # Error raised when setting values do not meet their type expectations. + # + # Its message collects all the individual errors that can be raised for each setting. + # + # @api public + # @since 2.0.0 + class InvalidSettingsError < Hanami::Error + # @api private + def initialize(errors) + super() + @errors = errors + end + + # Returns the exception's message. + # + # @return [String] + # + # @api public + # @since 2.0.0 + def to_s + <<~STR.strip + Could not initialize settings. The following settings were invalid: + + #{@errors.map { |setting, message| "#{setting}: #{message}" }.join("\n")} + STR + end + end + class << self + # Defines a nested `Types` constant in `Settings` subclasses if dry-types is bundled. + # + # @see https://dry-rb.org/gems/dry-types + # + # @api private def inherited(subclass) super @@ -94,25 +146,6 @@ module Hanami end end - # Exception for errors in the definition of settings. - # - # Its message collects all the individual errors that can be raised for each setting. - # - # @api public - InvalidSettingsError = Class.new(StandardError) do - def initialize(errors) - @errors = errors - end - - def to_s - <<~STR.strip - Could not initialize settings. The following settings were invalid: - - #{@errors.map { |setting, message| "#{setting}: #{message}" }.join("\n")} - STR - end - end - # @api private Undefined = Dry::Core::Constants::Undefined @@ -143,14 +176,47 @@ module Hanami config.finalize! end + # Returns a string containing a human-readable representation of the settings. + # + # This includes setting names only, not any values, to ensure that sensitive values do not + # inadvertently leak. + # + # Use {#inspect_values} to inspect settings with their values. + # + # @example + # settings.inspect + # # => # + # + # @return [String] + # + # @see #inspect_values + # + # @api public + # @since 2.0.0 def inspect - "#<#{self.class.to_s} [#{config._settings.map(&:name).join(", ")}]>" + "#<#{self.class} [#{config._settings.map(&:name).join(", ")}]>" end + # rubocop:disable Layout/LineLength + + # Returns a string containing a human-readable representation of the settings and their values. + # + # @example + # settings.inspect_values + # # => # + # + # @return [String] + # + # @see #inspect + # + # @api public + # @since 2.0.0 def inspect_values - "#<#{self.class.to_s} #{config._settings.map { |setting| "#{setting.name}=#{config[setting.name].inspect}" }.join(" ")}>" + "#<#{self.class} #{config._settings.map { |setting| "#{setting.name}=#{config[setting.name].inspect}" }.join(" ")}>" end + # rubocop:enable Layout/LineLength + private def method_missing(name, *args, &block) diff --git a/lib/hanami/settings/env_store.rb b/lib/hanami/settings/env_store.rb index a0fcea68..2c0ce6a3 100644 --- a/lib/hanami/settings/env_store.rb +++ b/lib/hanami/settings/env_store.rb @@ -4,7 +4,7 @@ require "dry/core/constants" module Hanami class Settings - # The default store for {Hanami:Settings}, loading setting values from `ENV`. + # The default store for {Hanami::Settings}, loading setting values from `ENV`. # # If your app loads the dotenv gem, then `ENV` will also be populated from various `.env` files when # you subclass `Hanami::App`. diff --git a/lib/hanami/slice.rb b/lib/hanami/slice.rb index 630db6db..cb67b360 100644 --- a/lib/hanami/slice.rb +++ b/lib/hanami/slice.rb @@ -35,6 +35,7 @@ module Hanami class Slice @_mutex = Mutex.new + # @api private def self.inherited(subclass) super @@ -51,14 +52,69 @@ module Hanami # rubocop:disable Metrics/ModuleLength module ClassMethods - attr_reader :parent, :autoloader, :container + # Returns the slice's parent. + # + # For top-level slices defined in `slices/` or `config/slices/`, this will be the Hanami app + # itself (`Hanami.app`). For nested slices, this will be the slice in which they were + # registered. + # + # @return [Hanami::Slice] + # + # @see #register_slice + # + # @api public + # @since 2.0.0 + attr_reader :parent + # Returns the slice's autoloader. + # + # Each slice has its own `Zeitwerk::Loader` autoloader instance, which is setup when the slice + # is {#prepare prepared}. + # + # @return [Zeitwerk::Loader] + # + # @see https://github.com/fxn/zeitwerk + # + # @api public + # @since 2.0.0 + attr_reader :autoloader + + # Returns the slice's container. + # + # This is a `Dry::System::Container` that is already configured for the slice. + # + # In ordinary usage, you shouldn't need direct access the container at all, since the slice + # provides its own methods for interacting with the container (such as {#[]}, {#keys}, {#key?} + # {#register}, {#register_provider}, {#prepare}, {#start}, {#stop}). + # + # If you need to configure the container directly, use {#prepare_container}. + # + # @see https://dry-rb.org/gems/dry-system + # + # @api public + # @since 2.0.0 + attr_reader :container + + # Returns the Hanami app. + # + # @return [Hanami::App] + # + # @api public + # @since 2.0.0 def app Hanami.app end - # A slice's config is copied from the app config at time of first access. The app should have - # its config completed before slices are loaded. + # Returns the slice's config. + # + # A slice's config is copied from the app config at time of first access. + # + # @return [Hanami::Config] + # + # @see App::ClassMethods.config + # + # @api public + # @since 2.0.0 def config @config ||= app.config.dup.tap do |slice_config| # Remove specific values from app that will not apply to this slice @@ -66,14 +122,41 @@ module Hanami end end + # Returns a {SliceName} for the slice, an object with methods returning the name of the slice + # in various formats. + # + # @return [SliceName] + # + # @api public + # @since 2.0.0 def slice_name @slice_name ||= SliceName.new(self, inflector: method(:inflector)) end + # Returns the constant for the slice's module namespace. + # + # @example + # MySlice::Slice.namespace # => MySlice + # + # @return [Module] the namespace module constant + # + # @see SliceName#namespace + # + # @api public + # @since 2.0.0 def namespace slice_name.namespace end + # Returns the slice's root, either the root as explicitly configured, or a default fallback of + # the slice's name within the app's `slices/` dir. + # + # @return [Pathname] + # + # @see Config#root + # + # @api public + # @since 2.0.0 def root # Provide a best guess for a root when it is not yet configured. # @@ -89,23 +172,105 @@ module Hanami config.root || app.root.join(SLICES_DIR, slice_name.to_s) end + # Returns the slice's configured inflector. + # + # Unless explicitly re-configured for the slice, this will be the app's inflector. + # + # @return [Dry::Inflector] + # + # @see Config#inflector + # @see Config#inflections + # + # @api public + # @since 2.0.0 def inflector config.inflector end + # @overload prepare + # Prepares the slice. + # + # This will define the slice's `Slice` and `Deps` constants, make all Ruby source files + # inside the slice's root dir autoloadable, as well as lazily loadable as container + # components. + # + # Call `prepare` when you want to access particular components within the slice while still + # minimizing load time. Preparing slices is the approach taken when loading the Hanami + # console or when running tests. + # + # @return [self] + # + # @see #boot + # + # @api public + # @since 2.0.0 + # + # @overload prepare(provider_name) + # Prepares a provider. + # + # This triggers the provider's `prepare` lifecycle step. + # + # @param provider_name [Symbol] the name of the provider to start + # + # @return [self] + # + # @api public + # @since 2.0.0 def prepare(provider_name = nil) if provider_name container.prepare(provider_name) - self else prepare_slice end + + self end + # Captures the given block to be called with the slice's container during the slice's + # `prepare` step, after the slice has already configured the container. + # + # This is intended for advanced usage only and should not be needed for ordinary slice + # configuration and usage. + # + # @example + # module MySlice + # class Sliice < Hanami::Slice + # prepare_container do |container| + # # ... + # end + # end + # end + # + # @yieldparam container [Dry::System::Container] the slice's container + # + # @return [self] + # + # @see #prepare + # + # @api public + # @since 2.0.0 def prepare_container(&block) @prepare_container_block = block + self end + # Boots the slice. + # + # This will prepare the slice (if not already prepared), start each of its providers, register + # all the slice's components from its Ruby source files, and import components from any other + # slices. It will also boot any of the slice's own registered nested slices. It will then + # freeze its container so no further components can be registered. + # + # Call `boot` if you want to fully load a slice and incur all load time up front, such as when + # preparing an app to serve web requests. Booting slices is the approach taken when running + # Hanami's standard Puma setup (see `config.ru`). + # + # @return [self] + # + # @see #prepare + # + # @api public + # @since 2.0.0 def boot return self if booted? @@ -119,64 +284,345 @@ module Hanami self end + # Shuts down the slice's providers, as well as the providers in any nested slices. + # + # @return [self] + # + # @api public + # @since 2.0.0 def shutdown slices.each(&:shutdown) container.shutdown! self end + # Returns true if the slice has been prepared. + # + # @return [Boolean] + # + # @see #prepare + # + # @api public + # @since 2.0.0 def prepared? !!@prepared end + # Returns true if the slice has been booted. + # + # @return [Boolean] + # + # @see #boot + # + # @api public + # @since 2.0.0 def booted? !!@booted end + # Returns the slice's collection of nested slices. + # + # @return [SliceRegistrar] + # + # @see #register_slice + # + # @api public + # @since 2.0.0 def slices @slices ||= SliceRegistrar.new(self) end + # @overload register_slice(name, &block) + # Registers a nested slice with the given name. + # + # This will define a new {Slice} subclass for the slice. If a block is given, it is passed + # the class object, and will be evaluated in the context of the class like `class_eval`. + # + # @example + # MySlice::Slice.register_slice do + # # Configure the slice or do other class-level things here + # end + # + # @param name [Symbol] the identifier for the slice to be registered + # @yieldparam slice [Hanami::Slice] the newly defined slice class + # + # @overload register_slice(name, slice_class) + # Registers a nested slice with the given name. + # + # The given `slice_class` will be registered as the slice. It must be a subclass of {Slice}. + # + # @param name [Symbol] the identifier for the slice to be registered + # @param slice_class [Hanami::Slice] + # + # @return [slices] + # + # @see SliceRegistrar#register + # + # @api public + # @since 2.0.0 def register_slice(...) slices.register(...) end + # Registers a component in the slice's container. + # + # @overload register(key, object) + # Registers the given object as the component. This same object will be returned whenever + # the component is resolved. + # + # @param key [String] the component's key + # @param object [Object] the object to register as the component + # + # @overload reigster(key, memoize: false, &block) + # Registers the given block as the component. When the component is resolved, the return + # value of the block will be returned. + # + # Since the block is not called until resolution-time, this is a useful way to register + # components that have dependencies on other components in the container, which as yet may + # be unavailable at the time of registration. + # + # All auto-registered components are registered in block form. + # + # When `memoize` is true, the component will be memoized upon first resolution and the same + # object returned on all subsequent resolutions, meaning the block is only called once. + # Otherwise, the block will be called and a new object returned on every resolution. + # + # @param key [String] the component's key + # @param memoize [Boolean] + # @yieldreturn [Object] the object to register as the component + # + # @overload reigster(key, call: true, &block) + # Registers the given block as the component. When `call: false` is given, then the block + # itself will become the component. + # + # When such a component is resolved, the block will not be called, and instead the `Proc` + # object for that block will be returned. + # + # @param key [String] the component's key + # @param call [Booelan] + # + # @return [container] + # + # @see #[] + # @see #resolve + # + # @api public + # @since 2.0.0 def register(...) container.register(...) end + # @overload register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block) + # Registers a provider and its lifecycle hooks. + # + # In most cases, you should call this from a dedicated file for the provider in your app or + # slice's `config/providers/` dir. This allows the provider to be loaded when individual + # matching components are resolved (for prepared slices) or when slices are booted. + # + # @example Simple provider + # # config/providers/db.rb + # Hanami.app.register_provider(:db) do + # start do + # require "db" + # register("db", DB.new) + # end + # end + # + # @example Provider with lifecycle steps, also using dependencies from the target container + # # config/providers/db.rb + # Hanami.app.register_provider(:db) do + # prepare do + # require "db" + # db = DB.new(target_container["settings"].database_url) + # register("db", db) + # end + # + # start do + # container["db"].establish_connection + # end + # + # stop do + # container["db"].close_connection + # end + # end + # + # @example Probvider registration under a namespace + # # config/providers/db.rb + # Hanami.app.register_provider(:persistence, namespace: true) do + # start do + # require "db" + # + # # Namespace option above means this will be registered as "persistence.db" + # register("db", DB.new) + # end + # end + # + # @param name [Symbol] the unique name for the provider + # @param namespace [Boolean, String, nil] register components from the provider with given + # namespace. May be an explicit string, or `true` for the namespace to be the provider's + # name + # @param from [Symbol, nil] the group for an external provider source to use, with the + # provider source name inferred from `name` or passsed explicitly as `source:` + # @param source [Symbol, nil] the name of the external provider source to use, if different + # from the value provided as `name` + # @param if [Boolean] a boolean-returning expression to determine whether to register the + # provider + # + # @return [container] + # + # @api public + # @since 2.0.0 def register_provider(...) container.register_provider(...) end + # @overload start(provider_name) + # Starts a provider. + # + # This triggers the provider's `prepare` and `start` lifecycle steps. + # + # @example + # MySlice::Slice.start(:persistence) + # + # @param provider_name [Symbol] the name of the provider to start + # + # @return [container] + # + # @api public + # @since 2.0.0 def start(...) container.start(...) end + # @overload stop(provider_name) + # Stops a provider. + # + # This triggers the provider's `stop` lifecycle hook. + # + # @example + # MySlice::Slice.stop(:persistence) + # + # @param provider_name [Symbol] the name of the provider to start + # + # @return [container] + # + # @api public + # @since 2.0.0 def stop(...) container.stop(...) end + # @overload key?(key) + # Returns true if the component with the given key is registered in the container. + # + # For a prepared slice, calling `key?` will also try to load the component if not loaded + # already. + # + # @param key [String, Symbol] the component key + # + # @return [Boolean] + # + # @api public + # @since 2.0.0 def key?(...) container.key?(...) end + # Returns an array of keys for all currently registered components in the container. + # + # For a prepared slice, this will be the set of components that have been previously resolved. + # For a booted slice, this will be all components available for the slice. + # + # @return [Array] + # + # @api public + # @since 2.0.0 def keys container.keys end + # @overload [](key) + # Resolves the component with the given key from the container. + # + # For a prepared slice, this will attempt to load and register the matching component if it + # is not loaded already. For a booted slice, this will return from already registered + # components only. + # + # @return [Object] the resolved component's object + # + # @raise Dry::Container::KeyError if the component could not be found or loaded + # + # @see #resolve + # + # @api public + # @since 2.0.0 def [](...) container.[](...) end + # @see #[] + # + # @api public + # @since 2.0.0 def resolve(...) container.resolve(...) end + # Specifies the components to export from the slice. + # + # Slices importing from this slice can import the specified components only. + # + # @example + # module MySlice + # class Slice < Hanami::Slice + # export ["search", "index_entity"] + # end + # end + # + # @param keys [Array] the component keys to export + # + # @return [self] + # + # @api public + # @since 2.0.0 def export(keys) container.config.exports = keys + self end + # @overload import(from:, as: nil, keys: nil) + # Specifies components to import from another slice. + # + # Booting a slice will register all imported components. For a prepared slice, these + # components will be be imported automatically when resolved. + # + # @example + # module MySlice + # class Slice < Hanami:Slice + # # Component from Search::Slice will import as "search.index_entity" + # import keys: ["index_entity"], from: :search + # end + # end + # + # @example Other import variations + # # Different key namespace: component will be "search_backend.index_entity" + # import keys: ["index_entity"], from: :search, as: "search_backend" + # + # # Import to root key namespace: component will be "index_entity" + # import keys: ["index_entity"], from: :search, as: nil + # + # # Import all components + # import from: :search + # + # @param keys [Array, nil] Array of component keys to import. To import all + # available components, omit this argument. + # @param from [Symbol] name of the slice to import from + # @param as [Symbol, String, nil] + # + # @see #export + # + # @api public + # @since 2.0.0 def import(from:, **kwargs) slice = self @@ -192,16 +638,51 @@ module Hanami end end + # Returns the slice's settings, or nil if no settings are defined. + # + # You can define your settings in `config/settings.rb`. + # + # @return [Hanami::Settings, nil] + # + # @see Hanami::Settings + # + # @api public + # @since 2.0.0 def settings return @settings if instance_variable_defined?(:@settings) @settings = Settings.load_for_slice(self) end + # Returns the slice's routes, or nil if no routes are defined. + # + # You can define your routes in `config/routes.rb`. + # + # @return [Hanami::Routes, nil] + # + # @see Hanami::Routes + # + # @api public + # @since 2.0.0 def routes @routes ||= load_routes end + # Returns the slice's router, if or nil if no routes are defined. + # + # An optional `inspector`, implementing the `Hanami::Router::Inspector` interface, may be + # provided at first call (the router is then memoized for subsequent accesses). An inspector + # is used by the `hanami routes` CLI comment to provide a list of available routes. + # + # The returned router is a {Slice::Router}, which provides all `Hanami::Router` functionality, + # with the addition of support for slice mounting with the {Slice::Router#slice}. + # + # @param inspector [Hanami::Router::Inspector, nil] an optional routes inspector + # + # @return [Hanami::Slice::Router, nil] + # + # @api public + # @since 2.0.0 def router(inspector: nil) raise SliceLoadError, "#{self} must be prepared before loading the router" unless prepared? @@ -210,12 +691,38 @@ module Hanami end end + # Returns a [Rack][rack] app for the slice, or nil if no routes are defined. + # + # The rack app will be memoized on first access. + # + # [rack]: https://github.com/rack/rack + # + # @return [#call, nil] the rack app, or nil if no routes are defined + # + # @see #routes + # @see #router + # + # @api public + # @since 2.0.0 def rack_app return unless router @rack_app ||= router.to_rack_app end + # @overload call(rack_env) + # Calls the slice's [Rack][rack] app and returns a Rack-compatible response object + # + # [rack]: https://github.com/rack/rack + # + # @param rack_env [Hash] the Rack environment for the request + # + # @return [Array] the three-element Rack response array + # + # @see #rack_app + # + # @api public + # @since 2.0.0 def call(...) rack_app.call(...) end diff --git a/lib/hanami/slice/router.rb b/lib/hanami/slice/router.rb index c2a88064..aa68508b 100644 --- a/lib/hanami/slice/router.rb +++ b/lib/hanami/slice/router.rb @@ -5,22 +5,27 @@ require_relative "routing/middleware/stack" module Hanami class Slice - # Hanami app router + # `Hanami::Router` subclass with enhancements for use within Hanami apps. + # + # This is loaded from Hanami apps and slices and made available as their + # {Hanami::Slice::ClassMethods#router router}. + # + # @api private # @since 2.0.0 class Router < ::Hanami::Router # @api private attr_reader :middleware_stack - # @since 2.0.0 # @api private + # @since 2.0.0 def initialize(routes:, middleware_stack: Routing::Middleware::Stack.new, **kwargs, &blk) @middleware_stack = middleware_stack instance_eval(&blk) super(**kwargs, &routes) end - # @since 2.0.0 # @api private + # @since 2.0.0 def freeze return self if frozen? @@ -28,20 +33,40 @@ module Hanami super end - # @since 2.0.0 # @api private + # @since 2.0.0 def use(...) middleware_stack.use(...) end - # @since 2.0.0 # @api private + # @since 2.0.0 def scope(*args, &blk) middleware_stack.with(args.first) do super end end + # Yields a block for routes to resolve their action components from the given slice. + # + # An optional URL prefix may be supplied with `at:`. + # + # @example + # # config/routes.rb + # + # module MyApp + # class Routes < Hanami::Routes + # slice :admin, at: "/admin" do + # # Will route to the "actions.posts.index" component in Admin::Slice + # get "posts", to: "posts.index" + # end + # end + # end + # + # @param slice_name [Symbol] the slice's name + # @param at [String, nil] optional URL prefix for the routes + # + # @api public # @since 2.0.0 def slice(slice_name, at:, &blk) blk ||= @resolver.find_slice(slice_name).routes @@ -54,8 +79,8 @@ module Hanami @resolver = prev_resolver end - # @since 2.0.0 # @api private + # @since 2.0.0 def to_rack_app middleware_stack.to_rack_app(self) end diff --git a/lib/hanami/slice/routing/middleware/stack.rb b/lib/hanami/slice/routing/middleware/stack.rb index d4d4c805..c19b74cf 100644 --- a/lib/hanami/slice/routing/middleware/stack.rb +++ b/lib/hanami/slice/routing/middleware/stack.rb @@ -11,8 +11,8 @@ module Hanami module Middleware # Wraps a rack app with a middleware stack # - # We use this class to add middlewares to the rack application generated - # from {Hanami::Slice::Router}. + # We use this class to add middlewares to the rack application generated from + # {Hanami::Slice::Router}. # # ``` # stack = Hanami::Slice::Routing::Middleware::Stack.new @@ -28,6 +28,8 @@ module Hanami # end # ``` # + # @see Hanami::Config#middleware + # # @since 2.0.0 # @api private class Stack @@ -42,8 +44,15 @@ module Hanami # @api private attr_reader :stack - # @since 2.0.0 + # Returns an array of Ruby namespaces from which to load middleware classes specified by + # symbol names given to {#use}. + # + # Defaults to `[Hanami::Middleware]`. + # + # @return [Array] + # # @api public + # @since 2.0.0 attr_reader :namespaces # @since 2.0.0 @@ -63,8 +72,30 @@ module Hanami @namespaces = namespaces.dup end + # Adds a middleware to the stack. + # + # @example + # # Using a symbol name; adds Hanami::Middleware::BodyParser.new([:json]) + # middleware.use :body_parser, :json + # + # # Using a class name + # middleware.use MyMiddleware + # + # # Adding a middleware before or after others + # middleware.use MyMiddleware, before: SomeMiddleware + # middleware.use MyMiddleware, after: OtherMiddleware + # + # @param spec [Symbol, Class] the middleware name or class name + # @param args [Array, nil] Arguments to pass to the middleware's `.new` method + # @param before [Class, nil] an optional (already added) middleware class to add the + # middleware before + # @param after [Class, nil] an optional (already added) middleware class to add the + # middleware after + # + # @return [self] + # + # @api public # @since 2.0.0 - # @api private def use(spec, *args, before: nil, after: nil, &blk) middleware = resolve_middleware_class(spec) item = [middleware, args, blk] diff --git a/lib/hanami/slice/routing/resolver.rb b/lib/hanami/slice/routing/resolver.rb index 833a1436..59822afa 100644 --- a/lib/hanami/slice/routing/resolver.rb +++ b/lib/hanami/slice/routing/resolver.rb @@ -4,9 +4,14 @@ require_relative "../../routes" module Hanami class Slice + # @api private module Routing # Hanami app router endpoint resolver # + # This resolves endpoints objects from a slice container using the strings passed to `to:` as + # their container keys. + # + # @api private # @since 2.0.0 class Resolver SLICE_ACTIONS_KEY_NAMESPACE = "actions" diff --git a/lib/hanami/slice/view_name_inferrer.rb b/lib/hanami/slice/view_name_inferrer.rb index 535284dc..887785c0 100644 --- a/lib/hanami/slice/view_name_inferrer.rb +++ b/lib/hanami/slice/view_name_inferrer.rb @@ -23,7 +23,7 @@ module Hanami # ViewNameInferrer.call(action_name: "Main::Actions::Posts::Create", slice: Main::Slice) # # => ["views.posts.create", "views.posts.new"] # - # @param action_name [String] action class name + # @param action_class_name [String] action class name # @param slice [Hanami::Slice, Hanami::Application] Hanami slice containing the action # # @return [Array] array of paired view container keys diff --git a/lib/hanami/web/rack_logger.rb b/lib/hanami/web/rack_logger.rb index 347cbaca..2791ec63 100644 --- a/lib/hanami/web/rack_logger.rb +++ b/lib/hanami/web/rack_logger.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true module Hanami + # @api private module Web # Rack logger for Hanami apps + # + # @api private + # @since 2.0.0 class RackLogger REQUEST_METHOD = "REQUEST_METHOD" private_constant :REQUEST_METHOD @@ -25,10 +29,14 @@ module Hanami CONTENT_LENGTH = "Content-Length" private_constant :CONTENT_LENGTH + # @api private + # @since 2.0.0 def initialize(logger) @logger = logger end + # @api private + # @since 2.0.0 def attach(rack_monitor) rack_monitor.on :stop do |event| log_request event[:env], event[:status], event[:time] @@ -39,6 +47,8 @@ module Hanami end end + # @api private + # @since 2.0.0 def log_request(env, status, elapsed) data = { verb: env[REQUEST_METHOD], @@ -54,6 +64,8 @@ module Hanami logger.info(data) end + # @api private + # @since 2.0.0 def log_exception(exception) logger.error exception.message logger.error exception.backtrace.join("\n") diff --git a/spec/unit/hanami/config/actions/content_security_policy_spec.rb b/spec/unit/hanami/config/actions/content_security_policy_spec.rb index 8b8ea2db..109f8321 100644 --- a/spec/unit/hanami/config/actions/content_security_policy_spec.rb +++ b/spec/unit/hanami/config/actions/content_security_policy_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Hanami::Config::Actions, "#content_security_policy" do %(style-src 'self' 'unsafe-inline' https:) ].join("\n") - expect(content_security_policy.to_str).to eq(expected) + expect(content_security_policy.to_s).to eq(expected) end end @@ -51,35 +51,35 @@ RSpec.describe Hanami::Config::Actions, "#content_security_policy" do content_security_policy[:script_src] += " #{cdn_url}" expect(content_security_policy[:script_src]).to eq("'self' #{cdn_url}") - expect(content_security_policy.to_str).to match("'self' #{cdn_url}") + expect(content_security_policy.to_s).to match("'self' #{cdn_url}") end it "overrides default values" do content_security_policy[:style_src] = cdn_url expect(content_security_policy[:style_src]).to eq(cdn_url) - expect(content_security_policy.to_str).to match(cdn_url) + expect(content_security_policy.to_s).to match(cdn_url) end it "nullifies value" do content_security_policy[:plugin_types] = nil expect(content_security_policy[:plugin_types]).to be(nil) - expect(content_security_policy.to_str).to match("plugin-types ;") + expect(content_security_policy.to_s).to match("plugin-types ;") end it "deletes key" do content_security_policy.delete(:object_src) expect(content_security_policy[:object_src]).to be(nil) - expect(content_security_policy.to_str).to_not match("object-src") + expect(content_security_policy.to_s).to_not match("object-src") end it "adds a custom key" do content_security_policy[:a_custom_key] = "foo" expect(content_security_policy[:a_custom_key]).to eq("foo") - expect(content_security_policy.to_str).to match("a-custom-key foo") + expect(content_security_policy.to_s).to match("a-custom-key foo") end end @@ -87,7 +87,7 @@ RSpec.describe Hanami::Config::Actions, "#content_security_policy" do it "sets default header" do config.finalize! - expect(config.default_headers.fetch("Content-Security-Policy")).to eq(content_security_policy.to_str) + expect(config.default_headers.fetch("Content-Security-Policy")).to eq(content_security_policy.to_s) end end diff --git a/spec/unit/hanami/config/actions/default_values_spec.rb b/spec/unit/hanami/config/actions/default_values_spec.rb index 204e5ec9..7050c7c7 100644 --- a/spec/unit/hanami/config/actions/default_values_spec.rb +++ b/spec/unit/hanami/config/actions/default_values_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Hanami::Config::Actions, "default values" do "X-Frame-Options" => "DENY", "X-Content-Type-Options" => "nosniff", "X-XSS-Protection" => "1; mode=block", - "Content-Security-Policy" => config.content_security_policy.to_str + "Content-Security-Policy" => config.content_security_policy.to_s ) } end diff --git a/spec/unit/hanami/config/logger_spec.rb b/spec/unit/hanami/config/logger_spec.rb index e0a1769d..5c8eab5e 100644 --- a/spec/unit/hanami/config/logger_spec.rb +++ b/spec/unit/hanami/config/logger_spec.rb @@ -102,32 +102,16 @@ RSpec.describe Hanami::Config::Logger do end describe "#colors" do - it "defaults to nil" do - expect(subject.colors).to eq(nil) - end - - context "when :test environment" do - let(:env) { :test } - - it "returns false" do - expect(subject.colors).to eq(false) - end - end - - context "when :production environment" do - let(:env) { :production } - - it "returns false" do - expect(subject.colors).to eq(false) - end + it "defaults to false" do + expect(subject.colors).to eq(false) end end describe "#colors=" do it "accepts a value" do - expect { subject.colors = false } + expect { subject.colors = true } .to change { subject.colors } - .to(false) + .to(true) end end