Class: ActiveSanction::Validators
- Inherits:
-
Object
- Object
- ActiveSanction::Validators
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/validators.rb
Overview
The cache validators a publisher handed back with a list, plus enough context to know whether they still apply.
validators.request_headers
# => { "If-None-Match" => "\"0953154d0fb5aff918c5ec1daf6e9c0e\"",
# "If-Modified-Since" => "Fri, 28 Aug 2026 14:02:55 GMT" }
Every launch source serves both, verified live -- OFAC's SDN.CSV, the UN's consolidated.xml and Canada's sema-lmes.xml -- so a routine sync of lists that change daily at most costs three 304s instead of tens of megabytes.
Both values are stored and echoed back verbatim. An ETag is an opaque
string that only its origin server can interpret, and Last-Modified is an
HTTP-date whose formatting is the server's business; parsing either one to
re-render it is how a working conditional GET turns into a full download
nobody notices.
url is the URL that was requested, not the one that finally answered:
OFAC redirects to blob storage, and the hop it lands on is not stable
enough to key a cache by. It is kept so validators can be discarded when a
publisher moves its file -- sending a previous file's ETag to a new URL
invites a 304 that means nothing.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
- #checked_at ⇒ Time readonly
-
#etag ⇒ String?
readonly
Opaque, and echoed back verbatim: only the origin server can interpret either of them.
- #last_modified ⇒ String? readonly
- #updated_at ⇒ Time readonly
-
#url ⇒ String
readonly
The URL that was requested, not the one that finally answered -- see the class comment.
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so validators survive the round-trip through the JSON the store writes.
-
.from_response(response, url:, at: nil) ⇒ T.attached_class
urlis the caller's URL rather thanresponse.urifor the reason in the class comment: the response may have come from a redirect target.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#confirmed_by(response, at: nil) ⇒ Validators
What to store after the publisher answered 304.
-
#empty? ⇒ Boolean
True when the publisher gave us nothing to be conditional with.
-
#for?(other) ⇒ Boolean
Validators belong to the URL they came from.
- #hash ⇒ Integer
-
#initialize(url:, etag: nil, last_modified: nil, checked_at: nil, updated_at: nil) ⇒ void
constructor
checked_atis when the publisher last confirmed this copy, whether by 200 or by 304;updated_atis when the bytes themselves last changed. - #inspect ⇒ String
- #present? ⇒ Boolean
-
#request_headers ⇒ Hash{String => String}
Both are sent when both are known.
- #to_h ⇒ Hash{Symbol => T.untyped}
Constructor Details
#initialize(url:, etag: nil, last_modified: nil, checked_at: nil, updated_at: nil) ⇒ void
checked_at is when the publisher last confirmed this copy, whether by
200 or by 304; updated_at is when the bytes themselves last changed.
Staleness follows the first, because "when did we last ask?" is the
question a sync schedule answers; the second is what a human wants when
reading why a list looks old.
89 90 91 92 93 94 95 96 |
# File 'lib/active_sanction/validators.rb', line 89 def initialize(url:, etag: nil, last_modified: nil, checked_at: nil, updated_at: nil) @url = T.let(url!(url), String) @etag = T.let(string_or_nil(etag), T.nilable(String)) @last_modified = T.let(string_or_nil(last_modified), T.nilable(String)) @checked_at = T.let(time!(checked_at), Time) @updated_at = T.let(updated_at.nil? ? @checked_at : time!(updated_at), Time) freeze end |
Instance Attribute Details
#checked_at ⇒ Time (readonly)
54 55 56 |
# File 'lib/active_sanction/validators.rb', line 54 def checked_at @checked_at end |
#etag ⇒ String? (readonly)
Opaque, and echoed back verbatim: only the origin server can interpret either of them.
48 49 50 |
# File 'lib/active_sanction/validators.rb', line 48 def etag @etag end |
#last_modified ⇒ String? (readonly)
51 52 53 |
# File 'lib/active_sanction/validators.rb', line 51 def last_modified @last_modified end |
#updated_at ⇒ Time (readonly)
57 58 59 |
# File 'lib/active_sanction/validators.rb', line 57 def updated_at @updated_at end |
#url ⇒ String (readonly)
The URL that was requested, not the one that finally answered -- see the class comment.
43 44 45 |
# File 'lib/active_sanction/validators.rb', line 43 def url @url end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds from #to_h output, accepting string keys so validators survive the round-trip through the JSON the store writes.
69 70 71 72 73 74 75 76 77 78 |
# File 'lib/active_sanction/validators.rb', line 69 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Validators attribute(s): #{unknown.join(", ")}" if unknown.any? # `new(**hash)` past a required keyword parameter is one of the few # things Sorbet cannot check statically. #initialize validates what # arrives, which is where a bad round-trip is caught. T.unsafe(self).new(**attributes) end |
.from_response(response, url:, at: nil) ⇒ T.attached_class
url is the caller's URL rather than response.uri for the reason in
the class comment: the response may have come from a redirect target.
62 63 64 |
# File 'lib/active_sanction/validators.rb', line 62 def self.from_response(response, url:, at: nil) new(url: url, etag: response.etag, last_modified: response.last_modified, checked_at: at) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
148 149 150 151 152 |
# File 'lib/active_sanction/validators.rb', line 148 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#confirmed_by(response, at: nil) ⇒ Validators
What to store after the publisher answered 304. The copy is unchanged,
so updated_at stands; only the moment we confirmed it moves. A 304 is
allowed to carry a fresh ETag, and when it does that value is the one to
send next time.
130 131 132 133 134 |
# File 'lib/active_sanction/validators.rb', line 130 def confirmed_by(response, at: nil) self.class.new(url: url, etag: response.etag || etag, last_modified: response.last_modified || last_modified, checked_at: at, updated_at: updated_at) end |
#empty? ⇒ Boolean
True when the publisher gave us nothing to be conditional with. Such a record is still worth storing -- it says we looked -- but it cannot save a download, so callers treat it as no validators at all.
102 |
# File 'lib/active_sanction/validators.rb', line 102 def empty? = etag.nil? && last_modified.nil? |
#for?(other) ⇒ Boolean
Validators belong to the URL they came from. A publisher that moves its file gets a full download rather than a 304 from whatever is now at the old address.
123 |
# File 'lib/active_sanction/validators.rb', line 123 def for?(other) = url == other.to_s |
#hash ⇒ Integer
156 |
# File 'lib/active_sanction/validators.rb', line 156 def hash = [self.class, to_h].hash |
#inspect ⇒ String
159 160 161 162 |
# File 'lib/active_sanction/validators.rb', line 159 def inspect "#<#{self.class} #{url} etag=#{etag.inspect} last_modified=#{last_modified.inspect} " \ "checked_at=#{checked_at.iso8601}>" end |
#present? ⇒ Boolean
105 |
# File 'lib/active_sanction/validators.rb', line 105 def present? = !empty? |
#request_headers ⇒ Hash{String => String}
Both are sent when both are known. RFC 9110 has the server prefer
If-None-Match and ignore the date, but a cache in front of it may only
honour one, and the second header costs 40 bytes on a request that is
trying to avoid 126 MB.
112 113 114 115 116 117 |
# File 'lib/active_sanction/validators.rb', line 112 def request_headers headers = {} headers["If-None-Match"] = etag if etag headers["If-Modified-Since"] = last_modified if last_modified headers.freeze end |
#to_h ⇒ Hash{Symbol => T.untyped}
137 138 139 140 141 142 143 144 145 |
# File 'lib/active_sanction/validators.rb', line 137 def to_h { url: url, etag: etag, last_modified: last_modified, checked_at: checked_at.iso8601, updated_at: updated_at.iso8601 } end |