Skip to content

Commit 7439544

Browse files
feat: rule validator (#202)
1 parent 454ab8d commit 7439544

7 files changed

Lines changed: 535 additions & 123 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ rand = "0.8.5"
9595
tower-http = { version = "0.6.2", features = ["trace"] }
9696
bytes = "1.10.1"
9797
strum = { version = "0.26.2", features = ["derive"] }
98+
regex = "1.10"
9899
# -------------------------------------
99100

100101
[dev-dependencies]

config/development.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ mobile_payment = { type = "enum", values = "direct_carrier_billing" }
8585
payment_method = { type = "enum", values = "card, card_redirect, pay_later, wallet, bank_redirect, bank_transfer, crypto, bank_debit, reward, real_time_payment, upi, voucher, gift_card, open_banking, mobile_payment" }
8686
card_type = { type = "enum", values = "debit, credit" }
8787
card = { type = "enum", values = "debit, credit" }
88-
payment_card_bin = { type = "udf" }
89-
issuer_name = { type = "str_value" }
88+
payment_card_bin = { type = "udf", exact_length = 6, regex = "^[0-9]{6}$" }
89+
issuer_name = { type = "str_value", min_length = 1 }
9090
payment_card_type = { type = "enum", values = "CREDIT, DEBIT" }
9191
mandate_acceptance_type = { type = "enum", values = "online, offline" }
9292
mandate_type = { type = "enum", values = "single_use, multi_use" }
@@ -97,12 +97,13 @@ authentication_type = { type = "enum", values = "three_ds, no_three_ds" }
9797
capture_methods = { type = "enum", values = "automatic, manual, manual_multiple, scheduled, sequential_automatic" }
9898
setup_future_usage = { type = "enum", values = "on_session, off_session" }
9999
payment_card_network = { type = "enum", values = "visa, mastercard, american_express, jcb, diners_club, discover, cartes_bancaires, union_pay, interac, rupay, maestro" }
100-
amount = { type = "integer" }
100+
amount = { type = "integer", min = 0 }
101101
login_date = { type = "str_value" }
102102
currency = { type = "enum", values = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, LSL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLE, SLL, SOS, SRD, SSP, STD, STN, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VES, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW, ZWL" }
103103
payment_card_issuer_country = { type = "enum", values = "AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, AZ, BS, BH, BD, BB, BY, BE, BZ, BJ, BM, BT, BO, BQ, BA, BW, BV, BR, IO, BN, BG, BF, BI, KH, CM, CA, CV, KY, CF, TD, CL, CN, CX, CC, CO, KM, CG, CD, CK, CR, CI, HR, CU, CW, CY, CZ, DK, DJ, DM, DO, EC, EG, SV, GQ, ER, EE, ET, FK, FO, FJ, FI, FR, GF, PF, TF, GA, GM, GE, DE, GH, GI, GR, GL, GD, GP, GU, GT, GG, GN, GW, GY, HT, HM, VA, HN, HK, HU, IS, IN, ID, IR, IQ, IE, IM, IL, IT, JM, JP, JE, JO, KZ, KE, KI, KP, KR, KW, KG, LA, LV, LB, LS, LR, LY, LI, LT, LU, MO, MK, MG, MW, MY, MV, ML, MT, MH, MQ, MR, MU, YT, MX, FM, MD, MC, MN, ME, MS, MA, MZ, MM, NA, NR, NP, NL, NC, NZ, NI, NE, NG, NU, NF, MP, NO, OM, PK, PW, PS, PA, PG, PY, PE, PH, PN, PL, PT, PR, QA, RE, RO, RU, RW, BL, SH, KN, LC, MF, PM, VC, WS, SM, ST, SA, SN, RS, SC, SL, SG, SX, SK, SI, SB, SO, ZA, GS, SS, ES, LK, SD, SR, SJ, SZ, SE, CH, SY, TW, TJ, TZ, TH, TL, TG, TK, TO, TT, TN, TR, TM, TC, TV, UG, UA, AE, GB, UM, UY, UZ, VU, VE, VN, VG, VI, WF, EH, YE, ZM, ZW, US" }
104-
card_bin = { type = "str_value" }
105-
extended_card_bin = { type = "str_value" }
104+
105+
card_bin = { type = "str_value", exact_length = 6, regex = "^[0-9]{6}$" }
106+
extended_card_bin = { type = "str_value", exact_length = 8, regex = "^[0-9]{8}$" }
106107
capture_method = { type = "enum", values = "automatic, manual" }
107108
new_customer = { type = "udf" }
108109
udf1 = { type = "str_value" }

src/euclid/errors.rs

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ pub enum EuclidErrors {
4343

4444
#[error("Invalid Sr Dimension Configuration")]
4545
InvalidSrDimensionConfig(String),
46+
47+
#[error("Field validation failed: {0}")]
48+
FieldValidationFailed(String),
49+
}
50+
51+
#[derive(Debug, Clone, serde::Serialize)]
52+
pub struct ValidationErrorDetails {
53+
pub field: String,
54+
pub error_type: String,
55+
pub message: String,
56+
}
57+
58+
impl ValidationErrorDetails {
59+
pub fn new(
60+
field: impl Into<String>,
61+
error_type: impl Into<String>,
62+
message: impl Into<String>,
63+
) -> Self {
64+
Self {
65+
field: field.into(),
66+
error_type: error_type.into(),
67+
message: message.into(),
68+
}
69+
}
4670
}
4771

4872
impl axum::response::IntoResponse for EuclidErrors {
@@ -195,15 +219,25 @@ impl axum::response::IntoResponse for EuclidErrors {
195219
)
196220
.into_response(),
197221

198-
EuclidErrors::InvalidSrDimensionConfig(msg) => (
199-
hyper::StatusCode::BAD_REQUEST,
200-
axum::Json(ApiErrorResponse::new(
201-
error_codes::TE_04,
202-
msg,
203-
None,
204-
)),
205-
)
206-
.into_response(),
222+
EuclidErrors::InvalidSrDimensionConfig(msg) => (
223+
hyper::StatusCode::BAD_REQUEST,
224+
axum::Json(ApiErrorResponse::new(
225+
error_codes::TE_04,
226+
msg,
227+
None,
228+
)),
229+
)
230+
.into_response(),
231+
232+
EuclidErrors::FieldValidationFailed(msg) => (
233+
hyper::StatusCode::BAD_REQUEST,
234+
axum::Json(ApiErrorResponse::new(
235+
error_codes::TE_04,
236+
format!("Field validation failed: {}", msg),
237+
None,
238+
)),
239+
)
240+
.into_response(),
207241
}
208242
}
209243
}

src/euclid/handlers/routing_rules.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
cgraph,
1010
interpreter::{evaluate_output, InterpreterBackend},
1111
types::{
12-
ActivateRoutingConfigRequest, Context, JsonifiedRoutingAlgorithm,
12+
ActivateRoutingConfigRequest, Context, JsonifiedRoutingAlgorithm, KeyDataType,
1313
RoutingAlgorithmMapperNew, RoutingDictionaryRecord, RoutingEvaluateResponse,
1414
RoutingRequest, RoutingRule, SrDimensionConfig, StaticRoutingAlgorithm,
1515
ELIGIBLE_DIMENSIONS,
@@ -145,34 +145,57 @@ pub async fn routing_create(
145145

146146
logger::debug!("Received routing config: {:?}", config);
147147

148-
if let Err(err) = validate_routing_rule(&config, &state.config.routing_config) {
149-
let source = err.get_inner();
148+
match validate_routing_rule(&config, &state.config.routing_config) {
149+
Ok(validation_result) => {
150+
if !validation_result.is_valid {
151+
for error in &validation_result.errors {
152+
logger::error!(
153+
field = %error.field,
154+
error_type = %error.error_type,
155+
message = %error.message,
156+
"Field validation error during routing rule creation"
157+
);
158+
}
159+
160+
let error_details: Vec<serde_json::Value> = validation_result
161+
.errors
162+
.iter()
163+
.map(|e| {
164+
serde_json::json!({
165+
"field": e.field,
166+
"error_type": e.error_type,
167+
"message": e.message,
168+
})
169+
})
170+
.collect();
150171

151-
if let EuclidErrors::FailedToValidateRoutingRule = source {
152-
if let Some(validation_messages) = err.downcast_ref::<Vec<String>>() {
153-
let detailed_error = validation_messages.join("; ");
154-
logger::error!("Routing rule validation failed with errors: {detailed_error}");
172+
let detailed_error = validation_result.to_error_message();
155173

156174
metrics::API_REQUEST_COUNTER
157175
.with_label_values(&["routing_create", "failure"])
158176
.inc();
159177
timer.observe_duration();
178+
160179
return Err(ContainerError::new_with_status_code_and_payload(
161-
EuclidErrors::FailedToValidateRoutingRule,
180+
EuclidErrors::FieldValidationFailed(detailed_error.clone()),
162181
axum::http::StatusCode::BAD_REQUEST,
163182
ApiErrorResponse::new(
164-
"INVALID_REQUEST_DATA",
183+
"FIELD_VALIDATION_FAILED",
165184
format!("Routing rule validation failed: {}", detailed_error),
166-
None,
185+
Some(serde_json::json!({ "validation_errors": error_details })),
167186
),
168187
));
169188
}
189+
logger::debug!("Routing rule validation passed successfully");
190+
}
191+
Err(err) => {
192+
logger::error!(error = ?err, "Failed to validate routing rule configuration");
193+
metrics::API_REQUEST_COUNTER
194+
.with_label_values(&["routing_create", "failure"])
195+
.inc();
196+
timer.observe_duration();
197+
return Err(err.into());
170198
}
171-
metrics::API_REQUEST_COUNTER
172-
.with_label_values(&["routing_create", "failure"])
173-
.inc();
174-
timer.observe_duration();
175-
return Err(err.into());
176199
}
177200

178201
let utc_date_time = time::OffsetDateTime::now_utc();
@@ -306,7 +329,7 @@ pub async fn routing_evaluate(
306329
}
307330

308331
if let Some(key_config) = routing_config.keys.keys.get(key) {
309-
if key_config.data_type == "enum" {
332+
if key_config.data_type == KeyDataType::Enum {
310333
if let Some(Some(ValueType::EnumVariant(value))) = parameters.get(key) {
311334
if !is_valid_enum_value(routing_config, key, value) {
312335
update_failure_metrics();

src/euclid/types.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,105 @@ impl Deref for Context {
268268
}
269269
}
270270

271+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
272+
#[serde(rename_all = "snake_case")]
273+
pub enum KeyDataType {
274+
#[serde(rename = "integer")]
275+
Integer,
276+
#[serde(rename = "enum")]
277+
Enum,
278+
#[serde(rename = "udf")]
279+
Udf,
280+
#[serde(rename = "str_value")]
281+
StrValue,
282+
#[serde(rename = "global_ref")]
283+
GlobalRef,
284+
}
285+
286+
impl KeyDataType {
287+
pub fn as_str(&self) -> &str {
288+
match self {
289+
KeyDataType::Integer => "integer",
290+
KeyDataType::Enum => "enum",
291+
KeyDataType::Udf => "udf",
292+
KeyDataType::StrValue => "str_value",
293+
KeyDataType::GlobalRef => "global_ref",
294+
}
295+
}
296+
}
297+
271298
/// Represents a key configuration in the TOML file
272299
#[derive(Clone, Debug, Deserialize, Serialize)]
273300
pub struct KeyConfig {
274301
#[serde(rename = "type")]
275-
pub data_type: String,
302+
pub data_type: KeyDataType,
276303
#[serde(default)]
277304
pub values: Option<String>,
305+
#[serde(default)]
306+
pub min_value: Option<i64>,
307+
#[serde(default)]
308+
pub max_value: Option<i64>,
309+
#[serde(default)]
310+
pub min_length: Option<usize>,
311+
#[serde(default)]
312+
pub max_length: Option<usize>,
313+
#[serde(default)]
314+
pub exact_length: Option<usize>,
315+
#[serde(default)]
316+
pub regex: Option<String>,
317+
}
318+
319+
impl KeyConfig {
320+
pub fn has_validation_constraints(&self) -> bool {
321+
self.min_value.is_some()
322+
|| self.max_value.is_some()
323+
|| self.min_length.is_some()
324+
|| self.max_length.is_some()
325+
|| self.exact_length.is_some()
326+
|| self.regex.is_some()
327+
}
328+
329+
pub fn build_validation_rules(&self) -> Result<FieldValidationRules, String> {
330+
let regex_pattern = match &self.regex {
331+
Some(pattern) => Some(
332+
regex::Regex::new(pattern)
333+
.map_err(|e| format!("Invalid regex pattern '{}': {}", pattern, e))?,
334+
),
335+
None => None,
336+
};
337+
338+
Ok(FieldValidationRules {
339+
min_value: self.min_value,
340+
max_value: self.max_value,
341+
min_length: self.min_length,
342+
max_length: self.max_length,
343+
exact_length: self.exact_length,
344+
regex_pattern,
345+
})
346+
}
347+
}
348+
349+
#[derive(Clone, Debug)]
350+
pub struct FieldValidationRules {
351+
pub min_value: Option<i64>,
352+
pub max_value: Option<i64>,
353+
pub min_length: Option<usize>,
354+
pub max_length: Option<usize>,
355+
pub exact_length: Option<usize>,
356+
pub regex_pattern: Option<regex::Regex>,
357+
}
358+
359+
impl Default for FieldValidationRules {
360+
fn default() -> Self {
361+
Self {
362+
min_value: None,
363+
max_value: None,
364+
min_length: None,
365+
max_length: None,
366+
exact_length: None,
367+
regex_pattern: None,
368+
}
369+
}
278370
}
279371

280372
/// Structure for the [keys] section in the TOML

0 commit comments

Comments
 (0)