Class: ActiveSanction::Snapshot
- Inherits:
-
Object
- Object
- ActiveSanction::Snapshot
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/snapshot.rb,
lib/active_sanction/snapshot/bundle.rb,
lib/active_sanction/snapshot/bundle/header.rb,
lib/active_sanction/snapshot/bundle/payload.rb,
lib/active_sanction/snapshot/bundle/signature.rb
Overview
One source's entities as they stood at one moment, with a checksum over their content. This is the unit of persistence (#24) and the anchor for reproducibility.
ActiveSanction::Snapshot.new(
source: :ofac_sdn,
entities: [Entity, ...],
fetched_at: Time.now.utc,
source_version: "2026-08-28" # the publisher's own date, if it gives one
)
A screening decision has to be reproducible months later, in front of an
examiner. MatchResult (#33) stamps checksum onto every result, so "why
did we clear this customer on 5 Jan" is answered by reloading the exact
list version that was screened against. Without that the library produces
unauditable results.
Instances are frozen on construction and compare by value.
Defined Under Namespace
Modules: Bundle Classes: ChecksumMismatch
Instance Attribute Summary collapse
-
#checksum ⇒ String
readonly
sha256:and 64 hex digits, over the content and nothing else. -
#entities ⇒ Array<T.untyped>
readonly
Deliberately not
T::Array[Entity], and the one member of the canonical model that is not declared. -
#fetched_at ⇒ Time
readonly
UTC, truncated to the second, which is the precision #to_h serializes.
- #record_count ⇒ Integer readonly
- #schema_version ⇒ Integer readonly
- #source ⇒ Symbol readonly
-
#source_version ⇒ String?
readonly
The publisher's own version string where it gives one, which is not something every list does.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds a snapshot from #to_h output, verifying the checksum as it goes.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Two fetches of an unchanged list are the same snapshot with different timestamps, and the checksum is what says so; equality follows it rather than #to_h so a re-fetch does not read as a new list version.
- #empty? ⇒ Boolean
- #hash ⇒ Integer
-
#initialize(source:, entities:, fetched_at: nil, checksum: nil, record_count: nil, schema_version: SCHEMA_VERSION, source_version: nil, trusted: false) ⇒ void
constructor
checksumandrecord_countare derived, not supplied. - #inspect ⇒ String
- #to_h ⇒ Hash{Symbol => T.untyped}
-
#trusted? ⇒ Boolean
Whether this snapshot's provenance was verified: it came out of a bundle (#57) whose signature checked out under a public key the caller supplied.
Constructor Details
#initialize(source:, entities:, fetched_at: nil, checksum: nil, record_count: nil, schema_version: SCHEMA_VERSION, source_version: nil, trusted: false) ⇒ void
checksum and record_count are derived, not supplied. Passing them --
which is what .from_h does with a stored snapshot -- asserts what the
content should be, and construction fails if it is not.
138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/active_sanction/snapshot.rb', line 138 def initialize(source:, entities:, fetched_at: nil, checksum: nil, record_count: nil, schema_version: SCHEMA_VERSION, source_version: nil, trusted: false) @source = T.let(symbol!(:source, source), Symbol) @entities = T.let(entities!(entities), T::Array[T.untyped]) @fetched_at = T.let(time!(fetched_at), Time) @schema_version = T.let(version!(schema_version), Integer) @source_version = T.let(string_or_nil(source_version), T.nilable(String)) @record_count = T.let(count!(record_count), Integer) @checksum = T.let(checksum!(checksum), String) @trusted = T.let(trusted == true, T::Boolean) freeze end |
Instance Attribute Details
#checksum ⇒ String (readonly)
sha256: and 64 hex digits, over the content and nothing else.
81 82 83 |
# File 'lib/active_sanction/snapshot.rb', line 81 def checksum @checksum end |
#entities ⇒ Array<T.untyped> (readonly)
Deliberately not T::Array[Entity], and the one member of the canonical
model that is not declared. The storage conformance group builds a
snapshot out of half-deserialized hashes on purpose, to prove it catches
a store that hands them back that way (#24); #entities! below states the
real contract -- anything that serializes -- in a message written for
whoever has to fix the adapter. An element type would raise a TypeError
there instead, one layer too early to say anything useful.
73 74 75 |
# File 'lib/active_sanction/snapshot.rb', line 73 def entities @entities end |
#fetched_at ⇒ Time (readonly)
UTC, truncated to the second, which is the precision #to_h serializes.
77 78 79 |
# File 'lib/active_sanction/snapshot.rb', line 77 def fetched_at @fetched_at end |
#record_count ⇒ Integer (readonly)
84 85 86 |
# File 'lib/active_sanction/snapshot.rb', line 84 def record_count @record_count end |
#schema_version ⇒ Integer (readonly)
87 88 89 |
# File 'lib/active_sanction/snapshot.rb', line 87 def schema_version @schema_version end |
#source ⇒ Symbol (readonly)
63 64 65 |
# File 'lib/active_sanction/snapshot.rb', line 63 def source @source end |
#source_version ⇒ String? (readonly)
The publisher's own version string where it gives one, which is not something every list does.
92 93 94 |
# File 'lib/active_sanction/snapshot.rb', line 92 def source_version @source_version end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds a snapshot from #to_h output, verifying the checksum as it goes. Accepts string keys, so a snapshot survives the round-trip through gzipped JSON that storage (#24) puts it through.
98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/active_sanction/snapshot.rb', line 98 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Snapshot attribute(s): #{unknown.join(", ")}" if unknown.any? attributes[:entities] &&= attributes[:entities].map { |value| build_entity(value) } # `new(**hash)` past required keyword parameters is one of the few things # Sorbet cannot check statically. #initialize validates what arrives -- # including, here, the checksum -- which is where a bad round-trip is # caught. T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Two fetches of an unchanged list are the same snapshot with different timestamps, and the checksum is what says so; equality follows it rather than #to_h so a re-fetch does not read as a new list version.
203 204 205 206 207 |
# File 'lib/active_sanction/snapshot.rb', line 203 def ==(other) return false unless other.instance_of?(self.class) checksum == other.checksum end |
#empty? ⇒ Boolean
152 |
# File 'lib/active_sanction/snapshot.rb', line 152 def empty? = entities.empty? |
#hash ⇒ Integer
211 212 213 |
# File 'lib/active_sanction/snapshot.rb', line 211 def hash [self.class, checksum].hash end |
#inspect ⇒ String
216 217 218 219 |
# File 'lib/active_sanction/snapshot.rb', line 216 def inspect "#<#{self.class} #{source} #{record_count} entities #{checksum} " \ "fetched_at=#{fetched_at.iso8601}#{" verified" if trusted?}>" end |
#to_h ⇒ Hash{Symbol => T.untyped}
187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/active_sanction/snapshot.rb', line 187 def to_h { source: source, entities: entities.map(&:to_h), fetched_at: fetched_at.iso8601, checksum: checksum, record_count: record_count, schema_version: schema_version, source_version: source_version } end |
#trusted? ⇒ Boolean
Whether this snapshot's provenance was verified: it came out of a bundle (#57) whose signature checked out under a public key the caller supplied. False for everything else, including a list this process fetched itself -- a sync proves that bytes parsed, not who published them.
Why it is not a member
It is out of MEMBERS, out of #to_h, out of the checksum and out of equality, because it is not a fact about the content. Two snapshots holding the same entities are the same list version whether or not one of them arrived signed, and a checksum that said otherwise would report a new list every time somebody imported one.
It does not survive storage
store.write_snapshot(bundle_snapshot)
store.read_snapshot(:ofac_sdn).trusted? # => false
Deliberately, and it is the honest answer. A signature attests to the bytes of a bundle file; once those entities have been rewritten into a store's own gzipped JSON or its own table, nothing signed covers what is on disk, and a stored flag claiming otherwise would be the library laundering an attestation it no longer holds. What a store guarantees is its own -- the checksum, re-derived on every read.
So a process that wants MatchResult#verified? to be true keeps the
bundle's snapshot in memory: Storage::Memory holds the object it was
given, and Matcher reads this off the very snapshot it indexed.
184 |
# File 'lib/active_sanction/snapshot.rb', line 184 def trusted? = @trusted |