# frozen_string_literal: true
#
# ronin-db-activerecord - ActiveRecord backend for the Ronin Database.
#
# Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-db-activerecord is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-db-activerecord is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-db-activerecord. If not, see .
#
require 'ronin/db/model'
require 'ronin/db/model/importable'
require 'active_record'
module Ronin
module DB
#
# Represents a person.
#
# @since 0.2.0
#
class Person < ActiveRecord::Base
include Model
include Model::Importable
# @!attribute [rw] id
# The primary key of the person.
#
# @return [Integer]
attribute :id, :integer
# @!attribute [rw] full_name
# The person's full name.
#
# @return [String]
attribute :full_name, :string
validates :full_name, presence: true,
uniqueness: true,
format: {
with: /\A(?:(?:Mr|Mrs|Ms|Miss|Dr|Sir|Madam|Master|Fr|Rev|Atty)\.?\s)?(?:\p{L}+(['-]\p{L}+)?)(?:(?:\s(?:(?:(?:\p{Lu})\.?)|(?:\p{Lu}\p{L}+(?:['-]\p{L}+)?)))?\s(?:\p{L}+(?:['-]\p{L}+)?)(?:,?\s(?:Jr|Sr|II|III|IV|V|Esq|CPA|Dc|Dds|Vm|Jd|Md|Phd)\.?)?)?\z/,
message: 'must be a valid personal name'
}
# @!attribute [rw] prefix
# The person's prefix.
#
# @return [String, nil]
attribute :prefix, :string
validates :prefix, inclusion: %w[
Mr Mrs Ms Miss Dr Sir Madam Master Fr Rev Atty
],
allow_nil: true
# @!attribute [rw] first_name
# The person's first name.
#
# @return [String]
attribute :first_name, :string
validates :first_name, presence: true,
format: {
with: /\A\p{L}+(?:['-]\p{L}+)?\z/,
message: 'must be a valid first name'
}
# @!attribute [rw] middle_name
# The person's middle name.
#
# @return [String, nil]
attribute :middle_name, :string
validates :middle_name, format: {
with: /\A\p{L}+(?:['-]\p{L}+)?\z/,
message: 'must be a valid middle name'
},
allow_nil: true
# @!attribute [rw] middle_initial
# The person's middle initial.
#
# @return [String, nil]
attribute :middle_initial, :string
validates :middle_initial, presence: true,
format: {
with: /\A\p{Lu}\z/,
message: 'must be a valid middle initial'
},
allow_nil: true
# @!attribute [rw] last_name
# The person's last name.
#
# @return [String, nil]
attribute :last_name, :string
validates :last_name, format: {
with: /\A\p{L}+(?:['-]\p{L}+)?\z/,
message: 'must be a valid last name'
},
allow_nil: true
# @!attribute [rw] suffix
# The person's suffix.
#
# @return [String, nil]
attribute :suffix, :string
validates :suffix, inclusion: %w[
Jr Sr II III IV V Esq CPA Dc Dds Vm Jd Md Phd
],
allow_nil: true
# @!attribute [rw] created_at
# Tracks when the person was first created.
#
# @return [Time]
attribute :created_at, :datetime
# @!attribute [rw] personal_street_addresses
# The association of street addresses associated with the person.
#
# @return [Array]
has_many :personal_street_addresses, dependent: :destroy
# @!attribute [rw] street_addresses
# The street addresses associated with the person.
#
# @return [Array]
has_many :street_addresses, through: :personal_street_addresses
# @!attribute [rw] personal_phone_numbers
# The perons's phone numbers.
#
# @return [Array]
has_many :personal_phone_numbers, dependent: :destroy
# @!attribute [rw] phone_numbers
# The phone numbers associated with the person.
#
# @return [Array]
has_many :phone_numbers, through: :personal_phone_numbers
# @!attribute [rw] personal_email_addresss
# The perons's email addresses.
#
# @return [Array]
has_many :personal_email_addresses, dependent: :destroy
# @!attribute [rw] email_addresses
# The email addresses associated with the person.
#
# @return [Array]
has_many :email_addresses, through: :personal_email_addresses
# @!attribute [rw] personal_connections
# The perons's connections with other people.
#
# @return [Array]
has_many :personal_connections, dependent: :destroy
# @!attribute [rw] connected_people
# The other people connected to the person.
#
# @return [Array]
has_many :connected_people, through: :personal_connections,
source: :other_person
# @!attribute [rw] organization_customers
# The perons's customer relationships with other organizations.
#
# @return [Array]
has_many :organization_customers, class_name: 'OrganizationCustomer',
foreign_key: :customer_id,
dependent: :destroy
# @!attribute [rw] vendors
# The vendor organizations of the person.
#
# @return [Array]
has_many :vendors, through: :organization_customers
# @!attribute [rw] notes
# The associated notes.
#
# @return [Array]
has_many :notes, dependent: :destroy
#
# Queries all people associated with the street address.
#
# @param [String] address
# The street address to search for.
#
# @return [Array]
# The people associated with the street address.
#
# @api public
#
def self.for_address(address)
joins(:street_addresses).where(street_addresses: {address: address})
end
#
# Queries all people associated with the city.
#
# @param [String] city
# The city to search for.
#
# @return [Array]
# The people associated with the city.
#
# @api public
#
def self.for_city(city)
joins(:street_addresses).where(street_addresses: {city: city})
end
#
# Queries all people associated with the state.
#
# @param [String] state
# The state to search for.
#
# @return [Array]
# The people associated with the state.
#
# @api public
#
def self.for_state(state)
joins(:street_addresses).where(street_addresses: {state: state})
end
#
# Queries all people associated with the province.
#
# @param [String] province
# The province to search for.
#
# @return [Array]
# The people associated with the province.
#
# @see for_state
#
# @api public
#
def self.for_province(province)
for_state(province)
end
#
# Queries all people associated with the zipcode.
#
# @param [String] zipcode
# The zipcode to search for.
#
# @return [Array]
# The people associated with the zipcode.
#
# @api public
#
def self.for_zipcode(zipcode)
joins(:street_addresses).where(street_addresses: {zipcode: zipcode})
end
#
# Queries all people associated with the country.
#
# @param [String] country
# The country to search for.
#
# @return [Array]
# The people associated with the country.
#
# @api public
#
def self.for_country(country)
joins(:street_addresses).where(street_addresses: {country: country})
end
#
# Queries all people with the given prefix.
#
# @param [String] prefix
# The name prefix to search for.
#
# @return [Array]
# The people with the matching {#prefix}.
#
# @api public
#
def self.with_prefix(prefix)
where(prefix: prefix)
end
#
# Queries all people with the matching first name.
#
# @param [String] first_name
# The first name to search for.
#
# @return [Array]
# The people with the matching {#first_name}.
#
# @api public
#
def self.with_first_name(first_name)
where(first_name: first_name)
end
#
# Queries all people with the matching middle name.
#
# @param [String] middle_name
# The middle name to search for.
#
# @return [Array]
# The people with the matching {#middle_name}.
#
# @api public
#
def self.with_middle_name(middle_name)
where(middle_name: middle_name)
end
#
# Queries all people with the matching middle initial.
#
# @param [String] initial
# The middle initial to search for.
#
# @return [Array]
# The people with the matching {#middle_initial}.
#
# @api public
#
def self.with_middle_initial(initial)
where(middle_initial: initial)
end
#
# Queries all people with the matching last name.
#
# @param [String] last_name
# The last name to search for.
#
# @return [Array]
# The people with the matching {#last_name}.
#
# @api public
#
def self.with_last_name(last_name)
where(last_name: last_name)
end
#
# Queries all people with the given suffix.
#
# @param [String] suffix
# The name suffix to search for.
#
# @return [Array]
# The people with the matching {#suffix}.
#
# @api public
#
def self.with_suffix(suffix)
where(suffix: suffix)
end
#
# Looks up a person by name.
#
# @param [String] name
# The person's full name to query.
#
# @return [Person, nil]
# The found person.
#
# @api public
#
def self.lookup(name)
find_by(full_name: name)
end
#
# Parses a person's full name into attributes.
#
# @param [String] name
# The person's full name to parse.
#
# @return [Hash{Symbol => String}]
# The parsed attributes.
#
# @api private
#
def self.parse(name)
if (match = name.match(/\A(?:(?Mr|Mrs|Ms|Miss|Dr|Sir|Madam|Master|Fr|Rev|Atty)\.?\s)?(?\p{L}+(?:['-]\p{L}+)?)(?:(?:\s(?:(?:(?\p{Lu})\.?)|(?(?\p{Lu})\p{L}+(?:['-]\p{L}+)?)))?\s(?!Jr|Sr|II|III|IV|V|Esq|CPA|Dc|Dds|Vm|Jd|Md|Phd)(?\p{L}+(?:['-]\p{L}+)?)(?:,?\s(?Jr|Sr|II|III|IV|V|Esq|CPA|Dc|Dds|Vm|Jd|Md|Phd)\.?)?)?\z/))
{
full_name: name,
prefix: match[:prefix],
first_name: match[:first_name],
middle_name: match[:middle_name],
middle_initial: match[:middle_initial],
last_name: match[:last_name],
suffix: match[:suffix]
}
else
raise(ArgumentError,"invalid personal name: #{name.inspect}")
end
end
#
# Imports a person by name.
#
# @param [String] name
# The person's full name.
#
# @return [Person]
# The imported person.
#
# @api public
#
def self.import(name)
create(parse(name))
end
#
# Converts the person into a String.
#
# @return [String]
# The person's full name.
#
def to_s
full_name
end
end
end
end
require 'ronin/db/personal_street_address'
require 'ronin/db/personal_phone_number'
require 'ronin/db/personal_connection'
require 'ronin/db/organization_customer'
require 'ronin/db/organization'
require 'ronin/db/note'