Module: Hanami::Slice::ClassMethods

Defined in:
gems/gems/hanami-2.1.0/lib/hanami/slice.rb

Overview

rubocop:disable Metrics/ModuleLength

Since:

  • 2.0.0

Constant Summary

ROUTER_NOT_ALLOWED_HANDLER =

Since:

  • 2.0.0

-> env, allowed_http_methods {
  raise Hanami::Router::NotAllowedError.new(env, allowed_http_methods)
}.freeze
ROUTER_NOT_FOUND_HANDLER =

Since:

  • 2.0.0

-> env {
  raise Hanami::Router::NotFoundError.new(env)
}.freeze

Instance Method Summary collapse

Instance Method Details

#[](key) ⇒ Object

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.

Returns:

  • (Object)

    the resolved component’s object

Raises:

  • Dry::Container::KeyError if the component could not be found or loaded

See Also:

  • #resolve

Since:

  • 2.0.0

Since:

  • 2.0.0

def [](...)
  container.[](...)
end

#assets_dir?Boolean (private)

Returns:

Since:

  • 2.0.0

def assets_dir?
  assets_path = app.eql?(self) ? root.join("app", "assets") : root.join("assets")
  assets_path.directory?
end

#call(rack_env) ⇒ Array

Calls the slice’s Rack app and returns a Rack-compatible response object

Parameters:

  • rack_env (Hash)

    the Rack environment for the request

Returns:

  • (Array)

    the three-element Rack response array

See Also:

  • #rack_app

Since:

  • 2.0.0

Since:

  • 2.0.0

def call(...)
  rack_app.call(...)
end

#ensure_rootObject (private)

Since:

  • 2.0.0

def ensure_root
  unless config.root
    raise SliceLoadError, "Slice must have a `config.root` before it can be prepared"
  end
end

#ensure_slice_constsObject (private)

Since:

  • 2.0.0

def ensure_slice_consts
  if namespace.const_defined?(:Container) || namespace.const_defined?(:Deps)
    raise(
      SliceLoadError,
      "#{namespace}::Container and #{namespace}::Deps constants must not already be defined"
    )
  end
end

#ensure_slice_nameObject (private)

Since:

  • 2.0.0

def ensure_slice_name
  unless name
    raise SliceLoadError, "Slice must have a class name before it can be prepared"
  end
end

#import(from: , as: nil, keys: nil) ⇒ Object

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.

Examples:

module MySlice
  class Slice < Hanami:Slice
    # Component from Search::Slice will import as "search.index_entity"
    import keys: ["index_entity"], from: :search
  end
end

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

Parameters:

  • keys (Array<String>, nil)

    Array of component keys to import. To import all available components, omit this argument.

  • from (Symbol)

    name of the slice to import from

  • as (Symbol, String, nil)

See Also:

  • #export

Since:

  • 2.0.0

Since:

  • 2.0.0

def import(from:, **kwargs)
  slice = self

  container.after(:configure) do
    if from.is_a?(Symbol) || from.is_a?(String)
      slice_name = from
      from = slice.parent.slices[from.to_sym].container
    end

    as = kwargs[:as] || slice_name

    import(from: from, as: as, **kwargs)
  end
end

#key?(key) ⇒ Boolean

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.

Parameters:

  • key (String, Symbol)

    the component key

Returns:

Since:

  • 2.0.0

Since:

  • 2.0.0

def key?(...)
  container.key?(...)
end

#load_router(inspector:) ⇒ Object (private)

Since:

  • 2.0.0

def load_router(inspector:)
  return unless routes

  require_relative "slice/router"

  slice = self
  config = self.config
  rack_monitor = self["rack.monitor"]

  show_welcome = Hanami.env?(:development) && routes.empty?
  render_errors = render_errors?
  render_detailed_errors = render_detailed_errors?

  error_handlers = {}.tap do |hsh|
    if render_errors || render_detailed_errors
      hsh[:not_allowed] = ROUTER_NOT_ALLOWED_HANDLER
      hsh[:not_found] = ROUTER_NOT_FOUND_HANDLER
    end
  end

  Slice::Router.new(
    inspector: inspector,
    routes: routes,
    resolver: config.router.resolver.new(slice: self),
    **error_handlers,
    **config.router.options
  ) do
    use(rack_monitor)

    use(Hanami::Web::Welcome) if show_welcome

    use(
      Hanami::Middleware::RenderErrors,
      config,
      Hanami::Middleware::PublicErrorsApp.new(slice.root.join("public"))
    )

    if render_detailed_errors
      require "hanami/webconsole"
      use(Hanami::Webconsole::Middleware, config)
    end

    if Hanami.bundled?("hanami-controller")
      if config.actions.method_override
        require "rack/method_override"
        use(Rack::MethodOverride)
      end

      if config.actions.sessions.enabled?
        use(*config.actions.sessions.middleware)
      end
    end

    if Hanami.bundled?("hanami-assets") && config.assets.serve
      use(Hanami::Middleware::Assets)
    end

    middleware_stack.update(config.middleware_stack)
  end
end

#load_routesObject (private)

Since:

  • 2.0.0

def load_routes
  return false unless Hanami.bundled?("hanami-router")

  if root.directory?
    routes_require_path = File.join(root, ROUTES_PATH)

    begin
      require_relative "./routes"
      require routes_require_path
    rescue LoadError => e
      raise e unless e.path == routes_require_path
    end
  end

  begin
    routes_class = namespace.const_get(ROUTES_CLASS_NAME)
    routes_class.routes
  rescue NameError => e
    raise e unless e.name == ROUTES_CLASS_NAME.to_sym
  end
end

#prepareself #prepare(provider_name) ⇒ self

Overloads:

  • #prepareself

    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.

    Returns:

    • (self)

    See Also:

    • #boot

    Since:

    • 2.0.0

  • #prepare(provider_name) ⇒ self

    Prepares a provider.

    This triggers the provider’s prepare lifecycle step.

    Parameters:

    • provider_name (Symbol)

      the name of the provider to start

    Returns:

    • (self)

    Since:

    • 2.0.0

Since:

  • 2.0.0

def prepare(provider_name = nil)
  if provider_name
    container.prepare(provider_name)
  else
    prepare_slice
  end

  self
end

#prepare_allObject (private)

Since:

  • 2.0.0

def prepare_all
  prepare_settings
  prepare_container_consts
  prepare_container_plugins
  prepare_container_base_config
  prepare_container_component_dirs
  prepare_container_imports
  prepare_container_providers
end

#prepare_autoloaderObject (private)

Since:

  • 2.0.0

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.

  # Everything in the slice root can be autoloaded except `config/` and `slices/`,
  # which are framework-managed directories

  if root.join(CONFIG_DIR)&.directory?
    autoloader.ignore(root.join(CONFIG_DIR))
  end

  if root.join(SLICES_DIR)&.directory?
    autoloader.ignore(root.join(SLICES_DIR))
  end

  autoloader.setup
end

#prepare_container_base_configObject (private)

Since:

  • 2.0.0

def prepare_container_base_config
  container.config.name = slice_name.to_sym
  container.config.root = root
  container.config.provider_dirs = [File.join("config", "providers")]
  container.config.registrations_dir = File.join("config", "registrations")

  container.config.env = config.env
  container.config.inflector = config.inflector
end

#prepare_container_component_dirsObject (private)

Since:

  • 2.0.0

def prepare_container_component_dirs
  return unless root.directory?

  # Component files in both the root and `lib/` define classes in the slice's
  # namespace

  if root.join(LIB_DIR)&.directory?
    container.config.component_dirs.add(LIB_DIR) do |dir|
      dir.namespaces.add_root(key: nil, const: slice_name.name)
    end
  end

  # When auto-registering components in the root, ignore files in `config/` (this is
  # for framework config only), `lib/` (these will be auto-registered as above), as
  # well as the configured no_auto_register_paths
  no_auto_register_paths = ([LIB_DIR, CONFIG_DIR] + config.no_auto_register_paths)
    .map { |path|
      path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}"
    }

  # TODO: Change `""` (signifying the root) once dry-rb/dry-system#238 is resolved
  container.config.component_dirs.add("") do |dir|
    dir.namespaces.add_root(key: nil, const: slice_name.name)
    dir.auto_register = -> component {
      relative_path = component.file_path.relative_path_from(root).to_s
      !relative_path.start_with?(*no_auto_register_paths)
    }
  end
end

#prepare_container_constsObject (private)

Since:

  • 2.0.0

def prepare_container_consts
  namespace.const_set :Container, container
  namespace.const_set :Deps, container.injector
end

#prepare_container_importsObject (private)

Since:

  • 2.0.0

def prepare_container_imports
  import(
    keys: config.shared_app_component_keys,
    from: app.container,
    as: nil
  )
end

#prepare_container_pluginsObject (private)

Since:

  • 2.0.0

def prepare_container_plugins
  container.use(:env, inferrer: -> { Hanami.env })

  container.use(
    :zeitwerk,
    loader: autoloader,
    run_setup: false,
    eager_load: false
  )
end

#prepare_container_providersObject (private)

Since:

  • 2.0.0

def prepare_container_providers
  # Check here for the `routes` definition only, not `router` itself, because the
  # `router` requires the slice to be prepared before it can be loaded, and at this
  # point we're still in the process of preparing.
  if routes
    require_relative "providers/routes"
    register_provider(:routes, source: Providers::Routes.for_slice(self))
  end

  if assets_dir? && Hanami.bundled?("hanami-assets")
    require_relative "providers/assets"
    register_provider(:assets, source: Providers::Assets.for_slice(self))
  end
end

#prepare_settingsObject (private)

Since:

  • 2.0.0

def prepare_settings
  container.register(:settings, settings) if settings
end

#prepare_sliceObject (private)

rubocop:disable Metrics/AbcSize

Since:

  • 2.0.0

def prepare_slice
  return self if prepared?

  config.finalize!

  ensure_slice_name
  ensure_slice_consts
  ensure_root

  prepare_all

  instance_exec(container, &@prepare_container_block) if @prepare_container_block
  container.configured!

  prepare_autoloader

  # Load child slices last, ensuring their parent is fully prepared beforehand
  # (useful e.g. for slices that may wish to access constants defined in the
  # parent's autoloaded directories)
  prepare_slices

  @prepared = true

  self
end

#prepare_slicesObject (private)

Since:

  • 2.0.0

def prepare_slices
  slices.load_slices.each(&:prepare)
  slices.freeze
end

#register_provider(name, namespace: nil, from: nil, source: nil) ⇒ container

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.

Examples:

Simple provider

# config/providers/db.rb
Hanami.app.register_provider(:db) do
  start do
    require "db"
    register("db", DB.new)
  end
end

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

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

Parameters:

  • name (Symbol)

    the unique name for the provider

  • 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

  • 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:

  • source (Symbol, nil)

    the name of the external provider source to use, if different from the value provided as name

  • if (Boolean)

    a boolean-returning expression to determine whether to register the provider

Returns:

  • (container)

Since:

  • 2.0.0

Since:

  • 2.0.0

def register_provider(...)
  container.register_provider(...)
end

#render_detailed_errors?Boolean (private)

Returns:

Since:

  • 2.0.0

def render_detailed_errors?
  config.render_detailed_errors && Hanami.bundled?("hanami-webconsole")
end

#render_errors?Boolean (private)

Returns:

Since:

  • 2.0.0

def render_errors?
  config.render_errors
end

#start(provider_name) ⇒ container

Starts a provider.

This triggers the provider’s prepare and start lifecycle steps.

Examples:

MySlice::Slice.start(:persistence)

Parameters:

  • provider_name (Symbol)

    the name of the provider to start

Returns:

  • (container)

Since:

  • 2.0.0

Since:

  • 2.0.0

def start(...)
  container.start(...)
end

#stop(provider_name) ⇒ container

Stops a provider.

This triggers the provider’s stop lifecycle hook.

Examples:

MySlice::Slice.stop(:persistence)

Parameters:

  • provider_name (Symbol)

    the name of the provider to start

Returns:

  • (container)

Since:

  • 2.0.0

Since:

  • 2.0.0

def stop(...)
  container.stop(...)
end