feat(router): Add Smart Routing to route payments efficiently (#2665)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shashank_attarde <shashank.attarde@juspay.in>
Co-authored-by: Aprabhat19 <amishaprabhat@gmail.com>
Co-authored-by: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2023-11-03 18:37:31 +05:30
committed by GitHub
parent 6c5de9cee4
commit 9b618d2447
96 changed files with 15376 additions and 233 deletions

View File

@ -0,0 +1,447 @@
//! Static Analysis for the Euclid Rule DSL
//!
//! Exposes certain functions that can be used to perform static analysis over programs
//! in the Euclid Rule DSL. These include standard control flow analyses like testing
//! conflicting assertions, to Domain Specific Analyses making use of the
//! [`Knowledge Graph Framework`](crate::dssa::graph).
use rustc_hash::{FxHashMap, FxHashSet};
use super::{graph::Memoization, types::EuclidAnalysable};
use crate::{
dssa::{graph, state_machine, truth, types},
frontend::{
ast,
dir::{self, EuclidDirFilter},
vir,
},
types::{DataType, Metadata},
};
/// Analyses conflicting assertions on the same key in a conjunctive context.
///
/// For example,
/// ```notrust
/// payment_method = card && ... && payment_method = bank_debit
/// ```notrust
/// This is a condition that will never evaluate to `true` given a single
/// payment method and needs to be caught in analysis.
pub fn analyze_conflicting_assertions(
keywise_assertions: &FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>>,
assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>,
) -> Result<(), types::AnalysisError> {
for (key, value_set) in keywise_assertions {
if value_set.len() > 1 {
let err_type = types::AnalysisErrorType::ConflictingAssertions {
key: key.clone(),
values: value_set
.iter()
.map(|val| types::ValueData {
value: (*val).clone(),
metadata: assertion_metadata
.get(val)
.map(|meta| (*meta).clone())
.unwrap_or_default(),
})
.collect(),
};
Err(types::AnalysisError {
error_type: err_type,
metadata: Default::default(),
})?;
}
}
Ok(())
}
/// Analyses exhaustive negations on the same key in a conjunctive context.
///
/// For example,
/// ```notrust
/// authentication_type /= three_ds && ... && authentication_type /= no_three_ds
/// ```notrust
/// This is a condition that will never evaluate to `true` given any authentication_type
/// since all the possible values authentication_type can take have been negated.
pub fn analyze_exhaustive_negations(
keywise_negations: &FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>>,
keywise_negation_metadata: &FxHashMap<dir::DirKey, Vec<&Metadata>>,
) -> Result<(), types::AnalysisError> {
for (key, negation_set) in keywise_negations {
let mut value_set = if let Some(set) = key.kind.get_value_set() {
set
} else {
continue;
};
value_set.retain(|val| !negation_set.contains(val));
if value_set.is_empty() {
let error_type = types::AnalysisErrorType::ExhaustiveNegation {
key: key.clone(),
metadata: keywise_negation_metadata
.get(key)
.cloned()
.unwrap_or_default()
.iter()
.cloned()
.cloned()
.collect(),
};
Err(types::AnalysisError {
error_type,
metadata: Default::default(),
})?;
}
}
Ok(())
}
fn analyze_negated_assertions(
keywise_assertions: &FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>>,
assertion_metadata: &FxHashMap<&dir::DirValue, &Metadata>,
keywise_negations: &FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>>,
negation_metadata: &FxHashMap<&dir::DirValue, &Metadata>,
) -> Result<(), types::AnalysisError> {
for (key, negation_set) in keywise_negations {
let assertion_set = if let Some(set) = keywise_assertions.get(key) {
set
} else {
continue;
};
let intersection = negation_set & assertion_set;
intersection.iter().next().map_or(Ok(()), |val| {
let error_type = types::AnalysisErrorType::NegatedAssertion {
value: (*val).clone(),
assertion_metadata: assertion_metadata
.get(*val)
.cloned()
.cloned()
.unwrap_or_default(),
negation_metadata: negation_metadata
.get(*val)
.cloned()
.cloned()
.unwrap_or_default(),
};
Err(types::AnalysisError {
error_type,
metadata: Default::default(),
})
})?;
}
Ok(())
}
fn perform_condition_analyses(
context: &types::ConjunctiveContext<'_>,
) -> Result<(), types::AnalysisError> {
let mut assertion_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default();
let mut keywise_assertions: FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>> =
FxHashMap::default();
let mut negation_metadata: FxHashMap<&dir::DirValue, &Metadata> = FxHashMap::default();
let mut keywise_negation_metadata: FxHashMap<dir::DirKey, Vec<&Metadata>> =
FxHashMap::default();
let mut keywise_negations: FxHashMap<dir::DirKey, FxHashSet<&dir::DirValue>> =
FxHashMap::default();
for ctx_val in context {
let key = if let Some(k) = ctx_val.value.get_key() {
k
} else {
continue;
};
if let dir::DirKeyKind::Connector = key.kind {
continue;
}
if !matches!(key.kind.get_type(), DataType::EnumVariant) {
continue;
}
match ctx_val.value {
types::CtxValueKind::Assertion(val) => {
keywise_assertions
.entry(key.clone())
.or_default()
.insert(val);
assertion_metadata.insert(val, ctx_val.metadata);
}
types::CtxValueKind::Negation(vals) => {
let negation_set = keywise_negations.entry(key.clone()).or_default();
for val in vals {
negation_set.insert(val);
negation_metadata.insert(val, ctx_val.metadata);
}
keywise_negation_metadata
.entry(key.clone())
.or_default()
.push(ctx_val.metadata);
}
}
}
analyze_conflicting_assertions(&keywise_assertions, &assertion_metadata)?;
analyze_exhaustive_negations(&keywise_negations, &keywise_negation_metadata)?;
analyze_negated_assertions(
&keywise_assertions,
&assertion_metadata,
&keywise_negations,
&negation_metadata,
)?;
Ok(())
}
fn perform_context_analyses(
context: &types::ConjunctiveContext<'_>,
knowledge_graph: &graph::KnowledgeGraph<'_>,
) -> Result<(), types::AnalysisError> {
perform_condition_analyses(context)?;
let mut memo = Memoization::new();
knowledge_graph
.perform_context_analysis(context, &mut memo)
.map_err(|err| types::AnalysisError {
error_type: types::AnalysisErrorType::GraphAnalysis(err, memo),
metadata: Default::default(),
})?;
Ok(())
}
pub fn analyze<O: EuclidAnalysable + EuclidDirFilter>(
program: ast::Program<O>,
knowledge_graph: Option<&graph::KnowledgeGraph<'_>>,
) -> Result<vir::ValuedProgram<O>, types::AnalysisError> {
let dir_program = ast::lowering::lower_program(program)?;
let selection_data = state_machine::make_connector_selection_data(&dir_program);
let mut ctx_manager = state_machine::AnalysisContextManager::new(&dir_program, &selection_data);
while let Some(ctx) = ctx_manager.advance().map_err(|err| types::AnalysisError {
metadata: Default::default(),
error_type: types::AnalysisErrorType::StateMachine(err),
})? {
perform_context_analyses(ctx, knowledge_graph.unwrap_or(&truth::ANALYSIS_GRAPH))?;
}
dir::lowering::lower_program(dir_program)
}
#[cfg(all(test, feature = "ast_parser"))]
mod tests {
#![allow(clippy::panic, clippy::expect_used)]
use std::{ops::Deref, sync::Weak};
use euclid_macros::knowledge;
use super::*;
use crate::{dirval, types::DummyOutput};
#[test]
fn test_conflicting_assertion_detection() {
let program_str = r#"
default: ["stripe", "adyen"]
stripe_first: ["stripe", "adyen"]
{
payment_method = wallet {
amount > 500 & capture_method = automatic
amount < 500 & payment_method = card
}
}
"#;
let (_, program) = ast::parser::program::<DummyOutput>(program_str).expect("Program");
let analysis_result = analyze(program, None);
if let Err(types::AnalysisError {
error_type: types::AnalysisErrorType::ConflictingAssertions { key, values },
..
}) = analysis_result
{
assert!(
matches!(key.kind, dir::DirKeyKind::PaymentMethod),
"Key should be payment_method"
);
let values: Vec<dir::DirValue> = values.into_iter().map(|v| v.value).collect();
assert_eq!(values.len(), 2, "There should be 2 conflicting conditions");
assert!(
values.contains(&dirval!(PaymentMethod = Wallet)),
"Condition should include payment_method = wallet"
);
assert!(
values.contains(&dirval!(PaymentMethod = Card)),
"Condition should include payment_method = card"
);
} else {
panic!("Did not receive conflicting assertions error");
}
}
#[test]
fn test_exhaustive_negation_detection() {
let program_str = r#"
default: ["stripe"]
rule_1: ["adyen"]
{
payment_method /= wallet {
capture_method = manual & payment_method /= card {
authentication_type = three_ds & payment_method /= pay_later {
amount > 1000 & payment_method /= bank_redirect {
payment_method /= crypto
& payment_method /= bank_debit
& payment_method /= bank_transfer
& payment_method /= upi
& payment_method /= reward
& payment_method /= voucher
& payment_method /= gift_card
}
}
}
}
}
"#;
let (_, program) = ast::parser::program::<DummyOutput>(program_str).expect("Program");
let analysis_result = analyze(program, None);
if let Err(types::AnalysisError {
error_type: types::AnalysisErrorType::ExhaustiveNegation { key, .. },
..
}) = analysis_result
{
assert!(
matches!(key.kind, dir::DirKeyKind::PaymentMethod),
"Expected key to be payment_method"
);
} else {
panic!("Expected exhaustive negation error");
}
}
#[test]
fn test_negated_assertions_detection() {
let program_str = r#"
default: ["stripe"]
rule_1: ["adyen"]
{
payment_method = wallet {
amount > 500 {
capture_method = automatic
}
amount < 501 {
payment_method /= wallet
}
}
}
"#;
let (_, program) = ast::parser::program::<DummyOutput>(program_str).expect("Program");
let analysis_result = analyze(program, None);
if let Err(types::AnalysisError {
error_type: types::AnalysisErrorType::NegatedAssertion { value, .. },
..
}) = analysis_result
{
assert_eq!(
value,
dirval!(PaymentMethod = Wallet),
"Expected to catch payment_method = wallet as conflict"
);
} else {
panic!("Expected negated assertion error");
}
}
#[test]
fn test_negation_graph_analysis() {
let graph = knowledge! {crate
CaptureMethod(Automatic) ->> PaymentMethod(Card);
};
let program_str = r#"
default: ["stripe"]
rule_1: ["adyen"]
{
amount > 500 {
payment_method = pay_later
}
amount < 500 {
payment_method /= wallet & payment_method /= pay_later
}
}
"#;
let (_, program) = ast::parser::program::<DummyOutput>(program_str).expect("Graph");
let analysis_result = analyze(program, Some(&graph));
let error_type = match analysis_result {
Err(types::AnalysisError { error_type, .. }) => error_type,
_ => panic!("Error_type not found"),
};
let a_err = match error_type {
types::AnalysisErrorType::GraphAnalysis(trace, memo) => (trace, memo),
_ => panic!("Graph Analysis not found"),
};
let (trace, metadata) = match a_err.0 {
graph::AnalysisError::NegationTrace { trace, metadata } => (trace, metadata),
_ => panic!("Negation Trace not found"),
};
let predecessor = match Weak::upgrade(&trace)
.expect("Expected Arc not found")
.deref()
.clone()
{
graph::AnalysisTrace::Value { predecessors, .. } => {
let _value = graph::NodeValue::Value(dir::DirValue::PaymentMethod(
dir::enums::PaymentMethod::Card,
));
let _relation = graph::Relation::Positive;
predecessors
}
_ => panic!("Expected Negation Trace for payment method = card"),
};
let pred = match predecessor {
Some(graph::ValueTracePredecessor::Mandatory(predecessor)) => predecessor,
_ => panic!("No predecessor found"),
};
assert_eq!(
metadata.len(),
2,
"Expected two metadats for wallet and pay_later"
);
assert!(matches!(
*Weak::upgrade(&pred)
.expect("Expected Arc not found")
.deref(),
graph::AnalysisTrace::Value {
value: graph::NodeValue::Value(dir::DirValue::CaptureMethod(
dir::enums::CaptureMethod::Automatic
)),
relation: graph::Relation::Positive,
info: None,
metadata: None,
predecessors: None,
}
));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,714 @@
use super::types::EuclidAnalysable;
use crate::{dssa::types, frontend::dir, types::Metadata};
#[derive(Debug, Clone, serde::Serialize, thiserror::Error)]
#[serde(tag = "type", content = "info", rename_all = "snake_case")]
pub enum StateMachineError {
#[error("Index out of bounds: {0}")]
IndexOutOfBounds(&'static str),
}
#[derive(Debug)]
struct ComparisonStateMachine<'a> {
values: &'a [dir::DirValue],
logic: &'a dir::DirComparisonLogic,
metadata: &'a Metadata,
count: usize,
ctx_idx: usize,
}
impl<'a> ComparisonStateMachine<'a> {
#[inline]
fn is_finished(&self) -> bool {
self.count + 1 >= self.values.len()
|| matches!(self.logic, dir::DirComparisonLogic::NegativeConjunction)
}
#[inline]
fn advance(&mut self) {
if let dir::DirComparisonLogic::PositiveDisjunction = self.logic {
self.count = (self.count + 1) % self.values.len();
}
}
#[inline]
fn reset(&mut self) {
self.count = 0;
}
#[inline]
fn put(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> {
if let dir::DirComparisonLogic::PositiveDisjunction = self.logic {
*context
.get_mut(self.ctx_idx)
.ok_or(StateMachineError::IndexOutOfBounds(
"in ComparisonStateMachine while indexing into context",
))? = types::ContextValue::assertion(
self.values
.get(self.count)
.ok_or(StateMachineError::IndexOutOfBounds(
"in ComparisonStateMachine while indexing into values",
))?,
self.metadata,
);
}
Ok(())
}
#[inline]
fn push(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> {
match self.logic {
dir::DirComparisonLogic::PositiveDisjunction => {
context.push(types::ContextValue::assertion(
self.values
.get(self.count)
.ok_or(StateMachineError::IndexOutOfBounds(
"in ComparisonStateMachine while pushing",
))?,
self.metadata,
));
}
dir::DirComparisonLogic::NegativeConjunction => {
context.push(types::ContextValue::negation(self.values, self.metadata));
}
}
Ok(())
}
}
#[derive(Debug)]
struct ConditionStateMachine<'a> {
state_machines: Vec<ComparisonStateMachine<'a>>,
start_ctx_idx: usize,
}
impl<'a> ConditionStateMachine<'a> {
fn new(condition: &'a [dir::DirComparison], start_idx: usize) -> Self {
let mut machines = Vec::<ComparisonStateMachine<'a>>::with_capacity(condition.len());
let mut machine_idx = start_idx;
for cond in condition {
let machine = ComparisonStateMachine {
values: &cond.values,
logic: &cond.logic,
metadata: &cond.metadata,
count: 0,
ctx_idx: machine_idx,
};
machines.push(machine);
machine_idx += 1;
}
Self {
state_machines: machines,
start_ctx_idx: start_idx,
}
}
fn init(&self, context: &mut types::ConjunctiveContext<'a>) -> Result<(), StateMachineError> {
for machine in &self.state_machines {
machine.push(context)?;
}
Ok(())
}
#[inline]
fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) {
context.truncate(self.start_ctx_idx);
}
#[inline]
fn is_finished(&self) -> bool {
!self
.state_machines
.iter()
.any(|machine| !machine.is_finished())
}
#[inline]
fn get_next_ctx_idx(&self) -> usize {
self.start_ctx_idx + self.state_machines.len()
}
fn advance(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
for machine in self.state_machines.iter_mut().rev() {
if machine.is_finished() {
machine.reset();
machine.put(context)?;
} else {
machine.advance();
machine.put(context)?;
break;
}
}
Ok(())
}
}
#[derive(Debug)]
struct IfStmtStateMachine<'a> {
condition_machine: ConditionStateMachine<'a>,
nested: Vec<&'a dir::DirIfStatement>,
nested_idx: usize,
}
impl<'a> IfStmtStateMachine<'a> {
fn new(stmt: &'a dir::DirIfStatement, ctx_start_idx: usize) -> Self {
let condition_machine = ConditionStateMachine::new(&stmt.condition, ctx_start_idx);
let nested: Vec<&'a dir::DirIfStatement> = match &stmt.nested {
None => Vec::new(),
Some(nested_stmts) => nested_stmts.iter().collect(),
};
Self {
condition_machine,
nested,
nested_idx: 0,
}
}
fn init(
&self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<Option<Self>, StateMachineError> {
self.condition_machine.init(context)?;
Ok(self
.nested
.first()
.map(|nested| Self::new(nested, self.condition_machine.get_next_ctx_idx())))
}
#[inline]
fn is_finished(&self) -> bool {
self.nested_idx + 1 >= self.nested.len()
}
#[inline]
fn is_condition_machine_finished(&self) -> bool {
self.condition_machine.is_finished()
}
#[inline]
fn destroy(&self, context: &mut types::ConjunctiveContext<'a>) {
self.condition_machine.destroy(context);
}
#[inline]
fn advance_condition_machine(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
self.condition_machine.advance(context)?;
Ok(())
}
fn advance(&mut self) -> Result<Option<Self>, StateMachineError> {
if self.nested.is_empty() {
Ok(None)
} else {
self.nested_idx = (self.nested_idx + 1) % self.nested.len();
Ok(Some(Self::new(
self.nested
.get(self.nested_idx)
.ok_or(StateMachineError::IndexOutOfBounds(
"in IfStmtStateMachine while advancing",
))?,
self.condition_machine.get_next_ctx_idx(),
)))
}
}
}
#[derive(Debug)]
struct RuleStateMachine<'a> {
connector_selection_data: &'a [(dir::DirValue, Metadata)],
connectors_added: bool,
if_stmt_machines: Vec<IfStmtStateMachine<'a>>,
running_stack: Vec<IfStmtStateMachine<'a>>,
}
impl<'a> RuleStateMachine<'a> {
fn new<O>(
rule: &'a dir::DirRule<O>,
connector_selection_data: &'a [(dir::DirValue, Metadata)],
) -> Self {
let mut if_stmt_machines: Vec<IfStmtStateMachine<'a>> =
Vec::with_capacity(rule.statements.len());
for stmt in rule.statements.iter().rev() {
if_stmt_machines.push(IfStmtStateMachine::new(
stmt,
connector_selection_data.len(),
));
}
Self {
connector_selection_data,
connectors_added: false,
if_stmt_machines,
running_stack: Vec::new(),
}
}
fn is_finished(&self) -> bool {
self.if_stmt_machines.is_empty() && self.running_stack.is_empty()
}
fn init_next(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
if self.if_stmt_machines.is_empty() || !self.running_stack.is_empty() {
return Ok(());
}
if !self.connectors_added {
for (dir_val, metadata) in self.connector_selection_data {
context.push(types::ContextValue::assertion(dir_val, metadata));
}
self.connectors_added = true;
}
context.truncate(self.connector_selection_data.len());
if let Some(mut next_running) = self.if_stmt_machines.pop() {
while let Some(nested_running) = next_running.init(context)? {
self.running_stack.push(next_running);
next_running = nested_running;
}
self.running_stack.push(next_running);
}
Ok(())
}
fn advance(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
let mut condition_machines_finished = true;
for stmt_machine in self.running_stack.iter_mut().rev() {
if !stmt_machine.is_condition_machine_finished() {
condition_machines_finished = false;
stmt_machine.advance_condition_machine(context)?;
break;
} else {
stmt_machine.advance_condition_machine(context)?;
}
}
if !condition_machines_finished {
return Ok(());
}
let mut maybe_next_running: Option<IfStmtStateMachine<'a>> = None;
while let Some(last) = self.running_stack.last_mut() {
if !last.is_finished() {
maybe_next_running = last.advance()?;
break;
} else {
last.destroy(context);
self.running_stack.pop();
}
}
if let Some(mut next_running) = maybe_next_running {
while let Some(nested_running) = next_running.init(context)? {
self.running_stack.push(next_running);
next_running = nested_running;
}
self.running_stack.push(next_running);
} else {
self.init_next(context)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct RuleContextManager<'a> {
context: types::ConjunctiveContext<'a>,
machine: RuleStateMachine<'a>,
init: bool,
}
impl<'a> RuleContextManager<'a> {
pub fn new<O>(
rule: &'a dir::DirRule<O>,
connector_selection_data: &'a [(dir::DirValue, Metadata)],
) -> Self {
Self {
context: Vec::new(),
machine: RuleStateMachine::new(rule, connector_selection_data),
init: false,
}
}
pub fn advance(&mut self) -> Result<Option<&types::ConjunctiveContext<'a>>, StateMachineError> {
if !self.init {
self.init = true;
self.machine.init_next(&mut self.context)?;
Ok(Some(&self.context))
} else if self.machine.is_finished() {
Ok(None)
} else {
self.machine.advance(&mut self.context)?;
if self.machine.is_finished() {
Ok(None)
} else {
Ok(Some(&self.context))
}
}
}
pub fn advance_mut(
&mut self,
) -> Result<Option<&mut types::ConjunctiveContext<'a>>, StateMachineError> {
if !self.init {
self.init = true;
self.machine.init_next(&mut self.context)?;
Ok(Some(&mut self.context))
} else if self.machine.is_finished() {
Ok(None)
} else {
self.machine.advance(&mut self.context)?;
if self.machine.is_finished() {
Ok(None)
} else {
Ok(Some(&mut self.context))
}
}
}
}
#[derive(Debug)]
pub struct ProgramStateMachine<'a> {
rule_machines: Vec<RuleStateMachine<'a>>,
current_rule_machine: Option<RuleStateMachine<'a>>,
is_init: bool,
}
impl<'a> ProgramStateMachine<'a> {
pub fn new<O>(
program: &'a dir::DirProgram<O>,
connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>],
) -> Self {
let mut rule_machines: Vec<RuleStateMachine<'a>> = program
.rules
.iter()
.zip(connector_selection_data.iter())
.rev()
.map(|(rule, connector_selection_data)| {
RuleStateMachine::new(rule, connector_selection_data)
})
.collect();
Self {
current_rule_machine: rule_machines.pop(),
rule_machines,
is_init: false,
}
}
pub fn is_finished(&self) -> bool {
self.current_rule_machine
.as_ref()
.map_or(true, |rsm| rsm.is_finished())
&& self.rule_machines.is_empty()
}
pub fn init(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
if !self.is_init {
if let Some(rsm) = self.current_rule_machine.as_mut() {
rsm.init_next(context)?;
}
self.is_init = true;
}
Ok(())
}
pub fn advance(
&mut self,
context: &mut types::ConjunctiveContext<'a>,
) -> Result<(), StateMachineError> {
if self
.current_rule_machine
.as_ref()
.map_or(true, |rsm| rsm.is_finished())
{
self.current_rule_machine = self.rule_machines.pop();
context.clear();
if let Some(rsm) = self.current_rule_machine.as_mut() {
rsm.init_next(context)?;
}
} else if let Some(rsm) = self.current_rule_machine.as_mut() {
rsm.advance(context)?;
}
Ok(())
}
}
pub struct AnalysisContextManager<'a> {
context: types::ConjunctiveContext<'a>,
machine: ProgramStateMachine<'a>,
init: bool,
}
impl<'a> AnalysisContextManager<'a> {
pub fn new<O>(
program: &'a dir::DirProgram<O>,
connector_selection_data: &'a [Vec<(dir::DirValue, Metadata)>],
) -> Self {
let machine = ProgramStateMachine::new(program, connector_selection_data);
let context: types::ConjunctiveContext<'a> = Vec::new();
Self {
context,
machine,
init: false,
}
}
pub fn advance(&mut self) -> Result<Option<&types::ConjunctiveContext<'a>>, StateMachineError> {
if !self.init {
self.init = true;
self.machine.init(&mut self.context)?;
Ok(Some(&self.context))
} else if self.machine.is_finished() {
Ok(None)
} else {
self.machine.advance(&mut self.context)?;
if self.machine.is_finished() {
Ok(None)
} else {
Ok(Some(&self.context))
}
}
}
}
pub fn make_connector_selection_data<O: EuclidAnalysable>(
program: &dir::DirProgram<O>,
) -> Vec<Vec<(dir::DirValue, Metadata)>> {
program
.rules
.iter()
.map(|rule| {
rule.connector_selection
.get_dir_value_for_analysis(rule.name.clone())
})
.collect()
}
#[cfg(all(test, feature = "ast_parser"))]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use crate::{dirval, frontend::ast, types::DummyOutput};
#[test]
fn test_correct_contexts() {
let program_str = r#"
default: ["stripe", "adyen"]
stripe_first: ["stripe", "adyen"]
{
payment_method = wallet {
payment_method = (card, bank_redirect) {
currency = USD
currency = GBP
}
payment_method = pay_later {
capture_method = automatic
capture_method = manual
}
}
payment_method = card {
payment_method = (card, bank_redirect) & capture_method = (automatic, manual) {
currency = (USD, GBP)
}
}
}
"#;
let (_, program) = ast::parser::program::<DummyOutput>(program_str).expect("Program");
let lowered = ast::lowering::lower_program(program).expect("Lowering");
let selection_data = make_connector_selection_data(&lowered);
let mut state_machine = ProgramStateMachine::new(&lowered, &selection_data);
let mut ctx: types::ConjunctiveContext<'_> = Vec::new();
state_machine.init(&mut ctx).expect("State machine init");
let expected_contexts: Vec<Vec<dir::DirValue>> = vec![
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = Card),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = BankRedirect),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = Card),
dirval!(PaymentCurrency = GBP),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = BankRedirect),
dirval!(PaymentCurrency = GBP),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = PayLater),
dirval!(CaptureMethod = Automatic),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Wallet),
dirval!(PaymentMethod = PayLater),
dirval!(CaptureMethod = Manual),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = Card),
dirval!(CaptureMethod = Automatic),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = Card),
dirval!(CaptureMethod = Automatic),
dirval!(PaymentCurrency = GBP),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = Card),
dirval!(CaptureMethod = Manual),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = Card),
dirval!(CaptureMethod = Manual),
dirval!(PaymentCurrency = GBP),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = BankRedirect),
dirval!(CaptureMethod = Automatic),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = BankRedirect),
dirval!(CaptureMethod = Automatic),
dirval!(PaymentCurrency = GBP),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = BankRedirect),
dirval!(CaptureMethod = Manual),
dirval!(PaymentCurrency = USD),
],
vec![
dirval!("MetadataKey" = "stripe"),
dirval!("MetadataKey" = "adyen"),
dirval!(PaymentMethod = Card),
dirval!(PaymentMethod = BankRedirect),
dirval!(CaptureMethod = Manual),
dirval!(PaymentCurrency = GBP),
],
];
let mut expected_idx = 0usize;
while !state_machine.is_finished() {
let values = ctx
.iter()
.flat_map(|c| match c.value {
types::CtxValueKind::Assertion(val) => vec![val],
types::CtxValueKind::Negation(vals) => vals.iter().collect(),
})
.collect::<Vec<&dir::DirValue>>();
assert_eq!(
values,
expected_contexts[expected_idx]
.iter()
.collect::<Vec<&dir::DirValue>>()
);
expected_idx += 1;
state_machine
.advance(&mut ctx)
.expect("State Machine advance");
}
assert_eq!(expected_idx, 14);
let mut ctx_manager = AnalysisContextManager::new(&lowered, &selection_data);
expected_idx = 0;
while let Some(ctx) = ctx_manager.advance().expect("Context Manager Context") {
let values = ctx
.iter()
.flat_map(|c| match c.value {
types::CtxValueKind::Assertion(val) => vec![val],
types::CtxValueKind::Negation(vals) => vals.iter().collect(),
})
.collect::<Vec<&dir::DirValue>>();
assert_eq!(
values,
expected_contexts[expected_idx]
.iter()
.collect::<Vec<&dir::DirValue>>()
);
expected_idx += 1;
}
assert_eq!(expected_idx, 14);
}
}

View File

@ -0,0 +1,29 @@
use euclid_macros::knowledge;
use once_cell::sync::Lazy;
use crate::dssa::graph;
pub static ANALYSIS_GRAPH: Lazy<graph::KnowledgeGraph<'_>> = Lazy::new(|| {
knowledge! {crate
// Payment Method should be `Card` for a CardType to be present
PaymentMethod(Card) ->> CardType(any);
// Payment Method should be `PayLater` for a PayLaterType to be present
PaymentMethod(PayLater) ->> PayLaterType(any);
// Payment Method should be `Wallet` for a WalletType to be present
PaymentMethod(Wallet) ->> WalletType(any);
// Payment Method should be `BankRedirect` for a BankRedirectType to
// be present
PaymentMethod(BankRedirect) ->> BankRedirectType(any);
// Payment Method should be `BankTransfer` for a BankTransferType to
// be present
PaymentMethod(BankTransfer) ->> BankTransferType(any);
// Payment Method should be `GiftCard` for a GiftCardType to
// be present
PaymentMethod(GiftCard) ->> GiftCardType(any);
}
});

View File

@ -0,0 +1,158 @@
use std::fmt;
use serde::Serialize;
use crate::{
dssa::{self, graph},
frontend::{ast, dir},
types::{DataType, EuclidValue, Metadata},
};
pub trait EuclidAnalysable: Sized {
fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(dir::DirValue, Metadata)>;
}
#[derive(Debug, Clone)]
pub enum CtxValueKind<'a> {
Assertion(&'a dir::DirValue),
Negation(&'a [dir::DirValue]),
}
impl<'a> CtxValueKind<'a> {
pub fn get_assertion(&self) -> Option<&dir::DirValue> {
if let Self::Assertion(val) = self {
Some(val)
} else {
None
}
}
pub fn get_negation(&self) -> Option<&[dir::DirValue]> {
if let Self::Negation(vals) = self {
Some(vals)
} else {
None
}
}
pub fn get_key(&self) -> Option<dir::DirKey> {
match self {
Self::Assertion(val) => Some(val.get_key()),
Self::Negation(vals) => vals.first().map(|v| (*v).get_key()),
}
}
}
#[derive(Debug, Clone)]
pub struct ContextValue<'a> {
pub value: CtxValueKind<'a>,
pub metadata: &'a Metadata,
}
impl<'a> ContextValue<'a> {
#[inline]
pub fn assertion(value: &'a dir::DirValue, metadata: &'a Metadata) -> Self {
Self {
value: CtxValueKind::Assertion(value),
metadata,
}
}
#[inline]
pub fn negation(values: &'a [dir::DirValue], metadata: &'a Metadata) -> Self {
Self {
value: CtxValueKind::Negation(values),
metadata,
}
}
}
pub type ConjunctiveContext<'a> = Vec<ContextValue<'a>>;
#[derive(Clone, Serialize)]
pub enum AnalyzeResult {
AllOk,
}
#[derive(Debug, Clone, Serialize, thiserror::Error)]
pub struct AnalysisError {
#[serde(flatten)]
pub error_type: AnalysisErrorType,
pub metadata: Metadata,
}
impl fmt::Display for AnalysisError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error_type.fmt(f)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ValueData {
pub value: dir::DirValue,
pub metadata: Metadata,
}
#[derive(Debug, Clone, Serialize, thiserror::Error)]
#[serde(tag = "type", content = "info", rename_all = "snake_case")]
pub enum AnalysisErrorType {
#[error("Invalid program key given: '{0}'")]
InvalidKey(String),
#[error("Invalid variant '{got}' received for key '{key}'")]
InvalidVariant {
key: String,
expected: Vec<String>,
got: String,
},
#[error(
"Invalid data type for value '{}' (expected {expected}, got {got})",
key
)]
InvalidType {
key: String,
expected: DataType,
got: DataType,
},
#[error("Invalid comparison '{operator:?}' for value type {value_type}")]
InvalidComparison {
operator: ast::ComparisonType,
value_type: DataType,
},
#[error("Invalid value received for length as '{value}: {:?}'", message)]
InvalidValue {
key: dir::DirKeyKind,
value: String,
message: Option<String>,
},
#[error("Conflicting assertions received for key '{}'", .key.kind)]
ConflictingAssertions {
key: dir::DirKey,
values: Vec<ValueData>,
},
#[error("Key '{}' exhaustively negated", .key.kind)]
ExhaustiveNegation {
key: dir::DirKey,
metadata: Vec<Metadata>,
},
#[error("The condition '{value}' was asserted and negated in the same condition")]
NegatedAssertion {
value: dir::DirValue,
assertion_metadata: Metadata,
negation_metadata: Metadata,
},
#[error("Graph analysis error: {0:#?}")]
GraphAnalysis(graph::AnalysisError, graph::Memoization),
#[error("State machine error")]
StateMachine(dssa::state_machine::StateMachineError),
#[error("Unsupported program key '{0}'")]
UnsupportedProgramKey(dir::DirKeyKind),
#[error("Ran into an unimplemented feature")]
NotImplemented,
#[error("The payment method type is not supported under the payment method")]
NotSupported,
}
#[derive(Debug, Clone)]
pub enum ValueType {
EnumVariants(Vec<EuclidValue>),
Number,
}

View File

@ -0,0 +1 @@
pub struct Unpacker;