Class: ActiveSanction::Address

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

Overview

A place a sanctions list attached to an entity. OFAC ships 25,078 of them in ADD.CSV; the UN ships far fewer and far thinner ones.

ActiveSanction::Address.new(
street:         "Ave. Luis Maria Drago 1136",
city:           "Buenos Aires",
state_province: nil,
postal_code:    "C1414",
country:        "Argentina",
note:           "as of early 2016"
)

Every field is optional because the publishers populate wildly different subsets: the UN routinely supplies only COUNTRY plus a free-text NOTE, and an address type that insisted on a street would drop those rows entirely. What it will not accept is an address that says nothing at all -- a record with every field blank is a parsing accident, not a location.

A pure data holder: it stores what the publisher said and nothing more. No geocoding, no country-code lookup, no case folding. Instances are frozen on construction and compare by value.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(street: nil, city: nil, state_province: nil, postal_code: nil, country: nil, note: nil) ⇒ void

Untyped on purpose, and the same choice Entity makes: all six are the publisher's own text arriving as whatever the parser made of it, and #string_or_nil says below what happens to it.

Parameters:

  • street (T.untyped) (defaults to: nil)
  • city (T.untyped) (defaults to: nil)
  • state_province (T.untyped) (defaults to: nil)
  • postal_code (T.untyped) (defaults to: nil)
  • country (T.untyped) (defaults to: nil)
  • note (T.untyped) (defaults to: nil)


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

def initialize(street: nil, city: nil, state_province: nil, postal_code: nil, country: nil, note: nil)
  @street = T.let(string_or_nil(street), T.nilable(String))
  @city = T.let(string_or_nil(city), T.nilable(String))
  @state_province = T.let(string_or_nil(state_province), T.nilable(String))
  @postal_code = T.let(string_or_nil(postal_code), T.nilable(String))
  @country = T.let(string_or_nil(country), T.nilable(String))
  @note = T.let(string_or_nil(note), T.nilable(String))
  reject_empty!
  freeze
end

Instance Attribute Details

#city ⇒ String? (readonly)

Returns:

  • (String, nil)


50
51
52
# File 'lib/active_sanction/address.rb', line 50

def city
  @city
end

#country ⇒ String? (readonly)

Returns:

  • (String, nil)


59
60
61
# File 'lib/active_sanction/address.rb', line 59

def country
  @country
end

#note ⇒ String? (readonly)

Returns:

  • (String, nil)


62
63
64
# File 'lib/active_sanction/address.rb', line 62

def note
  @note
end

#postal_code ⇒ String? (readonly)

Returns:

  • (String, nil)


56
57
58
# File 'lib/active_sanction/address.rb', line 56

def postal_code
  @postal_code
end

#state_province ⇒ String? (readonly)

Returns:

  • (String, nil)


53
54
55
# File 'lib/active_sanction/address.rb', line 53

def state_province
  @state_province
end

#street ⇒ String? (readonly)

Every field is nilable because the publishers populate wildly different subsets of them; what #initialize refuses is all six being empty at once.

Returns:

  • (String, nil)


47
48
49
# File 'lib/active_sanction/address.rb', line 47

def street
  @street
end

Class Method Details

.from_h(hash) ⇒ T.attached_class

Rebuilds an address from #to_h output. Accepts string keys, so a record that has been through JSON round-trips without a separate coercion step.

Parameters:

  • hash (T.untyped)

Returns:

  • (T.attached_class)

Raises:



67
68
69
70
71
72
73
# File 'lib/active_sanction/address.rb', line 67

def self.from_h(hash)
  attributes = hash.to_h.transform_keys(&:to_sym)
  unknown = attributes.keys - MEMBERS
  raise InvalidArgument, "unknown Address attribute(s): #{unknown.join(", ")}" if unknown.any?

  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)


131
132
133
134
135
# File 'lib/active_sanction/address.rb', line 131

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

  to_h == other.to_h
end

#hash ⇒ Integer

Returns:

  • (Integer)


139
140
141
# File 'lib/active_sanction/address.rb', line 139

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

#inspect ⇒ String

Returns:

  • (String)


144
145
146
# File 'lib/active_sanction/address.rb', line 144

def inspect
  "#<#{self.class} #{to_s.inspect}>"
end

#note_only? ⇒ Boolean

True when nothing but a note survived parsing -- the UN's "as of early 2016" with no place attached. Such an address is worth keeping (it is evidence the publisher had something) but is not worth matching on.

Returns:

  • (Boolean)


103
# File 'lib/active_sanction/address.rb', line 103

def note_only? = parts.empty?

#parts ⇒ Array<String>

The address parts the publisher actually filled in, in canonical order.

Returns:

  • (Array<String>)


95
96
97
# File 'lib/active_sanction/address.rb', line 95

def parts
  PARTS.filter_map { |member| public_send(member) }
end

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

Returns:

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


106
107
108
109
110
111
112
113
114
115
# File 'lib/active_sanction/address.rb', line 106

def to_h
  {
    street: street,
    city: city,
    state_province: state_province,
    postal_code: postal_code,
    country: country,
    note: note
  }
end

#to_s ⇒ String

A single line, which is how an address is displayed in a hit list and how the normalizer (#26) will want it before folding.

Returns:

  • (String)


120
121
122
123
124
125
126
# File 'lib/active_sanction/address.rb', line 120

def to_s
  line = parts.join(", ")
  note = self.note
  return line if note.nil?

  line.empty? ? note : "#{line} (#{note})"
end