Class: ActiveSanction::Snapshot::Bundle::Header
- Inherits:
-
Object
- Object
- ActiveSanction::Snapshot::Bundle::Header
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/snapshot/bundle/header.rb
Overview
What a bundle says about itself, in one line, before any of its records are read.
header = ActiveSanction::Snapshot::Bundle.header(io)
header.source # => :ofac_sdn
header.record_count # => 19015
header.snapshot_checksum # => "sha256:9f86d081884c7d65..."
header.generator # => "active_sanction/1.0.0"
header.fetched_at # => 2026-08-28 09:30:00 UTC
This is Storage::Meta's job for a file somebody sent you, and the two
are deliberately the same shape: small, cheap, and answerable without
inflating tens of megabytes. active_sanction import prints one, a
mirror indexes them, and a deploy decides whether it already holds this
list version by comparing snapshot_checksum against what it stored.
It is the thing that gets signed
A signature covers these bytes and nothing else -- see Bundle::Signature.
It can, because payload_digest is a SHA-256 over every record in the
file: signing ~300 bytes here transitively covers all 19,015 of them,
and a verifier settles who published a bundle before it inflates a byte
of what they sent.
So every field that could change what the payload means is in here, and a field whose value is not reproducible from the snapshot is not: there is no written-at timestamp, because two writes of one snapshot have to produce identical files.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#fetched_at ⇒ Time
readonly
UTC, truncated to the second: when the publisher's file was fetched, not when this bundle was written.
- #format_version ⇒ Integer readonly
-
#gem_version ⇒ String
readonly
The active_sanction that serialized the records, which is what a
schema_versionthis reader does not know is diagnosed against. - #generator ⇒ String readonly
-
#payload_bytes ⇒ Integer
readonly
The length of the uncompressed payload.
-
#payload_compression ⇒ String
readonly
deflatein v1: a raw zlib stream, RFC 1950. -
#payload_digest ⇒ String
readonly
SHA-256 over the uncompressed payload.
-
#payload_encoding ⇒ String
readonly
How the records are laid out once decompressed.
- #record_count ⇒ Integer readonly
-
#schema_version ⇒ Integer
readonly
Snapshot::SCHEMA_VERSION the records were written under.
-
#snapshot_checksum ⇒ String
readonly
The checksum of the list itself: content only, and identical to what the snapshot had before it was ever written to a file.
- #source ⇒ Symbol readonly
- #source_version ⇒ String? readonly
Class Method Summary collapse
-
.default_generator ⇒ String
Who wrote the file, defaulted to this gem and this version.
-
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a header survives the round-trip through JSON.
-
.from_snapshot(snapshot, payload_digest:, payload_bytes:, generator: nil) ⇒ T.attached_class
The header for a snapshot about to be written, given what the payload came to.
-
.parse(line) ⇒ T.attached_class
Reads one serialized header line.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #hash ⇒ Integer
- #initialize(format_version:, gem_version:, generator:, source:, schema_version:, snapshot_checksum:, record_count:, fetched_at:, payload_digest:, payload_bytes:, source_version: nil, payload_encoding: ENCODING, payload_compression: COMPRESSION) ⇒ void constructor
- #inspect ⇒ String
-
#to_h ⇒ Hash{Symbol => T.untyped}
The documented shape, in the documented order.
-
#to_line ⇒ String
The exact bytes that go into the file, and the exact bytes a signature is computed over.
Constructor Details
#initialize(format_version:, gem_version:, generator:, source:, schema_version:, snapshot_checksum:, record_count:, fetched_at:, payload_digest:, payload_bytes:, source_version: nil, payload_encoding: ENCODING, payload_compression: COMPRESSION) ⇒ void
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 192 def initialize(format_version:, gem_version:, generator:, source:, schema_version:, snapshot_checksum:, record_count:, fetched_at:, payload_digest:, payload_bytes:, source_version: nil, payload_encoding: ENCODING, payload_compression: COMPRESSION) @format_version = T.let(version!(:format_version, format_version), Integer) @gem_version = T.let(string!(:gem_version, gem_version), String) @generator = T.let(string!(:generator, generator), String) @source = T.let(Sources::Definition.key!(source), Symbol) @schema_version = T.let(version!(:schema_version, schema_version), Integer) @snapshot_checksum = T.let(digest!(:snapshot_checksum, snapshot_checksum), String) @record_count = T.let(count!(:record_count, record_count), Integer) @fetched_at = T.let(time!(fetched_at), Time) @source_version = T.let(string_or_nil(source_version), T.nilable(String)) @payload_encoding = T.let(string!(:payload_encoding, payload_encoding), String) @payload_compression = T.let(string!(:payload_compression, payload_compression), String) @payload_digest = T.let(digest!(:payload_digest, payload_digest), String) @payload_bytes = T.let(count!(:payload_bytes, payload_bytes), Integer) freeze end |
Instance Attribute Details
#fetched_at ⇒ Time (readonly)
UTC, truncated to the second: when the publisher's file was fetched, not when this bundle was written.
158 159 160 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 158 def fetched_at @fetched_at end |
#format_version ⇒ Integer (readonly)
128 129 130 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 128 def format_version @format_version end |
#gem_version ⇒ String (readonly)
The active_sanction that serialized the records, which is what a
schema_version this reader does not know is diagnosed against.
133 134 135 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 133 def gem_version @gem_version end |
#generator ⇒ String (readonly)
136 137 138 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 136 def generator @generator end |
#payload_bytes ⇒ Integer (readonly)
The length of the uncompressed payload. Checked as a reader inflates, so a bundle that decompresses to more than it declared is refused part way through rather than absorbed.
184 185 186 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 184 def payload_bytes @payload_bytes end |
#payload_compression ⇒ String (readonly)
deflate in v1: a raw zlib stream, RFC 1950.
171 172 173 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 171 def payload_compression @payload_compression end |
#payload_digest ⇒ String (readonly)
SHA-256 over the uncompressed payload. Over the uncompressed bytes because that is the half of the file two machines can be held to: zlib builds differ in what they emit for identical input, and the records do not.
178 179 180 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 178 def payload_digest @payload_digest end |
#payload_encoding ⇒ String (readonly)
How the records are laid out once decompressed. ndjson in v1, and
named so that a later version can add another without a reader having
to guess which it is looking at.
167 168 169 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 167 def payload_encoding @payload_encoding end |
#record_count ⇒ Integer (readonly)
153 154 155 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 153 def record_count @record_count end |
#schema_version ⇒ Integer (readonly)
Snapshot::SCHEMA_VERSION the records were written under.
143 144 145 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 143 def schema_version @schema_version end |
#snapshot_checksum ⇒ String (readonly)
The checksum of the list itself: content only, and identical to what the snapshot had before it was ever written to a file. This is what a stored MatchResult cites, so a bundle can be matched to a screening decision made years earlier.
150 151 152 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 150 def snapshot_checksum @snapshot_checksum end |
#source ⇒ Symbol (readonly)
139 140 141 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 139 def source @source end |
#source_version ⇒ String? (readonly)
161 162 163 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 161 def source_version @source_version end |
Class Method Details
.default_generator ⇒ String
Who wrote the file, defaulted to this gem and this version. A
publisher that is not this gem -- a commercial mirror, a bank's
internal pipeline -- overrides it, which is the point of the field:
gem_version says what code serialized the records, and this says
whose bundle it is.
81 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 81 def self.default_generator = -"active_sanction/#{VERSION}" |
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so a header survives the round-trip through JSON.
113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 113 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown bundle header field(s): #{unknown.join(", ")}" if unknown.any? missing = MEMBERS - attributes.keys - OPTIONAL_MEMBERS raise InvalidArgument, "bundle header is missing #{missing.join(", ")}" if missing.any? # `new(**hash)` past required keyword parameters is one of the few # things Sorbet cannot check statically. #initialize validates what # arrives, which is where a bad header is caught. T.unsafe(self).new(**attributes) end |
.from_snapshot(snapshot, payload_digest:, payload_bytes:, generator: nil) ⇒ T.attached_class
The header for a snapshot about to be written, given what the payload came to. Both payload values are measurements of bytes that already exist rather than promises about bytes to come -- see Bundle.write.
90 91 92 93 94 95 96 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 90 def self.from_snapshot(snapshot, payload_digest:, payload_bytes:, generator: nil) new(format_version: FORMAT_VERSION, gem_version: VERSION, generator: generator || default_generator, source: snapshot.source, schema_version: snapshot.schema_version, snapshot_checksum: snapshot.checksum, record_count: snapshot.record_count, fetched_at: snapshot.fetched_at, source_version: snapshot.source_version, payload_digest: payload_digest, payload_bytes: payload_bytes) end |
.parse(line) ⇒ T.attached_class
Reads one serialized header line. Everything that can be wrong with it raises -- an unknown key, a missing one, a digest that is not a digest -- because a header this code half understands is a header it cannot say the signature covers.
103 104 105 106 107 108 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 103 def self.parse(line) parsed = JSON.parse(line.to_s) raise InvalidArgument, "a bundle header is a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash) from_h(parsed) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
231 232 233 234 235 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 231 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#hash ⇒ Integer
239 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 239 def hash = [self.class, to_h].hash |
#inspect ⇒ String
242 243 244 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 242 def inspect "#<#{self.class} #{source} #{record_count} entities #{snapshot_checksum} by #{generator}>" end |
#to_h ⇒ Hash{Symbol => T.untyped}
The documented shape, in the documented order.
213 214 215 216 217 218 219 220 221 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 213 def to_h { format_version: format_version, gem_version: gem_version, generator: generator, source: source.to_s, schema_version: schema_version, snapshot_checksum: snapshot_checksum, record_count: record_count, fetched_at: fetched_at.iso8601, source_version: source_version, payload_encoding: payload_encoding, payload_compression: payload_compression, payload_digest: payload_digest, payload_bytes: payload_bytes } end |
#to_line ⇒ String
The exact bytes that go into the file, and the exact bytes a signature is computed over. No trailing newline: the newline is the container's, not the header's, so a reader that strips line endings differently still verifies.
228 |
# File 'lib/active_sanction/snapshot/bundle/header.rb', line 228 def to_line = JSON.generate(to_h) |