Skip to main content

smg_wasm/
module.rs

1//! WASM Module Data Structures and Types
2//!
3//! This module defines the core data structures for managing WebAssembly components:
4//! - Module metadata (UUID, name, file path, hash, timestamps, metrics)
5//! - Module types and attachment points (Middleware hooks: OnRequest, OnResponse, OnError)
6//! - API request/response types for module management
7//! - Execution metrics and statistics
8//!
9//! The module provides custom serialization for:
10//! - SHA256 hashes (hex string representation)
11//! - Timestamps (ISO 8601 format for JSON output)
12
13use std::sync::Arc;
14
15use serde::{Deserialize, Serialize, Serializer};
16use uuid::Uuid;
17
18/// Serialize [u8; 32] as hex string
19fn serialize_sha256_hash<S>(hash: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
20where
21    S: Serializer,
22{
23    let hex_string = hash.iter().map(|b| format!("{b:02x}")).collect::<String>();
24    serializer.serialize_str(&hex_string)
25}
26
27/// Deserialize hex string to [u8; 32]
28fn deserialize_sha256_hash<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
29where
30    D: serde::Deserializer<'de>,
31{
32    use serde::Deserialize;
33    let hex_string = String::deserialize(deserializer)?;
34
35    // Parse hex string to bytes
36    if hex_string.len() != 64 {
37        return Err(serde::de::Error::custom(format!(
38            "Invalid SHA256 hash length: expected 64 hex characters, got {}",
39            hex_string.len()
40        )));
41    }
42
43    let mut hash = [0u8; 32];
44    for (i, chunk) in hex_string.as_bytes().chunks(2).enumerate() {
45        if chunk.len() != 2 {
46            return Err(serde::de::Error::custom("Invalid hex string format"));
47        }
48        let byte_str = std::str::from_utf8(chunk)
49            .map_err(|e| serde::de::Error::custom(format!("Invalid UTF-8: {e}")))?;
50        hash[i] = u8::from_str_radix(byte_str, 16)
51            .map_err(|e| serde::de::Error::custom(format!("Invalid hex digit: {e}")))?;
52    }
53
54    Ok(hash)
55}
56
57/// Serialize u64 timestamp (nanoseconds since epoch) as ISO 8601 string
58#[expect(
59    clippy::trivially_copy_pass_by_ref,
60    reason = "serde serialize_with requires &T signature"
61)]
62fn serialize_timestamp<S>(timestamp: &u64, serializer: S) -> Result<S::Ok, S::Error>
63where
64    S: Serializer,
65{
66    use chrono::{DateTime, Utc};
67
68    // Convert nanoseconds to seconds and remaining nanoseconds
69    let secs = (*timestamp / 1_000_000_000) as i64;
70    let nanos = (*timestamp % 1_000_000_000) as u32;
71
72    match DateTime::<Utc>::from_timestamp(secs, nanos) {
73        Some(dt) => {
74            let s = dt.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true);
75            serializer.serialize_str(&s)
76        }
77        None => {
78            // Fallback: format manually if timestamp is out of range
79            let s = format!("{timestamp}");
80            serializer.serialize_str(&s)
81        }
82    }
83}
84
85/// Deserialize ISO 8601 string to u64 timestamp (nanoseconds since epoch)
86fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<u64, D::Error>
87where
88    D: serde::Deserializer<'de>,
89{
90    use chrono::{DateTime, Utc};
91    use serde::Deserialize;
92
93    let timestamp_str = String::deserialize(deserializer)?;
94
95    // Try to parse as ISO 8601 datetime (RFC 3339)
96    match DateTime::parse_from_rfc3339(&timestamp_str) {
97        Ok(dt) => {
98            // Convert to UTC and then to nanoseconds since epoch
99            let dt_utc = dt.with_timezone(&Utc);
100            let secs = dt_utc.timestamp();
101            let nanos = dt_utc.timestamp_subsec_nanos();
102            Ok((secs as u64) * 1_000_000_000 + (nanos as u64))
103        }
104        Err(_) => {
105            // Fallback: try to parse as u64 directly
106            timestamp_str
107                .parse::<u64>()
108                .map_err(|e| serde::de::Error::custom(format!("Invalid timestamp format: {e}")))
109        }
110    }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct WasmModule {
115    // unique identifier for the module
116    pub module_uuid: Uuid,
117    pub module_meta: WasmModuleMeta,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub enum WasmModuleAddResult {
122    Success(Uuid),
123    Error(String),
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct WasmModuleDescriptor {
128    pub name: String,
129    pub file_path: String,
130    pub module_type: WasmModuleType,
131    pub attach_points: Vec<WasmModuleAttachPoint>,
132    pub add_result: Option<WasmModuleAddResult>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct WasmModuleMeta {
137    // module name provided by the user
138    pub name: String,
139    // path to the module file
140    pub file_path: String,
141    // sha256 hash of the module file
142    #[serde(
143        serialize_with = "serialize_sha256_hash",
144        deserialize_with = "deserialize_sha256_hash"
145    )]
146    pub sha256_hash: [u8; 32],
147    // size of the module file in bytes
148    pub size_bytes: u64,
149    // timestamp of when the module was created (nanoseconds since epoch)
150    #[serde(
151        serialize_with = "serialize_timestamp",
152        deserialize_with = "deserialize_timestamp"
153    )]
154    pub created_at: u64,
155    // timestamp of when the module was last accessed (nanoseconds since epoch)
156    #[serde(
157        serialize_with = "serialize_timestamp",
158        deserialize_with = "deserialize_timestamp"
159    )]
160    pub last_accessed_at: u64,
161    // number of times the module was accessed
162    pub access_count: u64,
163    // attach points for the module
164    pub attach_points: Vec<WasmModuleAttachPoint>,
165    // Pre-loaded WASM component bytes (loaded into memory for faster execution)
166    // Wrapped in Arc to avoid cloning full bytes on every execution request.
167    #[serde(skip)]
168    pub wasm_bytes: Arc<Vec<u8>>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
172pub enum WasmModuleType {
173    Middleware,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
177#[expect(
178    clippy::enum_variant_names,
179    reason = "On* prefix is the standard naming convention for middleware lifecycle hooks"
180)]
181pub enum MiddlewareAttachPoint {
182    OnRequest,
183    OnResponse,
184    OnError,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
188pub enum WasmModuleAttachPoint {
189    Middleware(MiddlewareAttachPoint),
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct WasmModuleAddRequest {
194    pub modules: Vec<WasmModuleDescriptor>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct WasmModuleAddResponse {
199    pub modules: Vec<WasmModuleDescriptor>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct WasmModuleListResponse {
204    pub modules: Vec<WasmModule>,
205    pub metrics: WasmMetrics,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct WasmMetrics {
210    pub total_executions: u64,
211    pub successful_executions: u64,
212    pub failed_executions: u64,
213    pub total_execution_time_ms: u64,
214    pub max_execution_time_ms: u64,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub average_execution_time_ms: Option<f64>,
217}