Class: ActiveSanction::Entity

Inherits:
Object
  • Object
show all
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 =

vessel and aircraft are 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

Class Method Summary collapse

Instance Method Summary collapse

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.

Parameters:

  • source (T.untyped)
  • type (T.untyped)
  • id (T.untyped) (defaults to: nil)
  • source_ref (T.untyped) (defaults to: nil)
  • names (Array<Name>, nil) (defaults to: [])
  • addresses (Array<Address>, nil) (defaults to: [])
  • identifiers (Array<Identifier>, nil) (defaults to: [])
  • dates_of_birth (Array<PartialDate>, nil) (defaults to: [])
  • nationalities (T.untyped) (defaults to: [])
  • programs (T.untyped) (defaults to: [])
  • listed_on (PartialDate, nil) (defaults to: nil)
  • remarks (T.untyped) (defaults to: nil)


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)

Returns:



93
94
95
# File 'lib/active_sanction/entity.rb', line 93

def addresses
  @addresses
end

#dates_of_birth ⇒ Array<PartialDate> (readonly)

Returns:



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.

Returns:

  • (String)


78
79
80
# File 'lib/active_sanction/entity.rb', line 78

def id
  @id
end

#identifiers ⇒ Array<Identifier> (readonly)

Returns:



96
97
98
# File 'lib/active_sanction/entity.rb', line 96

def identifiers
  @identifiers
end

#listed_on ⇒ PartialDate? (readonly)

Returns:



108
109
110
# File 'lib/active_sanction/entity.rb', line 108

def listed_on
  @listed_on
end

#names ⇒ Array<Name> (readonly)

Returns:



90
91
92
# File 'lib/active_sanction/entity.rb', line 90

def names
  @names
end

#nationalities ⇒ Array<String> (readonly)

Returns:

  • (Array<String>)


102
103
104
# File 'lib/active_sanction/entity.rb', line 102

def nationalities
  @nationalities
end

#programs ⇒ Array<String> (readonly)

Returns:

  • (Array<String>)


105
106
107
# File 'lib/active_sanction/entity.rb', line 105

def programs
  @programs
end

#remarks ⇒ String? (readonly)

Returns:

  • (String, nil)


111
112
113
# File 'lib/active_sanction/entity.rb', line 111

def remarks
  @remarks
end

#source ⇒ Symbol (readonly)

Returns:

  • (Symbol)


81
82
83
# File 'lib/active_sanction/entity.rb', line 81

def source
  @source
end

#source_ref ⇒ String? (readonly)

Returns:

  • (String, nil)


84
85
86
# File 'lib/active_sanction/entity.rb', line 84

def source_ref
  @source_ref
end

#type ⇒ Symbol (readonly)

Returns:

  • (Symbol)


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.

Parameters:

  • hash (T.untyped)

Returns:

  • (T.attached_class)

Raises:



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.

Parameters:

  • other (T.untyped)

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


202
# File 'lib/active_sanction/entity.rb', line 202

def dates_of_birth? = dates_of_birth.any?

#hash ⇒ Integer

Returns:

  • (Integer)


241
242
243
# File 'lib/active_sanction/entity.rb', line 241

def hash
  [self.class, to_h].hash
end

#inspect ⇒ String

Returns:

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

Returns:



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}

Returns:

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