1use crate::rating::RatedUsageRecord;
7
8pub trait RatedRecordExporter: Send + Sync {
38 fn export(&self, record: &RatedUsageRecord) -> Result<(), ExportError>;
46}
47
48#[derive(Debug, Clone)]
50pub enum ExportError {
51 ConnectionError(String),
53 AuthError(String),
55 InvalidData(String),
57 RateLimited { retry_after_secs: Option<u64> },
59 Other(String),
61}
62
63impl std::fmt::Display for ExportError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 ExportError::ConnectionError(e) => write!(f, "Export connection error: {e}"),
67 ExportError::AuthError(e) => write!(f, "Export auth error: {e}"),
68 ExportError::InvalidData(e) => write!(f, "Export invalid data: {e}"),
69 ExportError::RateLimited {
70 retry_after_secs: _,
71 } => write!(f, "Export rate limited"),
72 ExportError::Other(e) => write!(f, "Export error: {e}"),
73 }
74 }
75}
76
77impl std::error::Error for ExportError {}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::identity::UsageEventId;
83 use crate::observation::{MeterKind, MeterSet, UsageObservation, UsageOutcome, UsageSource, UsageTiming, Attributes};
84 use crate::pricing::ModelRef;
85 use crate::rating::{RatedLineItem, RatingResult};
86 use crate::CurrencyCode;
87 use chrono::Utc;
88 use rust_decimal_macros::dec;
89 use std::sync::{Arc, Mutex};
90
91 struct StdoutExporter {
93 exported: Arc<Mutex<Vec<String>>>,
94 }
95
96 impl StdoutExporter {
97 fn new() -> Self {
98 Self {
99 exported: Arc::new(Mutex::new(Vec::new())),
100 }
101 }
102 }
103
104 impl RatedRecordExporter for StdoutExporter {
105 fn export(&self, record: &RatedUsageRecord) -> Result<(), ExportError> {
106 let msg = format!(
107 "{} {}",
108 record.rating.total_cost, record.rating.currency.0
109 );
110 self.exported.lock().unwrap().push(msg);
111 Ok(())
112 }
113 }
114
115 #[test]
116 fn stdout_exporter_works() {
117 let exporter = StdoutExporter::new();
118
119 let record = RatedUsageRecord {
121 rated_record_id: "test-1:v1".to_string(),
122 observation: UsageObservation {
123 event_id: UsageEventId::from_raw("test-1"),
124 subject: crate::identity::BillingSubject::default(),
125 meter_set: MeterSet::new(),
126 model_ref: ModelRef {
127 billable_model: "test".to_string(),
128 vendor: None,
129 region: None,
130 tier: None,
131 },
132 provider_ref: None,
133 source: UsageSource::Estimated,
134 outcome: UsageOutcome::Success,
135 timing: UsageTiming {
136 observed_at: Utc::now(),
137 completed_at: None,
138 },
139 correlation: crate::identity::CorrelationIds::default(),
140 attributes: Attributes::new(),
141 },
142 rating: RatingResult {
143 line_items: vec![RatedLineItem {
144 meter_kind: MeterKind::InputTokens,
145 quantity: 100,
146 unit_price: dec!(0.0003),
147 subtotal: dec!(0.03),
148 }],
149 total_cost: dec!(0.03),
150 currency: CurrencyCode::usd(),
151 price_snapshot_id: "snap-1".to_string(),
152 rated_at: Utc::now(),
153 },
154 supersedes: None,
155 };
156
157 exporter.export(&record).unwrap();
158 assert_eq!(exporter.exported.lock().unwrap().len(), 1);
159 }
160
161 #[test]
162 fn export_error_display() {
163 let err = ExportError::ConnectionError("timeout".to_string());
164 assert_eq!(err.to_string(), "Export connection error: timeout");
165 }
166}