Class: ActiveSanction::Doctor::Diagnosis
- Inherits:
-
Object
- Object
- ActiveSanction::Doctor::Diagnosis
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/doctor/diagnosis.rb
Overview
What the doctor found out about one source.
diagnosis.source # => :ofac_sdn
diagnosis.ok? # => false
diagnosis.severity # => :warn
diagnosis.findings # => [Finding, ...]
diagnosis.profile # => Profile, what this run measured
diagnosis.compared? # => true, there was a previous list to compare with
Two statuses, and they are about the diagnosis rather than about the list:
:checked the list was fetched, parsed and measured
:failed it could not be, and the exception is the finding
A source that fails to fetch is not a source in good health, but nor is
it one this can say anything about -- which is why the failure is
recorded as an error finding and the profile is nil, rather than a
profile of nothing being compared against the last good one and reported
as every field collapsing at once.
Nothing here is stored. A diagnosis is what the doctor returns, and keeping it -- to compare a warning class against next week, to graph a fill rate -- is the host application's decision, which is why #to_h serializes the profile along with the findings.
Instances are frozen on construction and compare by value.
Constant Summary collapse
- STATUSES =
Whether the doctor got far enough to have an opinion.
failedmeans the list could not be read at all, which is a different report from one that read it and found something wrong with it. T.let(%i[checked failed].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
-
#baseline ⇒ Profile?
readonly
What it was measured against: the profile of the snapshot in storage, or the one a caller kept from the last run.
- #duration ⇒ Float readonly
-
#exception ⇒ Exception?
readonly
The exception behind a
:faileddiagnosis, for a caller that wants the backtrace. - #findings ⇒ Array<Finding> readonly
-
#profile ⇒ Profile?
readonly
What this run measured, or nil for a source that could not be read.
- #source ⇒ Symbol readonly
- #status ⇒ Symbol readonly
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #checked? ⇒ Boolean
-
#compared? ⇒ Boolean
Whether there was a previous list to measure this one against.
-
#error ⇒ String?
The failure on one line, for a log or a table.
- #error_class ⇒ String?
- #error_message ⇒ String?
- #errors ⇒ Array<Finding>
- #failed? ⇒ Boolean
- #hash ⇒ Integer
-
#headline(width = 0) ⇒ String
The heading a source gets in the report, followed by its findings:.
- #infos ⇒ Array<Finding>
- #initialize(source:, status:, findings: [], profile: nil, baseline: nil, duration: 0.0, error: nil) ⇒ void constructor
- #inspect ⇒ String
-
#label ⇒ String
OK,INFO,WARNorERROR-- what an operator's eye goes down the left-hand column looking for. -
#lines(width = 0) ⇒ Array<String>
This source's block of the report: its heading, then one line per finding.
-
#ok? ⇒ Boolean
Nothing above
info. - #record_count ⇒ Integer
-
#severity ⇒ Symbol?
The most serious severity found, or nil for a source with nothing to say about it at all.
- #to_h ⇒ Hash{Symbol => T.untyped}
- #to_s ⇒ String
- #warnings ⇒ Array<Finding>
Constructor Details
#initialize(source:, status:, findings: [], profile: nil, baseline: nil, duration: 0.0, error: nil) ⇒ void
92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 92 def initialize(source:, status:, findings: [], profile: nil, baseline: nil, duration: 0.0, error: nil) @source = T.let(symbol!(:source, source), Symbol) @status = T.let(status!(status), Symbol) @findings = T.let(findings!(findings), T::Array[Finding]) @profile = T.let(profile!(profile), T.nilable(Profile)) @baseline = T.let(profile!(baseline), T.nilable(Profile)) @duration = T.let(duration.to_f, Float) @exception = T.let(error.is_a?(Exception) ? error : nil, T.nilable(Exception)) @failure = T.let(failure!(error), T.nilable(T::Hash[Symbol, String])) freeze end |
Instance Attribute Details
#baseline ⇒ Profile? (readonly)
What it was measured against: the profile of the snapshot in storage, or the one a caller kept from the last run. nil when there was nothing to compare with, which is what makes the difference between "this is the first look" and "nothing changed".
68 69 70 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 68 def baseline @baseline end |
#duration ⇒ Float (readonly)
71 72 73 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 71 def duration @duration end |
#exception ⇒ Exception? (readonly)
The exception behind a :failed diagnosis, for a caller that wants the
backtrace. nil after a round-trip through #to_h, exactly as
Sync::Result has it.
77 78 79 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 77 def exception @exception end |
#findings ⇒ Array<Finding> (readonly)
57 58 59 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 57 def findings @findings end |
#profile ⇒ Profile? (readonly)
What this run measured, or nil for a source that could not be read.
61 62 63 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 61 def profile @profile end |
#source ⇒ Symbol (readonly)
51 52 53 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 51 def source @source end |
#status ⇒ Symbol (readonly)
54 55 56 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 54 def status @status end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
80 81 82 83 84 85 86 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 80 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Doctor::Diagnosis attribute(s): #{unknown.join(", ")}" if unknown.any? T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
185 186 187 188 189 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 185 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#checked? ⇒ Boolean
105 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 105 def checked? = status == :checked |
#compared? ⇒ Boolean
Whether there was a previous list to measure this one against. A run with no baseline is held to the adapter's committed floors instead, and says so rather than treating a first look as a regression.
114 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 114 def compared? = !baseline.nil? |
#error ⇒ String?
The failure on one line, for a log or a table. nil when nothing failed.
145 146 147 148 149 150 151 152 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 145 def error return nil unless error_class = return error_class if .nil? || .empty? || == error_class "#{error_class}: #{}" end |
#error_class ⇒ String?
138 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 138 def error_class = @failure&.fetch(:class) |
#error_message ⇒ String?
141 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 141 def = @failure&.fetch(:message) |
#errors ⇒ Array<Finding>
129 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 129 def errors = findings.select(&:error?) |
#failed? ⇒ Boolean
108 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 108 def failed? = status == :failed |
#hash ⇒ Integer
193 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 193 def hash = [self.class, to_h].hash |
#headline(width = 0) ⇒ String
The heading a source gets in the report, followed by its findings:
ofac_sdn WARN 3 findings
un_consolidated OK
168 169 170 171 172 173 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 168 def headline(width = 0) heading = "#{source.to_s.ljust(width)} #{label}" return heading if findings.empty? "#{heading} #{findings.size} finding#{"s" unless findings.size == 1}" end |
#infos ⇒ Array<Finding>
135 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 135 def infos = findings.select(&:info?) |
#inspect ⇒ String
196 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 196 def inspect = "#<#{self.class} #{source} #{label} #{findings.size} finding(s)>" |
#label ⇒ String
OK, INFO, WARN or ERROR -- what an operator's eye goes down the
left-hand column looking for.
201 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 201 def label = (severity || :ok).to_s.upcase |
#lines(width = 0) ⇒ Array<String>
This source's block of the report: its heading, then one line per
finding. width is how wide the source column is across the whole
run, so that the labels line up in a column an eye can run down.
179 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 179 def lines(width = 0) = [headline(width)] + findings.map(&:to_line) |
#ok? ⇒ Boolean
Nothing above info. The question the CLI's OK answers, and the one
a nightly job alerts on.
119 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 119 def ok? = findings.none? { |finding| finding.at_least?(:warn) } |
#record_count ⇒ Integer
155 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 155 def record_count = profile&.record_count || 0 |
#severity ⇒ Symbol?
The most serious severity found, or nil for a source with nothing to say about it at all.
124 125 126 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 124 def severity Finding::SEVERITIES.reverse.find { |level| findings.any? { |finding| finding.severity == level } } end |
#to_h ⇒ Hash{Symbol => T.untyped}
158 159 160 161 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 158 def to_h { source: source, status: status, findings: findings.map(&:to_h), profile: profile&.to_h, baseline: baseline&.to_h, duration: duration, error: @failure } end |
#to_s ⇒ String
182 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 182 def to_s = lines.join("\n") |
#warnings ⇒ Array<Finding>
132 |
# File 'lib/active_sanction/doctor/diagnosis.rb', line 132 def warnings = findings.select(&:warn?) |