use crate::rating::RatedUsageRecord;
pub trait RatedRecordExporter: Send + Sync {
fn export(&self, record: &RatedUsageRecord) -> Result<(), ExportError>;
}
#[derive(Debug, Clone)]
pub enum ExportError {
ConnectionError(String),
AuthError(String),
InvalidData(String),
RateLimited { retry_after_secs: Option<u64> },
Other(String),
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExportError::ConnectionError(e) => write!(f, "Export connection error: {e}"),
ExportError::AuthError(e) => write!(f, "Export auth error: {e}"),
ExportError::InvalidData(e) => write!(f, "Export invalid data: {e}"),
ExportError::RateLimited {
retry_after_secs: _,
} => write!(f, "Export rate limited"),
ExportError::Other(e) => write!(f, "Export error: {e}"),
}
}
}
impl std::error::Error for ExportError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::UsageEventId;
use crate::observation::{MeterKind, MeterSet, UsageObservation, UsageOutcome, UsageSource, UsageTiming, Attributes};
use crate::pricing::ModelRef;
use crate::rating::{RatedLineItem, RatingResult};
use crate::CurrencyCode;
use chrono::Utc;
use rust_decimal_macros::dec;
use std::sync::{Arc, Mutex};
struct StdoutExporter {
exported: Arc<Mutex<Vec<String>>>,
}
impl StdoutExporter {
fn new() -> Self {
Self {
exported: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl RatedRecordExporter for StdoutExporter {
fn export(&self, record: &RatedUsageRecord) -> Result<(), ExportError> {
let msg = format!(
"{} {}",
record.rating.total_cost, record.rating.currency.0
);
self.exported.lock().unwrap().push(msg);
Ok(())
}
}
#[test]
fn stdout_exporter_works() {
let exporter = StdoutExporter::new();
let record = RatedUsageRecord {
rated_record_id: "test-1:v1".to_string(),
observation: UsageObservation {
event_id: UsageEventId::from_raw("test-1"),
subject: crate::identity::BillingSubject::default(),
meter_set: MeterSet::new(),
model_ref: ModelRef {
billable_model: "test".to_string(),
vendor: None,
region: None,
tier: None,
},
provider_ref: None,
source: UsageSource::Estimated,
outcome: UsageOutcome::Success,
timing: UsageTiming {
observed_at: Utc::now(),
completed_at: None,
},
correlation: crate::identity::CorrelationIds::default(),
attributes: Attributes::new(),
},
rating: RatingResult {
line_items: vec![RatedLineItem {
meter_kind: MeterKind::InputTokens,
quantity: 100,
unit_price: dec!(0.0003),
subtotal: dec!(0.03),
}],
total_cost: dec!(0.03),
currency: CurrencyCode::usd(),
price_snapshot_id: "snap-1".to_string(),
rated_at: Utc::now(),
},
supersedes: None,
};
exporter.export(&record).unwrap();
assert_eq!(exporter.exported.lock().unwrap().len(), 1);
}
#[test]
fn export_error_display() {
let err = ExportError::ConnectionError("timeout".to_string());
assert_eq!(err.to_string(), "Export connection error: timeout");
}
}