lib/hanami/application.rb in hanami-2.0.0.alpha1 vs lib/hanami/application.rb in hanami-2.0.0.alpha2

- old
+ new

@@ -1,67 +1,363 @@ # frozen_string_literal: true +require "dry/system/container" require "hanami/configuration" -require "hanami/routes" -require "hanami/router" +require "pathname" +require "rack" +require_relative "slice" +require_relative "application/autoloader/inflector_adapter" +require_relative "application/settings" module Hanami - # Hanami application + # Hanami application class # # @since 2.0.0 class Application @_mutex = Mutex.new - def self.inherited(app) - @_mutex.synchronize do - app.class_eval do - @_mutex = Mutex.new - @_configuration = Hanami::Configuration.new(env: Hanami.env) + class << self + def inherited(klass) + @_mutex.synchronize do + klass.class_eval do + @_mutex = Mutex.new + @_configuration = Hanami::Configuration.new(env: Hanami.env) - extend ClassMethods - include InstanceMethods - end + extend ClassMethods + include InstanceMethods + end - Hanami.application = app + klass.send :prepare_base_load_path + + Hanami.application = klass + end end end - # Class method interface + # Application class interface # - # @since 2.0.0 + # rubocop:disable Metrics/ModuleLength module ClassMethods + def self.extended(klass) + klass.class_eval do + @inited = @booted = false + end + end + def configuration @_configuration end alias config configuration - def routes(&blk) + def init # rubocop:disable Metrics/MethodLength + return self if inited? + + configuration.finalize + + load_settings + + @container = prepare_container + @deps_module = prepare_deps_module + + load_slices + slices.values.each(&:init) + slices.freeze + + if configuration.autoloader + configuration.autoloader.inflector = Autoloader::InflectorAdapter.new(inflector) + configuration.autoloader.setup + end + + load_routes + + @inited = true + self + end + + def inited? + @inited + end + + def container + raise "Application not init'ed" unless defined?(@container) + + @container + end + + def deps + raise "Application not init'ed" unless defined?(@deps_module) + + @deps_module + end + + def slices + @slices ||= {} + end + + def register_slice(name, **slice_args) + raise "Slice +#{name}+ already registered" if slices.key?(name.to_sym) + + slice = Slice.new(self, name: name, **slice_args) + slice.namespace.const_set :Slice, slice if slice.namespace # rubocop:disable Style/SafeNavigation + slices[name.to_sym] = slice + end + + def register(*args, **opts, &block) + container.register(*args, **opts, &block) + end + + def register_bootable(*args, **opts, &block) + container.boot(*args, **opts, &block) + end + + def init_bootable(*args) + container.init(*args) + end + + def start_bootable(*args) + container.start(*args) + end + + def key?(*args) + container.key?(*args) + end + + def keys + container.keys + end + + def [](*args) + container[*args] + end + + def resolve(*args) + container.resolve(*args) + end + + def boot(&block) + return self if booted? + + init + + container.finalize!(&block) + + slices.values.each(&:boot) + + @booted = true + self + end + + def booted? + @booted + end + + def settings(&block) # rubocop:disable Metrics/MethodLength + if block + @_settings = Application::Settings.build( + configuration.settings_loader, + configuration.settings_loader_options, + &block + ) + elsif instance_variable_defined?(:@_settings) + @_settings + else + # Load settings lazily so they can be used to configure the + # Hanami::Application subclass (before the application has inited) + load_settings + @_settings ||= nil + end + end + + def routes(&block) @_mutex.synchronize do - if blk.nil? + if block.nil? raise "Hanami.application.routes not configured" unless defined?(@_routes) @_routes else - @_routes = Routes.new(&blk) + @_routes = block end end end + + MODULE_DELIMITER = "::" + private_constant :MODULE_DELIMITER + + def namespace + inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER)) + end + + def namespace_path + inflector.underscore(namespace) + end + + def application_name + inflector.underscore(namespace).to_sym + end + + def root + configuration.root + end + + def inflector + configuration.inflector + end + + # @api private + def component_provider(component) + raise "Hanami.application must be inited before detecting providers" unless inited? + + # [Admin, Main, MyApp] or [MyApp::Admin, MyApp::Main, MyApp] + providers = slices.values + [self] + + component_class = component.is_a?(Class) ? component : component.class + component_name = component_class.name + + return unless component_name + + providers.detect { |provider| component_name.include?(provider.namespace.to_s) } + end + + private + + def prepare_base_load_path + base_path = File.join(root, "lib") + $LOAD_PATH.unshift base_path unless $LOAD_PATH.include?(base_path) + end + + def prepare_container + define_container.tap do |container| + configure_container container + end + end + + def prepare_deps_module + define_deps_module + end + + def define_container + require "#{application_name}/container" + namespace.const_get :Container + rescue LoadError, NameError + namespace.const_set :Container, Class.new(Dry::System::Container) + end + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength + def configure_container(container) + container.use :env, inferrer: -> { Hanami.env } + container.use :notifications + + container.configure do |config| + config.inflector = configuration.inflector + + config.root = configuration.root + config.bootable_dirs = [ + "config/boot", + Pathname(__dir__).join("application/container/boot").realpath, + ] + + if configuration.autoloader + require "dry/system/loader/autoloading" + config.component_dirs.loader = Dry::System::Loader::Autoloading + config.component_dirs.add_to_load_path = false + end + + if root.join("lib").directory? + config.component_dirs.add "lib" do |dir| + dir.default_namespace = application_name.to_s + end + + configuration.autoloader&.push_dir(root.join("lib")) + end + end + + container + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength + + def define_deps_module + require "#{application_name}/deps" + namespace.const_get :Deps + rescue LoadError, NameError + namespace.const_set :Deps, container.injector + end + + def load_slices + Dir[File.join(slices_path, "*")] + .select(&File.method(:directory?)) + .each(&method(:load_slice)) + end + + def slices_path + File.join(root, config.slices_dir) + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def load_slice(slice_path) + slice_path = Pathname(slice_path) + + slice_name = slice_path.relative_path_from(Pathname(slices_path)).to_s + slice_const_name = inflector.camelize(slice_name) + + if config.slices_namespace.const_defined?(slice_const_name) + slice_module = config.slices_namespace.const_get(slice_const_name) + + raise "Cannot use slice +#{slice_const_name}+ since it is not a module" unless slice_module.is_a?(Module) + else + slice_module = Module.new + config.slices_namespace.const_set inflector.camelize(slice_name), slice_module + end + + register_slice( + slice_name, + namespace: slice_module, + root: slice_path.realpath + ) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def load_routes + require File.join(configuration.root, configuration.router.routes) + rescue LoadError # rubocop:disable Lint/SuppressedException + end + + def load_settings + prepare_base_load_path + require File.join(configuration.root, configuration.settings_path) + rescue LoadError # rubocop:disable Lint/SuppressedException + end end + # rubocop:enable Metrics/ModuleLength - # Instance method interface - # - # @since 2.0.0 + # Application instance interface module InstanceMethods - def initialize(configuration: self.class.configuration, routes: self.class.routes) - @app = Rack::Builder.new do - configuration.for_each_middleware do |m, *args| - use m, *args - end + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def initialize(application = self.class) + require_relative "application/router" - run Hanami::Router.new(**configuration.router_settings, &routes) + application.boot + + resolver = application.config.router.resolver.new( + slices: application.slices, + inflector: application.inflector + ) + + router = Application::Router.new( + routes: application.routes, + resolver: resolver, + **application.configuration.router.options, + ) do + use application[:rack_monitor] + + application.config.for_each_middleware do |m, *args, &block| + use(m, *args, &block) + end end + + @app = router.to_rack_app end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def call(env) @app.call(env) end end