Module: ActiveSanction::Sources::Definition
- Extended by:
- T::Helpers, T::Sig
- Included in:
- Base
- Defined in:
- lib/active_sanction/sources/definition.rb
Overview
What an adapter declares about the list it reads. Extended into Sources::Base, so every adapter's class body reads as a description of the list rather than as a constructor:
class UnConsolidated < ActiveSanction::Sources::Base
key :un_consolidated
jurisdiction :un
"United Nations Security Council"
format :xml
url :main, "https://scsanctions.un.org/resources/xml/en/consolidated.xml"
end
Every declaration reads back with no argument -- UnConsolidated.authority
-- and that is not decoration. It is what the registry files the adapter
under, what the CLI's sources command prints, and what a match result
cites when an examiner asks which list a name was found on.
Multiple URLs
OFAC publishes the SDN list as three files that only mean something joined -- the names, their aliases, their addresses -- so a source declares as many as it has:
url :sdn, "https://sanctionslistservice.ofac.treas.gov/api/download/SDN.CSV"
url :alt, "https://sanctionslistservice.ofac.treas.gov/api/download/ALT.CSV"
url :add, "https://sanctionslistservice.ofac.treas.gov/api/download/ADD.CSV"
Each is fetched, validated and cached independently, because they change independently: a sync where only ALT.CSV moved should download only ALT.CSV.
What is inherited, and what is not
Declarations resolve up the superclass chain, so adapters sharing a
publisher can share a base holding the jurisdiction, the authority and
the format. key is the exception: it is looked up on the exact class
and nowhere else, since a subclass silently inheriting its parent's key
would try to register under a name already taken -- which is the one
mistake the registry cannot let through.
Class Method Summary collapse
-
.key!(value) ⇒ Symbol
Shared with the registry, so a source registered without going through Base is held to the same rule as one that declared its key here.
Instance Method Summary collapse
-
#authority(value = UNSET) ⇒ String
The body behind the list, spelled the way it spells itself.
-
#declarations ⇒ Hash{Symbol => T.untyped}
The declarations made on this exact class, ignoring anything inherited.
-
#declared?(name) ⇒ Boolean
Whether a declaration was made, without raising if it was not.
-
#declared_floors ⇒ Hash{Symbol => Numeric}
The floors declared on this exact class, ignoring anything inherited.
- #declared_urls ⇒ Hash{Symbol => String}
-
#file_key(name) ⇒ Symbol
A source with several files needs each filed separately -- separate ETags, separate cache entries -- or three files sharing one name would evict each other out of a cache that retains N payloads per name.
-
#floor(name = UNSET, value = UNSET) ⇒ T.untyped
A lower bound this list is held to when there is nothing to compare it against -- a first sync, a new source, a store that was cleared:.
-
#floors ⇒ Hash{Symbol => Numeric}
Every floor that applies to this adapter, inherited ones included.
-
#format(value = UNSET) ⇒ Symbol?
What the publisher serves: :csv, :xml, :json.
-
#jurisdiction(value = UNSET) ⇒ Symbol
Who publishes the list, as a symbol: :us, :un, :ca, :eu.
-
#key(value = UNSET) ⇒ Symbol
The name this list answers to everywhere.
-
#licence_notice(value = UNSET) ⇒ String?
What the publisher says about reusing its list, and where it says it.
-
#licence_url(value = UNSET) ⇒ String?
Where the publisher states its terms.
- #multi_url? ⇒ Boolean
-
#to_h ⇒ Hash{Symbol => T.untyped}
A summary of the declarations, for a CLI listing or a bug report.
-
#url(name = UNSET, address = UNSET) ⇒ String
Declares a file with two arguments, reads one back with one, and with none returns the primary -- the first declared, conventionally :main.
-
#urls ⇒ Hash{Symbol => String}
Every declared file, in declaration order, inherited ones first.
Class Method Details
.key!(value) ⇒ Symbol
Shared with the registry, so a source registered without going through Base is held to the same rule as one that declared its key here.
79 80 81 82 83 84 85 86 |
# File 'lib/active_sanction/sources/definition.rb', line 79 def self.key!(value) key = value.to_s return key.to_sym if key.match?(KEY_PATTERN) raise DeclarationError, "#{value.inspect} is not a usable source key: it is typed into configuration and used as a " \ "directory name, so it must be lowercase snake_case starting with a letter, like :ofac_sdn" end |
Instance Method Details
#authority(value = UNSET) ⇒ String
The body behind the list, spelled the way it spells itself. This is what a compliance report prints beside a hit, so "United Nations Security Council", not "UN".
110 111 112 113 114 |
# File 'lib/active_sanction/sources/definition.rb', line 110 def (value = UNSET) return required(:authority) { "does not declare an authority, e.g. `authority \"...\"`" } if unset?(value) declarations[:authority] = string!(:authority, value) end |
#declarations ⇒ Hash{Symbol => T.untyped}
The declarations made on this exact class, ignoring anything inherited. Public because resolving a reader means walking the superclass chain asking each one what it declared.
259 260 261 |
# File 'lib/active_sanction/sources/definition.rb', line 259 def declarations @declarations ||= T.let({}, T.nilable(T::Hash[Symbol, T.untyped])) end |
#declared?(name) ⇒ Boolean
Whether a declaration was made, without raising if it was not. What a conformance spec (#16) asks before reporting which ones are missing.
241 242 243 |
# File 'lib/active_sanction/sources/definition.rb', line 241 def declared?(name) name == :key ? !declarations[:key].nil? : !declared(name).nil? end |
#declared_floors ⇒ Hash{Symbol => Numeric}
The floors declared on this exact class, ignoring anything inherited. Public for the same reason #declared_urls is: resolving one means walking the superclass chain asking each class what it declared.
272 273 274 |
# File 'lib/active_sanction/sources/definition.rb', line 272 def declared_floors @declared_floors ||= T.let({}, T.nilable(T::Hash[Symbol, Numeric])) end |
#declared_urls ⇒ Hash{Symbol => String}
264 265 266 |
# File 'lib/active_sanction/sources/definition.rb', line 264 def declared_urls @declared_urls ||= T.let({}, T.nilable(T::Hash[Symbol, String])) end |
#file_key(name) ⇒ Symbol
A source with several files needs each filed separately -- separate ETags, separate cache entries -- or three files sharing one name would evict each other out of a cache that retains N payloads per name. One file is filed under the source key itself, which keeps the common case legible on disk and in a validators.json somebody is reading to find out why a sync downloaded more than it should have.
231 232 233 |
# File 'lib/active_sanction/sources/definition.rb', line 231 def file_key(name) multi_url? ? :"#{key}-#{name}" : key end |
#floor(name = UNSET, value = UNSET) ⇒ T.untyped
A lower bound this list is held to when there is nothing to compare it against -- a first sync, a new source, a store that was cleared:
floor :record_count, 400
floor :remarks_coverage, 0.90
floor :fill_addresses, 0.30
The name is a Doctor check name; the value is the least it may be without the diagnosis saying so. Reads back with one argument, and every declared floor with none.
These are deliberately coarse and deliberately few. A number committed here goes stale on its own, and the day somebody widens one to make a build pass is the day it stops being read -- which is why Doctor compares against the last stored snapshot instead, and uses these only where there is no snapshot to compare with. Declare a floor no published version of the list has ever come close to, and let the baseline do the real work.
203 204 205 206 207 208 |
# File 'lib/active_sanction/sources/definition.rb', line 203 def floor(name = UNSET, value = UNSET) return floors if unset?(name) return floors[symbol!(:floor, name)] if unset?(value) declared_floors[symbol!(:floor, name)] = floor!(name, value) end |
#floors ⇒ Hash{Symbol => Numeric}
Every floor that applies to this adapter, inherited ones included. A subclass declaring the same name replaces its parent's, so an adapter over a list a tenth the size of its sibling's says so once.
214 215 216 |
# File 'lib/active_sanction/sources/definition.rb', line 214 def floors lineage.reverse.inject({}) { |all, klass| all.merge(klass.declared_floors) }.freeze end |
#format(value = UNSET) ⇒ Symbol?
What the publisher serves: :csv, :xml, :json. Informational, and deliberately not checked against a list of known formats -- the parser toolkits (#14, #15) are chosen by the adapter, not dispatched from here, and a source arriving as :fixed_width or :xlsx should be able to say so without waiting for a release of this gem. Optional: a source that builds entities from a database has no format to name.
168 169 170 171 172 |
# File 'lib/active_sanction/sources/definition.rb', line 168 def format(value = UNSET) return declared(:format) if unset?(value) declarations[:format] = symbol!(:format, value) end |
#jurisdiction(value = UNSET) ⇒ Symbol
Who publishes the list, as a symbol: :us, :un, :ca, :eu. Not validated against a country list -- an internal watchlist's jurisdiction is whatever its owner says it is.
100 101 102 103 104 |
# File 'lib/active_sanction/sources/definition.rb', line 100 def jurisdiction(value = UNSET) return required(:jurisdiction) { "does not declare a jurisdiction, e.g. `jurisdiction :un`" } if unset?(value) declarations[:jurisdiction] = symbol!(:jurisdiction, value) end |
#key(value = UNSET) ⇒ Symbol
The name this list answers to everywhere. Required, and never inherited.
90 91 92 93 94 |
# File 'lib/active_sanction/sources/definition.rb', line 90 def key(value = UNSET) return own(:key) { "does not declare a key. Add `key :something` to its class body" } if unset?(value) declarations[:key] = Definition.key!(value) end |
#licence_notice(value = UNSET) ⇒ String?
What the publisher says about reusing its list, and where it says it. Both optional, and both are a pointer rather than a legal opinion:
licence_notice "Crown copyright. Open Government Licence v3.0."
licence_url "https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/"
Why this is in the gem at all
Screening a name against a list is reading it, and nobody needs a licence to read. Publishing what a list said is redistribution -- which is what a signed bundle (#57) is, what a hosted screening API returning a matched name arguably is, and what any application storing hits in its own audit trail may be. These publishers do not agree with each other about that: two of the seven attach conditions to redistribution that the other five do not.
So the notice travels with the source rather than living in a document
somebody has to go and find. ActiveSanction::Sources[:canada_sema] .licence_notice answers in a console, in a bundle's header, and on the
catalogue page, from one declaration.
What it is not
It is not legal advice, it is not this project's reading of the terms, and it is not a grant of anything by us. It is a short, dated summary of what the publisher's own page says, plus the URL of that page, which is the thing that actually governs. A deployment redistributing any of these lists should read the URL and ask its own counsel.
145 146 147 148 149 |
# File 'lib/active_sanction/sources/definition.rb', line 145 def licence_notice(value = UNSET) return declared(:licence_notice) if unset?(value) declarations[:licence_notice] = string!(:licence_notice, value) end |
#licence_url(value = UNSET) ⇒ String?
Where the publisher states its terms. Validated as an http(s) URL for the same reason a list's own URL is: a notice pointing nowhere is worse than no notice, because it reads as though somebody checked.
155 156 157 158 159 |
# File 'lib/active_sanction/sources/definition.rb', line 155 def licence_url(value = UNSET) return declared(:licence_url) if unset?(value) declarations[:licence_url] = address!(:licence_url, value) end |
#multi_url? ⇒ Boolean
236 |
# File 'lib/active_sanction/sources/definition.rb', line 236 def multi_url? = urls.size > 1 |
#to_h ⇒ Hash{Symbol => T.untyped}
A summary of the declarations, for a CLI listing or a bug report. Reads what is there rather than insisting: an adapter missing a declaration is exactly what somebody printing this is trying to find out.
249 250 251 252 253 |
# File 'lib/active_sanction/sources/definition.rb', line 249 def to_h { key: declarations[:key], jurisdiction: declared(:jurisdiction), authority: declared(:authority), format: declared(:format), urls: urls, licence_notice: declared(:licence_notice), licence_url: declared(:licence_url) } end |
#url(name = UNSET, address = UNSET) ⇒ String
Declares a file with two arguments, reads one back with one, and with none returns the primary -- the first declared, conventionally :main.
177 178 179 180 181 182 |
# File 'lib/active_sanction/sources/definition.rb', line 177 def url(name = UNSET, address = UNSET) return primary_url if unset?(name) return read_url(name) if unset?(address) declared_urls[name.to_sym] = address!(name, address) end |
#urls ⇒ Hash{Symbol => String}
Every declared file, in declaration order, inherited ones first.
220 221 222 |
# File 'lib/active_sanction/sources/definition.rb', line 220 def urls lineage.reverse.inject({}) { |all, klass| all.merge(klass.declared_urls) }.freeze end |