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 (so foo.audit! and Something::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

Class Method Details

.catalogueObject

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.

Returns:

  • (Array<String>)


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.

Returns:

  • (Array<String>)


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).

Returns:

  • (nil)

    when the error is swallowed

Raises:

  • (StandardError)

    the original error when config.raise_on_audit_write_error is true



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.

Returns:

  • (Array<Class>)


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.

Parameters:

  • source (String, nil) (defaults to: nil)

    keep only classes whose defining file path contains this fragment, e.g. "/app/operations/". Recommended: it scopes the check to the host's own operations and drops anything defined in a spec file.



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.

Parameters:

  • within (Array<String>, nil) (defaults to: nil)

    check only this slice. Hosts whose catalogue also covers non-operation writers (a controller concern, a tool server, a notification-bus name) pass the operation-only slice here; the rest are written outside app/operations/ and would always look orphaned.

Returns:

  • (Array<String>)


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

.registeredObject

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.

Returns:

  • (Array<Class>)


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.

Returns:

  • (Array<Class>)


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.

Returns:

  • (Hash{Class => Array<String>})


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.

Returns:

  • (Boolean)


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