#![deny(unsafe_code)]
#![warn(missing_docs)]
use parallax_core::{
check_protocol, ErrorCode, ExecutionLimits, ParallaxError, RequestId, RuntimeKind,
PROTOCOL_VERSION,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const VERSION: u32 = PROTOCOL_VERSION;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Envelope {
pub v: u32,
pub id: RequestId,
pub op: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ok: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ProtocolError>,
}
impl Envelope {
pub fn request(op: impl Into<String>, payload: Value) -> Self {
Self {
v: VERSION,
id: RequestId::new(),
op: op.into(),
ok: None,
payload: Some(payload),
error: None,
}
}
pub fn ok_response(id: RequestId, op: impl Into<String>, payload: Value) -> Self {
Self {
v: VERSION,
id,
op: op.into(),
ok: Some(true),
payload: Some(payload),
error: None,
}
}
pub fn err_response(id: RequestId, op: impl Into<String>, error: ProtocolError) -> Self {
Self {
v: VERSION,
id,
op: op.into(),
ok: Some(false),
payload: None,
error: Some(error),
}
}
pub fn to_ndjson_line(&self) -> parallax_core::Result<String> {
let mut s = serde_json::to_string(self).map_err(ParallaxError::from)?;
s.push('\n');
Ok(s)
}
pub fn from_ndjson_line(line: &str) -> parallax_core::Result<Self> {
let env: Self = serde_json::from_str(line.trim()).map_err(|e| {
ParallaxError::new(ErrorCode::ProtocolViolation, e.to_string())
.with_source("parallax-protocol")
.with_operation("from_ndjson_line")
})?;
check_protocol(env.v).map_err(|e| {
e.with_source("parallax-protocol")
.with_operation("from_ndjson_line")
})?;
Ok(env)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProtocolError {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diagnostic: Option<String>,
}
impl ProtocolError {
pub fn from_parallax(err: &ParallaxError) -> Self {
Self {
code: err.code.to_string(),
message: err.message.clone(),
diagnostic: err.diagnostic.clone(),
}
}
pub fn into_parallax(self, runtime: Option<RuntimeKind>) -> ParallaxError {
let code = match self.code.as_str() {
"RUNTIME_UNAVAILABLE" => ErrorCode::RuntimeUnavailable,
"EXECUTION_FAILURE" => ErrorCode::ExecutionFailure,
"EXECUTION_TIMEOUT" => ErrorCode::ExecutionTimeout,
"EXECUTION_CANCELLED" => ErrorCode::ExecutionCancelled,
"CAPTURE_FAILURE" => ErrorCode::CaptureFailure,
"RESTORE_FAILURE" => ErrorCode::RestoreFailure,
"UNSUPPORTED_VALUE" => ErrorCode::UnsupportedValue,
"PROTOCOL_VIOLATION" => ErrorCode::ProtocolViolation,
"RESOURCE_LIMIT_EXCEEDED" => ErrorCode::ResourceLimitExceeded,
"ADAPTER_CRASHED" => ErrorCode::AdapterCrashed,
_ => ErrorCode::Internal,
};
let mut err = ParallaxError::new(code, self.message)
.with_source("worker")
.with_operation("protocol");
if let Some(rt) = runtime {
err = err.with_runtime(rt);
}
if let Some(d) = self.diagnostic {
err = err.with_diagnostic(d);
}
err
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HelloRequest {
pub protocol_version: u32,
pub runtime: RuntimeKind,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HelloResponse {
pub protocol_version: u32,
pub runtime: RuntimeKind,
pub host_version: String,
pub adapter_version: String,
}
pub fn validate_hello(resp: &HelloResponse) -> parallax_core::Result<()> {
check_protocol(resp.protocol_version).map_err(|e| {
e.with_source("parallax-protocol")
.with_operation("validate_hello")
})
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecuteRequest {
pub source: String,
pub filename: String,
#[serde(default)]
pub capture: Vec<String>,
pub limits: ExecutionLimits,
#[serde(default)]
pub continuum: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecuteResponse {
pub stdout: String,
pub stderr: String,
pub duration_us: u64,
#[serde(default)]
pub bindings: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exception: Option<Value>,
pub success: bool,
#[serde(default)]
pub suspended: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ues: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub safepoint: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ResumeCheckpointRequest {
pub ues: Value,
pub limits: ExecutionLimits,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ResumeCheckpointResponse {
pub success: bool,
pub stdout: String,
pub stderr: String,
pub duration_us: u64,
#[serde(default)]
pub bindings: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exception: Option<Value>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RestoreRequest {
pub bindings: Value,
pub limits: ExecutionLimits,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RestoreResponse {
pub success: bool,
#[serde(default)]
pub warnings: Vec<String>,
#[serde(default)]
pub restored: IndexMapCompat,
pub duration_us: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bindings: Option<Value>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct IndexMapCompat(pub std::collections::BTreeMap<String, String>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ndjson_round_trip() {
let env = Envelope::request("ping", serde_json::json!({}));
let line = env.to_ndjson_line().unwrap();
let parsed = Envelope::from_ndjson_line(&line).unwrap();
assert_eq!(parsed.op, "ping");
}
#[test]
fn rejects_future_protocol_major() {
let line = format!(
r#"{{"v":{},"id":"00000000-0000-4000-8000-000000000001","op":"ping"}}"#,
VERSION + 1
);
let err = Envelope::from_ndjson_line(&line).unwrap_err();
assert_eq!(err.code, ErrorCode::ProtocolViolation);
}
#[test]
fn hello_rejects_mismatched_protocol() {
let resp = HelloResponse {
protocol_version: VERSION + 7,
runtime: RuntimeKind::Python,
host_version: "3.12".into(),
adapter_version: "0.1.0".into(),
};
let err = validate_hello(&resp).unwrap_err();
assert_eq!(err.code, ErrorCode::ProtocolViolation);
}
}