1use super::{
9 ResearchCitationV1, ResearchClaimV1, ResearchContractError, ResearchEventV1,
10 ResearchEvidenceFactV1, ResearchEvidenceGraphV1, ResearchProvenanceReceiptV1,
11 ResearchReproducibilityManifestV1, ResearchRerunLineageV1, ResearchReviewBatchV1,
12 ResearchReviewFindingV1, ResearchRunV1, ResearchWorkflowPlanV1, ResearchWorkflowStepV1,
13};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use serde_json::Value;
16use thiserror::Error;
17
18pub const RESEARCH_PROTOCOL_VERSION_V1: u16 = 1;
20
21pub const RESEARCH_PROTOCOL_SCHEMA_V1: &str = "a3s.code.research-wire.v1";
23
24pub const RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES: usize = 32 * 1024 * 1024;
26
27macro_rules! define_research_wire_kinds_v1 {
28 ($( $variant:ident => $constant:ident = $wire_name:literal => $payload:ident ),+ $(,)?) => {
29 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31 #[serde(rename_all = "snake_case")]
32 pub enum ResearchWireKindV1 {
33 $( $variant, )+
34 }
35
36 #[derive(Debug, Clone, Copy)]
38 pub struct ResearchWireTypeV1;
39
40 impl ResearchWireTypeV1 {
41 $( pub const $constant: &'static str = $wire_name; )+
42 }
43
44 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48 pub struct ResearchWireKindDescriptorV1 {
49 pub kind: ResearchWireKindV1,
50 pub wire_name: &'static str,
51 pub constant_name: &'static str,
52 pub payload_type: &'static str,
53 }
54
55 pub const RESEARCH_WIRE_KIND_DESCRIPTORS_V1: &[ResearchWireKindDescriptorV1] = &[
57 $( ResearchWireKindDescriptorV1 {
58 kind: ResearchWireKindV1::$variant,
59 wire_name: $wire_name,
60 constant_name: stringify!($constant),
61 payload_type: stringify!($payload),
62 }, )+
63 ];
64
65 impl ResearchWireKindV1 {
66 pub const fn wire_name(self) -> &'static str {
68 match self {
69 $( Self::$variant => $wire_name, )+
70 }
71 }
72
73 pub const fn payload_type(self) -> &'static str {
75 match self {
76 $( Self::$variant => stringify!($payload), )+
77 }
78 }
79
80 pub fn from_wire_name(value: &str) -> Option<Self> {
82 match value {
83 $( $wire_name => Some(Self::$variant), )+
84 _ => None,
85 }
86 }
87 }
88 };
89}
90
91define_research_wire_kinds_v1! {
95 ResearchRun => RESEARCH_RUN = "research_run" => ResearchRunV1,
96 ResearchEvidenceFact => RESEARCH_EVIDENCE_FACT = "research_evidence_fact" => ResearchEvidenceFactV1,
97 ResearchClaim => RESEARCH_CLAIM = "research_claim" => ResearchClaimV1,
98 ResearchCitation => RESEARCH_CITATION = "research_citation" => ResearchCitationV1,
99 ResearchEvidenceGraph => RESEARCH_EVIDENCE_GRAPH = "research_evidence_graph" => ResearchEvidenceGraphV1,
100 ResearchWorkflowStep => RESEARCH_WORKFLOW_STEP = "research_workflow_step" => ResearchWorkflowStepV1,
101 ResearchWorkflowPlan => RESEARCH_WORKFLOW_PLAN = "research_workflow_plan" => ResearchWorkflowPlanV1,
102 ResearchRerunLineage => RESEARCH_RERUN_LINEAGE = "research_rerun_lineage" => ResearchRerunLineageV1,
103 ResearchProvenanceReceipt => RESEARCH_PROVENANCE_RECEIPT = "research_provenance_receipt" => ResearchProvenanceReceiptV1,
104 ResearchReproducibilityManifest => RESEARCH_REPRODUCIBILITY_MANIFEST = "research_reproducibility_manifest" => ResearchReproducibilityManifestV1,
105 ResearchReviewFinding => RESEARCH_REVIEW_FINDING = "research_review_finding" => ResearchReviewFindingV1,
106 ResearchReviewBatch => RESEARCH_REVIEW_BATCH = "research_review_batch" => ResearchReviewBatchV1,
107 ResearchEvent => RESEARCH_EVENT = "research_event" => ResearchEventV1,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Error)]
112pub enum ResearchProtocolError {
113 #[error("research wire schema is unsupported")]
114 UnsupportedSchema,
115 #[error("research wire version {0} is unsupported")]
116 UnsupportedVersion(u16),
117 #[error("research wire kind is unknown")]
118 UnknownKind,
119 #[error("research wire field `{0}` is invalid")]
120 InvalidField(&'static str),
121 #[error("research wire payload is invalid: {0}")]
122 Payload(String),
123 #[error("research wire value exceeds its bounded encoding")]
124 Encoding,
125 #[error("research wire serialization failed: {0}")]
126 Serialization(String),
127}
128
129impl ResearchProtocolError {
130 pub const fn code(&self) -> &'static str {
132 match self {
133 Self::UnsupportedSchema => "a3s.code.research_protocol.unsupported_schema",
134 Self::UnsupportedVersion(_) => "a3s.code.research_protocol.unsupported_version",
135 Self::UnknownKind => "a3s.code.research_protocol.unknown_kind",
136 Self::InvalidField(_) => "a3s.code.research_protocol.invalid_field",
137 Self::Payload(_) => "a3s.code.research_protocol.payload",
138 Self::Encoding => "a3s.code.research_protocol.encoding",
139 Self::Serialization(_) => "a3s.code.research_protocol.serialization",
140 }
141 }
142}
143
144impl From<ResearchContractError> for ResearchProtocolError {
145 fn from(error: ResearchContractError) -> Self {
146 Self::Payload(error.to_string())
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct ResearchWireEnvelopeV1 {
160 pub schema: String,
161 pub version: u16,
162 pub kind: ResearchWireKindV1,
163 pub payload: Value,
164}
165
166impl ResearchWireEnvelopeV1 {
167 pub fn new(kind: ResearchWireKindV1, payload: Value) -> Result<Self, ResearchProtocolError> {
169 let envelope = Self {
170 schema: RESEARCH_PROTOCOL_SCHEMA_V1.to_string(),
171 version: RESEARCH_PROTOCOL_VERSION_V1,
172 kind,
173 payload,
174 };
175 envelope.validate()?;
176 Ok(envelope)
177 }
178
179 pub fn from_slice(bytes: &[u8]) -> Result<Self, ResearchProtocolError> {
181 if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
182 return Err(ResearchProtocolError::Encoding);
183 }
184 let value: Value = serde_json::from_slice(bytes)
185 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
186 Self::from_value(value)
187 }
188
189 pub fn from_value(value: Value) -> Result<Self, ResearchProtocolError> {
191 let encoded = serde_json::to_vec(&value)
192 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
193 if encoded.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
194 return Err(ResearchProtocolError::Encoding);
195 }
196 let kind = value
197 .get("kind")
198 .and_then(Value::as_str)
199 .ok_or(ResearchProtocolError::InvalidField("kind"))?;
200 if ResearchWireKindV1::from_wire_name(kind).is_none() {
201 return Err(ResearchProtocolError::UnknownKind);
202 }
203 let envelope: Self = serde_json::from_value(value)
204 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
205 envelope.validate()?;
206 Ok(envelope)
207 }
208
209 pub fn to_vec(&self) -> Result<Vec<u8>, ResearchProtocolError> {
211 self.validate()?;
212 let bytes = serde_json::to_vec(self)
213 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
214 if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
215 return Err(ResearchProtocolError::Encoding);
216 }
217 Ok(bytes)
218 }
219
220 pub fn validate(&self) -> Result<(), ResearchProtocolError> {
222 if self.schema != RESEARCH_PROTOCOL_SCHEMA_V1 {
223 return Err(ResearchProtocolError::UnsupportedSchema);
224 }
225 if self.version != RESEARCH_PROTOCOL_VERSION_V1 {
226 return Err(ResearchProtocolError::UnsupportedVersion(self.version));
227 }
228 let encoded = serde_json::to_vec(self)
229 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
230 if encoded.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
231 return Err(ResearchProtocolError::Encoding);
232 }
233
234 match self.kind {
235 ResearchWireKindV1::ResearchRun => {
236 let payload: ResearchRunV1 = self.decode_payload()?;
237 payload.validate()?;
238 }
239 ResearchWireKindV1::ResearchEvidenceFact => {
240 let payload: ResearchEvidenceFactV1 = self.decode_payload()?;
241 payload.validate()?;
242 }
243 ResearchWireKindV1::ResearchClaim => {
244 let payload: ResearchClaimV1 = self.decode_payload()?;
245 payload.validate()?;
246 }
247 ResearchWireKindV1::ResearchCitation => {
248 let payload: ResearchCitationV1 = self.decode_payload()?;
249 payload.validate()?;
250 }
251 ResearchWireKindV1::ResearchEvidenceGraph => {
252 let payload: ResearchEvidenceGraphV1 = self.decode_payload()?;
253 payload.validate()?;
254 }
255 ResearchWireKindV1::ResearchWorkflowStep => {
256 let payload: ResearchWorkflowStepV1 = self.decode_payload()?;
257 payload.validate()?;
258 }
259 ResearchWireKindV1::ResearchWorkflowPlan => {
260 let payload: ResearchWorkflowPlanV1 = self.decode_payload()?;
261 payload.validate()?;
262 }
263 ResearchWireKindV1::ResearchRerunLineage => {
264 let payload: ResearchRerunLineageV1 = self.decode_payload()?;
265 payload.validate()?;
266 }
267 ResearchWireKindV1::ResearchProvenanceReceipt => {
268 let payload: ResearchProvenanceReceiptV1 = self.decode_payload()?;
269 payload.validate()?;
270 }
271 ResearchWireKindV1::ResearchReproducibilityManifest => {
272 let payload: ResearchReproducibilityManifestV1 = self.decode_payload()?;
273 payload.validate()?;
274 }
275 ResearchWireKindV1::ResearchReviewFinding => {
276 let payload: ResearchReviewFindingV1 = self.decode_payload()?;
277 payload.validate()?;
278 }
279 ResearchWireKindV1::ResearchReviewBatch => {
280 let payload: ResearchReviewBatchV1 = self.decode_payload()?;
281 payload.validate()?;
282 }
283 ResearchWireKindV1::ResearchEvent => {
284 let payload: ResearchEventV1 = self.decode_payload()?;
285 payload.validate()?;
286 }
287 }
288 Ok(())
289 }
290
291 pub const fn kind(&self) -> ResearchWireKindV1 {
293 self.kind
294 }
295
296 pub fn payload(&self) -> &Value {
298 &self.payload
299 }
300
301 pub fn from_research_event(payload: ResearchEventV1) -> Result<Self, ResearchProtocolError> {
303 Self::from_typed(ResearchWireKindV1::ResearchEvent, payload)
304 }
305
306 pub fn from_reproducibility_manifest(
308 payload: ResearchReproducibilityManifestV1,
309 ) -> Result<Self, ResearchProtocolError> {
310 Self::from_typed(ResearchWireKindV1::ResearchReproducibilityManifest, payload)
311 }
312
313 pub fn payload_as<T>(&self, expected: ResearchWireKindV1) -> Result<T, ResearchProtocolError>
315 where
316 T: DeserializeOwned,
317 {
318 self.validate()?;
319 if self.kind != expected {
320 return Err(ResearchProtocolError::InvalidField("kind"));
321 }
322 self.decode_payload()
323 }
324
325 fn from_typed<T: Serialize>(
326 kind: ResearchWireKindV1,
327 payload: T,
328 ) -> Result<Self, ResearchProtocolError> {
329 let value = serde_json::to_value(payload)
330 .map_err(|error| ResearchProtocolError::Serialization(error.to_string()))?;
331 Self::new(kind, value)
332 }
333
334 fn decode_payload<T: DeserializeOwned>(&self) -> Result<T, ResearchProtocolError> {
335 serde_json::from_value(self.payload.clone())
336 .map_err(|error| ResearchProtocolError::Payload(error.to_string()))
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use serde::Deserialize;
344
345 fn digest(ch: char) -> String {
346 format!("sha256:{}", ch.to_string().repeat(64))
347 }
348
349 fn fixture_event() -> ResearchEventV1 {
350 ResearchEventV1::new(
351 "project-1",
352 1,
353 Some("run-1".to_owned()),
354 1,
355 "research.run.admitted",
356 digest('1'),
357 1,
358 )
359 .unwrap()
360 }
361
362 #[test]
363 fn event_envelope_round_trips_and_rejects_unknown_kind() {
364 let event = fixture_event();
365 let envelope = ResearchWireEnvelopeV1::from_research_event(event.clone()).unwrap();
366 assert_eq!(envelope.kind(), ResearchWireKindV1::ResearchEvent);
367 let encoded = envelope.to_vec().unwrap();
368 let decoded = ResearchWireEnvelopeV1::from_slice(&encoded).unwrap();
369 let restored: ResearchEventV1 = decoded
370 .payload_as(ResearchWireKindV1::ResearchEvent)
371 .unwrap();
372 assert_eq!(restored, event);
373
374 let mut value = serde_json::to_value(&envelope).unwrap();
375 value["kind"] = Value::String("future_kind".to_owned());
376 assert_eq!(
377 ResearchWireEnvelopeV1::from_value(value),
378 Err(ResearchProtocolError::UnknownKind)
379 );
380 }
381
382 #[test]
383 fn envelope_rejects_unknown_top_level_and_version_drift() {
384 let envelope = ResearchWireEnvelopeV1::from_research_event(fixture_event()).unwrap();
385 let mut value = serde_json::to_value(&envelope).unwrap();
386 value["future_field"] = Value::Bool(true);
387 assert!(matches!(
388 ResearchWireEnvelopeV1::from_value(value.clone()),
389 Err(ResearchProtocolError::Serialization(_))
390 ));
391 value = serde_json::to_value(&envelope).unwrap();
392 value["version"] = Value::from(RESEARCH_PROTOCOL_VERSION_V1 + 1);
393 assert_eq!(
394 ResearchWireEnvelopeV1::from_value(value),
395 Err(ResearchProtocolError::UnsupportedVersion(
396 RESEARCH_PROTOCOL_VERSION_V1 + 1
397 ))
398 );
399 }
400
401 #[test]
402 fn generated_fixtures_accept_valid_and_reject_drift() {
403 #[derive(Deserialize)]
404 struct Fixtures {
405 valid: Value,
406 unknown_top_level_field: Value,
407 unknown_payload_field: Value,
408 unsupported_version: Value,
409 }
410 let fixtures: Fixtures = serde_json::from_str(include_str!(
411 "../../../sdk/research/research-wire-v1-fixtures.json"
412 ))
413 .expect("generated fixtures");
414 let valid = ResearchWireEnvelopeV1::from_value(fixtures.valid).unwrap();
415 assert_eq!(valid.kind(), ResearchWireKindV1::ResearchEvent);
416 assert!(matches!(
417 ResearchWireEnvelopeV1::from_value(fixtures.unknown_top_level_field),
418 Err(ResearchProtocolError::Serialization(_))
419 ));
420 assert!(ResearchWireEnvelopeV1::from_value(fixtures.unknown_payload_field).is_err());
421 assert_eq!(
422 ResearchWireEnvelopeV1::from_value(fixtures.unsupported_version),
423 Err(ResearchProtocolError::UnsupportedVersion(
424 RESEARCH_PROTOCOL_VERSION_V1 + 1
425 ))
426 );
427 }
428
429 #[test]
430 fn catalog_covers_every_research_contract_surface() {
431 assert_eq!(RESEARCH_WIRE_KIND_DESCRIPTORS_V1.len(), 13);
432 let names: Vec<_> = RESEARCH_WIRE_KIND_DESCRIPTORS_V1
433 .iter()
434 .map(|item| item.wire_name)
435 .collect();
436 assert!(names.contains(&"research_reproducibility_manifest"));
437 assert!(names.contains(&"research_event"));
438 assert_eq!(
439 names.len(),
440 names
441 .iter()
442 .collect::<std::collections::BTreeSet<_>>()
443 .len()
444 );
445 }
446}