ringkernel_core/
domain.rs

1//! Business domain classification for kernel messages.
2//!
3//! This module provides domain enumeration and traits for categorizing messages
4//! by their business function. Domains enable:
5//!
6//! - Type ID range allocation per domain
7//! - Domain-specific routing and observability
8//! - Cross-domain access control
9//!
10//! # Example
11//!
12//! ```ignore
13//! use ringkernel_core::domain::{Domain, DomainMessage};
14//!
15//! // Messages with domain derive get auto-assigned type IDs
16//! #[derive(RingMessage)]
17//! #[ring_message(type_id = 1, domain = "OrderMatching")]
18//! pub struct SubmitOrder {
19//!     #[message(id)]
20//!     id: MessageId,
21//!     symbol: String,
22//! }
23//! // Final type ID = 500 (OrderMatching base) + 1 = 501
24//! ```
25
26use std::fmt;
27
28/// Business domain classification for kernel messages.
29///
30/// Each domain has an assigned type ID range to prevent collisions:
31/// - General: 0-99
32/// - GraphAnalytics: 100-199
33/// - StatisticalML: 200-299
34/// - Compliance: 300-399
35/// - RiskManagement: 400-499
36/// - OrderMatching: 500-599
37/// - MarketData: 600-699
38/// - Settlement: 700-799
39/// - Accounting: 800-899
40/// - NetworkAnalysis: 900-999
41/// - FraudDetection: 1000-1099
42/// - TimeSeries: 1100-1199
43/// - Simulation: 1200-1299
44/// - Banking: 1300-1399
45/// - BehavioralAnalytics: 1400-1499
46/// - ProcessIntelligence: 1500-1599
47/// - Clearing: 1600-1699
48/// - TreasuryManagement: 1700-1799
49/// - PaymentProcessing: 1800-1899
50/// - FinancialAudit: 1900-1999
51/// - Custom: 10000+
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
53#[repr(u16)]
54#[non_exhaustive]
55pub enum Domain {
56    /// General-purpose messages (type IDs: 0-99).
57    #[default]
58    General = 0,
59
60    /// Graph analytics messages (type IDs: 100-199).
61    /// Includes: PageRank, community detection, centrality measures.
62    GraphAnalytics = 1,
63
64    /// Statistical/ML messages (type IDs: 200-299).
65    /// Includes: regression, clustering, classification.
66    StatisticalML = 2,
67
68    /// Compliance/regulatory messages (type IDs: 300-399).
69    /// Includes: AML checks, KYC validation, regulatory reporting.
70    Compliance = 3,
71
72    /// Risk management messages (type IDs: 400-499).
73    /// Includes: VaR calculation, stress testing, exposure analysis.
74    RiskManagement = 4,
75
76    /// Order matching messages (type IDs: 500-599).
77    /// Includes: order submission, matching, cancellation.
78    OrderMatching = 5,
79
80    /// Market data messages (type IDs: 600-699).
81    /// Includes: quotes, trades, order book updates.
82    MarketData = 6,
83
84    /// Settlement messages (type IDs: 700-799).
85    /// Includes: trade settlement, netting, reconciliation.
86    Settlement = 7,
87
88    /// Accounting messages (type IDs: 800-899).
89    /// Includes: journal entries, ledger updates, trial balance.
90    Accounting = 8,
91
92    /// Network analysis messages (type IDs: 900-999).
93    /// Includes: transaction flow, counterparty analysis.
94    NetworkAnalysis = 9,
95
96    /// Fraud detection messages (type IDs: 1000-1099).
97    /// Includes: anomaly detection, pattern matching.
98    FraudDetection = 10,
99
100    /// Time series messages (type IDs: 1100-1199).
101    /// Includes: forecasting, trend analysis, seasonality.
102    TimeSeries = 11,
103
104    /// Simulation messages (type IDs: 1200-1299).
105    /// Includes: Monte Carlo, scenario analysis, stress testing.
106    Simulation = 12,
107
108    /// Banking messages (type IDs: 1300-1399).
109    /// Includes: account management, transfers, statements.
110    Banking = 13,
111
112    /// Behavioral analytics messages (type IDs: 1400-1499).
113    /// Includes: user behavior, clickstream, session analysis.
114    BehavioralAnalytics = 14,
115
116    /// Process intelligence messages (type IDs: 1500-1599).
117    /// Includes: process mining, DFG, conformance checking.
118    ProcessIntelligence = 15,
119
120    /// Clearing messages (type IDs: 1600-1699).
121    /// Includes: CCP clearing, margin calculation, position netting.
122    Clearing = 16,
123
124    /// Treasury management messages (type IDs: 1700-1799).
125    /// Includes: cash management, liquidity, FX hedging.
126    TreasuryManagement = 17,
127
128    /// Payment processing messages (type IDs: 1800-1899).
129    /// Includes: payment initiation, routing, confirmation.
130    PaymentProcessing = 18,
131
132    /// Financial audit messages (type IDs: 1900-1999).
133    /// Includes: audit trails, evidence gathering, compliance verification.
134    FinancialAudit = 19,
135
136    /// Custom domain (type IDs: 10000+).
137    /// For application-specific domains not covered by predefined ones.
138    Custom = 100,
139}
140
141impl Domain {
142    /// Number of type IDs reserved per domain (except Custom).
143    pub const RANGE_SIZE: u64 = 100;
144
145    /// Base type ID for custom domains.
146    pub const CUSTOM_BASE: u64 = 10000;
147
148    /// Get the base type ID for this domain.
149    ///
150    /// Type IDs for messages in this domain should be: `base_type_id() + offset`
151    /// where offset is 0-99.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use ringkernel_core::domain::Domain;
157    ///
158    /// assert_eq!(Domain::General.base_type_id(), 0);
159    /// assert_eq!(Domain::OrderMatching.base_type_id(), 500);
160    /// assert_eq!(Domain::Custom.base_type_id(), 10000);
161    /// ```
162    #[inline]
163    pub const fn base_type_id(&self) -> u64 {
164        match self {
165            Self::General => 0,
166            Self::GraphAnalytics => 100,
167            Self::StatisticalML => 200,
168            Self::Compliance => 300,
169            Self::RiskManagement => 400,
170            Self::OrderMatching => 500,
171            Self::MarketData => 600,
172            Self::Settlement => 700,
173            Self::Accounting => 800,
174            Self::NetworkAnalysis => 900,
175            Self::FraudDetection => 1000,
176            Self::TimeSeries => 1100,
177            Self::Simulation => 1200,
178            Self::Banking => 1300,
179            Self::BehavioralAnalytics => 1400,
180            Self::ProcessIntelligence => 1500,
181            Self::Clearing => 1600,
182            Self::TreasuryManagement => 1700,
183            Self::PaymentProcessing => 1800,
184            Self::FinancialAudit => 1900,
185            Self::Custom => Self::CUSTOM_BASE,
186        }
187    }
188
189    /// Get the maximum type ID for this domain.
190    ///
191    /// # Example
192    ///
193    /// ```
194    /// use ringkernel_core::domain::Domain;
195    ///
196    /// assert_eq!(Domain::General.max_type_id(), 99);
197    /// assert_eq!(Domain::OrderMatching.max_type_id(), 599);
198    /// ```
199    #[inline]
200    pub const fn max_type_id(&self) -> u64 {
201        match self {
202            Self::Custom => u64::MAX,
203            _ => self.base_type_id() + Self::RANGE_SIZE - 1,
204        }
205    }
206
207    /// Check if a type ID is within this domain's range.
208    ///
209    /// # Example
210    ///
211    /// ```
212    /// use ringkernel_core::domain::Domain;
213    ///
214    /// assert!(Domain::OrderMatching.contains_type_id(500));
215    /// assert!(Domain::OrderMatching.contains_type_id(599));
216    /// assert!(!Domain::OrderMatching.contains_type_id(600));
217    /// ```
218    #[inline]
219    pub const fn contains_type_id(&self, type_id: u64) -> bool {
220        type_id >= self.base_type_id() && type_id <= self.max_type_id()
221    }
222
223    /// Determine which domain a type ID belongs to.
224    ///
225    /// Returns `None` if the type ID doesn't match any standard domain.
226    ///
227    /// # Example
228    ///
229    /// ```
230    /// use ringkernel_core::domain::Domain;
231    ///
232    /// assert_eq!(Domain::from_type_id(501), Some(Domain::OrderMatching));
233    /// assert_eq!(Domain::from_type_id(10500), Some(Domain::Custom));
234    /// ```
235    pub const fn from_type_id(type_id: u64) -> Option<Self> {
236        match type_id {
237            0..=99 => Some(Self::General),
238            100..=199 => Some(Self::GraphAnalytics),
239            200..=299 => Some(Self::StatisticalML),
240            300..=399 => Some(Self::Compliance),
241            400..=499 => Some(Self::RiskManagement),
242            500..=599 => Some(Self::OrderMatching),
243            600..=699 => Some(Self::MarketData),
244            700..=799 => Some(Self::Settlement),
245            800..=899 => Some(Self::Accounting),
246            900..=999 => Some(Self::NetworkAnalysis),
247            1000..=1099 => Some(Self::FraudDetection),
248            1100..=1199 => Some(Self::TimeSeries),
249            1200..=1299 => Some(Self::Simulation),
250            1300..=1399 => Some(Self::Banking),
251            1400..=1499 => Some(Self::BehavioralAnalytics),
252            1500..=1599 => Some(Self::ProcessIntelligence),
253            1600..=1699 => Some(Self::Clearing),
254            1700..=1799 => Some(Self::TreasuryManagement),
255            1800..=1899 => Some(Self::PaymentProcessing),
256            1900..=1999 => Some(Self::FinancialAudit),
257            10000.. => Some(Self::Custom),
258            _ => None,
259        }
260    }
261
262    /// Parse domain from string (case-insensitive).
263    ///
264    /// Supports various naming conventions:
265    /// - PascalCase: "OrderMatching"
266    /// - snake_case: "order_matching"
267    /// - lowercase: "ordermatching"
268    /// - Short forms: "risk", "ml", "sim"
269    ///
270    /// # Example
271    ///
272    /// ```
273    /// use ringkernel_core::domain::Domain;
274    ///
275    /// assert_eq!(Domain::from_str("OrderMatching"), Some(Domain::OrderMatching));
276    /// assert_eq!(Domain::from_str("order_matching"), Some(Domain::OrderMatching));
277    /// assert_eq!(Domain::from_str("risk"), Some(Domain::RiskManagement));
278    /// assert_eq!(Domain::from_str("unknown"), None);
279    /// ```
280    #[allow(clippy::should_implement_trait)]
281    pub fn from_str(s: &str) -> Option<Self> {
282        let normalized: String = s
283            .chars()
284            .filter(|c| c.is_alphanumeric())
285            .collect::<String>()
286            .to_lowercase();
287
288        match normalized.as_str() {
289            "general" | "gen" => Some(Self::General),
290            "graphanalytics" | "graph" => Some(Self::GraphAnalytics),
291            "statisticalml" | "ml" | "machinelearning" => Some(Self::StatisticalML),
292            "compliance" | "comp" | "regulatory" => Some(Self::Compliance),
293            "riskmanagement" | "risk" => Some(Self::RiskManagement),
294            "ordermatching" | "orders" | "order" | "matching" => Some(Self::OrderMatching),
295            "marketdata" | "market" | "mktdata" => Some(Self::MarketData),
296            "settlement" | "settle" => Some(Self::Settlement),
297            "accounting" | "acct" | "ledger" => Some(Self::Accounting),
298            "networkanalysis" | "network" | "netanalysis" => Some(Self::NetworkAnalysis),
299            "frauddetection" | "fraud" | "aml" => Some(Self::FraudDetection),
300            "timeseries" | "ts" | "temporal" => Some(Self::TimeSeries),
301            "simulation" | "sim" | "montecarlo" => Some(Self::Simulation),
302            "banking" | "bank" => Some(Self::Banking),
303            "behavioralanalytics" | "behavioral" | "behavior" => Some(Self::BehavioralAnalytics),
304            "processintelligence" | "process" | "processmining" => Some(Self::ProcessIntelligence),
305            "clearing" | "ccp" => Some(Self::Clearing),
306            "treasurymanagement" | "treasury" => Some(Self::TreasuryManagement),
307            "paymentprocessing" | "payment" | "payments" => Some(Self::PaymentProcessing),
308            "financialaudit" | "audit" => Some(Self::FinancialAudit),
309            "custom" => Some(Self::Custom),
310            _ => None,
311        }
312    }
313
314    /// Get the domain name as a static string.
315    ///
316    /// Returns the PascalCase canonical name.
317    #[inline]
318    pub const fn as_str(&self) -> &'static str {
319        match self {
320            Self::General => "General",
321            Self::GraphAnalytics => "GraphAnalytics",
322            Self::StatisticalML => "StatisticalML",
323            Self::Compliance => "Compliance",
324            Self::RiskManagement => "RiskManagement",
325            Self::OrderMatching => "OrderMatching",
326            Self::MarketData => "MarketData",
327            Self::Settlement => "Settlement",
328            Self::Accounting => "Accounting",
329            Self::NetworkAnalysis => "NetworkAnalysis",
330            Self::FraudDetection => "FraudDetection",
331            Self::TimeSeries => "TimeSeries",
332            Self::Simulation => "Simulation",
333            Self::Banking => "Banking",
334            Self::BehavioralAnalytics => "BehavioralAnalytics",
335            Self::ProcessIntelligence => "ProcessIntelligence",
336            Self::Clearing => "Clearing",
337            Self::TreasuryManagement => "TreasuryManagement",
338            Self::PaymentProcessing => "PaymentProcessing",
339            Self::FinancialAudit => "FinancialAudit",
340            Self::Custom => "Custom",
341        }
342    }
343
344    /// Get all standard domains (excluding Custom).
345    pub const fn all_standard() -> &'static [Domain] {
346        &[
347            Self::General,
348            Self::GraphAnalytics,
349            Self::StatisticalML,
350            Self::Compliance,
351            Self::RiskManagement,
352            Self::OrderMatching,
353            Self::MarketData,
354            Self::Settlement,
355            Self::Accounting,
356            Self::NetworkAnalysis,
357            Self::FraudDetection,
358            Self::TimeSeries,
359            Self::Simulation,
360            Self::Banking,
361            Self::BehavioralAnalytics,
362            Self::ProcessIntelligence,
363            Self::Clearing,
364            Self::TreasuryManagement,
365            Self::PaymentProcessing,
366            Self::FinancialAudit,
367        ]
368    }
369}
370
371impl fmt::Display for Domain {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        write!(f, "{}", self.as_str())
374    }
375}
376
377impl std::str::FromStr for Domain {
378    type Err = DomainParseError;
379
380    fn from_str(s: &str) -> Result<Self, Self::Err> {
381        Domain::from_str(s).ok_or_else(|| DomainParseError(s.to_string()))
382    }
383}
384
385/// Error returned when parsing an invalid domain string.
386#[derive(Debug, Clone)]
387pub struct DomainParseError(pub String);
388
389impl fmt::Display for DomainParseError {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        write!(f, "unknown domain: '{}'", self.0)
392    }
393}
394
395impl std::error::Error for DomainParseError {}
396
397/// Trait for messages that belong to a specific business domain.
398///
399/// This trait is automatically implemented by the `#[derive(RingMessage)]` macro
400/// when a `domain` attribute is specified.
401///
402/// # Example
403///
404/// ```ignore
405/// #[derive(RingMessage)]
406/// #[ring_message(type_id = 1, domain = "OrderMatching")]
407/// pub struct SubmitOrder {
408///     #[message(id)]
409///     id: MessageId,
410///     symbol: String,
411/// }
412///
413/// // Auto-generated:
414/// impl DomainMessage for SubmitOrder {
415///     fn domain() -> Domain { Domain::OrderMatching }
416/// }
417/// ```
418pub trait DomainMessage: crate::message::RingMessage {
419    /// Get the domain this message belongs to.
420    fn domain() -> Domain;
421
422    /// Get the type ID offset within the domain (0-99).
423    ///
424    /// This is calculated as: `message_type() - domain().base_type_id()`
425    fn domain_type_id() -> u64 {
426        Self::message_type().saturating_sub(Self::domain().base_type_id())
427    }
428
429    /// Check if this message type is within its domain's valid range.
430    fn is_valid_domain_type_id() -> bool {
431        Self::domain().contains_type_id(Self::message_type())
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_domain_base_type_ids() {
441        assert_eq!(Domain::General.base_type_id(), 0);
442        assert_eq!(Domain::GraphAnalytics.base_type_id(), 100);
443        assert_eq!(Domain::StatisticalML.base_type_id(), 200);
444        assert_eq!(Domain::OrderMatching.base_type_id(), 500);
445        assert_eq!(Domain::Custom.base_type_id(), 10000);
446    }
447
448    #[test]
449    fn test_domain_max_type_ids() {
450        assert_eq!(Domain::General.max_type_id(), 99);
451        assert_eq!(Domain::OrderMatching.max_type_id(), 599);
452        assert_eq!(Domain::Custom.max_type_id(), u64::MAX);
453    }
454
455    #[test]
456    fn test_domain_contains_type_id() {
457        assert!(Domain::General.contains_type_id(0));
458        assert!(Domain::General.contains_type_id(99));
459        assert!(!Domain::General.contains_type_id(100));
460
461        assert!(Domain::OrderMatching.contains_type_id(500));
462        assert!(Domain::OrderMatching.contains_type_id(599));
463        assert!(!Domain::OrderMatching.contains_type_id(600));
464
465        assert!(Domain::Custom.contains_type_id(10000));
466        assert!(Domain::Custom.contains_type_id(u64::MAX));
467    }
468
469    #[test]
470    fn test_domain_from_type_id() {
471        assert_eq!(Domain::from_type_id(0), Some(Domain::General));
472        assert_eq!(Domain::from_type_id(50), Some(Domain::General));
473        assert_eq!(Domain::from_type_id(99), Some(Domain::General));
474        assert_eq!(Domain::from_type_id(100), Some(Domain::GraphAnalytics));
475        assert_eq!(Domain::from_type_id(501), Some(Domain::OrderMatching));
476        assert_eq!(Domain::from_type_id(10500), Some(Domain::Custom));
477        assert_eq!(Domain::from_type_id(2500), None); // Gap in range
478    }
479
480    #[test]
481    fn test_domain_from_str() {
482        // PascalCase
483        assert_eq!(
484            Domain::from_str("OrderMatching"),
485            Some(Domain::OrderMatching)
486        );
487        assert_eq!(
488            Domain::from_str("RiskManagement"),
489            Some(Domain::RiskManagement)
490        );
491
492        // snake_case
493        assert_eq!(
494            Domain::from_str("order_matching"),
495            Some(Domain::OrderMatching)
496        );
497        assert_eq!(
498            Domain::from_str("risk_management"),
499            Some(Domain::RiskManagement)
500        );
501
502        // lowercase
503        assert_eq!(
504            Domain::from_str("ordermatching"),
505            Some(Domain::OrderMatching)
506        );
507
508        // Short forms
509        assert_eq!(Domain::from_str("risk"), Some(Domain::RiskManagement));
510        assert_eq!(Domain::from_str("ml"), Some(Domain::StatisticalML));
511        assert_eq!(Domain::from_str("sim"), Some(Domain::Simulation));
512
513        // Unknown
514        assert_eq!(Domain::from_str("unknown"), None);
515        assert_eq!(Domain::from_str(""), None);
516    }
517
518    #[test]
519    fn test_domain_as_str() {
520        assert_eq!(Domain::General.as_str(), "General");
521        assert_eq!(Domain::OrderMatching.as_str(), "OrderMatching");
522        assert_eq!(Domain::RiskManagement.as_str(), "RiskManagement");
523    }
524
525    #[test]
526    fn test_domain_display() {
527        assert_eq!(format!("{}", Domain::OrderMatching), "OrderMatching");
528    }
529
530    #[test]
531    fn test_domain_default() {
532        assert_eq!(Domain::default(), Domain::General);
533    }
534
535    #[test]
536    fn test_domain_all_standard() {
537        let all = Domain::all_standard();
538        assert_eq!(all.len(), 20);
539        assert!(all.contains(&Domain::General));
540        assert!(all.contains(&Domain::OrderMatching));
541        assert!(!all.contains(&Domain::Custom));
542    }
543
544    #[test]
545    fn test_domain_ranges_no_overlap() {
546        let domains = Domain::all_standard();
547        for (i, d1) in domains.iter().enumerate() {
548            for d2 in domains.iter().skip(i + 1) {
549                // Check that ranges don't overlap
550                assert!(
551                    d1.max_type_id() < d2.base_type_id() || d2.max_type_id() < d1.base_type_id(),
552                    "Domains {:?} and {:?} have overlapping ranges",
553                    d1,
554                    d2
555                );
556            }
557        }
558    }
559
560    #[test]
561    fn test_std_from_str() {
562        let domain: Domain = "OrderMatching".parse().unwrap();
563        assert_eq!(domain, Domain::OrderMatching);
564
565        let err = "invalid".parse::<Domain>().unwrap_err();
566        assert!(err.to_string().contains("unknown domain"));
567    }
568}