1use crate::acquisition::AcquisitionRequest;
5use crate::definition::{ProvenanceClass, TypeDecl};
6use crate::grade::Grade;
7use crate::ids::{AcquisitionRequestId, ContentDigest, SourceId};
8use crate::remote::{CancelSupport, RemoteCode, TransportPhase, WirePrincipal};
9use crate::run::Charge;
10use crate::serve::Embedded;
11use crate::snapshot::Snapshot;
12use crate::source::SourceDescriptor;
13use crate::{Document, DocumentError, Identified};
14use serde::{Deserialize, Serialize};
15
16pub const PROTOCOL: &str = "rustev.remote-context/1";
17pub const DESCRIBE: &str = "rustev.remote-context-describe/1";
18pub const FETCH: &str = "rustev.remote-context-fetch/1";
19pub const CANCEL: &str = "rustev.remote-context-cancel/1";
20pub const INVALIDATION: &str = "rustev.remote-context-invalidation/1";
21pub const INVALIDATIONS: &str = "rustev.remote-context-invalidations/1";
22pub const EXCHANGE: &str = "rustev.remote-context-exchange/1";
23pub const ACQUIRE: &str = "rustev.remote-context-acquire/1";
24
25pub(crate) fn invalid(message: impl Into<String>) -> DocumentError {
26 DocumentError::Parse(crate::bounded::ParseError::Schema {
27 message: message.into(),
28 })
29}
30
31macro_rules! checked_document {
33 ($ty:ty, $schema:expr, $limits:expr, $check:expr) => {
34 impl crate::Document for $ty {
35 const SCHEMA: &'static str = $schema;
36 const LIMITS: crate::bounded::ParseLimits = $limits;
37 fn schema(&self) -> &str {
38 &self.schema
39 }
40 fn parse(bytes: &[u8]) -> Result<Self, crate::DocumentError> {
41 let doc: Self = crate::bounded::parse_bounded(bytes, &Self::LIMITS)
42 .map_err(crate::DocumentError::Parse)?;
43 if doc.schema != Self::SCHEMA {
44 return Err(crate::DocumentError::Schema {
45 expected: Self::SCHEMA,
46 found: doc.schema.chars().take(80).collect(),
47 });
48 }
49 ($check)(&doc)?;
50 Ok(doc)
51 }
52 }
53 };
54}
55pub(crate) use checked_document;
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct DescribeRequest {
60 pub schema: String,
61 pub versions: Vec<u32>,
62}
63crate::document::document!(DescribeRequest, DESCRIBE, crate::limits::REMOTE_V1);
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(deny_unknown_fields)]
67pub struct DescribeResponse {
68 pub schema: String,
69 pub version: u32,
70 pub sources: Vec<HostedSource>,
71}
72checked_document!(
73 DescribeResponse,
74 DESCRIBE,
75 crate::limits::REMOTE_V1,
76 |d: &DescribeResponse| {
77 for source in &d.sources {
78 if let Some(hosted) = &source.descriptor {
79 hosted.check()?;
80 }
81 }
82 Ok(())
83 }
84);
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
87#[serde(deny_unknown_fields)]
88pub struct HostedSource {
89 pub source_id: String,
90 pub fields: Vec<ContextField>,
91 pub terms: ContextTerms,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub descriptor: Option<HostedDescriptor>,
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
100#[serde(deny_unknown_fields)]
101pub struct HostedDescriptor {
102 pub id: SourceId,
103 pub document: Embedded<SourceDescriptor>,
104}
105impl HostedDescriptor {
106 pub fn new(descriptor: &SourceDescriptor) -> Result<Self, DocumentError> {
107 let bytes = descriptor.canonical().map_err(|e| invalid(e.to_string()))?;
108 Ok(Self {
109 id: descriptor.id().map_err(|e| invalid(e.to_string()))?,
110 document: Embedded::parse(&bytes)?,
111 })
112 }
113 fn check(&self) -> Result<(), DocumentError> {
114 let descriptor = self.document.document()?;
115 let canonical = descriptor.canonical().map_err(|e| invalid(e.to_string()))?;
116 if canonical != self.document.bytes() {
117 return Err(invalid("source descriptor bytes are not canonical"));
118 }
119 if descriptor.id().map_err(|e| invalid(e.to_string()))? != self.id {
120 return Err(invalid("source descriptor id does not match its bytes"));
121 }
122 Ok(())
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
127#[serde(deny_unknown_fields)]
128pub struct ContextField {
129 pub name: String,
130 #[serde(rename = "type")]
131 pub ty: TypeDecl,
132 pub provenance: ProvenanceClass,
133 pub freshness: Freshness,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub grade: Option<Grade>,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
139#[serde(rename_all = "snake_case", deny_unknown_fields)]
140pub enum Freshness {
141 Observation { staleness: Staleness },
142 Revision { staleness: Staleness },
143}
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
145#[serde(rename_all = "snake_case", deny_unknown_fields)]
146pub enum Staleness {
147 Unknown,
148 Bounded { max_age_ms: u64 },
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
152#[serde(deny_unknown_fields)]
153pub struct ContextTerms {
154 pub cancellation: CancelSupport,
155 pub idempotency_window_ms: u64,
156 pub max_request_bytes: u64,
157 pub max_response_bytes: u64,
158 pub invalidation: InvalidationSupport,
159 pub endpoint_class: EndpointClass,
160}
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
162#[serde(rename_all = "snake_case")]
163pub enum EndpointClass {
164 HostOperated,
165}
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
167#[serde(rename_all = "snake_case", deny_unknown_fields)]
168pub enum InvalidationSupport {
169 None,
170 Push,
171 Poll { interval_ms: u64, max_lag_ms: u64 },
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct ContextCode(pub RemoteCode);
177impl Serialize for ContextCode {
178 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
179 s.serialize_str(if self.0 == RemoteCode::IdentityMismatch {
180 "describe_mismatch"
181 } else {
182 self.0.as_str()
183 })
184 }
185}
186impl ContextCode {
187 fn name(self) -> &'static str {
188 if self.0 == RemoteCode::IdentityMismatch {
189 "describe_mismatch"
190 } else {
191 self.0.as_str()
192 }
193 }
194 fn from_name(name: &str) -> Option<Self> {
195 RemoteCode::ALL
196 .into_iter()
197 .map(Self)
198 .find(|c| c.name() == name)
199 }
200}
201impl<'de> Deserialize<'de> for ContextCode {
202 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
203 let name = String::deserialize(d)?;
204 Self::from_name(&name)
205 .ok_or_else(|| serde::de::Error::custom("unknown context failure code"))
206 }
207}
208impl schemars::JsonSchema for ContextCode {
209 fn schema_name() -> std::borrow::Cow<'static, str> {
210 "ContextCode".into()
211 }
212 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
213 let codes: Vec<_> = RemoteCode::ALL
214 .into_iter()
215 .map(|c| ContextCode(c).name())
216 .collect();
217 schemars::json_schema!({ "type": "string", "enum": codes })
218 }
219}
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum AcquireCode {
225 Context(ContextCode),
226 Denied,
227}
228impl From<RemoteCode> for AcquireCode {
229 fn from(code: RemoteCode) -> Self {
230 Self::Context(ContextCode(code))
231 }
232}
233impl Serialize for AcquireCode {
234 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
235 match self {
236 Self::Context(c) => c.serialize(s),
237 Self::Denied => s.serialize_str("denied"),
238 }
239 }
240}
241impl<'de> Deserialize<'de> for AcquireCode {
242 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
243 let name = String::deserialize(d)?;
244 if name == "denied" {
245 return Ok(Self::Denied);
246 }
247 ContextCode::from_name(&name)
248 .map(Self::Context)
249 .ok_or_else(|| serde::de::Error::custom("unknown acquire failure code"))
250 }
251}
252impl schemars::JsonSchema for AcquireCode {
253 fn schema_name() -> std::borrow::Cow<'static, str> {
254 "AcquireCode".into()
255 }
256 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
257 let mut codes: Vec<_> = RemoteCode::ALL
258 .into_iter()
259 .map(|c| ContextCode(c).name())
260 .collect();
261 codes.push("denied");
262 schemars::json_schema!({ "type": "string", "enum": codes })
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
267#[serde(deny_unknown_fields)]
268pub struct FetchRequest {
269 pub schema: String,
270 pub version: u32,
271 pub source_id: String,
272 pub describe_digest: ContentDigest,
273 pub attempt_id: String,
274 pub budget_ms: u64,
275 pub trace_id: String,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub traceparent: Option<String>,
278 pub principal: WirePrincipal,
279 pub fields: Vec<String>,
280}
281checked_document!(
282 FetchRequest,
283 FETCH,
284 crate::limits::REMOTE_V1,
285 |d: &FetchRequest| {
286 if !d.principal.is_canonical() {
287 return Err(invalid("invalid principal reference"));
288 }
289 validate_traceparent(d.traceparent.as_deref())?;
290 if !valid_fetch_attempt(&d.attempt_id) {
291 return Err(invalid("invalid context attempt id"));
292 }
293 Ok(())
294 }
295);
296
297pub(crate) fn validate_traceparent(value: Option<&str>) -> Result<(), DocumentError> {
298 let Some(value) = value else {
299 return Ok(());
300 };
301 let b = value.as_bytes();
302 if b.len() != 55
303 || &b[..3] != b"00-"
304 || b[35] != b'-'
305 || b[52] != b'-'
306 || !b.iter().enumerate().all(|(i, c)| {
307 [2, 35, 52].contains(&i) || c.is_ascii_digit() || (b'a'..=b'f').contains(c)
308 })
309 || b[3..35].iter().all(|c| *c == b'0')
310 || b[36..52].iter().all(|c| *c == b'0')
311 {
312 return Err(invalid("invalid_trace_context"));
313 }
314 Ok(())
315}
316
317fn valid_fetch_attempt(value: &str) -> bool {
321 let Some((decision, n)) = value.split_once("/context/") else {
322 return false;
323 };
324 number(n) && escaped(decision)
325}
326fn number(n: &str) -> bool {
328 !n.is_empty() && !n.starts_with('0') && n.bytes().all(|c| c.is_ascii_digit())
329}
330fn index(n: &str) -> bool {
331 n == "0" || number(n)
332}
333fn escaped(component: &str) -> bool {
334 let mut decoded = 0;
335 let mut rest = component.as_bytes();
336 while !rest.is_empty() {
337 match rest[0] {
338 b'%' => {
339 if rest.len() < 3 || !matches!(&rest[1..3], b"25" | b"2F" | b"2C") {
340 return false;
341 }
342 rest = &rest[3..];
343 }
344 b'/' | b',' => return false,
345 _ => rest = &rest[1..],
346 }
347 decoded += 1;
348 }
349 (1..=crate::scope::MAX_HANDLE_BYTES).contains(&decoded)
350}
351fn valid_acquire_attempt(value: &str) -> bool {
354 let parts: Vec<&str> = value.split('/').collect();
355 let [decision, step, position, n] = parts.as_slice() else {
356 return false;
357 };
358 let Some((tier, page)) = position.split_once(',') else {
359 return false;
360 };
361 escaped(decision)
362 && escaped(step)
363 && index(tier)
364 && index(page)
365 && number(n.strip_prefix('r').unwrap_or(n))
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
369#[serde(deny_unknown_fields)]
370pub struct FetchResponse {
371 pub schema: String,
372 pub result: FetchResult,
373}
374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
375#[serde(rename_all = "snake_case", deny_unknown_fields)]
376pub enum FetchResult {
377 Snapshot(Snapshot),
378 Failure { code: ContextCode, detail: String },
379}
380checked_document!(
382 FetchResponse,
383 FETCH,
384 crate::limits::RECORD_V1,
385 |d: &FetchResponse| {
386 if let FetchResult::Snapshot(snapshot) = &d.result {
387 let bytes = serde_json::to_vec(snapshot).map_err(|e| invalid(e.to_string()))?;
388 Snapshot::parse(&bytes)?;
389 }
390 Ok(())
391 }
392);
393
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
395#[serde(deny_unknown_fields)]
396pub struct CancelRequest {
397 pub schema: String,
398 pub attempt_ids: Vec<String>,
399}
400crate::document::document!(CancelRequest, CANCEL, crate::limits::REMOTE_V1);
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
402#[serde(deny_unknown_fields)]
403pub struct CancelResponse {
404 pub schema: String,
405 pub answers: Vec<CancelItem>,
406}
407crate::document::document!(CancelResponse, CANCEL, crate::limits::REMOTE_V1);
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
409#[serde(deny_unknown_fields)]
410pub struct CancelItem {
411 pub attempt_id: String,
412 pub answer: CancelAnswer,
413}
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
415#[serde(rename_all = "snake_case")]
416pub enum CancelAnswer {
417 Stopped,
418 Unconfirmed,
419 UnknownAttempt,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
423#[serde(deny_unknown_fields)]
424pub struct Invalidation {
425 pub schema: String,
426 pub source_id: String,
427 pub sequence: u64,
428 pub kind: InvalidationKind,
429 pub fields: Vec<String>,
430 pub scope: InvalidationScope,
431}
432checked_document!(
433 Invalidation,
434 INVALIDATION,
435 crate::limits::REMOTE_V1,
436 |d: &Invalidation| {
437 if let InvalidationScope::Principal(p) = &d.scope
438 && !p.is_canonical()
439 {
440 return Err(invalid("invalid principal reference"));
441 }
442 Ok(())
443 }
444);
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
446#[serde(rename_all = "snake_case")]
447pub enum InvalidationKind {
448 Correction,
449 Revocation,
450 Erasure,
451}
452#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
453#[serde(rename_all = "snake_case", deny_unknown_fields)]
454pub enum InvalidationScope {
455 Principal(WirePrincipal),
456 AllScopes,
457}
458
459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
460#[serde(deny_unknown_fields)]
461pub struct InvalidationsRequest {
462 pub schema: String,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub sequence: Option<u64>,
465}
466crate::document::document!(
467 InvalidationsRequest,
468 INVALIDATIONS,
469 crate::limits::REMOTE_V1
470);
471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
472#[serde(deny_unknown_fields)]
473pub struct InvalidationsResponse {
474 pub schema: String,
475 pub sequence: u64,
476 pub items: Vec<Invalidation>,
477}
478checked_document!(
479 InvalidationsResponse,
480 INVALIDATIONS,
481 crate::limits::REMOTE_V1,
482 |d: &InvalidationsResponse| {
483 for item in &d.items {
484 Invalidation::parse(&serde_json::to_vec(item).map_err(|e| invalid(e.to_string()))?)?;
485 }
486 Ok(())
487 }
488);
489
490#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
492#[serde(deny_unknown_fields)]
493pub struct ExchangeRecord {
494 pub schema: String,
495 pub attempt_id: String,
496 pub request_digest: Option<ContentDigest>,
497 pub response_digest: Option<ContentDigest>,
498 pub code: Option<AcquireCode>,
499 pub phases: Vec<TransportPhase>,
500 pub late: bool,
501}
502crate::document::document!(ExchangeRecord, EXCHANGE, crate::limits::EXCHANGE_V1);
503
504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
507#[serde(deny_unknown_fields)]
508pub struct AcquireRequest {
509 pub schema: String,
510 pub version: u32,
511 pub source: SourceId,
512 pub operation: String,
513 pub request: Embedded<AcquisitionRequest>,
514 pub request_id: AcquisitionRequestId,
515 pub attempt_id: String,
516 pub budget_ms: u64,
517 pub trace_id: String,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub traceparent: Option<String>,
520 pub principal: WirePrincipal,
521}
522checked_document!(
523 AcquireRequest,
524 ACQUIRE,
525 crate::limits::REMOTE_V1,
526 |d: &AcquireRequest| {
527 if !d.principal.is_canonical() {
528 return Err(invalid("invalid principal reference"));
529 }
530 validate_traceparent(d.traceparent.as_deref())?;
531 if !valid_acquire_attempt(&d.attempt_id) {
532 return Err(invalid("invalid acquisition attempt id"));
533 }
534 let request = d.request.document()?;
535 if request.canonical().map_err(|e| invalid(e.to_string()))? != d.request.bytes() {
536 return Err(invalid("acquisition request bytes are not canonical"));
537 }
538 if request.id().map_err(|e| invalid(e.to_string()))? != d.request_id {
539 return Err(invalid("request_id does not match the request bytes"));
540 }
541 if request.source != d.source || request.operation != d.operation {
542 return Err(invalid("source or operation differs from the request"));
543 }
544 Ok(())
545 }
546);
547
548#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
550#[serde(deny_unknown_fields)]
551pub struct AcquireResponse {
552 pub schema: String,
553 pub result: AcquireResult,
554 pub charge: Charge,
555}
556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
557#[serde(rename_all = "snake_case", deny_unknown_fields)]
558pub enum AcquireResult {
559 Page(PageBytes),
560 Failure { code: AcquireCode, detail: String },
561}
562crate::document::document!(AcquireResponse, ACQUIRE, crate::limits::RECORD_V1);
565
566#[derive(Debug, Clone, Serialize)]
571#[serde(transparent)]
572pub struct PageBytes(Box<serde_json::value::RawValue>);
573impl PageBytes {
574 pub fn new(bytes: &[u8]) -> Result<Self, DocumentError> {
575 let raw: Box<serde_json::value::RawValue> =
576 serde_json::from_slice(bytes).map_err(|e| invalid(e.to_string()))?;
577 Self::object(raw).map_err(invalid)
578 }
579 fn object(raw: Box<serde_json::value::RawValue>) -> Result<Self, &'static str> {
580 if raw.get().starts_with('{') {
581 Ok(Self(raw))
582 } else {
583 Err("an acquisition page must be an object")
584 }
585 }
586 pub fn bytes(&self) -> &[u8] {
588 self.0.get().as_bytes()
589 }
590}
591impl PartialEq for PageBytes {
592 fn eq(&self, other: &Self) -> bool {
593 self.0.get() == other.0.get()
594 }
595}
596impl<'de> Deserialize<'de> for PageBytes {
597 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
598 Self::object(Box::<serde_json::value::RawValue>::deserialize(d)?)
599 .map_err(serde::de::Error::custom)
600 }
601}
602impl schemars::JsonSchema for PageBytes {
603 fn schema_name() -> std::borrow::Cow<'static, str> {
604 "PageBytes".into()
605 }
606 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
607 schemars::json_schema!({
608 "type": "object",
609 "description": "A `rustev.acquisition-page/1` document carried as its original bytes; the runtime parses and validates it."
610 })
611 }
612}