redevplugin-worker-sdk 3.0.37

Rust SDK for ReDevPlugin WASM workers
Documentation
use crate::WorkerError;
use crate::api;
use crate::decode_base64;
use base64::Engine as _;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ResourceLimits {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output_buffer_bytes: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_runtime_ms: Option<u32>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StartRequest {
    pub program: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub argv: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
    pub environment: std::collections::BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub environment_removals: Vec<String>,
    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
    pub secret_references: std::collections::BTreeMap<String, String>,
    pub client_key: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource_limits: Option<ResourceLimits>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Status {
    pub handle: String,
    pub client_key: String,
    pub state: String,
    #[serde(default)]
    pub exit_code: Option<i32>,
    #[serde(default)]
    pub termination_reason: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WriteResponse {
    pub written: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReadRequest {
    pub handle: String,
    #[serde(default)]
    pub cursor: u64,
    pub max_bytes: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReadResponse {
    pub data_base64: String,
    pub cursor: u64,
    pub eof: bool,
    pub process_exit: bool,
    pub stream_gap: bool,
    #[serde(default)]
    pub dropped_bytes: u64,
}

impl ReadResponse {
    pub fn data(&self) -> Result<Vec<u8>, WorkerError> {
        decode_base64(&self.data_base64)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExitResponse {
    #[serde(default)]
    pub exit_code: Option<i32>,
    pub termination_reason: String,
}

#[derive(Serialize)]
struct WriteRequest<'a> {
    handle: &'a str,
    data_base64: String,
}

#[derive(Serialize)]
struct HandleRequest<'a> {
    handle: &'a str,
}

#[derive(Serialize)]
struct ClientKeyRequest<'a> {
    client_key: &'a str,
}

pub fn start(request: &StartRequest) -> Result<Status, WorkerError> {
    call("process.start", request)
}

pub fn attach(client_key: &str) -> Result<Status, WorkerError> {
    call("process.attach", &ClientKeyRequest { client_key })
}

pub fn status(handle: &str) -> Result<Status, WorkerError> {
    call("process.status", &HandleRequest { handle })
}

pub fn write_stdin(handle: &str, data: &[u8]) -> Result<WriteResponse, WorkerError> {
    call(
        "process.write_stdin",
        &WriteRequest {
            handle,
            data_base64: base64::engine::general_purpose::STANDARD.encode(data),
        },
    )
}

pub fn close_stdin(handle: &str) -> Result<(), WorkerError> {
    call::<_, serde_json::Value>("process.close_stdin", &HandleRequest { handle }).map(|_| ())
}

pub fn read_stdout(request: &ReadRequest) -> Result<ReadResponse, WorkerError> {
    call("process.read_stdout", request)
}

pub fn read_stderr(request: &ReadRequest) -> Result<ReadResponse, WorkerError> {
    call("process.read_stderr", request)
}

pub fn wait(handle: &str) -> Result<ExitResponse, WorkerError> {
    call("process.wait", &HandleRequest { handle })
}

pub fn terminate(handle: &str) -> Result<(), WorkerError> {
    call::<_, serde_json::Value>("process.terminate", &HandleRequest { handle }).map(|_| ())
}

pub fn kill(handle: &str) -> Result<(), WorkerError> {
    call::<_, serde_json::Value>("process.kill", &HandleRequest { handle }).map(|_| ())
}

pub fn close(handle: &str) -> Result<(), WorkerError> {
    call::<_, serde_json::Value>("process.close", &HandleRequest { handle }).map(|_| ())
}

fn call<RequestValue, ResponseValue>(
    operation: &str,
    request: &RequestValue,
) -> Result<ResponseValue, WorkerError>
where
    RequestValue: Serialize,
    ResponseValue: for<'de> Deserialize<'de>,
{
    api::call(operation, request).map_err(Into::into)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn process_read_result_decodes_binary_data_without_text_assumptions() {
        let result: ReadResponse = serde_json::from_value(serde_json::json!({
            "data_base64": "AP8B",
            "cursor": 3,
            "eof": false,
            "process_exit": false,
            "stream_gap": false
        }))
        .unwrap();
        assert_eq!(result.data().unwrap(), vec![0, 255, 1]);
    }

    #[test]
    fn process_start_request_contains_secret_references_not_secret_values() {
        let request = StartRequest {
            program: "tool".into(),
            argv: vec!["--json".into()],
            cwd: None,
            environment: std::collections::BTreeMap::new(),
            environment_removals: Vec::new(),
            secret_references: std::collections::BTreeMap::from([(
                "TOKEN".into(),
                "secret.token".into(),
            )]),
            client_key: "client".into(),
            resource_limits: None,
        };
        let encoded = serde_json::to_value(request).unwrap();
        assert_eq!(encoded["secret_references"]["TOKEN"], "secret.token");
        assert!(encoded.get("TOKEN").is_none());
    }
}