n5i-plugin-common 0.11.5

API definitions for developing n5i plugins.
Documentation
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::api::runtime_plugin_server::RuntimePlugin as RawRuntimePlugin;
use crate::api::{
    InstallAppRequest, InstallAppResponse, IsSupportedAppRequest, IsSupportedAppResponse,
    ParseAppRequest, ParseAppResponse, PauseAppRequest, PauseAppResponse, ResumeAppRequest,
    ResumeAppResponse, UninstallAppRequest, UninstallAppResponse, UserState,
};
use flate2::read::GzDecoder;
use n5i_apps::internal::InternalAppRepresentation;
use std::path::Path;
use tar::Archive;
use tempfile::TempDir;
use tonic::{Request, Response, Status};

#[tonic::async_trait]
pub trait RuntimePlugin {
    async fn is_supported_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
    ) -> Result<bool, Status>;

    async fn parse_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
        init_domain: Option<String>,
        seed_base: String,
    ) -> Result<InternalAppRepresentation, Status>;

    async fn install_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
        init_domain: Option<String>,
        seed_base: String,
    ) -> Result<(), Status>;

    async fn uninstall_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
        seed_base: String,
    ) -> Result<(), Status>;

    async fn pause_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
        init_domain: Option<String>,
        seed_base: String,
    ) -> Result<(), Status>;

    async fn resume_app(
        &self,
        app_id: &str,
        app_path: &Path,
        user_state: UserState,
        init_domain: Option<String>,
        seed_base: String,
    ) -> Result<(), Status>;
}

macro_rules! app_unpack_prelude {
    ($req:expr) => {{
        let Some(user_state) = $req.user_state else {
            return Err(Status::invalid_argument("User state not provided"));
        };

        let tmp_dir = TempDir::new()?;
        let tmp_dir_path = tmp_dir.path();
        let archive = GzDecoder::new($req.app_directory.as_slice());
        let mut archive = Archive::new(archive);
        archive.unpack(tmp_dir_path)?;

        (tmp_dir, user_state)
    }};
}

pub struct RuntimePluginWrapper<T: RuntimePlugin + Send + Sync + 'static>(T);

impl<T: RuntimePlugin + Send + Sync + 'static> RuntimePluginWrapper<T> {
    pub fn new(plugin: T) -> Self {
        Self(plugin)
    }
}

#[tonic::async_trait]
impl<T: RuntimePlugin + Send + Sync + 'static> RawRuntimePlugin for RuntimePluginWrapper<T> {
    async fn is_supported_app(
        &self,
        request: Request<IsSupportedAppRequest>,
    ) -> Result<Response<IsSupportedAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        Ok(Response::new(IsSupportedAppResponse {
            supported: T::is_supported_app(&self.0, &req.app_id, tmp_dir.path(), user_state)
                .await?,
        }))
    }

    async fn parse_app(
        &self,
        request: Request<ParseAppRequest>,
    ) -> Result<Response<ParseAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        let converted = T::parse_app(
            &self.0,
            &req.app_id,
            tmp_dir.path(),
            user_state,
            req.initial_domain,
            req.app_seed_base,
        )
        .await?;

        let converted = serde_json::to_string(&converted).unwrap();
        Ok(Response::new(ParseAppResponse {
            parsed_app: converted,
        }))
    }

    async fn install_app(
        &self,
        request: Request<InstallAppRequest>,
    ) -> Result<Response<InstallAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        T::install_app(
            &self.0,
            &req.app_id,
            tmp_dir.path(),
            user_state,
            req.initial_domain,
            req.app_seed_base,
        )
        .await?;
        Ok(Response::new(InstallAppResponse {}))
    }

    async fn uninstall_app(
        &self,
        request: Request<UninstallAppRequest>,
    ) -> Result<Response<UninstallAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        T::uninstall_app(
            &self.0,
            &req.app_id,
            tmp_dir.path(),
            user_state,
            req.app_seed_base,
        )
        .await?;

        Ok(Response::new(UninstallAppResponse {}))
    }

    async fn pause_app(
        &self,
        request: Request<PauseAppRequest>,
    ) -> Result<Response<PauseAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        T::pause_app(
            &self.0,
            &req.app_id,
            tmp_dir.path(),
            user_state,
            req.initial_domain,
            req.app_seed_base,
        )
        .await?;

        Ok(Response::new(PauseAppResponse {}))
    }

    async fn resume_app(
        &self,
        request: Request<ResumeAppRequest>,
    ) -> Result<Response<ResumeAppResponse>, Status> {
        let req = request.into_inner();
        let (tmp_dir, user_state) = app_unpack_prelude!(req);

        T::resume_app(
            &self.0,
            &req.app_id,
            tmp_dir.path(),
            user_state,
            req.initial_domain,
            req.app_seed_base,
        )
        .await?;

        Ok(Response::new(ResumeAppResponse {}))
    }
}