tandem-server 0.6.5

HTTP server for Tandem engine APIs
use serde_json::Value;

use crate::automation_v2::types::{
    AutomationWebhookFeedbackLoopDecision, AutomationWebhookTriggerRecord,
};
use crate::ExternalActionRecord;

use super::{
    automation_webhook_feedback_decision_from_action, AppState,
    AutomationWebhookFeedbackLoopCandidate,
};

impl AppState {
    pub(crate) async fn classify_automation_webhook_feedback_loop(
        &self,
        trigger: &AutomationWebhookTriggerRecord,
        candidate: Option<&AutomationWebhookFeedbackLoopCandidate>,
    ) -> Option<AutomationWebhookFeedbackLoopDecision> {
        let candidate = candidate.filter(|candidate| !candidate.is_empty())?;
        let action = self
            .feedback_loop_candidate_action(candidate)
            .await
            .filter(|action| self.feedback_loop_candidate_matches_action(candidate, action))?;
        if !self
            .external_action_matches_webhook_tenant(trigger, &action, candidate)
            .await
        {
            return None;
        }
        Some(automation_webhook_feedback_decision_from_action(
            &action, candidate,
        ))
    }

    async fn feedback_loop_candidate_action(
        &self,
        candidate: &AutomationWebhookFeedbackLoopCandidate,
    ) -> Option<ExternalActionRecord> {
        if let Some(key) = candidate
            .source_idempotency_key
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        {
            if let Some(action) = self.get_external_action_by_idempotency_key(key).await {
                return Some(action);
            }
        }
        let action_id = candidate
            .source_action_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())?;
        self.get_external_action(action_id).await
    }

    fn feedback_loop_candidate_matches_action(
        &self,
        candidate: &AutomationWebhookFeedbackLoopCandidate,
        action: &ExternalActionRecord,
    ) -> bool {
        if let Some(candidate_action_id) = candidate.source_action_id.as_deref() {
            if candidate_action_id != action.action_id {
                return false;
            }
        }
        if let (Some(candidate_key), Some(action_key)) = (
            candidate.source_idempotency_key.as_deref(),
            action.idempotency_key.as_deref(),
        ) {
            if candidate_key.trim() != action_key.trim() {
                return false;
            }
        }
        if let Some(candidate_run_id) = candidate.source_run_id.as_deref() {
            if let Some(action_run_id) = external_action_run_id(action) {
                if candidate_run_id != action_run_id {
                    return false;
                }
            }
        }
        if let Some(candidate_node_id) = candidate.source_node_id.as_deref() {
            if let Some(action_node_id) = external_action_node_id(action) {
                if candidate_node_id != action_node_id {
                    return false;
                }
            }
        }
        if let (Some(resource_id), Some(target)) = (
            candidate.provider_resource_id.as_deref(),
            action.target.as_deref(),
        ) {
            if !target.contains(resource_id) {
                return false;
            }
        }
        true
    }

    async fn external_action_matches_webhook_tenant(
        &self,
        trigger: &AutomationWebhookTriggerRecord,
        action: &ExternalActionRecord,
        candidate: &AutomationWebhookFeedbackLoopCandidate,
    ) -> bool {
        if let Some(tenant_context) = external_action_tenant_context(action) {
            return tenant_context.org_id == trigger.tenant_context.org_id
                && tenant_context.workspace_id == trigger.tenant_context.workspace_id
                && tenant_context.deployment_id == trigger.tenant_context.deployment_id;
        }

        let run_id = candidate
            .source_run_id
            .as_deref()
            .map(ToOwned::to_owned)
            .or_else(|| external_action_run_id(action));
        let Some(run_id) = run_id else {
            return false;
        };
        self.get_automation_v2_run(&run_id)
            .await
            .is_some_and(|run| {
                run.tenant_context.org_id == trigger.tenant_context.org_id
                    && run.tenant_context.workspace_id == trigger.tenant_context.workspace_id
                    && run.tenant_context.deployment_id == trigger.tenant_context.deployment_id
            })
    }
}

fn external_action_metadata_value<'a>(
    action: &'a ExternalActionRecord,
    key: &str,
) -> Option<&'a Value> {
    action.metadata.as_ref()?.get(key)
}

fn external_action_metadata_string(action: &ExternalActionRecord, key: &str) -> Option<String> {
    external_action_metadata_value(action, key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn external_action_run_id(action: &ExternalActionRecord) -> Option<String> {
    external_action_metadata_string(action, "automationRunID")
        .or_else(|| external_action_metadata_string(action, "automation_run_id"))
        .or_else(|| {
            action
                .source_id
                .as_deref()
                .and_then(|source_id| source_id.split(':').next())
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned)
        })
}

fn external_action_node_id(action: &ExternalActionRecord) -> Option<String> {
    external_action_metadata_string(action, "nodeID")
        .or_else(|| external_action_metadata_string(action, "node_id"))
}

fn external_action_tenant_context(
    action: &ExternalActionRecord,
) -> Option<tandem_types::TenantContext> {
    action
        .metadata
        .as_ref()
        .and_then(|metadata| {
            metadata
                .get("tenantContext")
                .or_else(|| metadata.get("tenant_context"))
        })
        .cloned()
        .and_then(|value| serde_json::from_value(value).ok())
}