1use crate::DocumentError;
5use crate::definition::{ProvenanceClass, TypeDecl};
6use crate::grade::Grade;
7use crate::ids::ContentDigest;
8use crate::remote::{CancelSupport, RemoteCode, TransportPhase, WirePrincipal};
9use crate::snapshot::Snapshot;
10use serde::{Deserialize, Serialize};
11
12pub const PROTOCOL: &str = "rustev.remote-context/1";
13pub const DESCRIBE: &str = "rustev.remote-context-describe/1";
14pub const FETCH: &str = "rustev.remote-context-fetch/1";
15pub const CANCEL: &str = "rustev.remote-context-cancel/1";
16pub const INVALIDATION: &str = "rustev.remote-context-invalidation/1";
17pub const INVALIDATIONS: &str = "rustev.remote-context-invalidations/1";
18pub const EXCHANGE: &str = "rustev.remote-context-exchange/1";
19pub(crate) fn invalid(message: impl Into<String>) -> DocumentError {
22 DocumentError::Parse(crate::bounded::ParseError::Schema {
23 message: message.into(),
24 })
25}
26
27macro_rules! checked_document {
29 ($ty:ty, $schema:expr, $limits:expr, $check:expr) => {
30 impl crate::Document for $ty {
31 const SCHEMA: &'static str = $schema;
32 const LIMITS: crate::bounded::ParseLimits = $limits;
33 fn schema(&self) -> &str {
34 &self.schema
35 }
36 fn parse(bytes: &[u8]) -> Result<Self, crate::DocumentError> {
37 let doc: Self = crate::bounded::parse_bounded(bytes, &Self::LIMITS)
38 .map_err(crate::DocumentError::Parse)?;
39 if doc.schema != Self::SCHEMA {
40 return Err(crate::DocumentError::Schema {
41 expected: Self::SCHEMA,
42 found: doc.schema.chars().take(80).collect(),
43 });
44 }
45 ($check)(&doc)?;
46 Ok(doc)
47 }
48 }
49 };
50}
51pub(crate) use checked_document;
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
54#[serde(deny_unknown_fields)]
55pub struct DescribeRequest {
56 pub schema: String,
57 pub versions: Vec<u32>,
58}
59crate::document::document!(DescribeRequest, DESCRIBE, crate::limits::REMOTE_V1);
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
62#[serde(deny_unknown_fields)]
63pub struct DescribeResponse {
64 pub schema: String,
65 pub version: u32,
66 pub sources: Vec<HostedSource>,
67}
68crate::document::document!(DescribeResponse, DESCRIBE, crate::limits::REMOTE_V1);
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
71#[serde(deny_unknown_fields)]
72pub struct HostedSource {
73 pub source_id: String,
74 pub fields: Vec<ContextField>,
75 pub terms: ContextTerms,
76 }
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
80#[serde(deny_unknown_fields)]
81pub struct ContextField {
82 pub name: String,
83 #[serde(rename = "type")]
84 pub ty: TypeDecl,
85 pub provenance: ProvenanceClass,
86 pub freshness: Freshness,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub grade: Option<Grade>,
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
92#[serde(rename_all = "snake_case", deny_unknown_fields)]
93pub enum Freshness {
94 Observation { staleness: Staleness },
95 Revision { staleness: Staleness },
96}
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
98#[serde(rename_all = "snake_case", deny_unknown_fields)]
99pub enum Staleness {
100 Unknown,
101 Bounded { max_age_ms: u64 },
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
105#[serde(deny_unknown_fields)]
106pub struct ContextTerms {
107 pub cancellation: CancelSupport,
108 pub idempotency_window_ms: u64,
109 pub max_request_bytes: u64,
110 pub max_response_bytes: u64,
111 pub invalidation: InvalidationSupport,
112 pub endpoint_class: EndpointClass,
113}
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
115#[serde(rename_all = "snake_case")]
116pub enum EndpointClass {
117 HostOperated,
118}
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
120#[serde(rename_all = "snake_case", deny_unknown_fields)]
121pub enum InvalidationSupport {
122 None,
123 Push,
124 Poll { interval_ms: u64, max_lag_ms: u64 },
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct ContextCode(pub RemoteCode);
130impl Serialize for ContextCode {
131 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
132 s.serialize_str(if self.0 == RemoteCode::IdentityMismatch {
133 "describe_mismatch"
134 } else {
135 self.0.as_str()
136 })
137 }
138}
139impl<'de> Deserialize<'de> for ContextCode {
140 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
141 let name = String::deserialize(d)?;
142 if name == "describe_mismatch" {
143 return Ok(Self(RemoteCode::IdentityMismatch));
144 }
145 RemoteCode::ALL
146 .into_iter()
147 .find(|c| *c != RemoteCode::IdentityMismatch && c.as_str() == name)
148 .map(Self)
149 .ok_or_else(|| serde::de::Error::custom("unknown context failure code"))
150 }
151}
152impl schemars::JsonSchema for ContextCode {
153 fn schema_name() -> std::borrow::Cow<'static, str> {
154 "ContextCode".into()
155 }
156 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
157 let codes: Vec<_> = RemoteCode::ALL
158 .into_iter()
159 .map(|c| {
160 if c == RemoteCode::IdentityMismatch {
161 "describe_mismatch"
162 } else {
163 c.as_str()
164 }
165 })
166 .collect();
167 schemars::json_schema!({ "type": "string", "enum": codes })
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
172#[serde(deny_unknown_fields)]
173pub struct FetchRequest {
174 pub schema: String,
175 pub version: u32,
176 pub source_id: String,
177 pub describe_digest: ContentDigest,
178 pub attempt_id: String,
179 pub budget_ms: u64,
180 pub trace_id: String,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub traceparent: Option<String>,
183 pub principal: WirePrincipal,
184 pub fields: Vec<String>,
185}
186checked_document!(
187 FetchRequest,
188 FETCH,
189 crate::limits::REMOTE_V1,
190 |d: &FetchRequest| {
191 if !d.principal.is_canonical() {
192 return Err(invalid("invalid principal reference"));
193 }
194 validate_traceparent(d.traceparent.as_deref())?;
195 if !valid_fetch_attempt(&d.attempt_id) {
196 return Err(invalid("invalid context attempt id"));
197 }
198 Ok(())
199 }
200);
201
202pub(crate) fn validate_traceparent(value: Option<&str>) -> Result<(), DocumentError> {
203 let Some(value) = value else {
204 return Ok(());
205 };
206 let b = value.as_bytes();
207 if b.len() != 55
208 || &b[..3] != b"00-"
209 || b[35] != b'-'
210 || b[52] != b'-'
211 || !b.iter().enumerate().all(|(i, c)| {
212 [2, 35, 52].contains(&i) || c.is_ascii_digit() || (b'a'..=b'f').contains(c)
213 })
214 || b[3..35].iter().all(|c| *c == b'0')
215 || b[36..52].iter().all(|c| *c == b'0')
216 {
217 return Err(invalid("invalid_trace_context"));
218 }
219 Ok(())
220}
221
222fn valid_fetch_attempt(value: &str) -> bool {
226 let Some((decision, n)) = value.split_once("/context/") else {
227 return false;
228 };
229 if n.is_empty() || n.starts_with('0') || !n.bytes().all(|c| c.is_ascii_digit()) {
230 return false;
231 }
232 let mut decoded = 0;
233 let mut rest = decision.as_bytes();
234 while !rest.is_empty() {
235 match rest[0] {
236 b'%' => {
237 if rest.len() < 3 || !matches!(&rest[1..3], b"25" | b"2F" | b"2C") {
238 return false;
239 }
240 rest = &rest[3..];
241 }
242 b'/' | b',' => return false,
243 _ => rest = &rest[1..],
244 }
245 decoded += 1;
246 }
247 (1..=crate::scope::MAX_HANDLE_BYTES).contains(&decoded)
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
251#[serde(deny_unknown_fields)]
252pub struct FetchResponse {
253 pub schema: String,
254 pub result: FetchResult,
255}
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
257#[serde(rename_all = "snake_case", deny_unknown_fields)]
258pub enum FetchResult {
259 Snapshot(Snapshot),
260 Failure { code: ContextCode, detail: String },
261}
262checked_document!(
264 FetchResponse,
265 FETCH,
266 crate::limits::RECORD_V1,
267 |d: &FetchResponse| {
268 if let FetchResult::Snapshot(snapshot) = &d.result {
269 let bytes = serde_json::to_vec(snapshot).map_err(|e| invalid(e.to_string()))?;
270 Snapshot::parse(&bytes)?;
271 }
272 Ok(())
273 }
274);
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
277#[serde(deny_unknown_fields)]
278pub struct CancelRequest {
279 pub schema: String,
280 pub attempt_ids: Vec<String>,
281}
282crate::document::document!(CancelRequest, CANCEL, crate::limits::REMOTE_V1);
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
284#[serde(deny_unknown_fields)]
285pub struct CancelResponse {
286 pub schema: String,
287 pub answers: Vec<CancelItem>,
288}
289crate::document::document!(CancelResponse, CANCEL, crate::limits::REMOTE_V1);
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
291#[serde(deny_unknown_fields)]
292pub struct CancelItem {
293 pub attempt_id: String,
294 pub answer: CancelAnswer,
295}
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
297#[serde(rename_all = "snake_case")]
298pub enum CancelAnswer {
299 Stopped,
300 Unconfirmed,
301 UnknownAttempt,
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
305#[serde(deny_unknown_fields)]
306pub struct Invalidation {
307 pub schema: String,
308 pub source_id: String,
309 pub sequence: u64,
310 pub kind: InvalidationKind,
311 pub fields: Vec<String>,
312 pub scope: InvalidationScope,
313}
314checked_document!(
315 Invalidation,
316 INVALIDATION,
317 crate::limits::REMOTE_V1,
318 |d: &Invalidation| {
319 if let InvalidationScope::Principal(p) = &d.scope
320 && !p.is_canonical()
321 {
322 return Err(invalid("invalid principal reference"));
323 }
324 Ok(())
325 }
326);
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
328#[serde(rename_all = "snake_case")]
329pub enum InvalidationKind {
330 Correction,
331 Revocation,
332 Erasure,
333}
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
335#[serde(rename_all = "snake_case", deny_unknown_fields)]
336pub enum InvalidationScope {
337 Principal(WirePrincipal),
338 AllScopes,
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
342#[serde(deny_unknown_fields)]
343pub struct InvalidationsRequest {
344 pub schema: String,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub sequence: Option<u64>,
347}
348crate::document::document!(
349 InvalidationsRequest,
350 INVALIDATIONS,
351 crate::limits::REMOTE_V1
352);
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
354#[serde(deny_unknown_fields)]
355pub struct InvalidationsResponse {
356 pub schema: String,
357 pub sequence: u64,
358 pub items: Vec<Invalidation>,
359}
360checked_document!(
361 InvalidationsResponse,
362 INVALIDATIONS,
363 crate::limits::REMOTE_V1,
364 |d: &InvalidationsResponse| {
365 for item in &d.items {
366 Invalidation::parse(&serde_json::to_vec(item).map_err(|e| invalid(e.to_string()))?)?;
367 }
368 Ok(())
369 }
370);
371
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
374#[serde(deny_unknown_fields)]
375pub struct ExchangeRecord {
376 pub schema: String,
377 pub attempt_id: String,
378 pub request_digest: Option<ContentDigest>,
379 pub response_digest: Option<ContentDigest>,
380 pub code: Option<ContextCode>,
381 pub phases: Vec<TransportPhase>,
382 pub late: bool,
383}
384crate::document::document!(ExchangeRecord, EXCHANGE, crate::limits::EXCHANGE_V1);