Module: StandardAudit::Operation::Audit
- Defined in:
- lib/standard_audit/operation/audit.rb
Overview
The registry, the catalogue resolver, the write-error policy, and the analysis predicates behind the operation-audit meta-spec.
Everything here is PLAIN RUBY that returns data — no RSpec, no
assertions. A host can call these from a bespoke spec, a rake task, or a
CI script. standard_audit/rspec/operation is a thin shared-example
layer over exactly these methods and adds no logic of its own.
StandardAudit::Operation::Audit.undeclared # => [Class, ...]
StandardAudit::Operation::Audit.unknown_actions # => { Class => ["x.y"] }
StandardAudit::Operation::Audit.orphan_actions # => ["x.y"]
StandardAudit::Operation::Audit.missing_write_sites # => [Class, ...]
Constant Summary collapse
- WRITE_SITE_PATTERN =
Heuristic used by the source-scanning predicates: a call to the private
audit!helper, with or without parentheses, not preceded by a receiver (sofoo.audit!andSomething::audit!don't count).The scan is FILE-scoped, not class-scoped — two operations defined in one file share a verdict. Zeitwerk requires one class per file in a real app, so this only matters for fixtures. A commented-out or documented
audit!in the same file also counts as a write site; the predicates deliberately err towards a false pass rather than a false failure, since the runtime guard is the authoritative check. /(?<![\w.:])audit!\s*(?:\(|["':@$\w])/
Class Method Summary collapse
-
.catalogue ⇒ Object
The host's action vocabulary as an Array of Strings, or nil when no catalogue is configured (in which case membership is not checked).
-
.declared_actions(operations: self.operations) ⇒ Array<String>
Every action declared across the given operations, de-duplicated.
-
.duplicate_catalogue_entries(catalogue: self.catalogue) ⇒ Array<String>
Catalogue entries listed more than once.
-
.handle_write_error(error, action:, operation:) ⇒ nil
Applies the configured policy to a failed audit write (never to a DeclarationError, which
audit!re-raises first). -
.missing_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare
auditsbut whose source contains noaudit!call — the declaration is aspirational and nothing writes the row. -
.operations(source: nil) ⇒ Object
The real operations a meta-spec should hold to the contract.
-
.orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) ⇒ Array<String>
Catalogue entries no operation declares — dead vocabulary, which makes the catalogue a wish rather than a record.
- .register(klass) ⇒ Object
-
.registered ⇒ Object
Every class that has gained the contract, in registration order.
-
.reset_registry! ⇒ Object
Empties the registry.
-
.source_path(klass) ⇒ Object
Absolute path of the file that defines
klass, or nil. -
.undeclared(operations: self.operations) ⇒ Array<Class>
Operations that declare neither
auditsnoraudit_none!. -
.unexpected_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare
audit_none!but whose source callsaudit!anyway. -
.unknown_actions(operations: self.operations, catalogue: self.catalogue) ⇒ Hash{Class => Array<String>}
Declared actions that are absent from the configured catalogue.
-
.verify? ⇒ Boolean
Whether
audit!verifies declarations before writing.
Class Method Details
.catalogue ⇒ Object
The host's action vocabulary as an Array of Strings, or nil when no catalogue is configured (in which case membership is not checked).
config.audit_catalogue is normally a callable — see the note in
StandardAudit::Operation on why an eagerly-referenced autoloadable
constant breaks Zeitwerk reloading. A plain Array is accepted for the
case where the vocabulary really is a frozen literal.
94 95 96 97 98 99 100 101 102 |
# File 'lib/standard_audit/operation/audit.rb', line 94 def catalogue raw = StandardAudit.config.audit_catalogue return nil if raw.nil? resolved = raw.respond_to?(:call) ? raw.call : raw return nil if resolved.nil? Array(resolved).map(&:to_s) end |
.declared_actions(operations: self.operations) ⇒ Array<String>
Every action declared across the given operations, de-duplicated.
176 177 178 |
# File 'lib/standard_audit/operation/audit.rb', line 176 def declared_actions(operations: self.operations) operations.flat_map { |klass| declared_for(klass) }.uniq end |
.duplicate_catalogue_entries(catalogue: self.catalogue) ⇒ Array<String>
Catalogue entries listed more than once.
183 184 185 186 187 |
# File 'lib/standard_audit/operation/audit.rb', line 183 def duplicate_catalogue_entries(catalogue: self.catalogue) return [] if catalogue.nil? catalogue.tally.select { |_action, count| count > 1 }.keys end |
.handle_write_error(error, action:, operation:) ⇒ nil
Applies the configured policy to a failed audit write (never to a
DeclarationError, which audit! re-raises first).
120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/standard_audit/operation/audit.rb', line 120 def handle_write_error(error, action:, operation:) handler = StandardAudit.config.audit_write_error_handler if handler handler.call(error, action: action, operation: operation) else report_write_error(error, action: action, operation: operation) end raise error if StandardAudit.config.raise_on_audit_write_error nil end |
.missing_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare audits but whose source contains no audit!
call — the declaration is aspirational and nothing writes the row.
Source-based, therefore a heuristic: a class whose defining file can't be read is skipped rather than reported.
196 197 198 199 200 201 202 203 204 |
# File 'lib/standard_audit/operation/audit.rb', line 196 def missing_write_sites(operations: self.operations) operations.select do |klass| spec = klass.audit_spec next false unless spec.is_a?(Array) src = source_for(klass) src && !src.match?(WRITE_SITE_PATTERN) end end |
.operations(source: nil) ⇒ Object
The real operations a meta-spec should hold to the contract.
Excluded:
- anonymous classes (no name) — test doubles and `Class.new` fixtures
- classes that declared `audit_abstract!`
- classes that declare NOTHING and have subclasses — i.e. a shared
base such as `ApplicationOperation`. This is what lets an app
adopt the whole contract with one `include` on its base class and
no further configuration. A class that DID declare is never
excluded by this rule, even if it is subclassed, so subclassing a
real operation cannot quietly drop it from the check.
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/standard_audit/operation/audit.rb', line 65 def operations(source: nil) registered.select do |klass| next false if klass.name.nil? || klass.name.empty? next false if klass.audit_spec == :abstract next false if klass.audit_spec.nil? && subclassed?(klass) next true if source.nil? source_path(klass)&.include?(source) end end |
.orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) ⇒ Array<String>
Catalogue entries no operation declares — dead vocabulary, which makes the catalogue a wish rather than a record.
166 167 168 169 170 171 |
# File 'lib/standard_audit/operation/audit.rb', line 166 def orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) pool = within ? Array(within).map(&:to_s) : catalogue return [] if pool.nil? pool - declared_actions(operations: operations) end |
.register(klass) ⇒ Object
38 39 40 41 |
# File 'lib/standard_audit/operation/audit.rb', line 38 def register(klass) registered << klass unless registered.include?(klass) klass end |
.registered ⇒ Object
Every class that has gained the contract, in registration order. Includes bases, intermediates, and anonymous classes — use operations for the filtered view a meta-spec should assert on.
34 35 36 |
# File 'lib/standard_audit/operation/audit.rb', line 34 def registered @registered ||= [] end |
.reset_registry! ⇒ Object
Empties the registry. For the gem's own specs and for hosts that rebuild the constant graph mid-suite; a normal suite never needs it.
45 46 47 |
# File 'lib/standard_audit/operation/audit.rb', line 45 def reset_registry! @registered = [] end |
.source_path(klass) ⇒ Object
Absolute path of the file that defines klass, or nil.
77 78 79 80 81 82 83 |
# File 'lib/standard_audit/operation/audit.rb', line 77 def source_path(klass) return nil if klass.name.nil? || klass.name.empty? Object.const_source_location(klass.name)&.first rescue NameError nil end |
.undeclared(operations: self.operations) ⇒ Array<Class>
Operations that declare neither audits nor audit_none!. A new
mutating operation shipping with no audit trail shows up here.
140 141 142 |
# File 'lib/standard_audit/operation/audit.rb', line 140 def undeclared(operations: self.operations) operations.select { |klass| klass.audit_spec.nil? } end |
.unexpected_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare audit_none! but whose source calls audit!
anyway. The runtime guard catches this too, but only if that code path
is exercised; this catches it statically.
211 212 213 214 215 216 217 218 |
# File 'lib/standard_audit/operation/audit.rb', line 211 def unexpected_write_sites(operations: self.operations) operations.select do |klass| next false unless klass.audit_spec == :none src = source_for(klass) src&.match?(WRITE_SITE_PATTERN) end end |
.unknown_actions(operations: self.operations, catalogue: self.catalogue) ⇒ Hash{Class => Array<String>}
Declared actions that are absent from the configured catalogue. Empty when no catalogue is configured.
148 149 150 151 152 153 154 155 |
# File 'lib/standard_audit/operation/audit.rb', line 148 def unknown_actions(operations: self.operations, catalogue: self.catalogue) return {} if catalogue.nil? operations.each_with_object({}) do |klass, acc| unknown = declared_for(klass) - catalogue acc[klass] = unknown if unknown.any? end end |
.verify? ⇒ Boolean
Whether audit! verifies declarations before writing. Defaults to
"local environments only" — production writes silently, because a
developer's declaration mistake must not 500 a user.
109 110 111 112 |
# File 'lib/standard_audit/operation/audit.rb', line 109 def verify? resolver = StandardAudit.config.verify_audit_declarations resolver.respond_to?(:call) ? !!resolver.call : !!resolver end |