use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use super::super::{
CONTEXT_DELTA_SCHEMA_V2, ContextDeltaDegradation, ContextDeltaEnvelope, ContextDeltaError,
ContextDeltaFallbackReason, ContextDeltaFieldChange, ContextDeltaFieldChangeRedaction,
ContextDeltaItemSnapshot, ContextDeltaItems, ContextDeltaModifiedItem, ContextDeltaPayload,
ContextDeltaRedactionReason, ContextDeltaServerDecision, ContextDeltaTokenSavings,
};
struct UniqueMap<T>(BTreeMap<String, T>);
impl<'de, T: Deserialize<'de>> Deserialize<'de> for UniqueMap<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct UniqueMapVisitor<T>(PhantomData<T>);
impl<'de, T: Deserialize<'de>> Visitor<'de> for UniqueMapVisitor<T> {
type Value = UniqueMap<T>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an object with unique field names")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut values = BTreeMap::new();
while let Some((key, value)) = map.next_entry::<String, T>()? {
if values.insert(key, value).is_some() {
return Err(de::Error::custom("duplicate context-delta field"));
}
}
Ok(UniqueMap(values))
}
}
deserializer.deserialize_map(UniqueMapVisitor(PhantomData))
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireEnvelope {
schema: String,
success: bool,
data: WirePayload,
degraded: Vec<WireDegradation>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WirePayload {
prior_pack_hash: String,
new_pack_hash: String,
workspace_id: Option<String>,
base_db_generation: Option<u64>,
new_db_generation: Option<u64>,
prior_feature_flag_set_hash: Option<String>,
new_feature_flag_set_hash: Option<String>,
items: WireItems,
token_savings: WireTokenSavings,
server_decision: WireDecision,
trace: Option<UniqueMap<JsonValue>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireDecision {
computed_from_server_verified_pack_record: bool,
delta_chained: bool,
format: WireFormat,
fallback_reason: Option<ContextDeltaFallbackReason>,
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum WireFormat {
Json,
Markdown,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireDegradation {
code: String,
severity: WireSeverity,
message: String,
repair: Option<String>,
details: Option<UniqueMap<JsonValue>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum WireSeverity {
Info,
Low,
Warning,
Medium,
High,
Critical,
}
impl WireSeverity {
const fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Low => "low",
Self::Warning => "warning",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireItems {
added: Vec<WireItem>,
removed: Vec<String>,
modified: Vec<WireModifiedItem>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireItem {
id: String,
fields: UniqueMap<JsonValue>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireModifiedItem {
id: String,
field_changes: UniqueMap<WireFieldChange>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum WireFieldChange {
Pair([JsonValue; 2]),
Redacted(WireRedaction),
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireRedaction {
new_value: JsonValue,
old_value_omitted: bool,
reason: ContextDeltaRedactionReason,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireTokenSavings {
full_bytes: u64,
delta_bytes: u64,
saved_bytes: i64,
saved_percent: f64,
net_pack_tokens: u32,
}
impl WireEnvelope {
fn into_envelope(self) -> Result<ContextDeltaEnvelope, &'static str> {
if self.schema != CONTEXT_DELTA_SCHEMA_V2 {
return Err("unsupported context-delta schema");
}
if !self.success || self.data.server_decision.delta_chained {
return Err("context-delta v2 requires success and forbids delta chaining");
}
if !self.data.token_savings.saved_percent.is_finite() {
return Err("context-delta savings must be finite");
}
let items = self.data.items;
let modified = items
.modified
.into_iter()
.map(|item| {
let field_changes = item
.field_changes
.0
.into_iter()
.map(|(field, change)| {
let change = match change {
WireFieldChange::Pair(pair) => ContextDeltaFieldChange::Pair(pair),
WireFieldChange::Redacted(redaction) => {
if !redaction.old_value_omitted {
return Err("redacted change must omit its old value");
}
ContextDeltaFieldChange::Redacted(
ContextDeltaFieldChangeRedaction {
new_value: redaction.new_value,
old_value_omitted: true,
reason: redaction.reason,
},
)
}
};
Ok((field, change))
})
.collect::<Result<_, _>>()?;
Ok(ContextDeltaModifiedItem {
id: item.id,
field_changes,
})
})
.collect::<Result<_, &'static str>>()?;
let savings = self.data.token_savings;
let decision = self.data.server_decision;
Ok(ContextDeltaEnvelope {
schema: CONTEXT_DELTA_SCHEMA_V2,
success: true,
data: ContextDeltaPayload {
prior_pack_hash: self.data.prior_pack_hash,
new_pack_hash: self.data.new_pack_hash,
workspace_id: self.data.workspace_id,
base_db_generation: self.data.base_db_generation,
new_db_generation: self.data.new_db_generation,
prior_feature_flag_set_hash: self.data.prior_feature_flag_set_hash,
new_feature_flag_set_hash: self.data.new_feature_flag_set_hash,
items: ContextDeltaItems {
added: items
.added
.into_iter()
.map(|item| ContextDeltaItemSnapshot {
id: item.id,
fields: item.fields.0,
})
.collect(),
removed: items.removed,
modified,
},
token_savings: ContextDeltaTokenSavings {
full_bytes: savings.full_bytes,
delta_bytes: savings.delta_bytes,
saved_bytes: savings.saved_bytes,
saved_percent: savings.saved_percent,
net_pack_tokens: savings.net_pack_tokens,
},
server_decision: ContextDeltaServerDecision {
computed_from_server_verified_pack_record: decision
.computed_from_server_verified_pack_record,
delta_chained: false,
format: match decision.format {
WireFormat::Json => "json",
WireFormat::Markdown => "markdown",
},
fallback_reason: decision.fallback_reason,
},
trace: self.data.trace.map(|map| map.0),
},
degraded: self
.degraded
.into_iter()
.map(|entry| ContextDeltaDegradation {
code: entry.code,
severity: entry.severity.as_str().to_owned(),
message: entry.message,
repair: entry.repair,
details: entry.details.map(|map| map.0),
})
.collect(),
})
}
}
impl<'de> Deserialize<'de> for ContextDeltaEnvelope {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = WireEnvelope::deserialize(deserializer)
.map_err(|_| de::Error::custom("invalid context-delta v2 structure"))?;
wire.into_envelope().map_err(de::Error::custom)
}
}
impl ContextDeltaEnvelope {
pub const DEFAULT_MAX_JSON_BYTES: usize = 4 * 1024 * 1024;
pub fn from_json_slice(bytes: &[u8]) -> Result<Self, ContextDeltaError> {
Self::from_json_slice_with_limit(bytes, Self::DEFAULT_MAX_JSON_BYTES)
}
pub fn from_json_slice_with_limit(
bytes: &[u8],
max_bytes: usize,
) -> Result<Self, ContextDeltaError> {
if bytes.len() > max_bytes {
return Err(ContextDeltaError {
message:
"context delta exceeds the client input-byte limit; request a fresh full pack"
.to_owned(),
});
}
serde_json::from_slice(bytes).map_err(|_| ContextDeltaError {
message: "invalid context-delta v2 JSON; request a fresh full pack".to_owned(),
})
}
}
#[cfg(test)]
#[path = "context_delta_wire_tests.rs"]
mod tests;
#[path = "context_delta_scoped.rs"]
mod scoped;