Skip to main content

printobserver_types/
contract.rs

1//! This crate's own claim about the types it declares.
2//!
3//! Every type this crate declares as its own is listed in [`declared`], with
4//! the JSON Schema it emits and a canonical value of it. This is what the
5//! `printobserver-types:schemas` graph target generates the checked-in schemas
6//! from, and what this crate's contract tests walk, so that a type cannot fall
7//! out of the schema set or out of the round-trip corpus by being forgotten in
8//! a list somebody maintains by hand.
9//!
10//! Each entry carries two values rather than one: `full`, which carries every
11//! field the type declares, and `minimal`, which carries every optional field
12//! absent. The second is what proves that an absent optional serializes as
13//! absent rather than as `null`, a zero or an empty string.
14
15use 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
31/// A canonical value of one type, for the schema set and the round-trip corpus.
32pub trait Sample: Sized {
33    /// A value carrying every field the type declares.
34    fn sample_full() -> Self;
35
36    /// A value carrying every optional field absent.
37    #[must_use]
38    fn sample_minimal() -> Self {
39        Self::sample_full()
40    }
41
42    /// One value per further arm, for a type whose wire form is a union.
43    ///
44    /// A union's arms declare fields the other arms do not, so a corpus of one
45    /// value per type would leave those fields undriven. Every arm a type
46    /// declares beyond the one `sample_full` carries belongs here.
47    #[must_use]
48    fn sample_alternates() -> Vec<Self> {
49        Vec::new()
50    }
51}
52
53/// One type this crate declares, with its schema and its canonical values.
54#[derive(Debug, Clone, Copy)]
55pub struct TypeContract {
56    /// The type's own name, which is also its schema's file name.
57    pub name: &'static str,
58    /// The JSON Schema the type emits.
59    schema: fn() -> Value,
60    /// A value carrying every field the type declares, serialized.
61    full: fn() -> Value,
62    /// A value carrying every optional field absent, serialized.
63    minimal: fn() -> Value,
64    /// Parse a value as this type and serialize it back.
65    round_trip: fn(Value) -> Result<Value, String>,
66    /// One serialized value per further arm of a union.
67    alternates: fn() -> Vec<Value>,
68}
69
70impl TypeContract {
71    /// The contract of one type, under the name its schema is written under.
72    ///
73    /// This is how a crate other than this one declares a type of its own into
74    /// the schema set: its `schemas` test lists its types through this and
75    /// reconciles the files under `schemas/<crate>/` against them, walking the
76    /// same round trip and the same absent-optional rule this crate's own
77    /// contract tests walk.
78    #[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    /// The JSON Schema this type emits.
91    #[must_use]
92    pub fn schema(&self) -> Value {
93        (self.schema)()
94    }
95
96    /// A serialized value carrying every field the type declares.
97    #[must_use]
98    pub fn full(&self) -> Value {
99        (self.full)()
100    }
101
102    /// A serialized value carrying every optional field absent.
103    #[must_use]
104    pub fn minimal(&self) -> Value {
105        (self.minimal)()
106    }
107
108    /// Every canonical value of this type: the full one, the minimal one, and
109    /// one per further arm of a union.
110    #[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    /// Parse a value as this type and serialize the result back.
118    ///
119    /// # Errors
120    ///
121    /// Returns the parse or emission failure, as the message it carried.
122    pub fn round_trip(&self, value: Value) -> Result<Value, String> {
123        (self.round_trip)(value)
124    }
125}
126
127/// The JSON Schema one type emits, under this crate's own generator settings.
128///
129/// Sub-schemas are referenced rather than inlined, so that a schema names the
130/// types it is built from rather than flattening them into anonymous objects.
131///
132/// # Panics
133///
134/// Panics if the generated schema is not JSON, which it is by construction.
135#[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
142/// The member an event payload's schema carries naming the kind it is under.
143pub const EVENT_KIND_MARKER: &str = "x-event-kind";
144
145/// The JSON Schema one event payload emits, marked with the kind it is under.
146///
147/// The schema [`schema_of`] answers for the payload type, plus one top-level
148/// member, `x-event-kind`, whose value is the type's own
149/// [`KIND`](EventPayload::KIND). Nothing writes that member by hand: the crate
150/// that owns the kind writes its schema file through this, and the clients'
151/// kind-to-payload tables are generated from the marker.
152///
153/// # Panics
154///
155/// Panics if the generated schema is not an object, which it is by
156/// construction for a payload type.
157#[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
167/// Serialize a value, reporting a refusal as the message it carried.
168fn emit<T: Serialize>(value: &T) -> Result<Value, String> {
169    serde_json::to_value(value).map_err(|error| error.to_string())
170}
171
172/// Parse a value as `T` and serialize it back.
173fn 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
178/// Serialize a canonical value, which this crate's own types never refuse.
179fn canonical<T: Serialize>(value: &T) -> Value {
180    emit(value).expect("a canonical sample serializes")
181}
182
183/// One type's contract, under its own name or under a name given for it.
184macro_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
193/// Declare the contract of each named type.
194macro_rules! declare {
195    ($($ty:ty $(=> $name:literal)?),* $(,)?) => {
196        vec![$( contract_of!($ty $(, $name)?) ),*]
197    };
198}
199
200/// Every type this crate declares as its own.
201#[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
221/// A fixed print identifier, so that a sample is the same on every run.
222fn print_id() -> PrintId {
223    "0191f0a0-0000-7000-8000-000000000001"
224        .parse()
225        .expect("a fixed version 7 identifier")
226}
227
228/// A fixed event identifier.
229fn event_id() -> EventId {
230    "0191f0a0-0000-7000-8000-000000000002"
231        .parse()
232        .expect("a fixed version 7 identifier")
233}
234
235/// A fixed image identifier.
236fn image_id() -> ImageId {
237    "0191f0a0-0000-7000-8000-000000000003"
238        .parse()
239        .expect("a fixed version 7 identifier")
240}
241
242/// A fixed instant, so that a sample is the same on every run.
243fn 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/// One field as a generated schema declares it.
351///
352/// This is how the contract tests read a type's fields: off the schema the type
353/// generates rather than off a table maintained beside it.
354#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
355pub struct WireField {
356    /// The field's name on the wire.
357    pub name: String,
358    /// What the field is: a referenced type's name, or a primitive's.
359    pub descriptor: String,
360    /// Whether an instance must carry it.
361    pub required: bool,
362}
363
364/// The name a reference names.
365fn referenced(reference: &str) -> String {
366    reference.rsplit('/').next().unwrap_or(reference).to_owned()
367}
368
369/// The one non-null arm of a union, when there is exactly one.
370fn 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
378/// What one property schema declares the field to be.
379fn descriptor(schema: &Value) -> String {
380    // `schemars` emits the boolean schema `true` for a JSON value of any form,
381    // and, where the value carries a description, the empty object schema.
382    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
433/// The names one schema object marks required.
434fn 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
442/// Read the fields off one object schema into a map.
443fn 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/// Every field one generated schema declares, in name order.
461///
462/// A schema whose type flattens a tagged enum declares some of its fields
463/// inside the union's arms rather than beside them, so this reads both: a field
464/// every arm requires is required, a field no arm declares is absent, and a
465/// field the arms declare at differing types is `union`.
466#[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}