use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::error::ParseError;
use crate::framing::project_dto;
use crate::limits::Limits;
use crate::logs::{validate_log_record, LogRecord};
use crate::marker::MAX_SAFE_INTEGER;
use crate::roles::{valid_capability, Capability, ADAPTER_CAPABILITIES};
use crate::tree::Snapshot;
use crate::validate::{validate_snapshot, validate_tree_delta};
pub const PROTOCOL_ID: &str = "termwright/1";
pub const PROTOCOL_V2_ID: &str = "termwright/2";
pub const PROTOCOL_VERSION: u8 = 1;
const MAX_IDENTIFIER_LENGTH: usize = 1024;
const ERROR_CODES: [&str; 5] = [
"bad-token",
"bad-version",
"malformed",
"limit-exceeded",
"internal",
];
const LIMIT_FIELDS: [&str; 11] = [
"maxFrameBytes",
"maxSnapshotBytes",
"maxNodes",
"maxDepth",
"maxStringBytes",
"maxRelationTargets",
"maxQueuedFrames",
"maxPendingWaiters",
"maxSessions",
"maxLogRecordBytes",
"maxLogQueue",
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Hello {
#[serde(rename = "type")]
pub kind: String,
pub protocol: String,
pub token: String,
pub adapter: AdapterInfo,
pub capabilities: Vec<Capability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub probe: Option<ProbeInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProbeIdentityKind {
Stable,
FrameLocal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeInfo {
pub framework: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework_version: Option<String>,
pub probe_version: String,
pub identity_kind: ProbeIdentityKind,
pub capabilities: Vec<String>,
}
impl Hello {
pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
Self {
kind: "hello".into(),
protocol: PROTOCOL_ID.into(),
token: token.to_owned(),
adapter: AdapterInfo {
name: name.to_owned(),
version: version.to_owned(),
},
capabilities,
probe: None,
}
}
pub fn new_v2(
token: &str,
name: &str,
version: &str,
mut capabilities: Vec<Capability>,
) -> Self {
if !capabilities.contains(&Capability::QualifiedObservations) {
capabilities.push(Capability::QualifiedObservations);
}
let mut hello = Self::new(token, name, version, capabilities);
hello.protocol = PROTOCOL_V2_ID.into();
hello
}
#[must_use]
pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
self.probe = Some(probe);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkerConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogBudget {
pub enabled: bool,
pub max_records_per_second: i64,
pub burst: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HelloAck {
#[serde(rename = "type")]
pub kind: String,
pub protocol: String,
pub session_id: String,
pub limits: Limits,
pub subscribe: String,
pub marker: MarkerConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logs: Option<LogBudget>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevisionCommit {
#[serde(rename = "type")]
pub kind: &'static str,
pub revision: i64,
}
impl RevisionCommit {
pub fn new(revision: i64) -> Self {
Self {
kind: "revision-commit",
revision,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SnapshotMessage<'a> {
#[serde(rename = "type")]
pub kind: &'static str,
pub snapshot: &'a Snapshot,
}
impl<'a> SnapshotMessage<'a> {
pub fn new(snapshot: &'a Snapshot) -> Self {
Self {
kind: "snapshot",
snapshot,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTree {
#[serde(rename = "type")]
pub kind: String,
pub request_id: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<i64>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTreeResult {
#[serde(rename = "type")]
pub kind: &'static str,
pub request_id: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub snapshot: Option<Box<serde_json::value::RawValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl GetTreeResult {
pub fn found(request_id: i64, snapshot: Box<serde_json::value::RawValue>) -> Self {
Self {
kind: "get-tree-result",
request_id,
snapshot: Some(snapshot),
error: None,
}
}
pub fn missing(request_id: i64, detail: impl Into<String>) -> Self {
Self {
kind: "get-tree-result",
request_id,
snapshot: None,
error: Some(detail.into()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct LogMessage<'a> {
#[serde(rename = "type")]
pub kind: &'static str,
pub record: &'a LogRecord,
}
impl<'a> LogMessage<'a> {
pub fn new(record: &'a LogRecord) -> Self {
Self {
kind: "log",
record,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolErrorMessage {
#[serde(rename = "type")]
pub kind: String,
pub code: String,
pub message: String,
}
impl ProtocolErrorMessage {
pub fn new(code: &str, message: impl Into<String>) -> Self {
Self {
kind: "error".into(),
code: code.to_owned(),
message: message.into(),
}
}
}
pub fn default_capabilities() -> Vec<Capability> {
vec![
Capability::Tree,
Capability::Bounds,
Capability::AbsoluteBounds,
Capability::States,
Capability::Actions,
Capability::RenderRevisions,
]
}
fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project_dto(value, limits.max_depth).map_err(|violation| {
if violation.code == "dto-depth" {
ParseError::new("limit-exceeded", violation.to_string())
} else {
ParseError::malformed(violation.to_string())
}
})
}
fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
let object = value
.as_object()
.ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
let kind = object
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
Ok((object, kind))
}
fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
for key in required {
if !object.contains_key(*key) {
return Err(ParseError::malformed(format!("missing field \"{key}\"")));
}
}
Ok(())
}
fn require_keys(
object: &Map<String, Value>,
required: &[&str],
optional: &[&str],
) -> Result<(), ParseError> {
for key in required {
if !object.contains_key(*key) {
return Err(ParseError::malformed(format!("missing field \"{key}\"")));
}
}
for key in object.keys() {
if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
}
}
Ok(())
}
fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
let Some(text) = object.get(key).and_then(Value::as_str) else {
return Err(ParseError::malformed(format!("{key}: expected a string")));
};
if text.len() > MAX_IDENTIFIER_LENGTH {
return Err(ParseError::malformed(format!(
"{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
)));
}
if !allow_empty && text.is_empty() {
return Err(ParseError::malformed(format!(
"{key}: expected a non-empty string"
)));
}
Ok(())
}
fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
let number = object
.get(key)
.and_then(Value::as_i64)
.filter(|n| n.abs() <= MAX_SAFE_INTEGER);
match number {
Some(number) if positive && number > 0 => Ok(()),
Some(number) if !positive && number >= 0 => Ok(()),
_ if positive => Err(ParseError::malformed(format!(
"{key}: expected a positive safe integer"
))),
_ => Err(ParseError::malformed(format!(
"{key}: expected a non-negative safe integer"
))),
}
}
fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
match validate_snapshot(value, limits) {
Ok(()) => Ok(()),
Err(error) => {
let code = match error.code {
"bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
_ => "malformed",
};
Err(ParseError::new(code, format!("snapshot {error}")))
}
}
}
fn check_log_budget(value: &Value) -> Result<(), ParseError> {
let budget = value
.as_object()
.ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
if !budget["enabled"].is_boolean() {
return Err(ParseError::malformed("logs.enabled: expected a boolean"));
}
whole_number(budget, "maxRecordsPerSecond", true)?;
whole_number(budget, "burst", false)
}
fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
if strict {
require_keys(object, &["type", "code", "message"], &[])?;
} else {
required_keys(object, &["type", "code", "message"])?;
}
let code = object
.get("code")
.and_then(Value::as_str)
.unwrap_or_default();
if !ERROR_CODES.contains(&code) {
return Err(ParseError::malformed("code: unknown error code"));
}
identifier(object, "message", true)
}
fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
match object.get("protocol").and_then(Value::as_str) {
Some(protocol) if protocol != PROTOCOL_ID && protocol != PROTOCOL_V2_ID => Err(
ParseError::new("bad-version", format!("unsupported protocol {protocol}")),
),
_ => Ok(()),
}
}
pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project(value, limits)?;
let (object, kind) = as_message(value)?;
match kind {
"hello" => {
check_protocol_field(object)?;
require_keys(
object,
&["type", "protocol", "token", "adapter", "capabilities"],
&[],
)?;
identifier(object, "token", false)?;
let adapter = object
.get("adapter")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
require_keys(adapter, &["name", "version"], &[])?;
identifier(adapter, "name", false)?;
identifier(adapter, "version", false)?;
let capabilities = object
.get("capabilities")
.and_then(Value::as_array)
.ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
if capabilities.len() > ADAPTER_CAPABILITIES.len() {
return Err(ParseError::malformed("capabilities: too many entries"));
}
for item in capabilities {
match item.as_str() {
Some(name) if valid_capability(name) => {}
_ => return Err(ParseError::malformed("capabilities: unknown capability")),
}
}
let protocol = object
.get("protocol")
.and_then(Value::as_str)
.unwrap_or_default();
let qualified = capabilities
.iter()
.any(|item| item.as_str() == Some("qualified-observations"));
let pointer_grid = capabilities
.iter()
.any(|item| item.as_str() == Some("pointer-hit-grid"));
if (protocol == PROTOCOL_V2_ID) != qualified {
return Err(ParseError::malformed(
"termwright/2 and qualified-observations must be negotiated together",
));
}
if pointer_grid && !qualified {
return Err(ParseError::malformed(
"pointer-hit-grid requires qualified-observations",
));
}
Ok(())
}
"revision-commit" => {
require_keys(object, &["type", "revision"], &[])?;
whole_number(object, "revision", true)
}
"snapshot" => {
require_keys(object, &["type", "snapshot"], &[])?;
check_embedded_snapshot(&object["snapshot"], limits)
}
"get-tree-result" => {
require_keys(object, &["type", "requestId"], &["snapshot", "error"])?;
whole_number(object, "requestId", false)?;
let has_snapshot = object.contains_key("snapshot");
let has_error = object.contains_key("error");
if has_snapshot == has_error {
return Err(ParseError::malformed(
"exactly one of snapshot or error must be present",
));
}
if has_error {
return identifier(object, "error", true);
}
check_embedded_snapshot(&object["snapshot"], limits)
}
"tree-delta" => match validate_tree_delta(value, limits) {
Ok(()) => Ok(()),
Err(error) => {
let code = match error.code {
"bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
_ => "malformed",
};
Err(ParseError::new(code, format!("tree-delta {error}")))
}
},
"log" => {
require_keys(object, &["type", "record"], &[])?;
check_embedded_log_record(&object["record"], limits)
}
"error" => check_error_message(object, true),
_ => Err(ParseError::malformed("unknown or missing message type")),
}
}
fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
match validate_log_record(value, limits) {
Ok(()) => Ok(()),
Err(error) => {
let code = match error.code {
"bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
_ => "malformed",
};
Err(ParseError::new(code, format!("log record {error}")))
}
}
}
pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project(value, limits)?;
let (object, kind) = as_message(value)?;
match kind {
"hello-ack" => {
check_protocol_field(object)?;
required_keys(
object,
&[
"type",
"protocol",
"sessionId",
"limits",
"subscribe",
"marker",
],
)?;
identifier(object, "sessionId", false)?;
let limits_object = object
.get("limits")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
required_keys(limits_object, &LIMIT_FIELDS)?;
for field in LIMIT_FIELDS {
whole_number(limits_object, field, true)?;
}
match object.get("subscribe").and_then(Value::as_str) {
Some("snapshots") | Some("revisions") | Some("diffs") => {}
_ => {
return Err(ParseError::malformed(
"subscribe: expected 'snapshots', 'revisions' or 'diffs'",
))
}
}
let marker = object
.get("marker")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
required_keys(marker, &["enabled"])?;
if !marker["enabled"].is_boolean() {
return Err(ParseError::malformed("marker.enabled: expected a boolean"));
}
if let Some(logs) = object.get("logs") {
check_log_budget(logs)?;
}
Ok(())
}
"get-tree" => {
required_keys(object, &["type", "requestId"])?;
whole_number(object, "requestId", false)?;
if object.contains_key("revision") {
whole_number(object, "revision", true)?;
}
Ok(())
}
"error" => check_error_message(object, false),
_ => Err(ParseError::malformed("unknown or missing message type")),
}
}