Class: LcpRuby::Authorization::InvariantCheck::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/authorization/invariant_check/configuration.rb

Overview

Configuration for the runtime invariant validator. Hosts override defaults via:

LcpRuby.configuration.invariant_check = {
  severity: :error,
  severity_per_code: { "AUTH-005" => :off, "AUTH-002" => :error },
  fail_boot: true   # production-only; ignored in dev/test
}

in ‘config/initializers/lcp_ruby.rb`.

Mirrors ‘LcpRuby::I18nCheck::Configuration`’s shape so host apps learn one API for both linters. See docs/design/authorization_hardening.md § “Configuration class”.

Constant Summary collapse

VALID_SEVERITIES =
%i[error warn off].freeze
DEFAULT_SEVERITIES =

Per-code defaults for AUTH-001..AUTH-009. Single source of truth; both ‘Configuration#severity_for` (host introspection) and `RuntimeInvariantValidator#emit` (boot path) consult this table so they cannot diverge. Host overrides via `severity_per_code` win; otherwise this table; otherwise `severity` (top-level). See docs/reference/invariant_check.md § “Codes” for rationale per code.

{
  "AUTH-001" => :error, # custom scope method missing on klass
  "AUTH-002" => :error, # routable standalone page with no visible_when and no zone gate
  "AUTH-003" => :error, # malformed visible_when shape
  "AUTH-004" => :error, # field_match.field not a column on klass
  "AUTH-005" => :warn,  # user_field/current_user_<m> not on user class
  "AUTH-006" => :error, # association.field not a column on klass
  "AUTH-007" => :error, # inherits parent unresolvable
  "AUTH-008" => :error, # union scope with empty scopes: array
  "AUTH-009" => :warn   # preset prerequisite missing (reserved for generators)
  # NOTE: AUTH-010 (runtime evaluator failure) and AUTH-011 (sentinel
  # wiring check on first request) are runtime-only — they raise
  # directly without consulting this severity table.
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(severity: :error, severity_per_code: {}, fail_boot: nil) ⇒ Configuration

Returns a new instance of Configuration.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 45

def initialize(severity: :error, severity_per_code: {}, fail_boot: nil)
  @severity = normalize(severity)
  @severity_per_code = severity_per_code
    .each_with_object({}) { |(k, v), h| h[k.to_s] = normalize(v) }

  # Dev/test: always strict (knob ignored). Production: opt-in via
  # `fail_boot:` (default false — observability-only until host
  # trusts the validator). The asymmetry localizes deployment risk
  # to one knob while keeping the dev feedback loop tight. See
  # docs/design/authorization_hardening.md § "Dev/test boot-fail
  # is unconditional; production opt-in".
  @fail_boot = if defined?(Rails) && Rails.env.production?
                 fail_boot == true
  else
                 true
  end
end

Instance Attribute Details

#fail_bootObject (readonly)

Returns the value of attribute fail_boot.



43
44
45
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 43

def fail_boot
  @fail_boot
end

#severityObject (readonly)

Returns the value of attribute severity.



43
44
45
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 43

def severity
  @severity
end

#severity_per_codeObject (readonly)

Returns the value of attribute severity_per_code.



43
44
45
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 43

def severity_per_code
  @severity_per_code
end

Class Method Details

.coerce(value) ⇒ Object

Normalize an arbitrary host-supplied value into a Configuration:

nil / unset      → fresh defaults
Configuration    → returned as-is
Hash             → built eagerly via splat

Single source of truth for the three call sites that consume ‘LcpRuby.configuration.invariant_check`: the Configuration setter itself (eager normalization), the validator’s ‘resolve_config` (defensive against specs that bypass the setter), and the `lcp_ruby:invariant_check` rake task. Keeps the Hash↔Configuration branch table in one place.

Reload-resilience: a Configuration instance stored in ‘LcpRuby.configuration.invariant_check` survives `LcpRuby.reset_for_reload!` (because `@configuration` is preserved), but Zeitwerk reloads this class on dev autoreload — the stored instance is then a different class object than `self`, so `when self` (which is `is_a?` under the hood) misses. The duck-type fallback rebuilds a fresh Configuration from the stale instance’s public surface, so ‘Engine.reload!` doesn’t crash on the second pass through. See docs/design/boot_reload_lifecycle.md § Q5 / D9.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 94

def self.coerce(value)
  case value
  when nil           then new
  when self          then value
  when Hash          then new(**value.transform_keys(&:to_sym))
  else
    if value.respond_to?(:severity) &&
       value.respond_to?(:severity_per_code) &&
       value.respond_to?(:fail_boot)
      new(severity: value.severity,
          severity_per_code: value.severity_per_code,
          fail_boot: value.fail_boot)
    else
      raise ArgumentError,
            "expected a Hash, #{name}, or nil (got #{value.class})"
    end
  end
end

Instance Method Details

#severity_for(code) ⇒ Object

Resolve severity for a given code. Precedence:

1. host's `severity_per_code` override (explicit)
2. `DEFAULT_SEVERITIES` table (per-code shipping default)
3. top-level `severity` (catch-all for unlisted codes)


67
68
69
70
71
# File 'lib/lcp_ruby/authorization/invariant_check/configuration.rb', line 67

def severity_for(code)
  key = code.to_s
  return @severity_per_code[key] if @severity_per_code.key?(key)
  DEFAULT_SEVERITIES[key] || @severity
end