lib/hanami/settings.rb in hanami-2.0.0.beta3 vs lib/hanami/settings.rb in hanami-2.0.0.beta4
- old
+ new
@@ -1,9 +1,9 @@
# frozen_string_literal: true
+require "dry/core"
require "dry/configurable"
-require "dry/core/constants"
module Hanami
# App settings
#
# Users are expected to inherit from this class to define their app settings.
@@ -31,15 +31,76 @@
#
# 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`.
#
# @see Hanami::Settings::DotenvStore
+ #
+ # @api public
# @since 2.0.0
class Settings
+ class << self
+ def inherited(subclass)
+ super
+
+ if Hanami.bundled?("dry-types")
+ require "dry/types"
+ subclass.const_set(:Types, Dry.Types())
+ end
+ end
+
+ # Loads the settings for a slice.
+ #
+ # Returns nil if no settings class is defined.
+ #
+ # @return [Settings, nil]
+ #
+ # @api private
+ def load_for_slice(slice)
+ return unless settings_defined?(slice)
+
+ require_slice_settings(slice) unless slice_settings_class?(slice)
+
+ slice_settings_class(slice).new(slice.config.settings_store)
+ end
+
+ private
+
+ # Returns true if settings are defined for the slice.
+ #
+ # Settings are considered defined if a `Settings` class is already defined in the slice
+ # namespace, or a `config/settings.rb` exists under the slice root.
+ def settings_defined?(slice)
+ slice.namespace.const_defined?(SETTINGS_CLASS_NAME) ||
+ slice.root.join("#{SETTINGS_PATH}#{RB_EXT}").file?
+ end
+
+ def slice_settings_class?(slice)
+ slice.namespace.const_defined?(SETTINGS_CLASS_NAME)
+ end
+
+ def slice_settings_class(slice)
+ slice.namespace.const_get(SETTINGS_CLASS_NAME)
+ end
+
+ def require_slice_settings(slice)
+ require "hanami/settings"
+
+ slice_settings_require_path = File.join(slice.root, SETTINGS_PATH)
+
+ begin
+ require slice_settings_require_path
+ rescue LoadError => e
+ raise e unless e.path == slice_settings_require_path
+ end
+ 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
@@ -51,23 +112,44 @@
STR
end
end
# @api private
+ Undefined = Dry::Core::Constants::Undefined
+
+ # @api private
EMPTY_STORE = Dry::Core::Constants::EMPTY_HASH
include Dry::Configurable
# @api private
def initialize(store = EMPTY_STORE)
- errors = config._settings.map(&:name).reduce({}) do |errs, name|
- public_send("#{name}=", store.fetch(name) { Dry::Core::Constants::Undefined })
- errs
+ errors = config._settings.map(&:name).each_with_object({}) do |name, errs|
+ value = store.fetch(name, Undefined)
+
+ if value.eql?(Undefined)
+ # When a key is missing entirely from the store, _read_ its value from the config instead,
+ # which ensures its setting constructor runs (with a `nil` argument given) and raises any
+ # necessary errors.
+ public_send(name)
+ else
+ public_send("#{name}=", value)
+ end
rescue => e # rubocop:disable Style/RescueStandardError
- errs.merge(name => e)
+ errs[name] = e
end
raise InvalidSettingsError, errors if errors.any?
+
+ config.finalize!
+ end
+
+ def inspect
+ "#<#{self.class.to_s} [#{config._settings.map(&:name).join(", ")}]>"
+ end
+
+ def inspect_values
+ "#<#{self.class.to_s} #{config._settings.map { |setting| "#{setting.name}=#{config[setting.name].inspect}" }.join(" ")}>"
end
private
def method_missing(name, *args, &block)