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.
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}