Skip to main content

eth_id/claims/
engine.rs

1use super::types::*;
2use crate::error::{Result, EthIdError};
3use regex::Regex;
4use lazy_static::lazy_static;
5
6lazy_static! {
7    static ref AGE_PATTERN: Regex = Regex::new(r"(?i)(maior|acima|mais|over|above|greater\s+than)\s+(?:de\s+)?(\d+)\s+(?:anos?|years?(?:\s+old)?)").unwrap();
8    static ref AGE_LESS_PATTERN: Regex = Regex::new(r"(?i)(menor|abaixo|less\s+than|below|under)\s+(?:de\s+)?(\d+)\s+(?:anos?|years?(?:\s+old)?)").unwrap();
9    static ref DAYS_PATTERN: Regex = Regex::new(r"(?i)(?:nos\s+)?(?:Ășltimos|last|within(?:\s+last)?)\s+(\d+)\s+(?:dias?|days?)").unwrap();
10    static ref AMOUNT_PATTERN: Regex = Regex::new(r"(?i)(acima|maior|superior|above|greater|over)\s+(?:de\s+)?(?:que\s+)?(?:R\$\s*)?(\d+(?:[.,]\d+)?)").unwrap();
11    static ref AMOUNT_LESS_PATTERN: Regex = Regex::new(r"(?i)(abaixo|menor|inferior|below|less|under)\s+(?:de\s+)?(?:que\s+)?(?:R\$\s*)?(\d+(?:[.,]\d+)?)").unwrap();
12    static ref CPF_PATTERN: Regex = Regex::new(r"\d{3}\.\d{3}\.\d{3}-\d{2}").unwrap();
13    static ref SIGNATURE_PATTERN: Regex = Regex::new(r"(?i)assinado|signed|signature").unwrap();
14}
15
16pub struct ClaimEngine;
17
18impl ClaimEngine {
19    pub fn new() -> Self {
20        Self
21    }
22    
23    pub fn parse_claim(&self, claim: &str) -> Result<ClaimQuery> {
24        let claim_lower = claim.to_lowercase();
25        
26        if let Some(age_claim) = self.try_parse_age_claim(claim)? {
27            return Ok(ClaimQuery::Date(age_claim));
28        }
29        
30        if let Some(days_claim) = self.try_parse_days_claim(claim)? {
31            return Ok(ClaimQuery::Date(days_claim));
32        }
33        
34        if let Some(amount_claim) = self.try_parse_amount_claim(claim)? {
35            return Ok(ClaimQuery::Amount(amount_claim));
36        }
37        
38        if let Some(identity_claim) = self.try_parse_identity_claim(claim)? {
39            return Ok(ClaimQuery::Identity(identity_claim));
40        }
41        
42        if SIGNATURE_PATTERN.is_match(&claim_lower) {
43            return Ok(ClaimQuery::Signature(SignatureClaim {
44                required_signers: vec![],
45                all_required: true,
46            }));
47        }
48        
49        Ok(ClaimQuery::Presence(PresenceClaim {
50            field: claim.to_string(),
51            must_exist: true,
52            must_not_be_empty: true,
53        }))
54    }
55    
56    fn try_parse_age_claim(&self, claim: &str) -> Result<Option<DateClaim>> {
57        if let Some(caps) = AGE_PATTERN.captures(claim) {
58            let age: u32 = caps.get(2)
59                .ok_or_else(|| EthIdError::ClaimParsing("Failed to extract age".to_string()))?
60                .as_str()
61                .parse()
62                .map_err(|_| EthIdError::ClaimParsing("Invalid age number".to_string()))?;
63            
64            return Ok(Some(DateClaim {
65                operation: DateOperation::AgeGreaterThan,
66                threshold: None,
67                age_threshold: Some(age),
68                days_threshold: None,
69            }));
70        }
71        
72        if let Some(caps) = AGE_LESS_PATTERN.captures(claim) {
73            let age: u32 = caps.get(2)
74                .ok_or_else(|| EthIdError::ClaimParsing("Failed to extract age".to_string()))?
75                .as_str()
76                .parse()
77                .map_err(|_| EthIdError::ClaimParsing("Invalid age number".to_string()))?;
78            
79            return Ok(Some(DateClaim {
80                operation: DateOperation::AgeLessThan,
81                threshold: None,
82                age_threshold: Some(age),
83                days_threshold: None,
84            }));
85        }
86        
87        Ok(None)
88    }
89    
90    fn try_parse_days_claim(&self, claim: &str) -> Result<Option<DateClaim>> {
91        if let Some(caps) = DAYS_PATTERN.captures(claim) {
92            let days: i64 = caps.get(1)
93                .ok_or_else(|| EthIdError::ClaimParsing("Failed to extract days".to_string()))?
94                .as_str()
95                .parse()
96                .map_err(|_| EthIdError::ClaimParsing("Invalid days number".to_string()))?;
97            
98            return Ok(Some(DateClaim {
99                operation: DateOperation::IssuedWithinDays,
100                threshold: None,
101                age_threshold: None,
102                days_threshold: Some(days),
103            }));
104        }
105        
106        Ok(None)
107    }
108    
109    fn try_parse_amount_claim(&self, claim: &str) -> Result<Option<AmountClaim>> {
110        if let Some(caps) = AMOUNT_PATTERN.captures(claim) {
111            let amount_str = caps.get(2)
112                .ok_or_else(|| EthIdError::ClaimParsing("Failed to extract amount".to_string()))?
113                .as_str()
114                .replace(",", ".");
115            
116            let amount: f64 = amount_str
117                .parse()
118                .map_err(|_| EthIdError::ClaimParsing("Invalid amount number".to_string()))?;
119            
120            return Ok(Some(AmountClaim {
121                field: "amount".to_string(),
122                operation: AmountOperation::GreaterThan,
123                threshold: amount,
124            }));
125        }
126        
127        if let Some(caps) = AMOUNT_LESS_PATTERN.captures(claim) {
128            let amount_str = caps.get(2)
129                .ok_or_else(|| EthIdError::ClaimParsing("Failed to extract amount".to_string()))?
130                .as_str()
131                .replace(",", ".");
132            
133            let amount: f64 = amount_str
134                .parse()
135                .map_err(|_| EthIdError::ClaimParsing("Invalid amount number".to_string()))?;
136            
137            return Ok(Some(AmountClaim {
138                field: "amount".to_string(),
139                operation: AmountOperation::LessThan,
140                threshold: amount,
141            }));
142        }
143        
144        Ok(None)
145    }
146    
147    fn try_parse_identity_claim(&self, claim: &str) -> Result<Option<IdentityClaim>> {
148        if let Some(cpf_match) = CPF_PATTERN.find(claim) {
149            return Ok(Some(IdentityClaim {
150                field: IdentityField::CPF,
151                expected_value: Some(cpf_match.as_str().to_string()),
152                operation: IdentityOperation::Matches,
153            }));
154        }
155        
156        let claim_lower = claim.to_lowercase();
157        
158        if claim_lower.contains("nome") || claim_lower.contains("name") {
159            return Ok(Some(IdentityClaim {
160                field: IdentityField::Name,
161                expected_value: None,
162                operation: IdentityOperation::Contains,
163            }));
164        }
165        
166        Ok(None)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_parse_age_claim_pt() {
176        let engine = ClaimEngine::new();
177        let result = engine.parse_claim("maior de 18 anos").unwrap();
178        
179        match result {
180            ClaimQuery::Date(claim) => {
181                assert_eq!(claim.age_threshold, Some(18));
182                assert!(matches!(claim.operation, DateOperation::AgeGreaterThan));
183            }
184            _ => panic!("Expected DateClaim"),
185        }
186    }
187
188    #[test]
189    fn test_parse_age_claim_en() {
190        let engine = ClaimEngine::new();
191        let result = engine.parse_claim("over 21 years old").unwrap();
192        
193        match result {
194            ClaimQuery::Date(claim) => {
195                assert_eq!(claim.age_threshold, Some(21));
196            }
197            _ => panic!("Expected DateClaim"),
198        }
199    }
200
201    #[test]
202    fn test_parse_days_claim() {
203        let engine = ClaimEngine::new();
204        let result = engine.parse_claim("emitido nos Ășltimos 90 dias").unwrap();
205        
206        match result {
207            ClaimQuery::Date(claim) => {
208                assert_eq!(claim.days_threshold, Some(90));
209                assert!(matches!(claim.operation, DateOperation::IssuedWithinDays));
210            }
211            _ => panic!("Expected DateClaim"),
212        }
213    }
214
215    #[test]
216    fn test_parse_amount_claim() {
217        let engine = ClaimEngine::new();
218        let result = engine.parse_claim("renda acima de 5000").unwrap();
219        
220        match result {
221            ClaimQuery::Amount(claim) => {
222                assert_eq!(claim.threshold, 5000.0);
223                assert!(matches!(claim.operation, AmountOperation::GreaterThan));
224            }
225            _ => panic!("Expected AmountClaim"),
226        }
227    }
228
229    #[test]
230    fn test_parse_cpf_claim() {
231        let engine = ClaimEngine::new();
232        let result = engine.parse_claim("CPF bate com 123.456.789-00").unwrap();
233        
234        match result {
235            ClaimQuery::Identity(claim) => {
236                assert_eq!(claim.expected_value, Some("123.456.789-00".to_string()));
237                assert!(matches!(claim.field, IdentityField::CPF));
238            }
239            _ => panic!("Expected IdentityClaim"),
240        }
241    }
242}