platform_provider/
runtime.rs1use 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}