Class: ActiveSanction::Name

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/active_sanction/name.rb

Overview

A single name variant attached to an entity. Entities routinely carry more aliases than primary names -- OFAC ships 19,321 primary names against 20,147 aliases -- so an alias is the common case, not the exception.

ActiveSanction::Name.new(
value:   "AERO-CARIBBEAN",
kind:    :aka,
quality: :good,
script:  :latin
)

A pure data holder: it stores what the publisher said and nothing more. The normalized and phonetic forms a search actually compares against are built by the normalizer (#26) and the indexer (#31), which need the untouched original to work from.

Instances are frozen on construction and compare by value.

Constant Summary collapse

KINDS =

OFAC's ALT.CSV supplies alt_type as aka / fka / nka directly, and the distinction matters downstream: a former name (fka) is still a real hit, but ranking it identically to a currently-used one costs precision.

T.let(%i[primary aka fka nka].freeze, T::Array[Symbol])
QUALITIES =

The UN consolidated list grades each alias Good or Low. A Low alias is a weaker signal -- the scorer (#32) penalizes it -- so the grade has to survive parsing rather than being flattened away here. Every other source publishes no grade at all, which is nil: unstated, not good.

T.let(%i[good low].freeze, T::Array[Symbol])
SCRIPTS =

The writing system value is published in, which is what tells the normalizer (#26) which transliteration path to take -- a Cyrillic name folded by the Latin rules comes out as noise. Adapters map their source's own vocabulary onto these: OFAC labels some names by language rather than script, so "Farsi" arrives here as :arabic.

Closed, so a typo is caught at the boundary instead of quietly minting a script nothing downstream handles. It is sized to what the lists actually publish rather than to all ~200 of ISO 15924; a source shipping one we have not seen is a one-line addition here, which the raised message asks for by name.

T.let(%i[
  latin cyrillic arabic hebrew greek han kana hangul
  thai devanagari bengali tamil myanmar khmer armenian georgian ethiopic syriac
].freeze, T::Array[Symbol])

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value:, kind: :primary, quality: nil, script: nil) ⇒ void

kind defaults to :primary because that is what a source with no alias data at all means: Canada publishes no aliases, so every Canadian name is a primary one.

Untyped on purpose, and the same choice Entity makes: every one of these is the publisher's text arriving as whatever the parser made of it. The coercions below say what happens to it, in messages written for whoever has to fix the record.

Parameters:

  • value (T.untyped)
  • kind (T.untyped) (defaults to: :primary)
  • quality (T.untyped) (defaults to: nil)
  • script (T.untyped) (defaults to: nil)


105
106
107
108
109
110
111
# File 'lib/active_sanction/name.rb', line 105

def initialize(value:, kind: :primary, quality: nil, script: nil)
  @value = T.let(value!(value), String)
  @kind = T.let(enum!(:kind, kind), Symbol)
  @quality = T.let(quality.nil? ? nil : enum!(:quality, quality), T.nilable(Symbol))
  @script = T.let(script.nil? ? nil : enum!(:script, script), T.nilable(Symbol))
  freeze
end

Instance Attribute Details

#kind ⇒ Symbol (readonly)

Returns:

  • (Symbol)


69
70
71
# File 'lib/active_sanction/name.rb', line 69

def kind
  @kind
end

#quality ⇒ Symbol? (readonly)

nil where the source publishes no grade, which is every source but the UN. See #low_quality?: unstated is not low.

Returns:

  • (Symbol, nil)


74
75
76
# File 'lib/active_sanction/name.rb', line 74

def quality
  @quality
end

#script ⇒ Symbol? (readonly)

Returns:

  • (Symbol, nil)


77
78
79
# File 'lib/active_sanction/name.rb', line 77

def script
  @script
end

#value ⇒ String (readonly)

The publisher's own string, stripped of surrounding whitespace and otherwise untouched.

Returns:

  • (String)


66
67
68
# File 'lib/active_sanction/name.rb', line 66

def value
  @value
end

Class Method Details

.from_h(hash) ⇒ T.attached_class

Rebuilds a name from #to_h output. Accepts string keys and string values for the enum members, so a name that has been through JSON round-trips without a separate coercion step.

Parameters:

  • hash (T.untyped)

Returns:

  • (T.attached_class)

Raises:



83
84
85
86
87
88
89
90
91
92
# File 'lib/active_sanction/name.rb', line 83

def self.from_h(hash)
  attributes = hash.to_h.transform_keys(&:to_sym)
  unknown = attributes.keys - MEMBERS
  raise InvalidArgument, "unknown Name 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

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

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)


139
140
141
142
143
# File 'lib/active_sanction/name.rb', line 139

def ==(other)
  return false unless other.instance_of?(self.class)

  to_h == other.to_h
end

#alias? ⇒ Boolean

Every kind except :primary. Reads better at call sites than !primary? and keeps the definition of "alias" in one place if a kind is ever added.

Returns:

  • (Boolean)


119
# File 'lib/active_sanction/name.rb', line 119

def alias? = !primary?

#hash ⇒ Integer

Returns:

  • (Integer)


147
148
149
# File 'lib/active_sanction/name.rb', line 147

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

#inspect ⇒ String

Returns:

  • (String)


152
153
154
# File 'lib/active_sanction/name.rb', line 152

def inspect
  "#<#{self.class} #{value.inspect} kind=#{kind.inspect}#{" quality=#{quality.inspect}" if quality}>"
end

#low_quality? ⇒ Boolean

nil quality is not low quality: only the UN grades aliases, so an ungraded name must not be penalized for a field its source never publishes. Compared by identity because symbols are interned and quality is nilable: nil == :low is a call on NilClass, which Sorbet will not make.

Returns:

  • (Boolean)


126
# File 'lib/active_sanction/name.rb', line 126

def low_quality? = quality.equal?(:low)

#primary? ⇒ Boolean

Returns:

  • (Boolean)


114
# File 'lib/active_sanction/name.rb', line 114

def primary? = kind == :primary

#to_h ⇒ Hash{Symbol => T.untyped}

Returns:

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


129
130
131
# File 'lib/active_sanction/name.rb', line 129

def to_h
  { value: value, kind: kind, quality: quality, script: script }
end

#to_s ⇒ String

Returns:

  • (String)


134
# File 'lib/active_sanction/name.rb', line 134

def to_s = value