Module: Axn::Configurable::Settings

Includes:
PerClassOverrides
Included in:
Axn::Configuration
Defined in:
lib/axn/configurable.rb

Overview

Class-level flavor: declare validated instance settings on a class, reusing the same Setting kernel (defaults, one_of:/validate:). Used to dogfood Axn's own Configuration without contorting the module-singleton DSL above. overridable: true mints the same per-class override accessors (via PerClassOverrides), resolving their library-level fallback from a live singleton the extending class registers.

class Configuration
extend Axn::Configurable::Settings
overridable_config_source { Axn.config }
setting :log_level, default: :info
setting :sidekiq_job_tag_sources, default: [...], overridable: true
end

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PerClassOverrides

#_validate_override_setter!, #config_namespace, #overrides, #resolve_override_for

Class Method Details

.extended(base) ⇒ Object

reset! is an INSTANCE method on the extending class (a config object), so it is installed here rather than declared in this module's body. It resolves the settings it operates on from self.class up through its ancestry, so an instance of a subclass sees both settings declared on the subclass and settings declared on any ancestor that extended Settings — regardless of whether the subclass re-extends Settings itself.

A reset! the class already provides — its own, or one inherited from a non-axn ancestor — wins: axn generates this one, so it defers rather than replacing behavior the author wrote, leaving a debug breadcrumb instead. Settings still reset through the flat <name>= writers.



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
# File 'lib/axn/configurable.rb', line 576

def self.extended(base)
  if base.method_defined?(:reset!) || base.private_method_defined?(:reset!)
    if defined?(Axn.config)
      owner = base.instance_method(:reset!).owner
      Axn::Extensions.best_effort("logging a reset! collision", action: base) do
        Axn.config.logger.debug do
          "[Axn] #{base.name || base}: instance method `reset!` is already defined by #{owner}, so the " \
            "Configurable settings DSL leaves it alone. Per-setting reset is unavailable on this class."
        end
      end
    end
    return
  end

  # INCLUDED as a module rather than defined on the class. A method defined directly on the class
  # outranks every module in the lookup chain, so it would beat a `reset!` the author includes
  # LATER — making the deferral depend on whether their include sits above or below the `extend`.
  # From a module, the class's own definition still wins (as it should) and so does anything
  # included after axn, while the pre-check above still covers what was already there.
  generated = Module.new
  base.include(generated)
  generated.send(:define_method, :reset!) do |*names|
    # Deferral has to be decided HERE, not only at extend time. The pre-check above covers a
    # `reset!` that already existed, and being a module covers one included later on this class —
    # but neither covers one that arrives later on an ANCESTOR, since the generated module sits
    # ahead of the superclass in a subclass's chain and would silently win. `super` resolves to
    # exactly the ancestors below this module, so asking for it makes the deferral independent of
    # whether the author's include ran before or after the extend.
    # Arguments explicit: `define_method` forbids implicit-argument `super`.
    return super(*names) if defined?(super)

    # Not `self.class`: a setting may be named `class`, and its generated reader would shadow the
    # real one — leaving reset! resolving its targets from a setting value.
    declared = Axn::Configurable.declared_settings_for(Axn::Internal::Identity.class_of(self))
    targets = names.empty? ? declared.keys : names.map(&:to_sym)
    # Validate the WHOLE list before touching anything, so `reset!(:real, :typo)` leaves the
    # config exactly as it was instead of half-reset behind the raise.
    unknown = targets.reject { |name| declared.key?(name) }
    if unknown.any?
      raise ArgumentError,
            "reset! got unknown setting #{unknown.first.inspect}. Declared settings are: " \
            "#{declared.keys.map(&:inspect).join(', ')}."
    end

    targets.each do |name|
      ivar = :"@#{name}"
      remove_instance_variable(ivar) if instance_variable_defined?(ivar)
    end
    self
  end
end

Instance Method Details

#_declared_settingsObject

Declared Setting objects by name, on THIS class only — not merged with an ancestor's. The class flavor otherwise keeps no registry (only overridable settings are tracked, by PerClassOverrides). reset! reads across the full ancestry via Axn::Configurable.declared_settings_for, not through this method, so that walk never mints an empty registry on a class that never declared anything.



565
# File 'lib/axn/configurable.rb', line 565

def _declared_settings = @_declared_settings ||= {}

#overridable_config_source(&block) ⇒ Object

Registers the live singleton whose values are the library-level fallback for per-class overrides (e.g. Axn.config). Read lazily on each resolution, so a swapped singleton is picked up. Must be declared before any overridable: true setting.



632
633
634
# File 'lib/axn/configurable.rb', line 632

def overridable_config_source(&block)
  @_overridable_config_source = block
end

#setting(name, default: nil, one_of: nil, validate: nil, overridable: false) ⇒ Object

Raises:

  • (ArgumentError)


636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# File 'lib/axn/configurable.rb', line 636

def setting(name, default: nil, one_of: nil, validate: nil, overridable: false)
  name = Axn::Configurable.canonical_setting_name!(name)
  setting = Setting.new(name:, default:, one_of:, validate:, overridable:)
  _declared_settings[setting.name] = setting
  ivar = :"@#{name}"

  define_method(name) do
    return instance_variable_get(ivar) if instance_variable_defined?(ivar)
    return setting.default.call if setting.dynamic_default?

    # A literal default IS memoized: mutating it in place (`config.some_list << :x`) is a
    # supported way to extend one, which a fresh dup per read would silently discard.
    instance_variable_set(ivar, setting.dup_default)
  end

  define_method(:"#{name}?") { !!public_send(name) }

  define_method(:"#{name}=") do |value|
    setting.validate!(value)
    instance_variable_set(ivar, value)
  end

  return unless overridable

  raise ArgumentError, "setting #{name}: overridable: true requires overridable_config_source to be declared first" unless @_overridable_config_source

  source = @_overridable_config_source
  _define_override_methods(setting, -> { source.call.public_send(setting.name) })
end