1use 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
85pub const FORMAT_NAME: &str = "axiolid-geometry-graph";
87
88pub const FORMAT_VERSION: FormatVersion = FormatVersion::new(1, 5);
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct FormatVersion {
97 pub major: u32,
99 pub minor: u32,
101}
102
103impl FormatVersion {
104 #[must_use]
106 pub const fn new(major: u32, minor: u32) -> Self {
107 Self { major, minor }
108 }
109
110 #[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 #[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#[non_exhaustive]
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum WireError {
142 UnknownFormat {
144 found: Option<String>,
146 },
147 MissingVersion,
149 UnsupportedVersion {
152 found: String,
154 supported: FormatVersion,
156 },
157 UnknownKind {
160 kind: String,
162 version: FormatVersion,
164 },
165 UnknownField {
167 field: String,
169 version: FormatVersion,
171 },
172 NonFinite {
174 path: String,
177 },
178 InvalidGraph {
181 node: Option<usize>,
183 error: GraphError,
185 },
186 Malformed {
189 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#[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
285fn 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
331fn 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
347fn 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
360fn 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
372fn 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
387const BASE_VERSION: FormatVersion = FormatVersion::new(1, 0);
390
391const SEAM_WINDOW_SINCE: FormatVersion = FormatVersion::new(1, 1);
393
394const RUN_SEAMS_SINCE: FormatVersion = FormatVersion::new(1, 2);
397
398const BASIS_END_SINCE: FormatVersion = FormatVersion::new(1, 3);
400
401const ALONG_Z_SINCE: FormatVersion = FormatVersion::new(1, 4);
404
405const RULED_SINCE: FormatVersion = FormatVersion::new(1, 5);
407
408fn 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
435fn 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
475fn uses_seam_window(node: &GeometryNode) -> bool {
479 oriented_stations(node)
480 .iter()
481 .any(|station| station.seam_window.to_bits() != 0)
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487enum RunSeams {
488 Kind(&'static str),
490 Field(&'static str),
492}
493
494fn 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
558fn 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
585fn 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
633fn uses_basis_end(node: &GeometryNode) -> bool {
637 run_stations(node)
638 .iter()
639 .any(|station| station.basis_end.is_some())
640}
641
642fn is_along_z(node: &GeometryNode) -> bool {
645 matches!(
646 node,
647 GeometryNode::SolidOperation(SolidOperation::BoundedHalfSpaceAlongZ { .. })
648 )
649}
650
651fn is_ruled(node: &GeometryNode) -> bool {
653 matches!(
654 node,
655 GeometryNode::SurfaceRelation(SurfaceRelation::Ruled { .. })
656 )
657}
658
659fn 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
710fn 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
733pub fn to_json(graph: &GeometryGraph) -> Result<String, WireError> {
739 checked_value(graph)?;
740 serde_json::to_string(&envelope(graph)).map_err(malformed)
741}
742
743pub 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
760pub 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
772pub 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
797const SELF_DESCRIBE: u64 = 55799;
800
801fn integer_is_exact(integer: i128) -> bool {
804 integer as f64 as i128 == integer
805}
806
807fn 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 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 pub fn to_json(&self) -> Result<String, WireError> {
897 to_json(self)
898 }
899
900 pub fn from_json(text: &str) -> Result<Self, WireError> {
907 from_json(text)
908 }
909
910 pub fn to_cbor(&self) -> Result<Vec<u8>, WireError> {
916 to_cbor(self)
917 }
918
919 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}