1use std::collections::BTreeMap;
16
17use schemars::{JsonSchema, generate::SchemaSettings};
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21
22use crate::event::{EventBody, EventKind, EventPayload, EventRecord, EventSource};
23use crate::file_name::FileName;
24use crate::ids::{EventId, ImageId, PrintId};
25use crate::image::ImageRef;
26use crate::raw::RawBytes;
27use crate::reported::{Range, Reported};
28use crate::timestamp::Timestamp;
29
30pub trait Sample: Sized {
32 fn sample_full() -> Self;
34
35 #[must_use]
37 fn sample_minimal() -> Self {
38 Self::sample_full()
39 }
40
41 #[must_use]
47 fn sample_alternates() -> Vec<Self> {
48 Vec::new()
49 }
50}
51
52#[derive(Debug, Clone, Copy)]
54pub struct TypeContract {
55 pub name: &'static str,
57 schema: fn() -> Value,
59 full: fn() -> Value,
61 minimal: fn() -> Value,
63 round_trip: fn(Value) -> Result<Value, String>,
65 alternates: fn() -> Vec<Value>,
67}
68
69impl TypeContract {
70 #[must_use]
78 pub fn of<T: Sample + JsonSchema + Serialize + DeserializeOwned>(name: &'static str) -> Self {
79 Self {
80 name,
81 schema: schema_of::<T>,
82 full: || canonical(&T::sample_full()),
83 minimal: || canonical(&T::sample_minimal()),
84 round_trip: round_trip_as::<T>,
85 alternates: || T::sample_alternates().iter().map(canonical).collect(),
86 }
87 }
88
89 #[must_use]
91 pub fn schema(&self) -> Value {
92 (self.schema)()
93 }
94
95 #[must_use]
97 pub fn full(&self) -> Value {
98 (self.full)()
99 }
100
101 #[must_use]
103 pub fn minimal(&self) -> Value {
104 (self.minimal)()
105 }
106
107 #[must_use]
110 pub fn samples(&self) -> Vec<Value> {
111 let mut values = vec![self.full(), self.minimal()];
112 values.extend((self.alternates)());
113 values
114 }
115
116 pub fn round_trip(&self, value: Value) -> Result<Value, String> {
122 (self.round_trip)(value)
123 }
124}
125
126#[must_use]
135pub fn schema_of<T: JsonSchema>() -> Value {
136 let generator = SchemaSettings::draft2020_12().into_generator();
137 serde_json::to_value(generator.into_root_schema_for::<T>())
138 .expect("a generated schema is JSON by construction")
139}
140
141pub const EVENT_KIND_MARKER: &str = "x-event-kind";
143
144#[must_use]
157pub fn event_schema_of<P: EventPayload>() -> Value {
158 let mut schema = schema_of::<P>();
159 schema
160 .as_object_mut()
161 .expect("a payload's schema is an object")
162 .insert(EVENT_KIND_MARKER.to_owned(), Value::from(P::KIND));
163 schema
164}
165
166fn emit<T: Serialize>(value: &T) -> Result<Value, String> {
168 serde_json::to_value(value).map_err(|error| error.to_string())
169}
170
171fn round_trip_as<T: Serialize + DeserializeOwned>(value: Value) -> Result<Value, String> {
173 let parsed: T = serde_json::from_value(value).map_err(|error| error.to_string())?;
174 emit(&parsed)
175}
176
177fn canonical<T: Serialize>(value: &T) -> Value {
179 emit(value).expect("a canonical sample serializes")
180}
181
182macro_rules! contract_of {
184 ($ty:ty) => {
185 contract_of!($ty, stringify!($ty))
186 };
187 ($ty:ty, $name:expr) => {
188 TypeContract::of::<$ty>($name)
189 };
190}
191
192macro_rules! declare {
194 ($($ty:ty $(=> $name:literal)?),* $(,)?) => {
195 vec![$( contract_of!($ty $(, $name)?) ),*]
196 };
197}
198
199#[must_use]
201pub fn declared() -> Vec<TypeContract> {
202 declare![
203 EventId,
204 EventBody,
205 EventKind,
206 EventRecord,
207 EventSource,
208 FileName,
209 ImageId,
210 ImageRef,
211 PrintId,
212 Range,
213 RawBytes,
214 Reported<f64> => "Reported",
215 Timestamp,
216 ]
217}
218
219fn print_id() -> PrintId {
221 "0191f0a0-0000-7000-8000-000000000001"
222 .parse()
223 .expect("a fixed version 7 identifier")
224}
225
226fn event_id() -> EventId {
228 "0191f0a0-0000-7000-8000-000000000002"
229 .parse()
230 .expect("a fixed version 7 identifier")
231}
232
233fn image_id() -> ImageId {
235 "0191f0a0-0000-7000-8000-000000000003"
236 .parse()
237 .expect("a fixed version 7 identifier")
238}
239
240fn instant() -> Timestamp {
242 "2026-03-01T12:00:00Z"
243 .parse()
244 .expect("a fixed RFC 3339 instant")
245}
246
247impl Sample for PrintId {
248 fn sample_full() -> Self {
249 print_id()
250 }
251}
252
253impl Sample for EventId {
254 fn sample_full() -> Self {
255 event_id()
256 }
257}
258
259impl Sample for ImageId {
260 fn sample_full() -> Self {
261 image_id()
262 }
263}
264
265impl Sample for Timestamp {
266 fn sample_full() -> Self {
267 instant()
268 }
269}
270
271impl Sample for FileName {
272 fn sample_full() -> Self {
273 Self::new("benchy.gcode").expect("a name carrying no forbidden property")
274 }
275}
276
277impl Sample for RawBytes {
278 fn sample_full() -> Self {
279 Self::new(b"{\"event\":{}}".to_vec())
280 }
281}
282
283impl Sample for Range {
284 fn sample_full() -> Self {
285 Self::new(0.5, 1.5)
286 }
287}
288
289impl Sample for Reported<f64> {
290 fn sample_full() -> Self {
291 Self::new(1.0, Range::sample_full())
292 }
293}
294
295impl Sample for ImageRef {
296 fn sample_full() -> Self {
297 Self {
298 id: image_id(),
299 sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned(),
300 }
301 }
302}
303
304impl Sample for EventSource {
305 fn sample_full() -> Self {
306 Self::new("sample_source")
307 }
308}
309
310impl Sample for EventKind {
311 fn sample_full() -> Self {
312 Self::new("sample_event").expect("a fixed kind name")
313 }
314}
315
316impl Sample for EventBody {
317 fn sample_full() -> Self {
318 Self {
319 kind: EventKind::sample_full(),
320 payload: serde_json::json!({ "detail": "a payload in the form its kind declares" }),
321 }
322 }
323}
324
325impl Sample for EventRecord {
326 fn sample_full() -> Self {
327 Self {
328 id: event_id(),
329 print_id: Some(print_id()),
330 source: EventSource::sample_full(),
331 received_at: instant(),
332 image: Some(ImageRef::sample_full()),
333 body: EventBody::sample_full(),
334 raw: Some(RawBytes::sample_full()),
335 }
336 }
337
338 fn sample_minimal() -> Self {
339 Self {
340 print_id: None,
341 image: None,
342 raw: None,
343 ..Self::sample_full()
344 }
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
353pub struct WireField {
354 pub name: String,
356 pub descriptor: String,
358 pub required: bool,
360}
361
362fn referenced(reference: &str) -> String {
364 reference.rsplit('/').next().unwrap_or(reference).to_owned()
365}
366
367fn sole_arm(arms: &[Value]) -> Option<&Value> {
369 let mut kept = arms
370 .iter()
371 .filter(|arm| arm.get("type") != Some(&Value::from("null")));
372 let first = kept.next()?;
373 kept.next().is_none().then_some(first)
374}
375
376fn descriptor(schema: &Value) -> String {
378 if schema == &Value::Bool(true) {
381 return "any".to_owned();
382 }
383 if let Some(object) = schema.as_object()
384 && object.keys().all(|key| key == "description")
385 {
386 return "any".to_owned();
387 }
388 if let Some(Value::String(reference)) = schema.get("$ref") {
389 return referenced(reference);
390 }
391 for key in ["anyOf", "oneOf"] {
392 if let Some(Value::Array(arms)) = schema.get(key) {
393 return match sole_arm(arms) {
394 Some(arm) => descriptor(arm),
395 None => "enum".to_owned(),
396 };
397 }
398 }
399 let kind = match schema.get("type") {
400 Some(Value::String(name)) => name.clone(),
401 Some(Value::Array(names)) => names
402 .iter()
403 .filter_map(Value::as_str)
404 .find(|name| *name != "null")
405 .unwrap_or("unknown")
406 .to_owned(),
407 _ => "unknown".to_owned(),
408 };
409 match kind.as_str() {
410 "array" => format!(
411 "array:{}",
412 schema
413 .get("items")
414 .map_or_else(|| "unknown".to_owned(), descriptor)
415 ),
416 "object" => match (
417 schema.get("additionalProperties"),
418 schema.get("patternProperties"),
419 ) {
420 (Some(value), _) if value.is_object() => format!("map:{}", descriptor(value)),
421 (_, Some(Value::Object(patterns))) if patterns.len() == 1 => {
422 let value = patterns.values().next().expect("one pattern");
423 format!("map:{}", descriptor(value))
424 }
425 _ => "object".to_owned(),
426 },
427 other => other.to_owned(),
428 }
429}
430
431fn required_names(schema: &Value) -> Vec<&str> {
433 schema
434 .get("required")
435 .and_then(Value::as_array)
436 .map(|names| names.iter().filter_map(Value::as_str).collect())
437 .unwrap_or_default()
438}
439
440fn collect(schema: &Value, into: &mut BTreeMap<String, WireField>) {
442 let required = required_names(schema);
443 let Some(Value::Object(properties)) = schema.get("properties") else {
444 return;
445 };
446 for (name, property) in properties {
447 into.insert(
448 name.clone(),
449 WireField {
450 name: name.clone(),
451 descriptor: descriptor(property),
452 required: required.contains(&name.as_str()),
453 },
454 );
455 }
456}
457
458#[must_use]
465pub fn wire_fields(schema: &Value) -> Vec<WireField> {
466 let mut fields = BTreeMap::new();
467 collect(schema, &mut fields);
468 for key in ["anyOf", "oneOf", "allOf"] {
469 let Some(Value::Array(arms)) = schema.get(key) else {
470 continue;
471 };
472 let mut from_arms: BTreeMap<String, WireField> = BTreeMap::new();
473 for arm in arms {
474 let mut one = BTreeMap::new();
475 collect(arm, &mut one);
476 for (name, field) in one {
477 from_arms
478 .entry(name)
479 .and_modify(|held| {
480 held.required &= field.required;
481 if held.descriptor != field.descriptor {
482 "union".clone_into(&mut held.descriptor);
483 }
484 })
485 .or_insert(field);
486 }
487 }
488 fields.extend(from_arms);
489 }
490 fields.into_values().collect()
491}