Skip to main content

axiolid_model/
wire.rs

1//! The versioned wire format of a [`GeometryGraph`] (ADR 0085).
2//!
3//! One serde data model, two encodings: JSON (RFC 8259) and CBOR
4//! (RFC 8949). Every payload is an envelope naming the format and its
5//! version around the graph:
6//!
7//! ```json
8//! {
9//!   "format": "axiolid-geometry-graph",
10//!   "version": "1.0",
11//!   "graph": { "nodes": [{ "Primitive": { "Sphere": { "radius": 1.0 } } }], "roots": [0] }
12//! }
13//! ```
14//!
15//! `graph.nodes` lists the nodes in insertion order, which is topological,
16//! and a node reference is the zero-based index of an earlier node.
17//! Enums are externally tagged by their variant names, fields by their
18//! names, a point or vector is an array of its coordinates and a transform
19//! the columns of its linear part then its translation. Real numbers
20//! round-trip bit-exactly and are finite.
21//!
22//! # Versions
23//!
24//! Adding a node kind, an enum variant or an optional field is a minor
25//! version; renaming or removing anything, or changing its type, meaning or
26//! units, is a major version. A reader reads its own major up to its own
27//! minor, so every older minor of [`FORMAT_VERSION`] stays readable.
28//!
29//! A writer labels each payload with the lowest version that represents
30//! it (#297), not with [`FORMAT_VERSION`]: a graph that uses nothing a
31//! minor added is written exactly as that minor's predecessor wrote it,
32//! so an older reader keeps reading every graph that does not use newer
33//! features. A graph with no seam-snapping window is a 1.0 payload, one
34//! whose run stations carry no seam side or window a 1.0 or 1.1 payload,
35//! one with no run station at its basis's start or end at most a 1.2
36//! payload, one with no half-space bounded along its placement's `z` at
37//! most a 1.3 payload, and one with no ruled surface at most a 1.4 payload.
38//!
39//! | Version | Added |
40//! | --- | --- |
41//! | 1.0 | The format (#267). |
42//! | 1.1 | `OrientedCurveStation.seam_window` (#294), a real omitted at its default `+0.0`; absent, as in every 1.0 payload, it reads as `0`, which is what a 1.0 station meant. |
43//! | 1.2 | Seam sides and windows on run stations (#301): `SectionAtStation.seam` and `.seam_window`, omitted at `Outgoing` and `+0.0` and read so when absent; the variant `CurveRelation::OffsetsAtStations`, whose `RunStation`s carry the same two fields. |
44//! | 1.3 | Run stations at their basis's ends (#303): `RunStation.basis_end`, `"Start"` or `"End"`, omitted when absent (a stated distance) and read so. |
45//! | 1.4 | A half-space bounded along its placement's `z` (#321): the variant `SolidOperation::BoundedHalfSpaceAlongZ`, fields as `BoundedHalfSpace`'s. |
46//! | 1.5 | A surface ruled between two sides (#402): the variant `SurfaceRelation::Ruled` with its `RuledSide`s, a curve node over an interval or an apex point. |
47//!
48//! # Refusals
49//!
50//! A reader never returns part of a payload. It refuses, by a named
51//! [`WireError`]: a missing or other format name, a missing version, a
52//! version it does not support (another major, or a newer minor, checked
53//! before the graph is read), a node kind or variant it does not know
54//! ([`WireError::UnknownKind`], never skipped), a field it does not know, a
55//! non-finite number, anything that does not decode, and a graph that the
56//! builder's validation refuses: a decoded graph is rebuilt node by node
57//! through [`GeometryGraphBuilder::push`], so a payload cannot build a graph
58//! the API could not.
59//!
60//! A writer refuses a graph holding a non-finite number, which JSON cannot
61//! carry, rather than write a payload that would read back differently.
62//!
63//! # Integers where reals are expected
64//!
65//! Both readers accept an integer where a real is expected when it is
66//! exactly a double, as host encoders write whole-number doubles as
67//! integers: a JSON integer literal, or a CBOR integer item `i` with
68//! `i as f64 as i128 == i` (every `|i| <= 2^53`, and larger ones that are
69//! doubles). Integer zero has no sign, so it reads as `+0.0`; a writer
70//! keeps `-0.0` a float. A CBOR integer that is not exactly a double is
71//! refused as [`WireError::Malformed`], naming its path, wherever it
72//! stands: no index or count of the format reaches `2^53`.
73
74use core::fmt;
75
76use ciborium::Value;
77use serde::{Deserialize, Serialize};
78
79use crate::id::{GraphId, WireBrand};
80use crate::{
81    CurveRelation, GeometryGraph, GeometryGraphBuilder, GeometryNode, GraphError, NodeId,
82    OrientedCurveStation, RunStation, SeamSide, SectionAtStation, SolidOperation, SurfaceRelation,
83};
84
85/// The format name every payload carries in its `format` entry.
86pub const FORMAT_NAME: &str = "axiolid-geometry-graph";
87
88/// The newest format version this crate reads and writes. A payload is
89/// labelled with the lowest version that represents it, which is this one
90/// only when the graph uses what this version added (see the
91/// [module documentation](self#versions)).
92pub const FORMAT_VERSION: FormatVersion = FormatVersion::new(1, 5);
93
94/// A `MAJOR.MINOR` wire format version.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct FormatVersion {
97    /// Incremented by any change an older reader would misread.
98    pub major: u32,
99    /// Incremented by additions: a node kind, a variant, an optional field.
100    pub minor: u32,
101}
102
103impl FormatVersion {
104    /// The version `major.minor`.
105    #[must_use]
106    pub const fn new(major: u32, minor: u32) -> Self {
107        Self { major, minor }
108    }
109
110    /// Parse `"MAJOR.MINOR"`, two decimal integers; `None` for anything else.
111    #[must_use]
112    pub fn parse(text: &str) -> Option<Self> {
113        let (major, minor) = text.split_once('.')?;
114        let number = |part: &str| {
115            if part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()) {
116                None
117            } else {
118                part.parse().ok()
119            }
120        };
121        Some(Self::new(number(major)?, number(minor)?))
122    }
123
124    /// Whether a reader of this version reads a payload of `payload`: the
125    /// same major, and a minor no newer than its own.
126    #[must_use]
127    pub const fn reads(self, payload: Self) -> bool {
128        payload.major == self.major && payload.minor <= self.minor
129    }
130}
131
132impl fmt::Display for FormatVersion {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}.{}", self.major, self.minor)
135    }
136}
137
138/// Why a graph could not be written or a payload could not be read.
139#[non_exhaustive]
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum WireError {
142    /// The payload's `format` entry is missing or names another format.
143    UnknownFormat {
144        /// The format named, if any.
145        found: Option<String>,
146    },
147    /// The payload has no `version` entry.
148    MissingVersion,
149    /// The payload's version is not `MAJOR.MINOR`, is of another major, or
150    /// is a newer minor than this reader supports.
151    UnsupportedVersion {
152        /// The version as the payload states it.
153        found: String,
154        /// The newest version this reader reads.
155        supported: FormatVersion,
156    },
157    /// A node kind or enum variant this reader does not know. The payload
158    /// is refused whole: skipping the kind would lose geometry silently.
159    UnknownKind {
160        /// The kind or variant name.
161        kind: String,
162        /// The payload's format version.
163        version: FormatVersion,
164    },
165    /// A field this reader does not know.
166    UnknownField {
167        /// The field name.
168        field: String,
169        /// The payload's format version.
170        version: FormatVersion,
171    },
172    /// A number that is NaN or infinite, which neither encoding carries.
173    NonFinite {
174        /// Where it is: a path into the payload for a graph or a CBOR
175        /// payload, the decoder's position for a JSON payload.
176        path: String,
177    },
178    /// The decoded graph fails the validation every graph construction
179    /// runs.
180    InvalidGraph {
181        /// The index of the node refused, or `None` when the roots are.
182        node: Option<usize>,
183        /// Why.
184        error: GraphError,
185    },
186    /// The payload does not decode: bad syntax, a wrong type, a missing
187    /// field, trailing bytes, or nesting past the decoder's limit.
188    Malformed {
189        /// The decoder's message.
190        detail: String,
191    },
192}
193
194impl fmt::Display for WireError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            Self::UnknownFormat { found: Some(found) } => {
198                write!(f, "payload format `{found}` is not `{FORMAT_NAME}`")
199            }
200            Self::UnknownFormat { found: None } => {
201                write!(f, "payload names no format; expected `{FORMAT_NAME}`")
202            }
203            Self::MissingVersion => write!(f, "payload names no format version"),
204            Self::UnsupportedVersion { found, supported } => write!(
205                f,
206                "format version `{found}` is not supported; this reader reads \
207                 {major}.0 to {supported}",
208                major = supported.major
209            ),
210            Self::UnknownKind { kind, version } => write!(
211                f,
212                "unknown kind `{kind}` in a format {version} payload; refusing the whole payload"
213            ),
214            Self::UnknownField { field, version } => {
215                write!(f, "unknown field `{field}` in a format {version} payload")
216            }
217            Self::NonFinite { path } => write!(f, "non-finite number at {path}"),
218            Self::InvalidGraph {
219                node: Some(node),
220                error,
221            } => write!(f, "invalid graph at node {node}: {error}"),
222            Self::InvalidGraph { node: None, error } => write!(f, "invalid graph roots: {error}"),
223            Self::Malformed { detail } => write!(f, "malformed payload: {detail}"),
224        }
225    }
226}
227
228impl std::error::Error for WireError {}
229
230#[derive(Serialize)]
231struct EnvelopeOut<'a> {
232    format: &'static str,
233    version: String,
234    graph: GraphOut<'a>,
235}
236
237#[derive(Serialize)]
238struct GraphOut<'a> {
239    nodes: &'a [GeometryNode],
240    roots: &'a [NodeId],
241}
242
243/// The envelope's identifying entries, read before the graph so that the
244/// version is checked before any vocabulary is.
245#[derive(Deserialize)]
246struct Header {
247    format: Option<String>,
248    version: Option<String>,
249}
250
251#[derive(Deserialize)]
252#[serde(deny_unknown_fields)]
253struct EnvelopeIn {
254    #[allow(dead_code)]
255    format: String,
256    #[allow(dead_code)]
257    version: String,
258    graph: GraphIn,
259}
260
261#[derive(Deserialize)]
262#[serde(deny_unknown_fields)]
263struct GraphIn {
264    nodes: Vec<GeometryNode>,
265    roots: Vec<NodeId>,
266}
267
268fn envelope(graph: &GeometryGraph) -> EnvelopeOut<'_> {
269    EnvelopeOut {
270        format: FORMAT_NAME,
271        version: required_version(graph.nodes()).to_string(),
272        graph: GraphOut {
273            nodes: graph.nodes(),
274            roots: graph.roots(),
275        },
276    }
277}
278
279fn malformed(error: impl fmt::Display) -> WireError {
280    WireError::Malformed {
281        detail: error.to_string(),
282    }
283}
284
285/// The value tree of `graph`'s payload, refused if it holds a non-finite
286/// number.
287fn checked_value(graph: &GeometryGraph) -> Result<Value, WireError> {
288    let value = Value::serialized(&envelope(graph)).map_err(malformed)?;
289    check_finite(&value, &mut String::new())?;
290    Ok(value)
291}
292
293fn check_finite(value: &Value, path: &mut String) -> Result<(), WireError> {
294    let at = path.len();
295    let result = match value {
296        Value::Float(number) if !number.is_finite() => Err(WireError::NonFinite {
297            path: if path.is_empty() {
298                "the payload root".to_owned()
299            } else {
300                path.clone()
301            },
302        }),
303        Value::Array(items) => items.iter().enumerate().try_for_each(|(index, item)| {
304            path.truncate(at);
305            path.push_str(&format!("[{index}]"));
306            check_finite(item, path)
307        }),
308        Value::Map(entries) => entries.iter().try_for_each(|(key, item)| {
309            path.truncate(at);
310            match key {
311                Value::Text(name) if at == 0 => path.push_str(name),
312                Value::Text(name) => {
313                    path.push('.');
314                    path.push_str(name);
315                }
316                _ => {
317                    check_finite(key, path)?;
318                    path.truncate(at);
319                    path.push_str("[key]");
320                }
321            }
322            check_finite(item, path)
323        }),
324        Value::Tag(_, inner) => check_finite(inner, path),
325        _ => Ok(()),
326    };
327    path.truncate(at);
328    result
329}
330
331/// The version a payload's header states, if this reader reads it.
332fn check_header(header: Header) -> Result<FormatVersion, WireError> {
333    match header.format {
334        Some(format) if format == FORMAT_NAME => {}
335        found => return Err(WireError::UnknownFormat { found }),
336    }
337    let found = header.version.ok_or(WireError::MissingVersion)?;
338    match FormatVersion::parse(&found) {
339        Some(version) if FORMAT_VERSION.reads(version) => Ok(version),
340        _ => Err(WireError::UnsupportedVersion {
341            found,
342            supported: FORMAT_VERSION,
343        }),
344    }
345}
346
347/// The name serde quotes after `prefix` in a decoder message, up to the
348/// clause that follows it.
349fn quoted_after(message: &str, prefix: &str) -> Option<String> {
350    let start = message.find(prefix)? + prefix.len();
351    let rest = &message[start..];
352    let end = ["`, expected", "`, there are no"]
353        .iter()
354        .filter_map(|clause| rest.rfind(clause))
355        .max()
356        .or_else(|| rest.find('`'))?;
357    Some(rest[..end].to_owned())
358}
359
360/// Name a decoder failure: an unknown kind or field by its name, anything
361/// else as malformed.
362fn decode_error(message: String, version: FormatVersion) -> WireError {
363    if let Some(kind) = quoted_after(&message, "unknown variant `") {
364        return WireError::UnknownKind { kind, version };
365    }
366    if let Some(field) = quoted_after(&message, "unknown field `") {
367        return WireError::UnknownField { field, version };
368    }
369    WireError::Malformed { detail: message }
370}
371
372/// A JSON decoder failure: a number literal past the double range is a
373/// non-finite number.
374fn json_error(error: &serde_json::Error, version: Option<FormatVersion>) -> WireError {
375    let message = error.to_string();
376    if message.starts_with("number out of range") {
377        return WireError::NonFinite {
378            path: format!("line {} column {}", error.line(), error.column()),
379        };
380    }
381    match version {
382        Some(version) => decode_error(message, version),
383        None => WireError::Malformed { detail: message },
384    }
385}
386
387/// The first version of the format, which a payload using no later
388/// addition is labelled with.
389const BASE_VERSION: FormatVersion = FormatVersion::new(1, 0);
390
391/// The version that introduced the seam-snapping window (#294).
392const SEAM_WINDOW_SINCE: FormatVersion = FormatVersion::new(1, 1);
393
394/// The version that introduced seam sides and windows on run stations and
395/// `CurveRelation::OffsetsAtStations` (#301).
396const RUN_SEAMS_SINCE: FormatVersion = FormatVersion::new(1, 2);
397
398/// The version that introduced run stations at their basis's ends (#303).
399const BASIS_END_SINCE: FormatVersion = FormatVersion::new(1, 3);
400
401/// The version that introduced `SolidOperation::BoundedHalfSpaceAlongZ`
402/// (#321).
403const ALONG_Z_SINCE: FormatVersion = FormatVersion::new(1, 4);
404
405/// The version that introduced `SurfaceRelation::Ruled` (#402).
406const RULED_SINCE: FormatVersion = FormatVersion::new(1, 5);
407
408/// The lowest version that represents `nodes` (#297): the version every
409/// payload is labelled with, so an older reader reads every graph that
410/// uses nothing newer than it knows.
411///
412/// Every minor version after 1.0 adds its own feature test here, the same
413/// test [`newer_feature`] refuses an older payload by: a writer never
414/// labels a payload with a version whose reader would refuse it.
415fn required_version(nodes: &[GeometryNode]) -> FormatVersion {
416    let mut version = BASE_VERSION;
417    if nodes.iter().any(uses_seam_window) {
418        version = version.max(SEAM_WINDOW_SINCE);
419    }
420    if nodes.iter().any(|node| run_seams(node).is_some()) {
421        version = version.max(RUN_SEAMS_SINCE);
422    }
423    if nodes.iter().any(uses_basis_end) {
424        version = version.max(BASIS_END_SINCE);
425    }
426    if nodes.iter().any(is_along_z) {
427        version = version.max(ALONG_Z_SINCE);
428    }
429    if nodes.iter().any(is_ruled) {
430        version = version.max(RULED_SINCE);
431    }
432    version
433}
434
435/// Every oriented station `node` carries, wherever it stands in the node.
436///
437/// The match names every node kind, so a new kind cannot be added without
438/// deciding whether it carries a station; a kind that comes to carry
439/// several returns them all.
440fn oriented_stations(node: &GeometryNode) -> &[OrientedCurveStation] {
441    match node {
442        GeometryNode::OrientedCurveStation(station) => core::slice::from_ref(station),
443        GeometryNode::InstanceAtStation(placed) => core::slice::from_ref(&placed.station),
444        GeometryNode::Point2(_)
445        | GeometryNode::Point3(_)
446        | GeometryNode::Vector2(_)
447        | GeometryNode::Vector3(_)
448        | GeometryNode::Frame2(_)
449        | GeometryNode::Frame3(_)
450        | GeometryNode::Transform(_)
451        | GeometryNode::PointList2(_)
452        | GeometryNode::PointList3(_)
453        | GeometryNode::Curve2(_)
454        | GeometryNode::Curve3(_)
455        | GeometryNode::CurveRelation(_)
456        | GeometryNode::PointOnCurve(_)
457        | GeometryNode::Surface(_)
458        | GeometryNode::SurfaceRelation(_)
459        | GeometryNode::PointOnSurface(_)
460        | GeometryNode::Profile(_)
461        | GeometryNode::OpenProfile(_)
462        | GeometryNode::Primitive(_)
463        | GeometryNode::HalfSpace(_)
464        | GeometryNode::SolidOperation(_)
465        | GeometryNode::BRep(_)
466        | GeometryNode::PolygonMesh(_)
467        | GeometryNode::TriMesh(_)
468        | GeometryNode::BoundingBox(_)
469        | GeometryNode::Instance(_)
470        | GeometryNode::Collection(_)
471        | GeometryNode::CurveStation(_) => &[],
472    }
473}
474
475/// Whether `node` writes a seam-snapping window (format 1.1): a station
476/// whose window is not `+0.0`, the one value the writer omits. (`-0.0` is
477/// written, so it needs 1.1 too.)
478fn uses_seam_window(node: &GeometryNode) -> bool {
479    oriented_stations(node)
480        .iter()
481        .any(|station| station.seam_window.to_bits() != 0)
482}
483
484/// What format 1.2 added that a node uses (#301), named as an older
485/// reader would refuse it.
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487enum RunSeams {
488    /// A node kind or variant 1.2 added.
489    Kind(&'static str),
490    /// A field 1.2 added.
491    Field(&'static str),
492}
493
494/// Every section at a station `node` carries, wherever it stands in the
495/// node.
496///
497/// The matches name every node kind and every variant of the relations
498/// and operations, so a new one cannot be added without deciding whether
499/// it carries run stations.
500fn run_sections(node: &GeometryNode) -> &[SectionAtStation] {
501    match node {
502        GeometryNode::SolidOperation(operation) => match operation {
503            SolidOperation::SectionsAtStations { sections, .. } => sections,
504            SolidOperation::Extrusion { .. }
505            | SolidOperation::TaperedExtrusion { .. }
506            | SolidOperation::Revolution { .. }
507            | SolidOperation::TaperedRevolution { .. }
508            | SolidOperation::SweptDisk { .. }
509            | SolidOperation::FixedReferenceSweep { .. }
510            | SolidOperation::SurfaceCurveSweep { .. }
511            | SolidOperation::SectionedSpine { .. }
512            | SolidOperation::Boolean { .. }
513            | SolidOperation::BoundedHalfSpace { .. }
514            | SolidOperation::BoundedHalfSpaceAlongZ { .. }
515            | SolidOperation::StationedSpine { .. } => &[],
516        },
517        GeometryNode::SurfaceRelation(relation) => match relation {
518            SurfaceRelation::OpenSectionsAtStations { sections, .. } => sections,
519            SurfaceRelation::CurveBounded { .. }
520            | SurfaceRelation::RectangularTrimmed { .. }
521            | SurfaceRelation::Offset { .. }
522            | SurfaceRelation::LinearExtrusion { .. }
523            | SurfaceRelation::Revolution { .. }
524            | SurfaceRelation::SectionedSurface { .. }
525            | SurfaceRelation::Ruled { .. } => &[],
526        },
527        GeometryNode::Point2(_)
528        | GeometryNode::Point3(_)
529        | GeometryNode::Vector2(_)
530        | GeometryNode::Vector3(_)
531        | GeometryNode::Frame2(_)
532        | GeometryNode::Frame3(_)
533        | GeometryNode::Transform(_)
534        | GeometryNode::PointList2(_)
535        | GeometryNode::PointList3(_)
536        | GeometryNode::Curve2(_)
537        | GeometryNode::Curve3(_)
538        | GeometryNode::CurveRelation(_)
539        | GeometryNode::PointOnCurve(_)
540        | GeometryNode::Surface(_)
541        | GeometryNode::PointOnSurface(_)
542        | GeometryNode::Profile(_)
543        | GeometryNode::OpenProfile(_)
544        | GeometryNode::Primitive(_)
545        | GeometryNode::HalfSpace(_)
546        | GeometryNode::BRep(_)
547        | GeometryNode::PolygonMesh(_)
548        | GeometryNode::TriMesh(_)
549        | GeometryNode::BoundingBox(_)
550        | GeometryNode::Instance(_)
551        | GeometryNode::Collection(_)
552        | GeometryNode::CurveStation(_)
553        | GeometryNode::OrientedCurveStation(_)
554        | GeometryNode::InstanceAtStation(_) => &[],
555    }
556}
557
558/// Whether `node` uses what format 1.2 added (#301), and what: a
559/// `CurveRelation::OffsetsAtStations`, whose stations carry sides and
560/// windows; or a section at a station whose seam side is written (any but
561/// `Outgoing`) or whose window is (any but `+0.0`, so `-0.0` too).
562fn run_seams(node: &GeometryNode) -> Option<RunSeams> {
563    if let GeometryNode::CurveRelation(relation) = node {
564        return match relation {
565            CurveRelation::OffsetsAtStations { .. } => Some(RunSeams::Kind("OffsetsAtStations")),
566            CurveRelation::Composite { .. }
567            | CurveRelation::Trimmed { .. }
568            | CurveRelation::Offset { .. }
569            | CurveRelation::SurfaceCurve { .. }
570            | CurveRelation::ParameterCurve { .. }
571            | CurveRelation::OffsetByStations { .. } => None,
572        };
573    }
574    run_sections(node).iter().find_map(|section| {
575        if section.seam != SeamSide::Outgoing {
576            Some(RunSeams::Field("seam"))
577        } else if section.seam_window.to_bits() != 0 {
578            Some(RunSeams::Field("seam_window"))
579        } else {
580            None
581        }
582    })
583}
584
585/// Every run station `node` carries, wherever it stands in the node (#303).
586///
587/// The matches name every node kind and every variant of the curve
588/// relations, so a new one cannot be added without deciding whether it
589/// carries run stations.
590fn run_stations(node: &GeometryNode) -> &[RunStation] {
591    match node {
592        GeometryNode::CurveRelation(relation) => match relation {
593            CurveRelation::OffsetsAtStations { stations, .. } => stations,
594            CurveRelation::Composite { .. }
595            | CurveRelation::Trimmed { .. }
596            | CurveRelation::Offset { .. }
597            | CurveRelation::SurfaceCurve { .. }
598            | CurveRelation::ParameterCurve { .. }
599            | CurveRelation::OffsetByStations { .. } => &[],
600        },
601        GeometryNode::Point2(_)
602        | GeometryNode::Point3(_)
603        | GeometryNode::Vector2(_)
604        | GeometryNode::Vector3(_)
605        | GeometryNode::Frame2(_)
606        | GeometryNode::Frame3(_)
607        | GeometryNode::Transform(_)
608        | GeometryNode::PointList2(_)
609        | GeometryNode::PointList3(_)
610        | GeometryNode::Curve2(_)
611        | GeometryNode::Curve3(_)
612        | GeometryNode::PointOnCurve(_)
613        | GeometryNode::Surface(_)
614        | GeometryNode::SurfaceRelation(_)
615        | GeometryNode::PointOnSurface(_)
616        | GeometryNode::Profile(_)
617        | GeometryNode::OpenProfile(_)
618        | GeometryNode::Primitive(_)
619        | GeometryNode::HalfSpace(_)
620        | GeometryNode::SolidOperation(_)
621        | GeometryNode::BRep(_)
622        | GeometryNode::PolygonMesh(_)
623        | GeometryNode::TriMesh(_)
624        | GeometryNode::BoundingBox(_)
625        | GeometryNode::Instance(_)
626        | GeometryNode::Collection(_)
627        | GeometryNode::CurveStation(_)
628        | GeometryNode::OrientedCurveStation(_)
629        | GeometryNode::InstanceAtStation(_) => &[],
630    }
631}
632
633/// Whether `node` uses what format 1.3 added (#303): a run station at its
634/// basis's start or end, the one `basis_end` the writer writes (it omits
635/// `None`).
636fn uses_basis_end(node: &GeometryNode) -> bool {
637    run_stations(node)
638        .iter()
639        .any(|station| station.basis_end.is_some())
640}
641
642/// Whether `node` is what format 1.4 added (#321): a half-space bounded
643/// along its placement's `z`.
644fn is_along_z(node: &GeometryNode) -> bool {
645    matches!(
646        node,
647        GeometryNode::SolidOperation(SolidOperation::BoundedHalfSpaceAlongZ { .. })
648    )
649}
650
651/// Whether `node` is what format 1.5 added (#402): a ruled surface.
652fn is_ruled(node: &GeometryNode) -> bool {
653    matches!(
654        node,
655        GeometryNode::SurfaceRelation(SurfaceRelation::Ruled { .. })
656    )
657}
658
659/// What `node` holds that `version` does not have, refused as an older
660/// reader would refuse it: a seam-snapping window on an oriented station
661/// in a 1.0 payload; a seam side or window on a run station, or an
662/// `OffsetsAtStations`, in a payload older than 1.2; a run station at its
663/// basis's start or end in a payload older than 1.3; a half-space bounded
664/// along its placement's `z` in a payload older than 1.4; a ruled surface in
665/// a payload older than 1.5.
666/// No writer of those versions wrote them. (A default is what its absence
667/// means, so it is not told apart.)
668fn newer_feature(node: &GeometryNode, version: FormatVersion) -> Option<WireError> {
669    if version < SEAM_WINDOW_SINCE && uses_seam_window(node) {
670        return Some(WireError::UnknownField {
671            field: "seam_window".to_owned(),
672            version,
673        });
674    }
675    if version < RUN_SEAMS_SINCE {
676        if let Some(feature) = run_seams(node) {
677            return Some(match feature {
678                RunSeams::Kind(kind) => WireError::UnknownKind {
679                    kind: kind.to_owned(),
680                    version,
681                },
682                RunSeams::Field(field) => WireError::UnknownField {
683                    field: field.to_owned(),
684                    version,
685                },
686            });
687        }
688    }
689    if version < BASIS_END_SINCE && uses_basis_end(node) {
690        return Some(WireError::UnknownField {
691            field: "basis_end".to_owned(),
692            version,
693        });
694    }
695    if version < ALONG_Z_SINCE && is_along_z(node) {
696        return Some(WireError::UnknownKind {
697            kind: "BoundedHalfSpaceAlongZ".to_owned(),
698            version,
699        });
700    }
701    if version < RULED_SINCE && is_ruled(node) {
702        return Some(WireError::UnknownKind {
703            kind: "Ruled".to_owned(),
704            version,
705        });
706    }
707    None
708}
709
710/// Rebuild the decoded graph through the builder's validation.
711fn build(
712    owner: GraphId,
713    graph: GraphIn,
714    version: FormatVersion,
715) -> Result<GeometryGraph, WireError> {
716    let mut builder = GeometryGraphBuilder::with_owner(owner);
717    for (index, node) in graph.nodes.into_iter().enumerate() {
718        if let Some(refusal) = newer_feature(&node, version) {
719            return Err(refusal);
720        }
721        builder
722            .push(node)
723            .map_err(|error| WireError::InvalidGraph {
724                node: Some(index),
725                error,
726            })?;
727    }
728    builder
729        .finish(graph.roots)
730        .map_err(|error| WireError::InvalidGraph { node: None, error })
731}
732
733/// Write `graph` as a JSON payload.
734///
735/// # Errors
736///
737/// [`WireError::NonFinite`] if the graph holds a NaN or an infinity.
738pub fn to_json(graph: &GeometryGraph) -> Result<String, WireError> {
739    checked_value(graph)?;
740    serde_json::to_string(&envelope(graph)).map_err(malformed)
741}
742
743/// Read a graph from a JSON payload.
744///
745/// # Errors
746///
747/// A named [`WireError`] for every payload this reader cannot read whole;
748/// see the [module documentation](self).
749pub fn from_json(text: &str) -> Result<GeometryGraph, WireError> {
750    let header: Header = serde_json::from_str(text).map_err(|error| json_error(&error, None))?;
751    let version = check_header(header)?;
752    let owner = GraphId::fresh();
753    let envelope: EnvelopeIn = {
754        let _brand = WireBrand::enter(owner);
755        serde_json::from_str(text).map_err(|error| json_error(&error, Some(version)))?
756    };
757    build(owner, envelope.graph, version)
758}
759
760/// Write `graph` as a CBOR payload.
761///
762/// # Errors
763///
764/// [`WireError::NonFinite`] if the graph holds a NaN or an infinity.
765pub fn to_cbor(graph: &GeometryGraph) -> Result<Vec<u8>, WireError> {
766    let value = checked_value(graph)?;
767    let mut bytes = Vec::new();
768    ciborium::into_writer(&value, &mut bytes).map_err(malformed)?;
769    Ok(bytes)
770}
771
772/// Read a graph from a CBOR payload: one data item, nothing after it.
773///
774/// # Errors
775///
776/// A named [`WireError`] for every payload this reader cannot read whole;
777/// see the [module documentation](self).
778pub fn from_cbor(bytes: &[u8]) -> Result<GeometryGraph, WireError> {
779    let mut rest = bytes;
780    let value: Value = ciborium::from_reader(&mut rest).map_err(malformed)?;
781    if !rest.is_empty() {
782        return Err(WireError::Malformed {
783            detail: format!("{} trailing bytes after the payload", rest.len()),
784        });
785    }
786    let header: Header = value.deserialized().map_err(malformed)?;
787    let version = check_header(header)?;
788    let value = data_model(&value, &mut String::new())?;
789    let owner = GraphId::fresh();
790    let envelope: EnvelopeIn = {
791        let _brand = WireBrand::enter(owner);
792        EnvelopeIn::deserialize(value).map_err(|error| decode_error(error.to_string(), version))?
793    };
794    build(owner, envelope.graph, version)
795}
796
797/// The CBOR self-describe tag (RFC 8949 ยง3.4.6), the one tag a reader
798/// unwraps.
799const SELF_DESCRIBE: u64 = 55799;
800
801/// Whether a CBOR integer is exactly a double, so reading it as a real
802/// loses nothing. Integer zero has no sign and reads as `+0.0`.
803fn integer_is_exact(integer: i128) -> bool {
804    integer as f64 as i128 == integer
805}
806
807/// A CBOR value tree as the JSON data model, so that both encodings are
808/// read by one deserialiser and an integer item reads as a real the way a
809/// JSON integer literal does.
810///
811/// Refused, with the path: a non-finite float ([`WireError::NonFinite`]);
812/// an integer that is not exactly a double, wherever it stands (no index
813/// or count of the format reaches `2^53`), a map key that is not text, a
814/// repeated key, a byte string, a simple value, and any tag but the
815/// self-describe tag ([`WireError::Malformed`]).
816fn data_model(value: &Value, path: &mut String) -> Result<serde_json::Value, WireError> {
817    let at = path.len();
818    let here = |path: &String| {
819        if path.is_empty() {
820            "the payload root".to_owned()
821        } else {
822            path.clone()
823        }
824    };
825    let refuse = |path: &String, what: &str| WireError::Malformed {
826        detail: format!("{what} at {}", here(path)),
827    };
828    let result = match value {
829        Value::Null => Ok(serde_json::Value::Null),
830        Value::Bool(flag) => Ok(serde_json::Value::Bool(*flag)),
831        Value::Text(text) => Ok(serde_json::Value::String(text.clone())),
832        Value::Float(number) => serde_json::Number::from_f64(*number)
833            .map(serde_json::Value::Number)
834            .ok_or_else(|| WireError::NonFinite { path: here(path) }),
835        Value::Integer(integer) => {
836            let integer = i128::from(*integer);
837            if !integer_is_exact(integer) {
838                return Err(refuse(
839                    path,
840                    &format!("integer {integer} is not exactly a double"),
841                ));
842            }
843            // Past the 64-bit range only a real can hold it (exactly).
844            let number = match (u64::try_from(integer), i64::try_from(integer)) {
845                (Ok(unsigned), _) => Some(serde_json::Number::from(unsigned)),
846                (_, Ok(signed)) => Some(serde_json::Number::from(signed)),
847                _ => serde_json::Number::from_f64(integer as f64),
848            };
849            number
850                .map(serde_json::Value::Number)
851                .ok_or_else(|| refuse(path, "integer out of range"))
852        }
853        Value::Array(items) => items
854            .iter()
855            .enumerate()
856            .map(|(index, item)| {
857                path.truncate(at);
858                path.push_str(&format!("[{index}]"));
859                data_model(item, path)
860            })
861            .collect::<Result<Vec<_>, _>>()
862            .map(serde_json::Value::Array),
863        Value::Map(entries) => {
864            let mut map = serde_json::Map::new();
865            for (key, item) in entries {
866                path.truncate(at);
867                let Value::Text(name) = key else {
868                    return Err(refuse(path, "a map key that is not text"));
869                };
870                if at != 0 {
871                    path.push('.');
872                }
873                path.push_str(name);
874                let item = data_model(item, path)?;
875                if map.insert(name.clone(), item).is_some() {
876                    return Err(refuse(path, "a repeated key"));
877                }
878            }
879            Ok(serde_json::Value::Object(map))
880        }
881        Value::Tag(SELF_DESCRIBE, inner) => data_model(inner, path),
882        Value::Tag(tag, _) => Err(refuse(path, &format!("tag {tag}"))),
883        Value::Bytes(_) => Err(refuse(path, "a byte string")),
884        _ => Err(refuse(path, "a simple value")),
885    };
886    path.truncate(at);
887    result
888}
889
890impl GeometryGraph {
891    /// This graph as a JSON payload of the wire format; see [`to_json`].
892    ///
893    /// # Errors
894    ///
895    /// [`WireError::NonFinite`] if the graph holds a NaN or an infinity.
896    pub fn to_json(&self) -> Result<String, WireError> {
897        to_json(self)
898    }
899
900    /// A graph read from a JSON payload of the wire format; see
901    /// [`from_json`].
902    ///
903    /// # Errors
904    ///
905    /// A named [`WireError`] for every payload that cannot be read whole.
906    pub fn from_json(text: &str) -> Result<Self, WireError> {
907        from_json(text)
908    }
909
910    /// This graph as a CBOR payload of the wire format; see [`to_cbor`].
911    ///
912    /// # Errors
913    ///
914    /// [`WireError::NonFinite`] if the graph holds a NaN or an infinity.
915    pub fn to_cbor(&self) -> Result<Vec<u8>, WireError> {
916        to_cbor(self)
917    }
918
919    /// A graph read from a CBOR payload of the wire format; see
920    /// [`from_cbor`].
921    ///
922    /// # Errors
923    ///
924    /// A named [`WireError`] for every payload that cannot be read whole.
925    pub fn from_cbor(bytes: &[u8]) -> Result<Self, WireError> {
926        from_cbor(bytes)
927    }
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    #[test]
935    fn versions_parse_strictly() {
936        assert_eq!(FormatVersion::parse("1.0"), Some(FormatVersion::new(1, 0)));
937        assert_eq!(
938            FormatVersion::parse("12.34"),
939            Some(FormatVersion::new(12, 34))
940        );
941        for bad in ["1", "1.", ".0", "1.0.0", "+1.0", "1.-0", "v1.0", " 1.0", ""] {
942            assert_eq!(FormatVersion::parse(bad), None, "{bad:?}");
943        }
944    }
945
946    #[test]
947    fn a_reader_reads_its_own_major_up_to_its_own_minor() {
948        let reader = FormatVersion::new(1, 2);
949        assert!(reader.reads(FormatVersion::new(1, 0)));
950        assert!(reader.reads(FormatVersion::new(1, 2)));
951        assert!(!reader.reads(FormatVersion::new(1, 3)));
952        assert!(!reader.reads(FormatVersion::new(2, 0)));
953        assert!(!reader.reads(FormatVersion::new(0, 9)));
954    }
955
956    #[test]
957    fn decoder_messages_name_the_unknown_kind_or_field() {
958        let version = FORMAT_VERSION;
959        assert_eq!(
960            decode_error(
961                "unknown variant `Hyperboloid`, expected one of `Plane`, `Cylinder` at line 1 column 9"
962                    .to_owned(),
963                version
964            ),
965            WireError::UnknownKind {
966                kind: "Hyperboloid".to_owned(),
967                version
968            }
969        );
970        assert_eq!(
971            decode_error(
972                "unknown field `colour`, expected `radius`".to_owned(),
973                version
974            ),
975            WireError::UnknownField {
976                field: "colour".to_owned(),
977                version
978            }
979        );
980        assert_eq!(
981            decode_error(
982                "unknown variant `A`b`, there are no variants".to_owned(),
983                version
984            ),
985            WireError::UnknownKind {
986                kind: "A`b".to_owned(),
987                version
988            }
989        );
990        assert!(matches!(
991            decode_error("invalid type: string, expected f64".to_owned(), version),
992            WireError::Malformed { .. }
993        ));
994    }
995}