Class: ActiveSanction::Entity
- Inherits:
-
Object
- Object
- ActiveSanction::Entity
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/entity.rb
Overview
The single source-agnostic record every adapter produces. Nothing downstream -- storage, index, matcher -- should ever need to know which government published a record.
ActiveSanction::Entity.new(
id: "ofac_sdn:2674",
source: :ofac_sdn,
source_ref: "2674",
type: :individual,
names: [Name, ...],
addresses: [Address, ...],
identifiers: [Identifier, ...],
dates_of_birth: [PartialDate, ...],
nationalities: ["EG"],
programs: ["SDGT"],
listed_on: PartialDate,
remarks: "..."
)
Instances are frozen on construction and compare by value.
Constant Summary collapse
- TYPES =
vesselandaircraftare first-class because they are ~10% of the OFAC SDN list (1,540 vessels, 342 aircraft) and carry name-like strings. Without a distinct type a search for a person can rank a ship. T.let(%i[individual organization vessel aircraft].freeze, T::Array[Symbol])
Instance Attribute Summary collapse
- #addresses ⇒ Array<Address> readonly
- #dates_of_birth ⇒ Array<PartialDate> readonly
-
#id ⇒ String
readonly
Namespaced, and never nil: #initialize derives one from the source and the publisher's own reference when the caller gives none.
- #identifiers ⇒ Array<Identifier> readonly
- #listed_on ⇒ PartialDate? readonly
- #names ⇒ Array<Name> readonly
- #nationalities ⇒ Array<String> readonly
- #programs ⇒ Array<String> readonly
- #remarks ⇒ String? readonly
- #source ⇒ Symbol readonly
- #source_ref ⇒ String? readonly
- #type ⇒ Symbol readonly
Class Method Summary collapse
-
.from_h(hash) ⇒ T.attached_class
Rebuilds an entity from #to_h output.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Compared through #to_h so nested members only have to serialize, not implement value equality themselves.
-
#dates_of_birth? ⇒ Boolean
True when the publisher gave no date at all, which is most of OFAC -- its dates are prose in Remarks and stay there until #19 reads them.
- #hash ⇒ Integer
-
#initialize(source:, type:, id: nil, source_ref: nil, names: [], addresses: [], identifiers: [], dates_of_birth: [], nationalities: [], programs: [], listed_on: nil, remarks: nil) ⇒ void
constructor
Each collection is nilable because nil is how "the publisher listed none" arrives from a store or a half-built hash; #list! turns it into the empty array the reader hands back.
- #inspect ⇒ String
-
#primary_name ⇒ Name?
The name an adapter marked
:primary, falling back to the first name for sources such as Canada that publish no alias kinds at all. - #to_h ⇒ Hash{Symbol => T.untyped}
Constructor Details
#initialize(source:, type:, id: nil, source_ref: nil, names: [], addresses: [], identifiers: [], dates_of_birth: [], nationalities: [], programs: [], listed_on: nil, remarks: nil) ⇒ void
Each collection is nilable because nil is how "the publisher listed none" arrives from a store or a half-built hash; #list! turns it into the empty array the reader hands back.
The four collection members and listed_on are declared, and the rest is
T.untyped on purpose. The difference is who wrote the value: the nested
members are canonical objects an adapter builds, and declaring them is
what makes srb tc refuse an adapter that hands over the string a
publisher wrote where a PartialDate belongs. The runtime check that comes
with the signature is shallow -- it sees the Array and not what is in it
-- so the adapter conformance group goes on asserting the element types
per fixture, which is what covers an adapter written outside this repo.
Everything else is the publisher's own text arriving as whatever the parser made of it, and the coercions below say what happens to it in messages written for whoever has to fix the record. A type error would say less.
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/active_sanction/entity.rb', line 181 def initialize(source:, type:, id: nil, source_ref: nil, names: [], addresses: [], identifiers: [], dates_of_birth: [], nationalities: [], programs: [], listed_on: nil, remarks: nil) @source = T.let(symbol!(:source, source), Symbol) @type = T.let(type!(type), Symbol) @source_ref = T.let(string_or_nil(source_ref), T.nilable(String)) @id = T.let(string_or_nil(id) || derived_id, String) @names = T.let(list!(:names, names), T::Array[Name]) @addresses = T.let(list!(:addresses, addresses), T::Array[Address]) @identifiers = T.let(list!(:identifiers, identifiers), T::Array[Identifier]) @dates_of_birth = T.let(list!(:dates_of_birth, dates_of_birth), T::Array[PartialDate]) @nationalities = T.let(strings!(:nationalities, nationalities), T::Array[String]) @programs = T.let(strings!(:programs, programs), T::Array[String]) @listed_on = T.let(listed_on, T.nilable(PartialDate)) # original free text, always retained verbatim @remarks = T.let(string_or_nil(remarks), T.nilable(String)) freeze end |
Instance Attribute Details
#addresses ⇒ Array<Address> (readonly)
93 94 95 |
# File 'lib/active_sanction/entity.rb', line 93 def addresses @addresses end |
#dates_of_birth ⇒ Array<PartialDate> (readonly)
99 100 101 |
# File 'lib/active_sanction/entity.rb', line 99 def dates_of_birth @dates_of_birth end |
#id ⇒ String (readonly)
Namespaced, and never nil: #initialize derives one from the source and the publisher's own reference when the caller gives none.
78 79 80 |
# File 'lib/active_sanction/entity.rb', line 78 def id @id end |
#identifiers ⇒ Array<Identifier> (readonly)
96 97 98 |
# File 'lib/active_sanction/entity.rb', line 96 def identifiers @identifiers end |
#listed_on ⇒ PartialDate? (readonly)
108 109 110 |
# File 'lib/active_sanction/entity.rb', line 108 def listed_on @listed_on end |
#names ⇒ Array<Name> (readonly)
90 91 92 |
# File 'lib/active_sanction/entity.rb', line 90 def names @names end |
#nationalities ⇒ Array<String> (readonly)
102 103 104 |
# File 'lib/active_sanction/entity.rb', line 102 def nationalities @nationalities end |
#programs ⇒ Array<String> (readonly)
105 106 107 |
# File 'lib/active_sanction/entity.rb', line 105 def programs @programs end |
#remarks ⇒ String? (readonly)
111 112 113 |
# File 'lib/active_sanction/entity.rb', line 111 def remarks @remarks end |
#source ⇒ Symbol (readonly)
81 82 83 |
# File 'lib/active_sanction/entity.rb', line 81 def source @source end |
#source_ref ⇒ String? (readonly)
84 85 86 |
# File 'lib/active_sanction/entity.rb', line 84 def source_ref @source_ref end |
#type ⇒ Symbol (readonly)
87 88 89 |
# File 'lib/active_sanction/entity.rb', line 87 def type @type end |
Class Method Details
.from_h(hash) ⇒ T.attached_class
Rebuilds an entity from #to_h output. Accepts string keys too, so a record that has been through JSON round-trips without a separate coercion step.
116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/active_sanction/entity.rb', line 116 def self.from_h(hash) attributes = hash.to_h.transform_keys(&:to_sym) unknown = attributes.keys - MEMBERS raise InvalidArgument, "unknown Entity attribute(s): #{unknown.join(", ")}" if unknown.any? # `new(**hash)` past required keyword parameters is one of the few # things Sorbet cannot check statically. The hash is validated on the two # lines above and by #initialize below, so what is lost here is only the # checker's ability to see it happen. T.unsafe(self).new(**coerce_members(attributes)) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Compared through #to_h so nested members only have to serialize, not implement value equality themselves. Class is part of the comparison to keep #== and #hash agreeing, which is what Hash and Set rely on.
233 234 235 236 237 |
# File 'lib/active_sanction/entity.rb', line 233 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#dates_of_birth? ⇒ Boolean
True when the publisher gave no date at all, which is most of OFAC -- its dates are prose in Remarks and stay there until #19 reads them.
202 |
# File 'lib/active_sanction/entity.rb', line 202 def dates_of_birth? = dates_of_birth.any? |
#hash ⇒ Integer
241 242 243 |
# File 'lib/active_sanction/entity.rb', line 241 def hash [self.class, to_h].hash end |
#inspect ⇒ String
246 247 248 |
# File 'lib/active_sanction/entity.rb', line 246 def inspect "#<#{self.class} id=#{id.inspect} type=#{type.inspect} names=#{names.size}>" end |
#primary_name ⇒ Name?
The name an adapter marked :primary, falling back to the first name for
sources such as Canada that publish no alias kinds at all.
207 208 209 |
# File 'lib/active_sanction/entity.rb', line 207 def primary_name names.find(&:primary?) || names.first end |
#to_h ⇒ Hash{Symbol => T.untyped}
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/active_sanction/entity.rb', line 212 def to_h { id: id, source: source, source_ref: source_ref, type: type, names: names.map(&:to_h), addresses: addresses.map(&:to_h), identifiers: identifiers.map(&:to_h), dates_of_birth: dates_of_birth.map(&:to_h), nationalities: nationalities, programs: programs, listed_on: listed_on&.to_h, remarks: remarks } end |