use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const ABI_VERSION: u32 = 3;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MetricDataType {
Float,
Integer,
Boolean,
#[default]
String,
Binary,
Enum {
options: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MetricValue {
Float(f64),
Integer(i64),
Boolean(bool),
String(String),
Binary(Vec<u8>),
#[default]
Null,
}
impl From<f64> for MetricValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<i64> for MetricValue {
fn from(v: i64) -> Self {
Self::Integer(v)
}
}
impl From<bool> for MetricValue {
fn from(v: bool) -> Self {
Self::Boolean(v)
}
}
impl From<String> for MetricValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for MetricValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl From<Vec<u8>> for MetricValue {
fn from(v: Vec<u8>) -> Self {
Self::Binary(v)
}
}
pub type ParamMetricValue = MetricValue;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MetricDescriptor {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub data_type: MetricDataType,
#[serde(default)]
pub unit: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default)]
pub required: bool,
}
impl MetricDescriptor {
pub fn new(
name: impl Into<String>,
display_name: impl Into<String>,
data_type: MetricDataType,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
data_type,
unit: String::new(),
min: None,
max: None,
required: false,
}
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = unit.into();
self
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.min = Some(min);
self.max = Some(max);
self
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParameterDefinition {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub param_type: MetricDataType,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<MetricValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default)]
pub options: Vec<String>,
}
impl ParameterDefinition {
pub fn new(name: impl Into<String>, param_type: MetricDataType) -> Self {
Self {
name: name.into(),
display_name: String::new(),
description: String::new(),
param_type,
required: true,
default_value: None,
min: None,
max: None,
options: Vec::new(),
}
}
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = display_name.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_default(mut self, default: MetricValue) -> Self {
self.default_value = Some(default);
self.required = false;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParameterGroup {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub parameters: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CommandDescriptor {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub payload_template: String,
#[serde(default)]
pub parameters: Vec<ParameterDefinition>,
#[serde(default)]
pub fixed_values: HashMap<String, serde_json::Value>,
#[serde(default)]
pub samples: Vec<serde_json::Value>,
#[serde(default)]
pub parameter_groups: Vec<ParameterGroup>,
}
impl CommandDescriptor {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = display_name.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn param(mut self, param: ParameterDefinition) -> Self {
self.parameters.push(param);
self
}
pub fn sample(mut self, sample: serde_json::Value) -> Self {
self.samples.push(sample);
self
}
}
pub type ExtensionCommand = CommandDescriptor;
pub type CommandDefinition = CommandDescriptor;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionMetadata {
pub id: String,
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(skip)]
pub file_path: Option<std::path::PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_parameters: Option<Vec<ParameterDefinition>>,
}
impl ExtensionMetadata {
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
version: version.into(),
description: None,
author: None,
homepage: None,
license: None,
file_path: None,
config_parameters: None,
}
}
pub fn new_with_semver(
id: impl Into<String>,
name: impl Into<String>,
version: semver::Version,
) -> Self {
Self::new(id, name, version.to_string())
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_homepage(mut self, homepage: impl Into<String>) -> Self {
self.homepage = Some(homepage.into());
self
}
pub fn with_license(mut self, license: impl Into<String>) -> Self {
self.license = Some(license.into());
self
}
pub fn with_config_parameters(mut self, params: Vec<ParameterDefinition>) -> Self {
self.config_parameters = Some(params);
self
}
pub fn parse_version(&self) -> std::result::Result<semver::Version, semver::Error> {
semver::Version::parse(&self.version)
}
pub fn validate(&self) -> std::result::Result<(), &'static str> {
const MAX_ID_LEN: usize = 256;
const MAX_NAME_LEN: usize = 512;
const MAX_VERSION_LEN: usize = 64;
const MAX_DESCRIPTION_LEN: usize = 4096;
const MAX_AUTHOR_LEN: usize = 256;
const MAX_HOMEPAGE_LEN: usize = 1024;
const MAX_LICENSE_LEN: usize = 128;
if self.id.len() > MAX_ID_LEN {
return Err("Extension ID exceeds maximum length (256 bytes)");
}
if self.name.len() > MAX_NAME_LEN {
return Err("Extension name exceeds maximum length (512 bytes)");
}
if self.version.len() > MAX_VERSION_LEN {
return Err("Extension version exceeds maximum length (64 bytes)");
}
if let Some(ref desc) = self.description {
if desc.len() > MAX_DESCRIPTION_LEN {
return Err("Extension description exceeds maximum length (4096 bytes)");
}
}
if let Some(ref author) = self.author {
if author.len() > MAX_AUTHOR_LEN {
return Err("Extension author exceeds maximum length (256 bytes)");
}
}
if let Some(ref homepage) = self.homepage {
if homepage.len() > MAX_HOMEPAGE_LEN {
return Err("Extension homepage exceeds maximum length (1024 bytes)");
}
}
if let Some(ref license) = self.license {
if license.len() > MAX_LICENSE_LEN {
return Err("Extension license exceeds maximum length (128 bytes)");
}
}
if !self
.id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err("Extension ID contains invalid characters (only alphanumeric, hyphen, underscore allowed)");
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionDescriptor {
pub metadata: ExtensionMetadata,
#[serde(default)]
pub commands: Vec<CommandDescriptor>,
#[serde(default)]
pub metrics: Vec<MetricDescriptor>,
}
impl ExtensionDescriptor {
pub fn new(metadata: ExtensionMetadata) -> Self {
Self {
metadata,
commands: Vec::new(),
metrics: Vec::new(),
}
}
pub fn with_capabilities(
metadata: ExtensionMetadata,
commands: Vec<CommandDescriptor>,
metrics: Vec<MetricDescriptor>,
) -> Self {
Self {
metadata,
commands,
metrics,
}
}
pub fn id(&self) -> &str {
&self.metadata.id
}
pub fn name(&self) -> &str {
&self.metadata.name
}
pub fn has_config(&self) -> bool {
false
}
pub fn config_parameters(&self) -> Option<&[ParameterDefinition]> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionMetricValue {
pub name: String,
pub value: MetricValue,
pub timestamp: i64,
}
impl ExtensionMetricValue {
pub fn new(name: impl Into<String>, value: MetricValue) -> Self {
Self {
name: name.into(),
value,
timestamp: current_timestamp_ms(),
}
}
pub fn with_timestamp(name: impl Into<String>, value: MetricValue, timestamp: i64) -> Self {
Self {
name: name.into(),
value,
timestamp,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ExtensionError {
CommandNotFound(String),
MetricNotFound(String),
InvalidArguments(String),
ExecutionFailed(String),
Timeout(String),
NotFound(String),
InvalidFormat(String),
LoadFailed(String),
SecurityError(String),
SymbolNotFound(String),
IncompatibleVersion { expected: u32, got: u32 },
NullPointer,
AlreadyRegistered(String),
NotSupported(String),
InvalidStreamData(String),
SessionNotFound(String),
SessionAlreadyExists(String),
InferenceFailed(String),
Io(String),
Json(String),
ConfigurationError(String),
InternalError(String),
Other(String),
}
impl std::fmt::Display for ExtensionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CommandNotFound(cmd) => write!(f, "Command not found: {}", cmd),
Self::MetricNotFound(metric) => write!(f, "Metric not found: {}", metric),
Self::InvalidArguments(msg) => write!(f, "Invalid arguments: {}", msg),
Self::ExecutionFailed(msg) => write!(f, "Execution failed: {}", msg),
Self::Timeout(msg) => write!(f, "Timeout: {}", msg),
Self::NotFound(msg) => write!(f, "Not found: {}", msg),
Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
Self::LoadFailed(msg) => write!(f, "Load failed: {}", msg),
Self::SecurityError(msg) => write!(f, "Security error: {}", msg),
Self::SymbolNotFound(msg) => write!(f, "Symbol not found: {}", msg),
Self::IncompatibleVersion { expected, got } => {
write!(
f,
"Incompatible version: expected {}, got {}",
expected, got
)
}
Self::NullPointer => write!(f, "Null pointer"),
Self::AlreadyRegistered(msg) => write!(f, "Already registered: {}", msg),
Self::NotSupported(msg) => write!(f, "Not supported: {}", msg),
Self::InvalidStreamData(msg) => write!(f, "Invalid stream data: {}", msg),
Self::SessionNotFound(msg) => write!(f, "Session not found: {}", msg),
Self::SessionAlreadyExists(msg) => write!(f, "Session already exists: {}", msg),
Self::InferenceFailed(msg) => write!(f, "Inference failed: {}", msg),
Self::Io(msg) => write!(f, "IO error: {}", msg),
Self::Json(msg) => write!(f, "JSON error: {}", msg),
Self::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg),
Self::InternalError(msg) => write!(f, "Internal error: {}", msg),
Self::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
impl std::error::Error for ExtensionError {}
impl From<serde_json::Error> for ExtensionError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e.to_string())
}
}
impl From<std::io::Error> for ExtensionError {
fn from(e: std::io::Error) -> Self {
Self::Io(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, ExtensionError>;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExtensionRuntimeState {
pub is_running: bool,
pub is_isolated: bool,
pub loaded_at: Option<i64>,
pub restart_count: u64,
pub last_restart_at: Option<i64>,
pub start_count: u64,
pub stop_count: u64,
pub error_count: u64,
pub last_error: Option<String>,
}
impl ExtensionRuntimeState {
pub fn new() -> Self {
Self::default()
}
pub fn isolated() -> Self {
Self {
is_isolated: true,
..Self::default()
}
}
pub fn mark_running(&mut self) {
self.is_running = true;
self.start_count += 1;
if self.loaded_at.is_none() {
self.loaded_at = Some(current_timestamp_secs());
}
}
pub fn mark_stopped(&mut self) {
self.is_running = false;
self.stop_count += 1;
}
pub fn record_error(&mut self, error: String) {
self.error_count += 1;
self.last_error = Some(error);
}
pub fn increment_restart(&mut self) {
self.restart_count += 1;
self.last_restart_at = Some(current_timestamp_secs());
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExtensionStats {
pub metrics_produced: u64,
pub commands_executed: u64,
pub total_execution_time_ms: u64,
pub last_execution_time_ms: Option<i64>,
pub start_count: u64,
pub stop_count: u64,
pub error_count: u64,
pub last_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ValidationRule {
#[serde(default)]
pub rule_type: String,
#[serde(default)]
pub params: HashMap<String, serde_json::Value>,
}
mod base64_vec {
use ::base64::{engine::general_purpose::STANDARD, Engine};
use ::serde::de::Error as DeError;
use ::serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(data: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
STANDARD.encode(data).serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let value = ::serde_json::Value::deserialize(d)?;
match &value {
::serde_json::Value::String(s) => STANDARD.decode(s).map_err(DeError::custom),
::serde_json::Value::Array(arr) => Ok(arr
.iter()
.filter_map(|v| v.as_u64().map(|n| n as u8))
.collect()),
_ => Err(DeError::custom("expected base64 string or number array")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushOutputMessage {
pub session_id: String,
pub sequence: u64,
#[serde(with = "base64_vec")]
pub data: Vec<u8>,
pub data_type: String,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl PushOutputMessage {
pub fn new(
session_id: impl Into<String>,
sequence: u64,
data: Vec<u8>,
data_type: impl Into<String>,
) -> Self {
Self {
session_id: session_id.into(),
sequence,
data,
data_type: data_type.into(),
timestamp: current_timestamp_ms(),
metadata: None,
}
}
pub fn json(
session_id: impl Into<String>,
sequence: u64,
value: serde_json::Value,
) -> serde_json::Result<Self> {
Ok(Self {
session_id: session_id.into(),
sequence,
data: serde_json::to_vec(&value)?,
data_type: "application/json".to_string(),
timestamp: current_timestamp_ms(),
metadata: None,
})
}
pub fn image_jpeg(session_id: impl Into<String>, sequence: u64, data: Vec<u8>) -> Self {
Self {
session_id: session_id.into(),
sequence,
data,
data_type: "image/jpeg".to_string(),
timestamp: current_timestamp_ms(),
metadata: None,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct CExtensionMetadata {
pub abi_version: u32,
pub id: *const std::os::raw::c_char,
pub name: *const std::os::raw::c_char,
pub version: *const std::os::raw::c_char,
pub description: *const std::os::raw::c_char,
pub author: *const std::os::raw::c_char,
pub metric_count: usize,
pub command_count: usize,
}
#[cfg(not(target_arch = "wasm32"))]
pub fn current_timestamp_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
#[cfg(target_arch = "wasm32")]
pub fn current_timestamp_ms() -> i64 {
0
}
#[cfg(not(target_arch = "wasm32"))]
pub fn current_timestamp_secs() -> i64 {
chrono::Utc::now().timestamp()
}
#[cfg(target_arch = "wasm32")]
pub fn current_timestamp_secs() -> i64 {
0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchCommand {
pub command: String,
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResult {
pub command: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub elapsed_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResultsVec {
pub results: Vec<BatchResult>,
pub total_elapsed_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamClientInfo {
pub client_id: String,
pub ip_addr: Option<String>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamDataChunk {
pub sequence: u64,
pub data_type: String,
pub data: Vec<u8>,
pub timestamp: i64,
pub is_last: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorKind {
CommandNotFound,
InvalidArguments,
ExecutionFailed,
Timeout,
NotFound,
InvalidFormat,
NotInitialized,
Internal,
Security,
}
impl From<ExtensionError> for ErrorKind {
fn from(error: ExtensionError) -> Self {
match error {
ExtensionError::CommandNotFound(_) => ErrorKind::CommandNotFound,
ExtensionError::InvalidArguments(_) => ErrorKind::InvalidArguments,
ExtensionError::ExecutionFailed(_) => ErrorKind::ExecutionFailed,
ExtensionError::Timeout(_) => ErrorKind::Timeout,
ExtensionError::NotFound(_) => ErrorKind::NotFound,
ExtensionError::InvalidFormat(_) => ErrorKind::InvalidFormat,
ExtensionError::MetricNotFound(_) => ErrorKind::NotFound,
ExtensionError::LoadFailed(_) => ErrorKind::Internal,
ExtensionError::SecurityError(_) => ErrorKind::Security,
ExtensionError::SymbolNotFound(_) => ErrorKind::Internal,
ExtensionError::IncompatibleVersion { .. } => ErrorKind::Internal,
ExtensionError::NullPointer => ErrorKind::Internal,
ExtensionError::AlreadyRegistered(_) => ErrorKind::Internal,
ExtensionError::NotSupported(_) => ErrorKind::Internal,
ExtensionError::InvalidStreamData(_) => ErrorKind::InvalidFormat,
ExtensionError::SessionNotFound(_) => ErrorKind::NotFound,
ExtensionError::SessionAlreadyExists(_) => ErrorKind::Internal,
ExtensionError::InferenceFailed(_) => ErrorKind::ExecutionFailed,
ExtensionError::ConfigurationError(_) => ErrorKind::Internal,
ExtensionError::InternalError(_) => ErrorKind::Internal,
ExtensionError::Io(_) => ErrorKind::Internal,
ExtensionError::Json(_) => ErrorKind::InvalidFormat,
ExtensionError::Other(_) => ErrorKind::Internal,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IpcMessage {
Init {
config: serde_json::Value,
},
ExecuteCommand {
command: String,
args: serde_json::Value,
request_id: u64,
},
ProduceMetrics {
request_id: u64,
},
HealthCheck {
request_id: u64,
},
GetMetadata {
request_id: u64,
},
GetEventSubscriptions {
request_id: u64,
},
GetStats {
request_id: u64,
},
Shutdown,
Ping {
timestamp: i64,
},
InitStreamSession {
request_id: u64,
session_id: String,
extension_id: String,
config: serde_json::Value,
client_info: StreamClientInfo,
},
CloseStreamSession {
request_id: u64,
session_id: String,
},
ProcessStreamChunk {
request_id: u64,
session_id: String,
chunk: StreamDataChunk,
},
GetStreamCapability {
request_id: u64,
},
ProcessChunk {
request_id: u64,
chunk: StreamDataChunk,
},
StartPush {
request_id: u64,
session_id: String,
},
StopPush {
request_id: u64,
session_id: String,
},
ExecuteBatch {
commands: Vec<BatchCommand>,
request_id: u64,
},
InvokeCapability {
request_id: u64,
capability: String,
params: serde_json::Value,
},
SubscribeEvents {
request_id: u64,
event_types: Vec<String>,
filter: Option<serde_json::Value>,
},
UnsubscribeEvents {
request_id: u64,
subscription_id: String,
},
PollEvents {
request_id: u64,
subscription_id: String,
},
EventPush {
event_type: String,
payload: serde_json::Value,
timestamp: i64,
},
CapabilityResult {
request_id: u64,
result: serde_json::Value,
error: Option<String>,
},
ConfigUpdate {
config: serde_json::Value,
},
}
impl IpcMessage {
pub fn to_bytes(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(bytes)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IpcResponse {
Ready {
descriptor: ExtensionDescriptor,
},
Success {
request_id: u64,
data: serde_json::Value,
},
Error {
request_id: u64,
error: String,
kind: ErrorKind,
},
Metrics {
request_id: u64,
metrics: Vec<ExtensionMetricValue>,
},
Health {
request_id: u64,
healthy: bool,
},
Metadata {
request_id: u64,
metadata: ExtensionMetadata,
},
EventSubscriptions {
request_id: u64,
event_types: Vec<String>,
},
Stats {
request_id: u64,
start_count: u64,
stop_count: u64,
error_count: u64,
last_error: Option<String>,
},
Pong {
timestamp: i64,
},
StreamSessionInit {
request_id: u64,
session_id: String,
success: bool,
error: Option<String>,
},
StreamSessionClosed {
request_id: u64,
session_id: String,
total_frames: u64,
duration_ms: u64,
},
StreamChunkResult {
request_id: u64,
session_id: String,
input_sequence: u64,
output_sequence: u64,
data: Vec<u8>,
data_type: String,
processing_ms: f32,
},
StreamCapability {
request_id: u64,
capability: Option<serde_json::Value>,
},
ChunkResult {
request_id: u64,
input_sequence: u64,
output_sequence: u64,
data: Vec<u8>,
data_type: String,
processing_ms: f32,
metadata: Option<serde_json::Value>,
},
PushStarted {
request_id: u64,
session_id: String,
success: bool,
error: Option<String>,
},
PushStopped {
request_id: u64,
session_id: String,
success: bool,
},
PushOutput {
session_id: String,
sequence: u64,
data: Vec<u8>,
data_type: String,
timestamp: i64,
metadata: Option<serde_json::Value>,
},
StreamError {
request_id: u64,
session_id: String,
code: String,
message: String,
},
BatchResults {
request_id: u64,
results: Vec<BatchResult>,
total_elapsed_ms: f64,
},
CapabilityResult {
request_id: u64,
result: serde_json::Value,
error: Option<String>,
},
EventSubscriptionResult {
request_id: u64,
subscription_id: Option<String>,
error: Option<String>,
},
EventPollResult {
request_id: u64,
events: Vec<serde_json::Value>,
},
CapabilityRequest {
request_id: u64,
capability: String,
params: serde_json::Value,
},
ShutdownAck,
ConfigUpdated {
success: bool,
error: Option<String>,
},
}
impl IpcResponse {
pub fn to_bytes(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(bytes)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error { .. })
}
pub fn is_push_output(&self) -> bool {
matches!(self, Self::PushOutput { .. })
}
pub fn is_stream_error(&self) -> bool {
matches!(self, Self::StreamError { .. })
}
pub fn is_capability_request(&self) -> bool {
matches!(self, Self::CapabilityRequest { .. })
}
pub fn request_id(&self) -> Option<u64> {
match self {
Self::Ready { .. } => None,
Self::Success { request_id, .. } => Some(*request_id),
Self::Error { request_id, .. } => Some(*request_id),
Self::Metrics { request_id, .. } => Some(*request_id),
Self::Health { request_id, .. } => Some(*request_id),
Self::Metadata { request_id, .. } => Some(*request_id),
Self::EventSubscriptions { request_id, .. } => Some(*request_id),
Self::Pong { .. } => None,
Self::StreamSessionInit { request_id, .. } => Some(*request_id),
Self::StreamSessionClosed { request_id, .. } => Some(*request_id),
Self::StreamChunkResult { request_id, .. } => Some(*request_id),
Self::StreamCapability { request_id, .. } => Some(*request_id),
Self::ChunkResult { request_id, .. } => Some(*request_id),
Self::PushStarted { request_id, .. } => Some(*request_id),
Self::PushStopped { request_id, .. } => Some(*request_id),
Self::PushOutput { .. } => None,
Self::StreamError { request_id, .. } => Some(*request_id),
Self::BatchResults { request_id, .. } => Some(*request_id),
Self::Stats { request_id, .. } => Some(*request_id),
Self::CapabilityResult { request_id, .. } => Some(*request_id),
Self::EventSubscriptionResult { request_id, .. } => Some(*request_id),
Self::EventPollResult { request_id, .. } => Some(*request_id),
Self::CapabilityRequest { request_id, .. } => Some(*request_id),
Self::ShutdownAck => None,
Self::ConfigUpdated { .. } => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushOutputData {
pub session_id: String,
pub sequence: u64,
pub data: Vec<u8>,
pub data_type: String,
pub timestamp: i64,
pub metadata: Option<serde_json::Value>,
}
impl From<IpcResponse> for Option<PushOutputData> {
fn from(response: IpcResponse) -> Self {
match response {
IpcResponse::PushOutput {
session_id,
sequence,
data,
data_type,
timestamp,
metadata,
} => Some(PushOutputData {
session_id,
sequence,
data,
data_type,
timestamp,
metadata,
}),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct IpcFrame {
pub payload: Vec<u8>,
}
pub const MAX_IPC_FRAME_SIZE: usize = 16 * 1024 * 1024;
impl IpcFrame {
pub fn new(payload: Vec<u8>) -> Self {
Self { payload }
}
pub fn encode(&self) -> Vec<u8> {
let len = self.payload.len() as u32;
let mut bytes = Vec::with_capacity(4 + self.payload.len());
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(&self.payload);
bytes
}
pub fn decode(bytes: &[u8]) -> std::result::Result<(Self, usize), &'static str> {
if bytes.len() < 4 {
return Err("Not enough bytes for length prefix");
}
let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
if len > MAX_IPC_FRAME_SIZE {
return Err("Frame exceeds maximum allowed size (16 MB)");
}
if bytes.len() < 4 + len {
return Err("Not enough bytes for payload");
}
let payload = bytes[4..4 + len].to_vec();
Ok((Self { payload }, 4 + len))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_data_type_serialization() {
let types = vec![
(MetricDataType::Float, r#""float""#),
(MetricDataType::Integer, r#""integer""#),
(MetricDataType::Boolean, r#""boolean""#),
(MetricDataType::String, r#""string""#),
(MetricDataType::Binary, r#""binary""#),
];
for (dtype, expected) in types {
let json = serde_json::to_string(&dtype).unwrap();
assert_eq!(json, expected);
let deserialized: MetricDataType = serde_json::from_str(expected).unwrap();
assert_eq!(dtype, deserialized);
}
}
#[test]
fn test_metric_value_from() {
let f: MetricValue = 42.0.into();
assert!(matches!(f, MetricValue::Float(42.0)));
let i: MetricValue = 42i64.into();
assert!(matches!(i, MetricValue::Integer(42)));
let b: MetricValue = true.into();
assert!(matches!(b, MetricValue::Boolean(true)));
let s: MetricValue = "test".into();
assert!(matches!(s, MetricValue::String(_)));
}
#[test]
fn test_extension_metadata() {
let meta = ExtensionMetadata::new("test-ext", "Test Extension", "1.0.0")
.with_description("A test extension")
.with_author("Test Author");
assert_eq!(meta.id, "test-ext");
assert_eq!(meta.name, "Test Extension");
assert_eq!(meta.description, Some("A test extension".to_string()));
assert_eq!(meta.author, Some("Test Author".to_string()));
}
#[test]
fn test_extension_descriptor_serialization() {
let descriptor = ExtensionDescriptor::with_capabilities(
ExtensionMetadata::new("test", "Test", "1.0.0"),
vec![CommandDescriptor::new("cmd1")],
vec![MetricDescriptor::new("m1", "M1", MetricDataType::Float)],
);
let json = serde_json::to_string(&descriptor).unwrap();
let deserialized: ExtensionDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(descriptor.metadata.id, deserialized.metadata.id);
assert_eq!(descriptor.commands.len(), deserialized.commands.len());
assert_eq!(descriptor.metrics.len(), deserialized.metrics.len());
}
#[test]
fn test_extension_error_display() {
let err = ExtensionError::CommandNotFound("test".to_string());
assert!(err.to_string().contains("Command not found"));
}
#[test]
fn test_abi_version() {
assert_eq!(ABI_VERSION, 3);
}
#[test]
fn test_ipc_message_serialization() {
let msg = IpcMessage::ExecuteCommand {
command: "test".to_string(),
args: serde_json::json!({"arg": 1}),
request_id: 1,
};
let bytes = msg.to_bytes().unwrap();
let decoded = IpcMessage::from_bytes(&bytes).unwrap();
match decoded {
IpcMessage::ExecuteCommand {
command,
args,
request_id,
} => {
assert_eq!(command, "test");
assert_eq!(request_id, 1);
assert_eq!(args, serde_json::json!({"arg": 1}));
}
_ => panic!("Wrong message type"),
}
}
#[test]
fn test_ipc_frame_encoding() {
let payload = b"hello world";
let frame = IpcFrame::new(payload.to_vec());
let encoded = frame.encode();
assert_eq!(encoded.len(), 4 + payload.len());
assert_eq!(&encoded[0..4], &(payload.len() as u32).to_le_bytes());
assert_eq!(&encoded[4..], payload);
let (decoded, consumed) = IpcFrame::decode(&encoded).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded.payload, payload);
}
#[test]
fn test_error_kind_from_extension_error() {
let err = ExtensionError::CommandNotFound("test".to_string());
let kind: ErrorKind = err.into();
assert_eq!(kind, ErrorKind::CommandNotFound);
let err = ExtensionError::Timeout("timeout".to_string());
let kind: ErrorKind = err.into();
assert_eq!(kind, ErrorKind::Timeout);
}
}