platform_core/function.rs
1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! The composable-function contract — Rust port of the Java
18//! `LambdaFunction` / `TypedLambdaFunction<I, O>`
19//! (`org.platformlambda.core.models`).
20//!
21//! [`ComposableFunction`] is the untyped registry currency (the `LambdaFunction`
22//! analog): envelope in, envelope out. [`TypedFunction`] is the recommended
23//! authoring surface (the `TypedLambdaFunction<I, O>` analog); wrap one in a
24//! [`TypedAdapter`] to register it. [`AppError`] is the `AppException` analog —
25//! HTTP-style status + message; a worker converts an `Err` into a response
26//! envelope carrying that status.
27
28use std::collections::HashMap;
29use std::marker::PhantomData;
30use std::sync::Arc;
31
32use async_trait::async_trait;
33use serde::de::DeserializeOwned;
34use serde::Serialize;
35
36use crate::envelope::EventEnvelope;
37
38/// The `AppException(status, message)` analog: HTTP-compatible status codes,
39/// where >= 400 is an error.
40#[derive(Clone, Debug, thiserror::Error)]
41#[error("({status}) {message}")]
42pub struct AppError {
43 status: i32,
44 message: String,
45}
46
47impl AppError {
48 pub fn new(status: i32, message: impl Into<String>) -> Self {
49 AppError {
50 status,
51 message: message.into(),
52 }
53 }
54
55 pub fn status(&self) -> i32 {
56 self.status
57 }
58
59 pub fn message(&self) -> &str {
60 &self.message
61 }
62}
63
64/// The untyped composable-function contract (the `LambdaFunction` analog) —
65/// what the [`Platform`](crate::platform::Platform) registry stores.
66///
67/// `instance` is the 1-based worker number (Java parity). Functions must stay
68/// stateless and never call other user functions directly — coupling is
69/// route-name + envelope only (`inv-never-couple-functions`).
70#[async_trait]
71pub trait ComposableFunction: Send + Sync {
72 async fn handle_event(
73 &self,
74 headers: HashMap<String, String>,
75 input: EventEnvelope,
76 instance: usize,
77 ) -> Result<EventEnvelope, AppError>;
78}
79
80/// The typed authoring surface (the `TypedLambdaFunction<I, O>` analog):
81/// implement this with concrete input/output types and register it via
82/// [`TypedAdapter`].
83#[async_trait]
84pub trait TypedFunction<I, O>: Send + Sync
85where
86 I: DeserializeOwned + Send,
87 O: Serialize,
88{
89 async fn handle_event(
90 &self,
91 headers: HashMap<String, String>,
92 input: I,
93 instance: usize,
94 ) -> Result<O, AppError>;
95}
96
97/// Bridges a [`TypedFunction`] to the untyped [`ComposableFunction`] registry
98/// currency: deserializes the envelope body into `I`, invokes the typed
99/// handler, and wraps `O` back into a response envelope.
100pub struct TypedAdapter<T, I, O> {
101 inner: T,
102 _marker: PhantomData<fn(I) -> O>,
103}
104
105impl<T, I, O> TypedAdapter<T, I, O> {
106 pub fn new(inner: T) -> Self {
107 TypedAdapter {
108 inner,
109 _marker: PhantomData,
110 }
111 }
112
113 /// Convenience: wrap directly into the `Arc<dyn ComposableFunction>` the
114 /// registry expects.
115 pub fn arc(inner: T) -> Arc<dyn ComposableFunction>
116 where
117 T: TypedFunction<I, O> + 'static,
118 I: DeserializeOwned + Send + Sync + 'static,
119 O: Serialize + Send + Sync + 'static,
120 {
121 Arc::new(TypedAdapter::new(inner))
122 }
123}
124
125#[async_trait]
126impl<T, I, O> ComposableFunction for TypedAdapter<T, I, O>
127where
128 T: TypedFunction<I, O>,
129 I: DeserializeOwned + Send + Sync,
130 O: Serialize + Send + Sync,
131{
132 async fn handle_event(
133 &self,
134 headers: HashMap<String, String>,
135 input: EventEnvelope,
136 instance: usize,
137 ) -> Result<EventEnvelope, AppError> {
138 let typed_input: I = input
139 .body_as()
140 .map_err(|e| AppError::new(400, format!("unable to map input: {e}")))?;
141 let output = self
142 .inner
143 .handle_event(headers, typed_input, instance)
144 .await?;
145 EventEnvelope::new().set_body(output)
146 }
147}
148
149/// The convenient no-operation function for event scripts (Java
150/// `NoOpFunction`, route `no.op`): an echo — reply headers and body mirror
151/// the input.
152pub struct NoOpFunction;
153
154#[async_trait]
155impl ComposableFunction for NoOpFunction {
156 async fn handle_event(
157 &self,
158 headers: HashMap<String, String>,
159 input: EventEnvelope,
160 _instance: usize,
161 ) -> Result<EventEnvelope, AppError> {
162 let mut response = EventEnvelope::new();
163 for (key, value) in &headers {
164 response = response.set_header(key, value);
165 }
166 Ok(response.set_raw_body(input.body().clone()))
167 }
168}