use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const GLASS_PROTOCOL_VERSION: u32 = 1;
const MAX_ID_BYTES: usize = 128;
const MAX_OPERATION_BYTES: usize = 96;
const MAX_ERROR_CODE_BYTES: usize = 64;
const MAX_MESSAGE_BYTES: usize = 512;
const MAX_DEADLINE_MS: u64 = 15 * 60 * 1_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MutationLeaseRef {
pub session_id: String,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GlassRequest {
pub protocol_version: u32,
pub request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub correlation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutation_lease: Option<MutationLeaseRef>,
pub operation: String,
pub payload: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline_ms: Option<u64>,
}
impl GlassRequest {
pub fn validate(&self) -> Result<(), ProtocolError> {
if self.protocol_version != GLASS_PROTOCOL_VERSION {
return Err(ProtocolError::UnsupportedVersion(self.protocol_version));
}
validate_identifier(&self.request_id, "requestId")?;
if let Some(correlation_id) = &self.correlation_id {
validate_identifier(correlation_id, "correlationId")?;
}
if let Some(session_id) = &self.session_id {
validate_identifier(session_id, "sessionId")?;
}
if let Some(lease) = &self.mutation_lease {
validate_identifier(&lease.session_id, "mutationLease.sessionId")?;
validate_identifier(&lease.token, "mutationLease.token")?;
}
if self.operation.is_empty() || self.operation.len() > MAX_OPERATION_BYTES {
return Err(ProtocolError::InvalidField(
"operation must be a bounded non-empty string".into(),
));
}
if self.operation.chars().any(char::is_whitespace) {
return Err(ProtocolError::InvalidField(
"operation must not contain whitespace".into(),
));
}
if let Some(deadline_ms) = self.deadline_ms
&& !(1..=MAX_DEADLINE_MS).contains(&deadline_ms)
{
return Err(ProtocolError::InvalidField(format!(
"deadlineMs must be 1..={MAX_DEADLINE_MS}"
)));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GlassResponse {
pub protocol_version: u32,
pub request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub correlation_id: Option<String>,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<GlassError>,
}
impl GlassResponse {
pub fn validate(&self) -> Result<(), ProtocolError> {
if self.protocol_version != GLASS_PROTOCOL_VERSION {
return Err(ProtocolError::UnsupportedVersion(self.protocol_version));
}
validate_identifier(&self.request_id, "requestId")?;
if let Some(correlation_id) = &self.correlation_id {
validate_identifier(correlation_id, "correlationId")?;
}
match (self.ok, self.result.is_some(), self.error.is_some()) {
(true, true, false) | (false, false, true) => Ok(()),
_ => Err(ProtocolError::InvalidField(
"ok responses require result and error responses require error".into(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GlassError {
pub code: String,
pub message: String,
pub retryable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
impl GlassError {
pub fn validate(&self) -> Result<(), ProtocolError> {
if self.code.is_empty() || self.code.len() > MAX_ERROR_CODE_BYTES {
return Err(ProtocolError::InvalidField(
"error code must be a bounded non-empty string".into(),
));
}
if self.message.is_empty() || self.message.len() > MAX_MESSAGE_BYTES {
return Err(ProtocolError::InvalidField(
"error message must be a bounded non-empty string".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProtocolError {
UnsupportedVersion(u32),
InvalidField(String),
}
impl std::fmt::Display for ProtocolError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedVersion(version) => {
write!(formatter, "unsupported Glass protocol version {version}")
}
Self::InvalidField(detail) => formatter.write_str(detail),
}
}
}
impl std::error::Error for ProtocolError {}
fn validate_identifier(value: &str, field: &str) -> Result<(), ProtocolError> {
if value.is_empty() || value.len() > MAX_ID_BYTES || value.chars().any(char::is_whitespace) {
return Err(ProtocolError::InvalidField(format!(
"{field} must be a bounded non-whitespace identifier"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> GlassRequest {
GlassRequest {
protocol_version: GLASS_PROTOCOL_VERSION,
request_id: "request-1".into(),
correlation_id: Some("run-1".into()),
session_id: Some("session-1".into()),
mutation_lease: Some(MutationLeaseRef {
session_id: "session-1".into(),
token: "lease-1".into(),
}),
operation: "browser.observe".into(),
payload: serde_json::json!({"level": "interactive"}),
deadline_ms: Some(5_000),
}
}
#[test]
fn request_round_trips_and_validates() {
let request = request();
request.validate().unwrap();
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["protocolVersion"], 1);
assert_eq!(value["mutationLease"]["sessionId"], "session-1");
let decoded: GlassRequest = serde_json::from_value(value).unwrap();
assert_eq!(decoded, request);
}
#[test]
fn response_requires_exactly_one_outcome() {
let response = GlassResponse {
protocol_version: GLASS_PROTOCOL_VERSION,
request_id: "request-1".into(),
correlation_id: None,
ok: false,
result: None,
error: Some(GlassError {
code: "leaseRequired".into(),
message: "a mutation lease is required".into(),
retryable: true,
details: None,
}),
};
response.validate().unwrap();
let mut invalid = response.clone();
invalid.ok = true;
assert!(invalid.validate().is_err());
}
#[test]
fn bounds_and_unknown_fields_fail_closed() {
let mut request = request();
request.operation = "bad operation".into();
assert!(request.validate().is_err());
let unknown = serde_json::json!({
"protocolVersion": 1,
"requestId": "request-1",
"operation": "browser.observe",
"payload": {},
"future": true
});
assert!(serde_json::from_value::<GlassRequest>(unknown).is_err());
}
}