Module: Axn::Configurable::PerClassOverrides

Included in:
Axn::Configurable, Settings
Defined in:
lib/axn/configurable.rb

Overview

Per-class override accessors, shared by both config flavors (the module-singleton Configurable and the class-level Settings). Included into each, so its methods become singleton methods of whatever module/class extends that flavor. The only per-flavor difference is where the resolution fallback reads the library-level value, so _define_override_methods takes that as a lambda.

Instance Method Summary collapse

Instance Method Details

#_validate_override_setter!(name, value) ⇒ Object

Eager validation for the configure writer when this source owns the namespace being written: rejects a setter name that isn't an overridable setting (a typo that would otherwise store silently and never resolve), then validates the value against the setting.

Raises:

  • (ArgumentError)


220
221
222
223
224
225
# File 'lib/axn/configurable.rb', line 220

def _validate_override_setter!(name, value)
  setting = _override_settings[name.to_sym]
  raise ArgumentError, "unknown overridable setting #{name.inspect} for namespace #{config_namespace.inspect}" unless setting

  setting.validate!(value)
end

#config_namespace(value = UNSET) ⇒ Object

The store namespace this config source owns. Overridable settings and their per-class overrides are keyed by [namespace, setting], so two modules that declare a same-named setting (e.g. a tool composing several adapter mixins) never collide in the consumer class's single override store. Declared once via config_namespace :mcp; the symbol is also what configure(:mcp) { … } targets. Defaults to the module/class itself — unique per source, so flat-accessor-only consumers stay collision-safe without declaring anything.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/axn/configurable.rb', line 189

def config_namespace(value = UNSET)
  return (@_config_namespace ||= self) if UNSET.equal?(value)

  # The namespace gets baked in the first time it's used — into each overridable setting's
  # accessor closures (at declaration) and into a class's source registry (at include). Changing
  # it afterward would strand those under the old key while `configure(value)` writes/validates
  # under the new one. Lock on first use and enforce the documented "declare it first" rule.
  if @_config_namespace_locked && value != @_config_namespace
    raise ArgumentError,
          "config_namespace must be declared before any overridable setting is defined or its " \
          "overrides are included (got #{value.inspect} after use under #{(@_config_namespace || self).inspect})"
  end

  @_config_namespace = value
end

#overridesObject

Returns a module that, when included in an action class, extends it with the per-class override accessors for each overridable setting. setting adds to a shared methods module as overridable settings are declared, and Ruby reflects those additions on already-extended classes — so it's insensitive to load order.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/axn/configurable.rb', line 147

def overrides
  @overrides ||= begin
    methods_module = _override_methods_module
    config_source = self
    Module.new do
      define_singleton_method(:included) do |base|
        # Breadcrumb before extending, while `base`'s own lookup still reflects only its
        # ancestors (not yet axn's accessors), so the check sees a genuine external definition.
        config_source.send(:_warn_on_shadowed_overrides, base)

        # Record which config source owns each namespace on this class, so the tolerant
        # `configure` writer can validate a setter eagerly when the namespace is registered
        # (schema known) and stay tolerant only when it isn't (adapter not loaded / not included).
        config_source.send(:_register_overrides_on, base)

        # `axn_configure` is the always-available, collision-proof writer. Bare `configure` is a
        # generic name a non-axn base class may already own; Ruby places an extended module above
        # the superclass chain, so installing it unconditionally would shadow that base hook and
        # reroute its `configure(...)` calls into axn's writer. Install the ergonomic bare alias
        # only when the name is free — same PRO-2875 discipline the Naming/SchemaReflection generic
        # names use — and always leave `axn_configure` as the guaranteed way to reach axn's config.
        base.extend(ClassConfigWriter)
        shadowed = defined?(Axn::Core::MethodShadowing) &&
                   Axn::Core::MethodShadowing.externally_defined?(base, :configure)
        unless shadowed
          base.define_singleton_method(:configure) do |namespace = :core, &block|
            axn_configure(namespace, &block)
          end
        end
        base.extend(methods_module)
      end
    end
  end
end

#resolve_override_for(klass, name) ⇒ Object

Resolves name for klass through the same override store + fallback the generated accessors use, WITHOUT dispatching to a class method on klass. For framework code that consumes an override: the generated <name> / <name>? readers are all shadowable by a same-named class method on the action (or a subclass), which would silently bypass the override store — so the framework resolves through this registry instead. Raises KeyError if name isn't an overridable setting (a declaration-time bug).



212
213
214
215
# File 'lib/axn/configurable.rb', line 212

def resolve_override_for(klass, name)
  _validate_slot_keys!(klass)
  _override_resolvers.fetch(name.to_sym).call(klass)
end