Skip to main content

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 — unless `O` IS an
100/// [`EventEnvelope`], which is honoured as the reply itself (status, headers
101/// and body), the Java `TypedLambdaFunction<I, EventEnvelope>` contract
102/// (`WorkerHandler.updateResponse`: `result instanceof EventEnvelope`).
103pub struct TypedAdapter<T, I, O> {
104    inner: T,
105    _marker: PhantomData<fn(I) -> O>,
106}
107
108impl<T, I, O> TypedAdapter<T, I, O> {
109    pub fn new(inner: T) -> Self {
110        TypedAdapter {
111            inner,
112            _marker: PhantomData,
113        }
114    }
115
116    /// Convenience: wrap directly into the `Arc<dyn ComposableFunction>` the
117    /// registry expects.
118    pub fn arc(inner: T) -> Arc<dyn ComposableFunction>
119    where
120        T: TypedFunction<I, O> + 'static,
121        I: DeserializeOwned + Send + Sync + 'static,
122        O: Serialize + Send + Sync + 'static,
123    {
124        Arc::new(TypedAdapter::new(inner))
125    }
126}
127
128#[async_trait]
129impl<T, I, O> ComposableFunction for TypedAdapter<T, I, O>
130where
131    T: TypedFunction<I, O>,
132    I: DeserializeOwned + Send + Sync,
133    O: Serialize + Send + Sync + 'static,
134{
135    async fn handle_event(
136        &self,
137        headers: HashMap<String, String>,
138        input: EventEnvelope,
139        instance: usize,
140    ) -> Result<EventEnvelope, AppError> {
141        let typed_input: I = input
142            .body_as()
143            .map_err(|e| AppError::new(400, format!("unable to map input: {e}")))?;
144        let output = self
145            .inner
146            .handle_event(headers, typed_input, instance)
147            .await?;
148        // Java WorkerHandler.updateResponse parity: a typed function may return
149        // an EventEnvelope to set the reply's status, headers and body - it is
150        // honoured AS the reply, never serialized into a body (EventEnvelope
151        // derives Serialize, so without this check the whole envelope would
152        // silently nest inside the reply body). The downcast is the Java
153        // `result instanceof EventEnvelope`.
154        let boxed: Box<dyn std::any::Any + Send> = Box::new(output);
155        match boxed.downcast::<EventEnvelope>() {
156            Ok(envelope) => Ok(*envelope),
157            Err(other) => {
158                let output = *other
159                    .downcast::<O>()
160                    .expect("a typed output is either an EventEnvelope or O");
161                EventEnvelope::new().set_body(output)
162            }
163        }
164    }
165}
166
167/// The convenient no-operation function for event scripts (Java
168/// `NoOpFunction`, route `no.op`): an echo — reply headers and body mirror
169/// the input.
170pub struct NoOpFunction;
171
172#[async_trait]
173impl ComposableFunction for NoOpFunction {
174    async fn handle_event(
175        &self,
176        headers: HashMap<String, String>,
177        input: EventEnvelope,
178        _instance: usize,
179    ) -> Result<EventEnvelope, AppError> {
180        let mut response = EventEnvelope::new();
181        for (key, value) in &headers {
182            response = response.set_header(key, value);
183        }
184        Ok(response.set_raw_body(input.body().clone()))
185    }
186}