Skip to main content

platform_provider/
runtime.rs

1use crate::ProviderHostEffectCoordinator;
2use crate::config::ProviderConfig;
3use crate::invocation::{self, InvocationContext};
4use crate::protocol::{ProviderFunctionInvokeRequest, ProviderFunctionInvokeResponse};
5use crate::protocol::{ProviderInvocationMode, ProviderOperationKind};
6use crate::validation::validate_path_segment;
7use platform_core::{AppError, AppResult, ErrorCode, ExecutionContext};
8use platform_runtime::{FunctionHandlerObservability, RuntimeFunction};
9use serde_json::Value;
10use std::time::Duration;
11
12#[derive(Debug, Clone)]
13pub struct ProviderRuntimeFunction {
14    client: reqwest::Client,
15    config: ProviderConfig,
16    function_name: String,
17    effects: ProviderHostEffectCoordinator,
18}
19
20impl ProviderRuntimeFunction {
21    pub fn new(
22        config: ProviderConfig,
23        function_name: impl Into<String>,
24        effects: ProviderHostEffectCoordinator,
25    ) -> AppResult<Self> {
26        let function_name = function_name.into();
27        validate_function_name(&function_name)?;
28        let client = reqwest::Client::builder()
29            .timeout(Duration::from_millis(config.timeout_ms))
30            .build()
31            .map_err(|error| {
32                AppError::new(
33                    ErrorCode::Internal,
34                    format!("failed to build provider runtime client: {error}"),
35                )
36            })?;
37        Ok(Self {
38            client,
39            config,
40            function_name,
41            effects,
42        })
43    }
44
45    pub async fn invoke(&self, ctx: ExecutionContext, input: Value) -> AppResult<Value> {
46        let invocation_id = ctx.execution_id.0.clone();
47        let request_body = ProviderFunctionInvokeRequest {
48            request_id: ctx.execution_id.0.clone(),
49            function_run_id: ctx.execution_id.0.clone(),
50            function_name: self.function_name.clone(),
51            attempt: ctx.attempt,
52            correlation_id: ctx.correlation_id.0,
53            causation_id: ctx.causation_id,
54            actor: ctx.actor.clone(),
55            trace: ctx.trace.clone(),
56            input,
57        };
58        let invocation = invocation::build(
59            &self.config,
60            ProviderOperationKind::RuntimeFunction,
61            &self.function_name,
62            "1",
63            ProviderInvocationMode::Durable,
64            InvocationContext {
65                invocation_id,
66                request_id: request_body.request_id.clone(),
67                attempt: request_body.attempt,
68                actor: request_body.actor.clone(),
69                correlation_id: request_body.correlation_id.clone(),
70                causation_id: request_body.causation_id.clone(),
71                trace: request_body.trace.clone(),
72            },
73            serde_json::to_value(request_body).map_err(|error| {
74                AppError::new(
75                    ErrorCode::Internal,
76                    format!("encode Provider payload: {error}"),
77                )
78            })?,
79        )?;
80        let outcome = invocation::send(
81            &self.client,
82            &self.config,
83            &self.effects,
84            "runtime:invoke",
85            &invocation,
86        )
87        .await?;
88        let value = invocation::result(&invocation, outcome)?;
89        let response: ProviderFunctionInvokeResponse =
90            serde_json::from_value(value).map_err(|error| {
91                AppError::new(
92                    ErrorCode::ExternalDependency,
93                    format!("Provider runtime result violated its contract: {error}"),
94                )
95            })?;
96        Ok(response.output)
97    }
98}
99
100#[async_trait::async_trait]
101impl RuntimeFunction for ProviderRuntimeFunction {
102    async fn call(&self, ctx: ExecutionContext, input: Value) -> AppResult<Value> {
103        self.invoke(ctx, input).await
104    }
105
106    fn observability(&self) -> Option<FunctionHandlerObservability> {
107        Some(FunctionHandlerObservability::new(
108            "provider_runtime",
109            serde_json::json!({
110                "module_name": &self.config.name,
111                "function_name": &self.function_name,
112                "provider_path": format!("/exports/{}/runtime:invoke", self.config.export_key),
113                "timeout_ms": self.config.timeout_ms,
114            }),
115        ))
116    }
117}
118
119pub(crate) fn validate_function_name(function_name: &str) -> AppResult<()> {
120    validate_path_segment(
121        function_name,
122        "provider runtime function name must be a stable path segment",
123    )
124}