camel-function 0.9.0

Function runtime service for out-of-process function execution
Documentation
use async_trait::async_trait;
use camel_api::function::*;
use camel_api::lifecycle::Lifecycle;
use std::sync::Arc;

#[test]
fn function_id_compute_is_deterministic() {
    let a = FunctionId::compute("deno", "return 1", 5000);
    let b = FunctionId::compute("deno", "return 1", 5000);
    assert_eq!(a, b);
}

#[test]
fn function_id_varies_on_inputs() {
    let a = FunctionId::compute("deno", "return 1", 5000);
    let b = FunctionId::compute("deno", "return 2", 5000);
    let c = FunctionId::compute("deno", "return 1", 10000);
    assert_ne!(a, b);
    assert_ne!(a, c);
}

#[test]
fn function_id_hex_format() {
    let id = FunctionId::compute("deno", "test", 5000);
    assert_eq!(id.0.len(), 32);
    assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
    assert!(!id.0.chars().any(|c| c.is_ascii_uppercase()));
}

#[test]
fn function_definition_clone_debug() {
    let def = FunctionDefinition {
        id: FunctionId::compute("deno", "test", 5000),
        runtime: "deno".into(),
        source: "test".into(),
        timeout_ms: 5000,
        route_id: Some("route-1".into()),
        step_index: Some(0),
    };
    let cloned = def.clone();
    assert_eq!(def.id, cloned.id);
    let _ = format!("{:?}", def);
}

#[test]
fn invocation_error_display() {
    let id = FunctionId::compute("deno", "test", 5000);
    let e = FunctionInvocationError::NotRegistered {
        function_id: id.clone(),
    };
    let msg = e.to_string();
    assert!(msg.contains("not registered"));

    let e = FunctionInvocationError::Timeout {
        function_id: id.clone(),
        timeout_ms: 5000,
    };
    assert!(e.to_string().contains("timed out"));

    let e = FunctionInvocationError::RunnerUnavailable {
        reason: "boom".into(),
    };
    assert!(e.to_string().contains("runner unavailable"));

    let e = FunctionInvocationError::UserError {
        function_id: id.clone(),
        message: "fail".into(),
        stack: Some("at line 1".into()),
    };
    assert!(e.to_string().contains("user code failed"));

    let e = FunctionInvocationError::Transport("net err".into());
    assert!(e.to_string().contains("transport"));

    let e = FunctionInvocationError::InvalidPatch("bad".into());
    assert!(e.to_string().contains("invalid patch"));
}

#[test]
fn invocation_error_is_std_error() {
    fn assert_error<E: std::error::Error>(_: &E) {}
    let id = FunctionId::compute("deno", "test", 5000);
    assert_error(&FunctionInvocationError::NotRegistered { function_id: id });
    assert_error(&FunctionInvocationError::Timeout {
        function_id: FunctionId("x".into()),
        timeout_ms: 100,
    });
    assert_error(&FunctionInvocationError::RunnerUnavailable { reason: "x".into() });
    assert_error(&FunctionInvocationError::UserError {
        function_id: FunctionId("x".into()),
        message: "x".into(),
        stack: None,
    });
    assert_error(&FunctionInvocationError::Transport("x".into()));
    assert_error(&FunctionInvocationError::InvalidPatch("x".into()));
}

#[test]
fn lifecycle_default_returns_none_for_function_invoker() {
    struct DummyService;
    #[async_trait]
    impl Lifecycle for DummyService {
        fn name(&self) -> &str {
            "dummy"
        }
        async fn start(&mut self) -> Result<(), camel_api::CamelError> {
            Ok(())
        }
        async fn stop(&mut self) -> Result<(), camel_api::CamelError> {
            Ok(())
        }
    }
    let svc = DummyService;
    assert!(svc.as_function_invoker().is_none());
}

#[test]
fn lifecycle_impl_returns_some_for_function_invoker() {
    struct InvokerService;
    impl FunctionInvokerSync for InvokerService {
        fn stage_pending(&self, _def: FunctionDefinition, _route_id: Option<&str>, _gen: u64) {}
        fn discard_staging(&self, _gen: u64) {}
        fn begin_reload(&self) -> u64 {
            0
        }
        fn function_refs_for_route(&self, _route_id: &str) -> Vec<(FunctionId, Option<String>)> {
            vec![]
        }
        fn staged_refs_for_route(
            &self,
            _route_id: &str,
            _generation: u64,
        ) -> Vec<(FunctionId, Option<String>)> {
            vec![]
        }
        fn staged_defs_for_route(
            &self,
            _route_id: &str,
            _generation: u64,
        ) -> Vec<(FunctionDefinition, Option<String>)> {
            vec![]
        }
    }
    #[async_trait]
    impl FunctionInvoker for InvokerService {
        async fn register(
            &self,
            _def: FunctionDefinition,
            _route_id: Option<&str>,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn unregister(
            &self,
            _id: &FunctionId,
            _route_id: Option<&str>,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn invoke(
            &self,
            _id: &FunctionId,
            _exchange: &camel_api::Exchange,
        ) -> Result<ExchangePatch, FunctionInvocationError> {
            Ok(ExchangePatch::default())
        }
        async fn prepare_reload(
            &self,
            _diff: FunctionDiff,
            _generation: u64,
        ) -> Result<PrepareToken, FunctionInvocationError> {
            Ok(PrepareToken::default())
        }
        async fn finalize_reload(
            &self,
            _diff: &FunctionDiff,
            _generation: u64,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn rollback_reload(
            &self,
            _token: PrepareToken,
            _generation: u64,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn commit_staged(&self) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
    }
    #[async_trait]
    impl Lifecycle for InvokerService {
        fn name(&self) -> &str {
            "invoker-svc"
        }
        async fn start(&mut self) -> Result<(), camel_api::CamelError> {
            Ok(())
        }
        async fn stop(&mut self) -> Result<(), camel_api::CamelError> {
            Ok(())
        }
        fn as_function_invoker(&self) -> Option<Arc<dyn FunctionInvoker>> {
            Some(Arc::new(InvokerService))
        }
    }
    let svc = InvokerService;
    assert!(svc.as_function_invoker().is_some());
}

#[test]
fn exchange_patch_default_is_empty() {
    let patch = ExchangePatch::default();
    assert!(patch.body.is_none());
    assert!(patch.headers_set.is_empty());
    assert!(patch.headers_removed.is_empty());
    assert!(patch.properties_set.is_empty());
}

#[test]
fn function_invoker_is_object_safe() {
    struct MockInvoker;
    impl FunctionInvokerSync for MockInvoker {
        fn stage_pending(&self, _def: FunctionDefinition, _route_id: Option<&str>, _gen: u64) {}
        fn discard_staging(&self, _gen: u64) {}
        fn begin_reload(&self) -> u64 {
            0
        }
        fn function_refs_for_route(&self, _route_id: &str) -> Vec<(FunctionId, Option<String>)> {
            vec![]
        }
        fn staged_refs_for_route(
            &self,
            _route_id: &str,
            _generation: u64,
        ) -> Vec<(FunctionId, Option<String>)> {
            vec![]
        }
        fn staged_defs_for_route(
            &self,
            _route_id: &str,
            _generation: u64,
        ) -> Vec<(FunctionDefinition, Option<String>)> {
            vec![]
        }
    }
    #[async_trait]
    impl FunctionInvoker for MockInvoker {
        async fn register(
            &self,
            _def: FunctionDefinition,
            _route_id: Option<&str>,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn unregister(
            &self,
            _id: &FunctionId,
            _route_id: Option<&str>,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn invoke(
            &self,
            _id: &FunctionId,
            _exchange: &camel_api::Exchange,
        ) -> Result<ExchangePatch, FunctionInvocationError> {
            Ok(ExchangePatch::default())
        }
        async fn prepare_reload(
            &self,
            _diff: FunctionDiff,
            _generation: u64,
        ) -> Result<PrepareToken, FunctionInvocationError> {
            Ok(PrepareToken::default())
        }
        async fn finalize_reload(
            &self,
            _diff: &FunctionDiff,
            _generation: u64,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn rollback_reload(
            &self,
            _token: PrepareToken,
            _generation: u64,
        ) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
        async fn commit_staged(&self) -> Result<(), FunctionInvocationError> {
            Ok(())
        }
    }
    let _: Arc<dyn FunctionInvoker> = Arc::new(MockInvoker);
}