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
authority    "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

Instance Method Summary collapse

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.

Parameters:

  • value (T.untyped)

Returns:

  • (Symbol)

Raises:



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".

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (String)


110
111
112
113
114
# File 'lib/active_sanction/sources/definition.rb', line 110

def authority(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.

Returns:

  • (Hash{Symbol => T.untyped})


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.

Parameters:

  • name (Symbol)

Returns:

  • (Boolean)


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.

Returns:

  • (Hash{Symbol => Numeric})


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}

Returns:

  • (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.

Parameters:

  • name (T.untyped)

Returns:

  • (Symbol)


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.

Parameters:

  • name (T.untyped) (defaults to: UNSET)
  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (T.untyped)


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.

Returns:

  • (Hash{Symbol => Numeric})


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.

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (Symbol, nil)


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.

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (Symbol)


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.

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (Symbol)


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.

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (String, nil)


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.

Parameters:

  • value (T.untyped) (defaults to: UNSET)

Returns:

  • (String, nil)


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

Returns:

  • (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.

Returns:

  • (Hash{Symbol => T.untyped})


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.

Parameters:

  • name (T.untyped) (defaults to: UNSET)
  • address (T.untyped) (defaults to: UNSET)

Returns:

  • (String)


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.

Returns:

  • (Hash{Symbol => String})


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