openzeppelin_relayer/api/controllers/
plugin.rs

1//! # Plugin Controller
2//!
3//! Handles HTTP endpoints for plugin operations including:
4//! - Calling plugins
5use crate::{
6    jobs::JobProducerTrait,
7    models::{
8        ApiError, ApiResponse, NetworkRepoModel, NotificationRepoModel, PaginationMeta,
9        PaginationQuery, PluginCallRequest, PluginModel, RelayerRepoModel, SignerRepoModel,
10        ThinDataAppState, TransactionRepoModel,
11    },
12    repositories::{
13        NetworkRepository, PluginRepositoryTrait, RelayerRepository, Repository,
14        TransactionCounterTrait, TransactionRepository,
15    },
16    services::plugins::{PluginCallResponse, PluginRunner, PluginService, PluginServiceTrait},
17};
18use actix_web::HttpResponse;
19use eyre::Result;
20use std::sync::Arc;
21
22/// Call plugin
23///
24/// # Arguments
25///
26/// * `plugin_id` - The ID of the plugin to call.
27/// * `plugin_call_request` - The plugin call request.
28/// * `state` - The application state containing the plugin repository.
29///
30/// # Returns
31///
32/// The result of the plugin call.
33pub async fn call_plugin<J, RR, TR, NR, NFR, SR, TCR, PR>(
34    plugin_id: String,
35    plugin_call_request: PluginCallRequest,
36    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR>,
37) -> Result<HttpResponse, ApiError>
38where
39    J: JobProducerTrait + Send + Sync + 'static,
40    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
41    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
42    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
43    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
44    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
45    TCR: TransactionCounterTrait + Send + Sync + 'static,
46    PR: PluginRepositoryTrait + Send + Sync + 'static,
47{
48    let plugin = state
49        .plugin_repository
50        .get_by_id(&plugin_id)
51        .await?
52        .ok_or_else(|| ApiError::NotFound(format!("Plugin with id {} not found", plugin_id)))?;
53
54    let plugin_runner = PluginRunner;
55    let plugin_service = PluginService::new(plugin_runner);
56    let result = plugin_service
57        .call_plugin(plugin, plugin_call_request, Arc::new(state))
58        .await;
59
60    match result {
61        Ok(plugin_result) => Ok(HttpResponse::Ok().json(ApiResponse::success(plugin_result))),
62        Err(e) => Ok(HttpResponse::Ok().json(ApiResponse::<PluginCallResponse>::error(e))),
63    }
64}
65
66/// List plugins
67///
68/// # Arguments
69///
70/// * `query` - The pagination query parameters.
71///     * `page` - The page number.
72///     * `per_page` - The number of items per page.
73/// * `state` - The application state containing the plugin repository.
74///
75/// # Returns
76///
77/// The result of the plugin list.
78pub async fn list_plugins<J, RR, TR, NR, NFR, SR, TCR, PR>(
79    query: PaginationQuery,
80    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR>,
81) -> Result<HttpResponse, ApiError>
82where
83    J: JobProducerTrait + Send + Sync + 'static,
84    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
85    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
86    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
87    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
88    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
89    TCR: TransactionCounterTrait + Send + Sync + 'static,
90    PR: PluginRepositoryTrait + Send + Sync + 'static,
91{
92    let plugins = state.plugin_repository.list_paginated(query).await?;
93
94    let plugin_items: Vec<PluginModel> = plugins.items.into_iter().collect();
95
96    Ok(HttpResponse::Ok().json(ApiResponse::paginated(
97        plugin_items,
98        PaginationMeta {
99            total_items: plugins.total,
100            current_page: plugins.page,
101            per_page: plugins.per_page,
102        },
103    )))
104}
105
106#[cfg(test)]
107mod tests {
108    use std::time::Duration;
109
110    use super::*;
111    use actix_web::web;
112
113    use crate::{
114        constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS, models::PluginModel,
115        utils::mocks::mockutils::create_mock_app_state,
116    };
117
118    #[actix_web::test]
119    async fn test_call_plugin() {
120        let plugin = PluginModel {
121            id: "test-plugin".to_string(),
122            path: "test-path".to_string(),
123            timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
124        };
125        let app_state = create_mock_app_state(None, None, None, Some(vec![plugin]), None).await;
126        let plugin_call_request = PluginCallRequest {
127            params: serde_json::json!({"key":"value"}),
128        };
129        let response = call_plugin(
130            "test-plugin".to_string(),
131            plugin_call_request,
132            web::ThinData(app_state),
133        )
134        .await;
135        assert!(response.is_ok());
136    }
137}