Update API docs for 2.0.0 (#1229)

This commit is contained in:
Tim Riley 2022-11-01 16:17:53 +11:00 committed by GitHub
parent 70156569b2
commit a34ff4426c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1496 additions and 243 deletions

View File

@ -16,6 +16,8 @@ Style/LambdaCall:
Enabled: false
Style/StabbyLambdaParentheses:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/TrailingCommaInArguments:
Enabled: false
Style/TrailingCommaInArrayLiteral:

4
.yardopts Normal file
View File

@ -0,0 +1,4 @@
--markup=markdown
--plugin junk
--junk-log-ignore UnknownError

View File

@ -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"

View File

@ -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<Symbol>] 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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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<String>, 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<String>]
#
# @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<String>] 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"] # => #<Hanami::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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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<String>]
#
# @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<Object>]
#
# @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,

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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]
#

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
# # => #<MyApp::Settings [database_url, session_secret, some_flag]>
#
# @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
# # => #<MyApp::Settings database_url="postgres://localhost/my_db", session_secret="xxx", some_flag=true]>
#
# @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)

View File

@ -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`.

View File

@ -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<String>]
#
# @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<String>] 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<String>, 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

View File

@ -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

View File

@ -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<Object>]
#
# @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]

View File

@ -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"

View File

@ -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<string>] array of paired view container keys

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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