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