Packages
rbt_weave
0.2.65
0.2.67
0.2.66
0.2.65
0.2.64
0.2.63
0.2.62
0.2.61
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.52
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.34
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Graph conflict detection and image processing NIFs for OSINT knowledge graphs, powered by Rust.
Current section
Files
Jump to
Current section
Files
crates/weave-content/src/domain.rs
//! Domain types for the OSINT case graph.
//!
//! These types represent the target data model per ADR-014. They are
//! pure value objects with no infrastructure dependencies.
use std::fmt;
use serde::Serialize;
// ---------------------------------------------------------------------------
// Entity labels
// ---------------------------------------------------------------------------
/// Graph node label — determines which fields are valid on an entity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityLabel {
Person,
Organization,
Event,
Document,
Asset,
Case,
}
impl fmt::Display for EntityLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Person => write!(f, "person"),
Self::Organization => write!(f, "organization"),
Self::Event => write!(f, "event"),
Self::Document => write!(f, "document"),
Self::Asset => write!(f, "asset"),
Self::Case => write!(f, "case"),
}
}
}
// ---------------------------------------------------------------------------
// Person enums
// ---------------------------------------------------------------------------
/// Role a person holds (multiple allowed per person).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Role {
Politician,
Executive,
CivilServant,
Military,
Judiciary,
LawEnforcement,
Journalist,
Academic,
Activist,
Athlete,
Lawyer,
Lobbyist,
Banker,
Accountant,
Consultant,
Militant,
Criminal,
ReligiousLeader,
RebelLeader,
/// Free-form value not in the predefined list.
Custom(String),
}
/// Maximum length of a custom enum value.
const MAX_CUSTOM_LEN: usize = 100;
impl Role {
/// All known non-custom values as `&str`.
pub const KNOWN: &[&str] = &[
"politician",
"executive",
"civil_servant",
"military",
"judiciary",
"law_enforcement",
"journalist",
"academic",
"activist",
"athlete",
"lawyer",
"lobbyist",
"banker",
"accountant",
"consultant",
"militant",
"criminal",
"religious_leader",
"rebel_leader",
];
}
/// Status of a person.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PersonStatus {
Active,
Deceased,
Imprisoned,
Fugitive,
Acquitted,
Pardoned,
Convicted,
}
impl PersonStatus {
pub const KNOWN: &[&str] = &[
"active",
"deceased",
"imprisoned",
"fugitive",
"acquitted",
"pardoned",
"convicted",
];
}
// ---------------------------------------------------------------------------
// Organization enums
// ---------------------------------------------------------------------------
/// Type of organization (`org_type` field).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrgType {
GovernmentMinistry,
GovernmentAgency,
LocalGovernment,
Legislature,
Court,
LawEnforcement,
Prosecutor,
Regulator,
PoliticalParty,
StateEnterprise,
Corporation,
Bank,
Ngo,
Media,
University,
SportsClub,
SportsBody,
TradeUnion,
LobbyGroup,
Military,
ReligiousBody,
MilitantGroup,
Custom(String),
}
impl OrgType {
pub const KNOWN: &[&str] = &[
"government_ministry",
"government_agency",
"local_government",
"legislature",
"court",
"law_enforcement",
"prosecutor",
"regulator",
"political_party",
"state_enterprise",
"corporation",
"bank",
"ngo",
"media",
"university",
"sports_club",
"sports_body",
"trade_union",
"lobby_group",
"military",
"religious_body",
"militant_group",
];
}
/// Status of an organization.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrgStatus {
Active,
Dissolved,
Suspended,
Merged,
Defunct,
}
impl OrgStatus {
pub const KNOWN: &[&str] = &["active", "dissolved", "suspended", "merged", "defunct"];
}
// ---------------------------------------------------------------------------
// Event enums
// ---------------------------------------------------------------------------
/// Type of event.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
Arrest,
Indictment,
Trial,
Conviction,
Acquittal,
Sentencing,
Appeal,
Pardon,
Parole,
Bribery,
Embezzlement,
Fraud,
Extortion,
MoneyLaundering,
Murder,
Assault,
Dismissal,
Resignation,
Appointment,
Election,
InvestigationOpened,
InvestigationClosed,
Raid,
Seizure,
Warrant,
FugitiveFlight,
FugitiveCapture,
PolicyChange,
ContractAward,
FinancialDefault,
Bailout,
WhistleblowerReport,
Bombing,
SuicideBombing,
Massacre,
Execution,
Death,
Release,
Report,
Discovery,
Disaster,
Deportation,
Shooting,
Kidnapping,
PrisonEscape,
Petition,
Surrender,
Confession,
Hijacking,
Assassination,
Protest,
Sanctions,
Custom(String),
}
impl EventType {
pub const KNOWN: &[&str] = &[
"arrest",
"indictment",
"trial",
"conviction",
"acquittal",
"sentencing",
"appeal",
"pardon",
"parole",
"bribery",
"embezzlement",
"fraud",
"extortion",
"money_laundering",
"murder",
"assault",
"dismissal",
"resignation",
"appointment",
"election",
"investigation_opened",
"investigation_closed",
"raid",
"seizure",
"warrant",
"fugitive_flight",
"fugitive_capture",
"policy_change",
"contract_award",
"financial_default",
"bailout",
"whistleblower_report",
"bombing",
"suicide_bombing",
"massacre",
"execution",
"death",
"release",
"report",
"discovery",
"disaster",
"deportation",
"shooting",
"kidnapping",
"prison_escape",
"petition",
"surrender",
"confession",
"hijacking",
"assassination",
"protest",
"sanctions",
];
}
/// Event severity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Minor,
Significant,
Major,
Critical,
}
impl Severity {
pub const KNOWN: &[&str] = &["minor", "significant", "major", "critical"];
}
// ---------------------------------------------------------------------------
// Document enums
// ---------------------------------------------------------------------------
/// Type of document.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DocType {
CourtRuling,
Indictment,
ChargeSheet,
Warrant,
Contract,
Permit,
AuditReport,
FinancialDisclosure,
Legislation,
Regulation,
PressRelease,
InvestigationReport,
SanctionsNotice,
Custom(String),
}
impl DocType {
pub const KNOWN: &[&str] = &[
"court_ruling",
"indictment",
"charge_sheet",
"warrant",
"contract",
"permit",
"audit_report",
"financial_disclosure",
"legislation",
"regulation",
"press_release",
"investigation_report",
"sanctions_notice",
];
}
// ---------------------------------------------------------------------------
// Asset enums
// ---------------------------------------------------------------------------
/// Type of asset.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssetType {
Cash,
BankAccount,
RealEstate,
Vehicle,
Equity,
ContractValue,
Grant,
BudgetAllocation,
SeizedAsset,
Custom(String),
}
impl AssetType {
pub const KNOWN: &[&str] = &[
"cash",
"bank_account",
"real_estate",
"vehicle",
"equity",
"contract_value",
"grant",
"budget_allocation",
"seized_asset",
];
}
/// Status of an asset.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssetStatus {
Active,
Frozen,
Seized,
Forfeited,
Returned,
}
impl AssetStatus {
pub const KNOWN: &[&str] = &["active", "frozen", "seized", "forfeited", "returned"];
}
// ---------------------------------------------------------------------------
// Case enums
// ---------------------------------------------------------------------------
/// Type of case.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CaseType {
Corruption,
Fraud,
Bribery,
Embezzlement,
Murder,
Assault,
CivilRights,
Regulatory,
Political,
Terrorism,
Negligence,
DrugTrafficking,
SexualAssault,
HumanTrafficking,
EnvironmentalCrime,
HumanRightsViolation,
OrganizedCrime,
DrugManufacturing,
Custom(String),
}
impl CaseType {
pub const KNOWN: &[&str] = &[
"corruption",
"fraud",
"bribery",
"embezzlement",
"murder",
"assault",
"civil_rights",
"regulatory",
"political",
"terrorism",
"negligence",
"drug_trafficking",
"sexual_assault",
"human_trafficking",
"environmental_crime",
"human_rights_violation",
"organized_crime",
"drug_manufacturing",
];
}
/// Status of a case.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CaseStatus {
Open,
UnderInvestigation,
Trial,
Convicted,
Acquitted,
Closed,
Appeal,
Pardoned,
}
impl CaseStatus {
pub const KNOWN: &[&str] = &[
"open",
"under_investigation",
"trial",
"convicted",
"acquitted",
"closed",
"appeal",
"pardoned",
];
}
// ---------------------------------------------------------------------------
// Structured value types
// ---------------------------------------------------------------------------
/// Monetary amount with currency and human-readable display.
///
/// `amount` is in the smallest currency unit (e.g. cents for USD, sen for IDR).
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Money {
pub amount: i64,
pub currency: String,
pub display: String,
}
/// Maximum length of the `currency` field (ISO 4217 = 3 chars).
pub const MAX_CURRENCY_LEN: usize = 3;
/// Maximum length of the `display` field.
pub const MAX_MONEY_DISPLAY_LEN: usize = 100;
/// Geographic jurisdiction: ISO 3166-1 country code with optional subdivision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Jurisdiction {
/// ISO 3166-1 alpha-2 country code (e.g. `ID`, `GB`).
pub country: String,
/// Optional subdivision name (e.g. `South Sulawesi`).
#[serde(skip_serializing_if = "Option::is_none")]
pub subdivision: Option<String>,
}
/// Maximum length of the `country` field (ISO 3166-1 alpha-2 = 2 chars).
pub const MAX_COUNTRY_LEN: usize = 13;
/// Maximum length of the `subdivision` field.
pub const MAX_SUBDIVISION_LEN: usize = 200;
/// A source of information (news article, official document, etc.).
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Source {
/// HTTPS URL of the source.
pub url: String,
/// Extracted domain (e.g. `kompas.com`).
pub domain: String,
/// Article or document title.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Publication date (ISO 8601 date string).
#[serde(skip_serializing_if = "Option::is_none")]
pub published_at: Option<String>,
/// Wayback Machine or other archive URL.
#[serde(skip_serializing_if = "Option::is_none")]
pub archived_url: Option<String>,
/// ISO 639-1 language code (e.g. `id`, `en`).
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
}
/// Maximum length of a source URL.
pub const MAX_SOURCE_URL_LEN: usize = 2048;
/// Maximum length of a source domain.
pub const MAX_SOURCE_DOMAIN_LEN: usize = 253;
/// Maximum length of a source title.
pub const MAX_SOURCE_TITLE_LEN: usize = 300;
/// Maximum length of a source language code (ISO 639-1 = 2 chars).
pub const MAX_SOURCE_LANGUAGE_LEN: usize = 2;
// ---------------------------------------------------------------------------
// Amount entries (structured financial amounts)
// ---------------------------------------------------------------------------
/// A single financial amount entry with currency, optional label, and
/// approximation flag.
///
/// Matches the Elixir `Loom.Graph.Amount` struct. Stored as a JSON array
/// string in Neo4j (`n.amounts` / `r.amounts`).
///
/// ## DSL Syntax
///
/// `[~]<value> <currency> [label]` — pipe-separated for multiple entries.
///
/// ```text
/// 660000 USD bribe
/// ~16800000000000 IDR state_loss
/// 660000 USD bribe | 250000000 IDR fine
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AmountEntry {
pub value: i64,
pub currency: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub approximate: bool,
}
/// Maximum number of amount entries per field.
pub const MAX_AMOUNT_ENTRIES: usize = 10;
/// Maximum length of an amount label.
const MAX_AMOUNT_LABEL_LEN: usize = 50;
/// Known amount label values (matches Elixir `EnumField` `:amount_label`).
pub const AMOUNT_LABEL_KNOWN: &[&str] = &[
"bribe",
"fine",
"restitution",
"state_loss",
"kickback",
"embezzlement",
"fraud",
"gratification",
"bailout",
"procurement",
"penalty",
"fee",
"donation",
"loan",
];
impl AmountEntry {
/// Parse a pipe-separated DSL string into a list of `AmountEntry`.
///
/// Returns an empty vec for `None` or blank input.
pub fn parse_dsl(input: &str) -> Result<Vec<Self>, String> {
let input = input.trim();
if input.is_empty() {
return Ok(Vec::new());
}
let entries: Vec<&str> = input
.split('|')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if entries.len() > MAX_AMOUNT_ENTRIES {
return Err(format!(
"too many amount entries ({}, max {MAX_AMOUNT_ENTRIES})",
entries.len()
));
}
entries.iter().map(|e| Self::parse_one(e)).collect()
}
fn parse_one(entry: &str) -> Result<Self, String> {
let (approximate, rest) = if let Some(r) = entry.strip_prefix('~') {
(true, r.trim_start())
} else {
(false, entry)
};
let parts: Vec<&str> = rest.splitn(3, char::is_whitespace).collect();
match parts.len() {
2 | 3 => {
let value = parts[0]
.parse::<i64>()
.map_err(|_| format!("invalid amount value: {:?}", parts[0]))?;
let currency = Self::validate_currency(parts[1])?;
let label = if parts.len() == 3 {
Some(Self::validate_label(parts[2])?)
} else {
None
};
Ok(Self {
value,
currency,
label,
approximate,
})
}
_ => Err(format!("invalid amount format: {entry:?}")),
}
}
fn validate_currency(s: &str) -> Result<String, String> {
let upper = s.to_uppercase();
if upper.len() > MAX_CURRENCY_LEN
|| upper.is_empty()
|| !upper.chars().all(|c| c.is_ascii_uppercase())
{
return Err(format!("invalid currency: {s:?}"));
}
Ok(upper)
}
fn validate_label(s: &str) -> Result<String, String> {
if s.len() > MAX_AMOUNT_LABEL_LEN {
return Err(format!("amount label too long: {s:?}"));
}
if AMOUNT_LABEL_KNOWN.contains(&s) || parse_custom(s).is_some() {
Ok(s.to_string())
} else {
Err(format!(
"unknown amount label: {s:?} (use custom:Value for custom)"
))
}
}
/// Format for human display (matches Elixir `Amount.format/1`).
pub fn format_display(&self) -> String {
let prefix = if self.approximate { "~" } else { "" };
let formatted_value = format_human_number(self.value);
let label_suffix = match &self.label {
None => String::new(),
Some(l) => {
let display = l.strip_prefix("custom:").unwrap_or(l);
format!(" ({})", display.replace('_', " "))
}
};
format!("{prefix}{} {formatted_value}{label_suffix}", self.currency)
}
/// Format a slice of entries for display, separated by "; ".
pub fn format_list(entries: &[Self]) -> String {
entries
.iter()
.map(Self::format_display)
.collect::<Vec<_>>()
.join("; ")
}
}
impl fmt::Display for AmountEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_display())
}
}
/// Format a number for human display.
///
/// Numbers >= 1 million use word suffixes (million, billion, trillion).
/// Numbers < 1 million use dot-separated thousands (e.g. 660.000).
///
/// Examples:
/// - `660_000` → `"660.000"`
/// - `1_500_000` → `"1.5 million"`
/// - `250_000_000` → `"250 million"`
/// - `4_580_000_000_000` → `"4.58 trillion"`
/// - `144_000_000_000_000` → `"144 trillion"`
fn format_human_number(n: i64) -> String {
let abs = n.unsigned_abs();
let neg = if n < 0 { "-" } else { "" };
let (divisor, suffix) = if abs >= 1_000_000_000_000 {
(1_000_000_000_000_u64, "trillion")
} else if abs >= 1_000_000_000 {
(1_000_000_000, "billion")
} else if abs >= 1_000_000 {
(1_000_000, "million")
} else {
return format_integer(n);
};
let whole = abs / divisor;
let remainder = abs % divisor;
if remainder == 0 {
return format!("{neg}{whole} {suffix}");
}
// Show up to 2 significant decimal digits
let frac = (remainder * 100) / divisor;
if frac == 0 {
format!("{neg}{whole} {suffix}")
} else if frac.is_multiple_of(10) {
format!("{neg}{whole}.{} {suffix}", frac / 10)
} else {
format!("{neg}{whole}.{frac:02} {suffix}")
}
}
/// Format an integer with dot separators (e.g. 660000 -> "660.000").
/// Used for numbers below 1 million.
fn format_integer(n: i64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let mut result = String::with_capacity(s.len() + s.len() / 3);
let start = usize::from(n < 0);
if n < 0 {
result.push('-');
}
let digits = &bytes[start..];
for (i, &b) in digits.iter().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
result.push('.');
}
result.push(b as char);
}
result
}
// ---------------------------------------------------------------------------
// Parsing helpers
// ---------------------------------------------------------------------------
/// Parse a `custom:Value` string. Returns `Some(value)` if the prefix is
/// present and the value is within length limits, `None` otherwise.
pub fn parse_custom(value: &str) -> Option<&str> {
let custom = value.strip_prefix("custom:")?;
if custom.is_empty() || custom.len() > MAX_CUSTOM_LEN {
return None;
}
Some(custom)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_label_display() {
assert_eq!(EntityLabel::Person.to_string(), "person");
assert_eq!(EntityLabel::Organization.to_string(), "organization");
assert_eq!(EntityLabel::Event.to_string(), "event");
assert_eq!(EntityLabel::Document.to_string(), "document");
assert_eq!(EntityLabel::Asset.to_string(), "asset");
assert_eq!(EntityLabel::Case.to_string(), "case");
}
#[test]
fn entity_label_serializes_snake_case() {
let json = serde_json::to_string(&EntityLabel::Organization).unwrap_or_default();
assert_eq!(json, "\"organization\"");
}
#[test]
fn money_serialization() {
let m = Money {
amount: 500_000_000_000,
currency: "IDR".into(),
display: "Rp 500 billion".into(),
};
let json = serde_json::to_string(&m).unwrap_or_default();
assert!(json.contains("\"amount\":500000000000"));
assert!(json.contains("\"currency\":\"IDR\""));
assert!(json.contains("\"display\":\"Rp 500 billion\""));
}
#[test]
fn jurisdiction_without_subdivision() {
let j = Jurisdiction {
country: "ID".into(),
subdivision: None,
};
let json = serde_json::to_string(&j).unwrap_or_default();
assert!(json.contains("\"country\":\"ID\""));
assert!(!json.contains("subdivision"));
}
#[test]
fn jurisdiction_with_subdivision() {
let j = Jurisdiction {
country: "ID".into(),
subdivision: Some("South Sulawesi".into()),
};
let json = serde_json::to_string(&j).unwrap_or_default();
assert!(json.contains("\"subdivision\":\"South Sulawesi\""));
}
#[test]
fn source_minimal() {
let s = Source {
url: "https://kompas.com/article".into(),
domain: "kompas.com".into(),
title: None,
published_at: None,
archived_url: None,
language: None,
};
let json = serde_json::to_string(&s).unwrap_or_default();
assert!(json.contains("\"domain\":\"kompas.com\""));
assert!(!json.contains("title"));
assert!(!json.contains("language"));
}
#[test]
fn source_full() {
let s = Source {
url: "https://kompas.com/article".into(),
domain: "kompas.com".into(),
title: Some("Breaking news".into()),
published_at: Some("2024-01-15".into()),
archived_url: Some(
"https://web.archive.org/web/2024/https://kompas.com/article".into(),
),
language: Some("id".into()),
};
let json = serde_json::to_string(&s).unwrap_or_default();
assert!(json.contains("\"title\":\"Breaking news\""));
assert!(json.contains("\"language\":\"id\""));
}
#[test]
fn parse_custom_valid() {
assert_eq!(parse_custom("custom:Kit Manager"), Some("Kit Manager"));
}
#[test]
fn parse_custom_empty() {
assert_eq!(parse_custom("custom:"), None);
}
#[test]
fn parse_custom_too_long() {
let long = format!("custom:{}", "a".repeat(101));
assert_eq!(parse_custom(&long), None);
}
#[test]
fn parse_custom_no_prefix() {
assert_eq!(parse_custom("politician"), None);
}
#[test]
fn amount_entry_parse_simple() {
let entries = AmountEntry::parse_dsl("660000 USD bribe").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].value, 660_000);
assert_eq!(entries[0].currency, "USD");
assert_eq!(entries[0].label.as_deref(), Some("bribe"));
assert!(!entries[0].approximate);
}
#[test]
fn amount_entry_parse_approximate() {
let entries = AmountEntry::parse_dsl("~16800000000000 IDR state_loss").unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].approximate);
assert_eq!(entries[0].value, 16_800_000_000_000);
assert_eq!(entries[0].currency, "IDR");
assert_eq!(entries[0].label.as_deref(), Some("state_loss"));
}
#[test]
fn amount_entry_parse_multiple() {
let entries = AmountEntry::parse_dsl("660000 USD bribe | 250000000 IDR fine").unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].currency, "USD");
assert_eq!(entries[1].currency, "IDR");
}
#[test]
fn amount_entry_parse_no_label() {
let entries = AmountEntry::parse_dsl("1000 EUR").unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].label.is_none());
}
#[test]
fn amount_entry_parse_empty() {
let entries = AmountEntry::parse_dsl("").unwrap();
assert!(entries.is_empty());
}
#[test]
fn amount_entry_parse_invalid_value() {
assert!(AmountEntry::parse_dsl("abc USD").is_err());
}
#[test]
fn amount_entry_parse_unknown_label() {
assert!(AmountEntry::parse_dsl("1000 USD unknown_label").is_err());
}
#[test]
fn amount_entry_parse_custom_label() {
let entries = AmountEntry::parse_dsl("1000 USD custom:MyLabel").unwrap();
assert_eq!(entries[0].label.as_deref(), Some("custom:MyLabel"));
}
#[test]
fn amount_entry_format_display() {
let entry = AmountEntry {
value: 660_000,
currency: "USD".into(),
label: Some("bribe".into()),
approximate: false,
};
assert_eq!(entry.format_display(), "USD 660.000 (bribe)");
}
#[test]
fn amount_entry_format_approximate() {
let entry = AmountEntry {
value: 16_800_000_000_000,
currency: "IDR".into(),
label: Some("state_loss".into()),
approximate: true,
};
assert_eq!(entry.format_display(), "~IDR 16.8 trillion (state loss)");
}
#[test]
fn amount_entry_format_no_label() {
let entry = AmountEntry {
value: 1000,
currency: "EUR".into(),
label: None,
approximate: false,
};
assert_eq!(entry.format_display(), "EUR 1.000");
}
#[test]
fn amount_entry_serialization() {
let entry = AmountEntry {
value: 660_000,
currency: "USD".into(),
label: Some("bribe".into()),
approximate: false,
};
let json = serde_json::to_string(&entry).unwrap_or_default();
assert!(json.contains("\"value\":660000"));
assert!(json.contains("\"currency\":\"USD\""));
assert!(json.contains("\"label\":\"bribe\""));
assert!(json.contains("\"approximate\":false"));
}
#[test]
fn amount_entry_serialization_no_label() {
let entry = AmountEntry {
value: 1000,
currency: "EUR".into(),
label: None,
approximate: false,
};
let json = serde_json::to_string(&entry).unwrap_or_default();
assert!(!json.contains("label"));
}
#[test]
fn format_integer_commas() {
assert_eq!(format_integer(0), "0");
assert_eq!(format_integer(999), "999");
assert_eq!(format_integer(1000), "1.000");
assert_eq!(format_integer(1_000_000), "1.000.000");
assert_eq!(format_integer(16_800_000_000_000), "16.800.000.000.000");
}
#[test]
fn format_human_number_below_million() {
assert_eq!(format_human_number(0), "0");
assert_eq!(format_human_number(999), "999");
assert_eq!(format_human_number(1_000), "1.000");
assert_eq!(format_human_number(660_000), "660.000");
assert_eq!(format_human_number(999_999), "999.999");
}
#[test]
fn format_human_number_millions() {
assert_eq!(format_human_number(1_000_000), "1 million");
assert_eq!(format_human_number(1_500_000), "1.5 million");
assert_eq!(format_human_number(250_000_000), "250 million");
assert_eq!(format_human_number(1_230_000), "1.23 million");
}
#[test]
fn format_human_number_billions() {
assert_eq!(format_human_number(1_000_000_000), "1 billion");
assert_eq!(format_human_number(4_580_000_000), "4.58 billion");
assert_eq!(format_human_number(100_000_000_000), "100 billion");
}
#[test]
fn format_human_number_trillions() {
assert_eq!(format_human_number(1_000_000_000_000), "1 trillion");
assert_eq!(format_human_number(4_580_000_000_000), "4.58 trillion");
assert_eq!(format_human_number(144_000_000_000_000), "144 trillion");
assert_eq!(format_human_number(16_800_000_000_000), "16.8 trillion");
}
#[test]
fn amount_entry_too_many() {
let dsl = (0..11)
.map(|i| format!("{i} USD"))
.collect::<Vec<_>>()
.join(" | ");
assert!(AmountEntry::parse_dsl(&dsl).is_err());
}
#[test]
fn role_known_values_count() {
assert_eq!(Role::KNOWN.len(), 19);
}
#[test]
fn event_type_known_values_count() {
assert_eq!(EventType::KNOWN.len(), 52);
}
#[test]
fn org_type_known_values_count() {
assert_eq!(OrgType::KNOWN.len(), 22);
}
#[test]
fn severity_known_values_count() {
assert_eq!(Severity::KNOWN.len(), 4);
}
}