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}