1pub mod v2 {
13 include!(concat!(env!("OUT_DIR"), "/ridl.ir.v2.rs"));
18 #[expect(clippy::useless_borrows_in_formatting)]
32 mod serde_impls {
33 use super::*;
34
35 include!(concat!(env!("OUT_DIR"), "/ridl.ir.v2.serde.rs"));
36 }
37
38 static DESCRIPTOR_POOL: std::sync::LazyLock<prost_reflect::DescriptorPool> =
46 std::sync::LazyLock::new(|| {
47 prost_reflect::DescriptorPool::decode(
48 include_bytes!(concat!(env!("OUT_DIR"), "/ir_descriptor.binpb")).as_slice(),
49 )
50 .expect("the embedded descriptor set decodes: build.rs wrote it from the schema compilation that generated these types")
51 });
52
53 fn descriptor(name: &str) -> prost_reflect::MessageDescriptor {
56 DESCRIPTOR_POOL
57 .get_message_by_name(name)
58 .unwrap_or_else(|| panic!("{name} is declared by the compiled schema"))
59 }
60
61 pub(crate) fn package_descriptor() -> prost_reflect::MessageDescriptor {
64 descriptor("ridl.ir.v2.Package")
65 }
66
67 pub(crate) fn system_descriptor() -> prost_reflect::MessageDescriptor {
70 descriptor("ridl.ir.v2.System")
71 }
72
73 pub(crate) fn codegen_request_descriptor() -> prost_reflect::MessageDescriptor {
76 descriptor("ridl.codegen.v1.CodegenRequest")
77 }
78
79 pub(crate) fn codegen_model_descriptor() -> prost_reflect::MessageDescriptor {
83 descriptor("ridl.codegen.v1.Model")
84 }
85
86 fn transcode<M: prost::Message>(
93 descriptor: prost_reflect::MessageDescriptor,
94 message: &M,
95 ) -> Result<prost_reflect::DynamicMessage, prost::DecodeError> {
96 let mut dynamic = prost_reflect::DynamicMessage::new(descriptor);
97 dynamic.transcode_from(message)?;
98 Ok(dynamic)
99 }
100
101 pub fn fallible_transport_identity(
107 interface: &str,
108 ordinal: u32,
109 fallible: &FallibleType,
110 ) -> String {
111 format!(
112 "{interface}#{ordinal}:{ok}|{err}",
113 ok = fallible.ok,
114 err = fallible.err
115 )
116 }
117
118 #[derive(Debug)]
125 pub enum SerializeError {
126 Json(serde_json::Error),
132 Text(prost::DecodeError),
137 Class(crate::class::InteractionClassError),
140 }
141
142 impl std::fmt::Display for SerializeError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 Self::Json(source) => write!(
146 f,
147 "cannot render the package as canonical protobuf JSON: {source}; the known \
148 cause is an enum field holding a discriminant outside the schema"
149 ),
150 Self::Text(source) => write!(
151 f,
152 "cannot render the package as prototext: {source}; the known cause \
153 is composite nesting deeper than the transcoding decoder's recursion limit"
154 ),
155 Self::Class(source) => write!(f, "{source}"),
156 }
157 }
158 }
159
160 impl std::error::Error for SerializeError {
161 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
162 match self {
163 Self::Json(source) => Some(source),
164 Self::Text(source) => Some(source),
165 Self::Class(source) => Some(source),
166 }
167 }
168 }
169
170 pub fn to_json_pretty(package: &Package) -> Result<String, SerializeError> {
191 crate::class::check_package(package).map_err(SerializeError::Class)?;
192 render_json(package)
193 }
194
195 pub fn system_to_json_pretty(system: &System) -> Result<String, SerializeError> {
199 render_json(system)
200 }
201
202 pub(crate) fn render_json<M: serde::Serialize>(message: &M) -> Result<String, SerializeError> {
206 let mut buf = Vec::new();
207 let mut serializer = serde_json::Serializer::pretty(&mut buf);
208 serde::Serialize::serialize(message, &mut serializer).map_err(SerializeError::Json)?;
209 Ok(String::from_utf8(buf).expect("serde_json emits UTF-8"))
210 }
211
212 pub(crate) const MAX_JSON_NESTING: usize = 1_000;
231
232 const JSON_PARSE_STACK: usize = 16 * 1024 * 1024;
237
238 pub(crate) fn max_json_nesting(text: &str) -> usize {
246 let mut depth = 0usize;
247 let mut deepest = 0usize;
248 let mut in_string = false;
249 let mut escaped = false;
250 for byte in text.bytes() {
251 if in_string {
252 if escaped {
253 escaped = false;
254 } else if byte == b'\\' {
255 escaped = true;
256 } else if byte == b'"' {
257 in_string = false;
258 }
259 } else {
260 match byte {
261 b'"' => in_string = true,
262 b'{' | b'[' => {
263 depth += 1;
264 deepest = deepest.max(depth);
265 }
266 b'}' | b']' => depth = depth.saturating_sub(1),
267 _ => {}
268 }
269 }
270 }
271 deepest
272 }
273
274 pub fn from_json(text: &str) -> Result<Package, serde_json::Error> {
300 let package: Package = read_json(text)?;
301 crate::class::check_package(&package).map_err(serde::de::Error::custom)?;
302 Ok(package)
303 }
304
305 #[derive(Debug)]
307 pub enum LoadError {
308 Io(std::io::Error),
310 Parse(String),
312 }
313
314 impl std::fmt::Display for LoadError {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 match self {
317 LoadError::Io(err) => write!(f, "cannot read the IR snapshot: {err}"),
318 LoadError::Parse(err) => {
319 write!(f, "the IR snapshot is not valid IR v2 JSON: {err}")
320 }
321 }
322 }
323 }
324
325 impl std::error::Error for LoadError {}
326
327 pub fn load_ir_json(path: &std::path::Path) -> Result<Package, LoadError> {
331 let text = std::fs::read_to_string(path).map_err(LoadError::Io)?;
332 from_json(&text).map_err(|err| LoadError::Parse(err.to_string()))
333 }
334
335 pub fn system_from_json(text: &str) -> Result<System, serde_json::Error> {
339 read_json(text)
340 }
341
342 pub(crate) fn read_json<M>(text: &str) -> Result<M, serde_json::Error>
345 where
346 M: serde::de::DeserializeOwned + Send,
347 {
348 check_nesting(text)?;
349 on_parse_stack(|| parse_json(text))
350 }
351
352 pub(crate) fn read_json_ignoring_unknown<M>(
370 descriptor: prost_reflect::MessageDescriptor,
371 text: &str,
372 ) -> Result<M, serde_json::Error>
373 where
374 M: serde::de::DeserializeOwned + Send,
375 {
376 use serde::de::DeserializeSeed as _;
377 check_nesting(text)?;
378 on_parse_stack(|| {
379 let mut deserializer = serde_json::Deserializer::from_str(text);
380 deserializer.disable_recursion_limit();
381 let known = JsonShape::Message(descriptor).deserialize(&mut deserializer)?;
382 deserializer.end()?;
383 serde::Deserialize::deserialize(known)
384 })
385 }
386
387 #[derive(Clone)]
390 enum JsonShape {
391 Message(prost_reflect::MessageDescriptor),
393 List(Box<JsonShape>),
395 Map(Box<JsonShape>),
397 Opaque,
399 }
400
401 impl JsonShape {
402 fn of(field: &prost_reflect::FieldDescriptor) -> JsonShape {
404 use prost_reflect::Kind;
405 if field.is_map() {
406 let Kind::Message(entry) = field.kind() else {
407 return JsonShape::Opaque;
408 };
409 return JsonShape::Map(Box::new(JsonShape::of_kind(
410 &entry.map_entry_value_field().kind(),
411 )));
412 }
413 let item = JsonShape::of_kind(&field.kind());
414 if field.is_list() {
415 JsonShape::List(Box::new(item))
416 } else {
417 item
418 }
419 }
420
421 fn of_kind(kind: &prost_reflect::Kind) -> JsonShape {
422 match kind {
423 prost_reflect::Kind::Message(message)
424 if !message.full_name().starts_with("google.protobuf.") =>
425 {
426 JsonShape::Message(message.clone())
427 }
428 _ => JsonShape::Opaque,
429 }
430 }
431 }
432
433 impl<'de> serde::de::DeserializeSeed<'de> for JsonShape {
434 type Value = serde_json::Value;
435
436 fn deserialize<D: serde::Deserializer<'de>>(
437 self,
438 deserializer: D,
439 ) -> Result<Self::Value, D::Error> {
440 match self {
441 JsonShape::Opaque => serde::Deserialize::deserialize(deserializer),
442 shape => deserializer.deserialize_any(ShapeVisitor(shape)),
443 }
444 }
445 }
446
447 struct ShapeVisitor(JsonShape);
448
449 impl<'de> serde::de::Visitor<'de> for ShapeVisitor {
450 type Value = serde_json::Value;
451
452 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 formatter.write_str(match self.0 {
454 JsonShape::List(_) => "an array",
455 _ => "an object",
456 })
457 }
458
459 fn visit_unit<E>(self) -> Result<Self::Value, E> {
461 Ok(serde_json::Value::Null)
462 }
463
464 fn visit_seq<A: serde::de::SeqAccess<'de>>(
465 self,
466 mut seq: A,
467 ) -> Result<Self::Value, A::Error> {
468 let JsonShape::List(item) = self.0 else {
469 return Err(serde::de::Error::invalid_type(
470 serde::de::Unexpected::Seq,
471 &self,
472 ));
473 };
474 let mut items = Vec::new();
475 while let Some(value) = seq.next_element_seed((*item).clone())? {
476 items.push(value);
477 }
478 Ok(serde_json::Value::Array(items))
479 }
480
481 fn visit_map<A: serde::de::MapAccess<'de>>(
482 self,
483 mut map: A,
484 ) -> Result<Self::Value, A::Error> {
485 use serde::de::Error as _;
486 let mut out = serde_json::Map::new();
487 match &self.0 {
488 JsonShape::Message(descriptor) => {
489 let mut seen = std::collections::HashSet::new();
490 while let Some(key) = map.next_key::<String>()? {
491 let field = descriptor
492 .get_field_by_json_name(&key)
493 .or_else(|| descriptor.get_field_by_name(&key));
494 match field {
495 Some(field) => {
496 if !seen.insert(field.number()) {
497 return Err(A::Error::custom(format!(
498 "duplicate field `{key}`"
499 )));
500 }
501 let value = map.next_value_seed(JsonShape::of(&field))?;
502 out.insert(key, value);
503 }
504 None => {
505 map.next_value::<serde::de::IgnoredAny>()?;
506 }
507 }
508 }
509 }
510 JsonShape::Map(value_shape) => {
511 while let Some(key) = map.next_key::<String>()? {
512 let value = map.next_value_seed((**value_shape).clone())?;
513 if out.insert(key.clone(), value).is_some() {
514 return Err(A::Error::custom(format!("duplicate key `{key}`")));
515 }
516 }
517 }
518 JsonShape::List(_) | JsonShape::Opaque => {
519 return Err(A::Error::invalid_type(serde::de::Unexpected::Map, &self));
520 }
521 }
522 Ok(serde_json::Value::Object(out))
523 }
524 }
525
526 fn check_nesting(text: &str) -> Result<(), serde_json::Error> {
528 if max_json_nesting(text) > MAX_JSON_NESTING {
529 return Err(<serde_json::Error as serde::de::Error>::custom(format!(
530 "the input nests deeper than {MAX_JSON_NESTING} JSON levels, the ceiling this \
531 reader enforces (ADR-0014 decision 14); real IR nests orders of magnitude \
532 shallower"
533 )));
534 }
535 Ok(())
536 }
537
538 fn on_parse_stack<R: Send>(parse: impl FnOnce() -> R + Send) -> R {
541 if cfg!(target_family = "wasm") {
542 parse()
551 } else {
552 std::thread::scope(|scope| {
553 let handle = std::thread::Builder::new()
554 .stack_size(JSON_PARSE_STACK)
555 .spawn_scoped(scope, parse)
556 .expect("the JSON parse thread spawns");
557 match handle.join() {
558 Ok(result) => result,
559 Err(payload) => std::panic::resume_unwind(payload),
560 }
561 })
562 }
563 }
564
565 fn parse_json<M: serde::de::DeserializeOwned>(text: &str) -> Result<M, serde_json::Error> {
570 let mut deserializer = serde_json::Deserializer::from_str(text);
571 deserializer.disable_recursion_limit();
572 let message: M = serde::Deserialize::deserialize(&mut deserializer)?;
573 deserializer.end()?;
574 Ok(message)
575 }
576
577 pub fn to_text_format(package: &Package) -> Result<String, SerializeError> {
591 crate::class::check_package(package).map_err(SerializeError::Class)?;
592 render_text_for(package_descriptor(), package)
593 }
594
595 pub fn system_to_text_format(system: &System) -> Result<String, SerializeError> {
599 render_text_for(system_descriptor(), system)
600 }
601
602 pub(crate) fn render_text_for<M: prost::Message>(
605 descriptor: prost_reflect::MessageDescriptor,
606 message: &M,
607 ) -> Result<String, SerializeError> {
608 let dynamic = transcode(descriptor, message).map_err(SerializeError::Text)?;
609 Ok(dynamic.to_text_format_with_options(
610 &prost_reflect::text_format::FormatOptions::new()
611 .pretty(true)
612 .skip_default_fields(false)
613 .print_message_fields_in_index_order(true),
614 ))
615 }
616
617 #[cfg(test)]
623 #[derive(Debug)]
624 pub(crate) enum TextFormatError {
625 Parse(prost_reflect::text_format::ParseError),
627 Transcode(prost::DecodeError),
629 }
630
631 #[cfg(test)]
632 impl std::fmt::Display for TextFormatError {
633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634 match self {
635 Self::Parse(source) => {
636 write!(f, "cannot parse the text as an IR package: {source}")
637 }
638 Self::Transcode(source) => write!(
639 f,
640 "cannot rebuild the parsed prototext as a package: {source}; the known cause \
641 is composite nesting deeper than the transcoding decoder's recursion limit"
642 ),
643 }
644 }
645 }
646
647 #[cfg(test)]
648 impl std::error::Error for TextFormatError {
649 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
650 match self {
651 Self::Parse(source) => Some(source),
652 Self::Transcode(source) => Some(source),
653 }
654 }
655 }
656
657 #[cfg(test)]
678 pub(crate) fn from_text_format(text: &str) -> Result<Package, TextFormatError> {
679 parse_text(package_descriptor(), text)
680 }
681
682 #[cfg(test)]
686 pub(crate) fn system_from_text_format(text: &str) -> Result<System, TextFormatError> {
687 parse_text(system_descriptor(), text)
688 }
689
690 #[cfg(test)]
691 fn parse_text<M: prost::Message + Default>(
692 descriptor: prost_reflect::MessageDescriptor,
693 text: &str,
694 ) -> Result<M, TextFormatError> {
695 let dynamic = prost_reflect::DynamicMessage::parse_text_format(descriptor, text)
696 .map_err(TextFormatError::Parse)?;
697 dynamic.transcode_to().map_err(TextFormatError::Transcode)
698 }
699
700 pub fn to_binary(package: &Package) -> Vec<u8> {
707 prost::Message::encode_to_vec(package)
708 }
709
710 pub fn from_binary(bytes: &[u8]) -> Result<Package, prost::DecodeError> {
713 let package: Package = prost::Message::decode(bytes)?;
714 crate::class::check_package(&package).map_err(|err| {
715 #[allow(deprecated)]
718 prost::DecodeError::new(err.to_string())
719 })?;
720 Ok(package)
721 }
722
723 pub fn system_to_binary(system: &System) -> Vec<u8> {
726 prost::Message::encode_to_vec(system)
727 }
728
729 pub fn system_from_binary(bytes: &[u8]) -> Result<System, prost::DecodeError> {
732 prost::Message::decode(bytes)
733 }
734
735 fn qualified(package: &str, name: &str) -> String {
740 if package.is_empty() {
741 name.to_string()
742 } else {
743 format!("{package}.{name}")
744 }
745 }
746
747 impl System {
748 pub fn qualified_name(&self) -> String {
751 qualified(&self.package, &self.name)
752 }
753 }
754
755 impl Component {
756 pub fn qualified_name(&self) -> String {
759 qualified(&self.package, &self.name)
760 }
761 }
762
763 impl Distribution {
764 pub fn qualified_name(&self) -> String {
767 qualified(&self.package, &self.name)
768 }
769 }
770
771 #[derive(Debug, Clone, Copy, PartialEq)]
802 pub struct InterfaceShape<'a> {
803 pub name: &'a str,
807 pub interface: &'a Interface,
809 pub service: Option<&'a Service>,
812 }
813
814 impl InterfaceShape<'_> {
815 pub fn visibility(&self) -> i32 {
820 match self.service {
821 Some(service) => service.visibility,
822 None => self.interface.visibility,
823 }
824 }
825
826 pub fn is_inline(&self) -> bool {
828 self.service.is_some()
829 }
830 }
831
832 impl Package {
833 pub fn shapes(&self) -> impl Iterator<Item = InterfaceShape<'_>> {
848 let named = self.interfaces.iter().map(|interface| InterfaceShape {
849 name: &interface.name,
850 interface,
851 service: None,
852 });
853 let inline = self.services.iter().flat_map(|service| {
854 service
855 .shapes
856 .iter()
857 .filter_map(move |slot| match slot.kind.as_ref()? {
858 service_shape::Kind::Inline(interface) => Some(InterfaceShape {
859 name: &service.name,
860 interface,
861 service: Some(service),
862 }),
863 service_shape::Kind::InterfaceRef(_) => None,
864 })
865 });
866 named.chain(inline)
867 }
868 }
869
870 pub fn unit_of(package: &Package) -> &str {
875 if package.unit.is_empty() {
876 &package.name
877 } else {
878 &package.unit
879 }
880 }
881
882 pub fn packages_of_unit<'a>(
884 unit: &str,
885 packages: &'a [Package],
886 ) -> impl Iterator<Item = &'a Package> {
887 select_unit(unit, packages.iter())
888 }
889
890 pub fn members_of_unit<'a>(
893 unit: &str,
894 packages: &[&'a Package],
895 ) -> impl Iterator<Item = &'a Package> {
896 select_unit(unit, packages.iter().copied())
897 }
898
899 fn select_unit<'a>(
902 unit: &str,
903 packages: impl Iterator<Item = &'a Package>,
904 ) -> impl Iterator<Item = &'a Package> {
905 let unit = unit.to_owned();
906 packages.filter(move |p| unit_of(p) == unit)
907 }
908
909 pub fn relative_name(unit: &str, package: &str, name: &str) -> String {
913 if package == unit {
914 return name.to_owned();
915 }
916 match package
920 .strip_prefix(unit)
921 .and_then(|rest| rest.strip_prefix('.'))
922 {
923 Some(below) => format!("{below}.{name}"),
924 None => format!("{package}.{name}"),
925 }
926 }
927
928 impl Package {
929 pub fn catalog_name(&self, shape: &InterfaceShape<'_>) -> String {
933 if shape.is_inline() {
934 shape.name.to_owned()
935 } else {
936 relative_name(unit_of(self), &self.name, shape.name)
937 }
938 }
939 }
940
941 pub fn unit_retired(unit: &str, packages: &[&Package]) -> Vec<RetiredInterface> {
948 let mut seen: Vec<&str> = Vec::new();
949 let mut retired: Vec<RetiredInterface> = members_of_unit(unit, packages)
950 .filter(|package| {
951 let first = !seen.contains(&package.name.as_str());
952 if first {
953 seen.push(&package.name);
954 }
955 first
956 })
957 .flat_map(|package| package.retired.iter().cloned())
958 .collect();
959 retired.sort_by_key(|entry| entry.number);
960 retired
961 }
962
963 pub fn referenced_packages(package: &Package) -> std::collections::BTreeSet<String> {
976 let mut found = std::collections::BTreeSet::new();
977 for decl in &package.decls {
978 walk_decl(decl, &mut found);
979 }
980 for interface in &package.interfaces {
981 for interaction in &interface.interactions {
982 walk_decl(interaction, &mut found);
983 }
984 }
985 for service in &package.services {
986 for slot in &service.shapes {
987 match &slot.kind {
988 Some(service_shape::Kind::InterfaceRef(reference)) => {
989 qualifier(reference, &mut found);
990 }
991 Some(service_shape::Kind::Inline(interface)) => {
992 for interaction in &interface.interactions {
993 walk_decl(interaction, &mut found);
994 }
995 }
996 None => {}
997 }
998 }
999 }
1000 found
1001 }
1002
1003 fn qualifier(reference: &str, found: &mut std::collections::BTreeSet<String>) {
1006 if let Some((package, _)) = reference.rsplit_once('.') {
1007 found.insert(package.to_string());
1008 }
1009 }
1010
1011 fn walk_decl(decl: &Decl, found: &mut std::collections::BTreeSet<String>) {
1014 match &decl.kind {
1015 Some(decl::Kind::TypeDef(type_def)) => walk_type_def(type_def, found),
1016 Some(decl::Kind::ConstDef(const_def)) => {
1017 if let Some(reference) = &const_def.type_ref {
1018 qualifier(reference, found);
1019 }
1020 }
1021 Some(decl::Kind::StructDef(struct_def)) => {
1022 for member in &struct_def.members {
1023 match &member.member {
1024 Some(struct_member::Member::Field(field)) => {
1025 if let Some(field_type) = &field.r#type {
1026 walk_field_type(field_type, found);
1027 }
1028 }
1029 Some(struct_member::Member::Reserved(_)) | None => {}
1031 }
1032 }
1033 }
1034 Some(decl::Kind::EnumDef(_)) => {}
1036 Some(decl::Kind::EnumSetDef(enum_set)) => {
1037 if let Some(reference) = &enum_set.backing_enum {
1038 qualifier(reference, found);
1039 }
1040 }
1041 Some(decl::Kind::UnionDef(union_def)) => {
1042 for arm in &union_def.arms {
1043 qualifier(&arm.type_ref, found);
1044 }
1045 }
1046 Some(decl::Kind::SignalDef(signal)) => qualifier(&signal.payload, found),
1047 Some(decl::Kind::EventDef(event)) => qualifier(&event.payload, found),
1048 Some(decl::Kind::CommandDef(command)) => {
1049 for param in &command.params {
1050 if let Some(field_type) = ¶m.r#type {
1051 walk_field_type(field_type, found);
1052 }
1053 }
1054 }
1055 Some(decl::Kind::QueryDef(query)) => {
1056 for param in &query.params {
1057 if let Some(field_type) = ¶m.r#type {
1058 walk_field_type(field_type, found);
1059 }
1060 }
1061 if let Some(return_type) = &query.return_type {
1062 walk_return_type(return_type, found);
1063 }
1064 }
1065 Some(decl::Kind::FixedDef(fixed)) => {
1066 if let Some(field_type) = &fixed.payload {
1067 walk_field_type(field_type, found);
1068 }
1069 }
1070 Some(decl::Kind::ReservedSlot(_)) | None => {}
1072 }
1073 }
1074
1075 fn walk_field_type(field_type: &FieldType, found: &mut std::collections::BTreeSet<String>) {
1078 match &field_type.kind {
1079 Some(field_type::Kind::Named(reference)) => qualifier(reference, found),
1080 Some(field_type::Kind::Primitive(_)) => {}
1082 Some(field_type::Kind::InlineScalar(type_def)) => walk_type_def(type_def, found),
1083 Some(field_type::Kind::Tuple(tuple)) => {
1084 for field in &tuple.fields {
1085 if let Some(inner) = &field.r#type {
1086 walk_field_type(inner, found);
1087 }
1088 }
1089 }
1090 Some(field_type::Kind::Array(array)) => {
1091 if let Some(element) = &array.element {
1092 walk_field_type(element, found);
1093 }
1094 }
1095 Some(field_type::Kind::Map(map)) => {
1096 if let Some(key) = &map.key {
1097 walk_field_type(key, found);
1098 }
1099 if let Some(value) = &map.value {
1100 walk_field_type(value, found);
1101 }
1102 }
1103 Some(field_type::Kind::Stream(stream)) => match &stream.element {
1104 Some(stream_type::Element::Named(reference)) => qualifier(reference, found),
1105 Some(stream_type::Element::Primitive(_)) | None => {}
1107 },
1108 None => {}
1109 }
1110 }
1111
1112 fn walk_type_def(type_def: &TypeDef, found: &mut std::collections::BTreeSet<String>) {
1114 if let Some(constraint) = &type_def.constraint
1115 && let Some(reference) = &constraint.pattern_const
1116 {
1117 qualifier(reference, found);
1118 }
1119 }
1120
1121 fn walk_return_type(return_type: &ReturnType, found: &mut std::collections::BTreeSet<String>) {
1122 match &return_type.kind {
1123 Some(return_type::Kind::Value(field_type)) => walk_field_type(field_type, found),
1124 Some(return_type::Kind::Fallible(fallible)) => {
1125 qualifier(&fallible.ok, found);
1126 qualifier(&fallible.err, found);
1127 }
1128 None => {}
1129 }
1130 }
1131
1132 pub fn constraint_is_vacuous(constraint: Option<&Constraint>) -> bool {
1148 let Some(c) = constraint else { return true };
1149 c.min.is_none()
1150 && c.max.is_none()
1151 && c.step.is_none()
1152 && c.len_min.is_none()
1153 && c.len_max.is_none()
1154 && c.pattern.is_none()
1155 && c.pattern_const.is_none()
1156 }
1157}
1158
1159pub mod catalog_hash;
1160pub mod class;
1161pub mod codegen;
1162pub mod name;
1163pub mod projection;
1164pub mod rules;
1165pub mod zero;
1166
1167#[cfg(test)]
1168mod v2_round_trip {
1169 use crate::v2;
1170
1171 fn interaction(name: &str, ordinal: u32, kind: v2::decl::Kind) -> v2::Decl {
1176 v2::Decl {
1177 limit: None,
1178 name: name.to_string(),
1179 visibility: v2::Visibility::Unspecified as i32,
1180 is_error: false,
1181 doc: String::new(),
1182 labels: Vec::new(),
1183 deprecated: None,
1184 ordinal,
1185 kind: Some(kind),
1186 links: Vec::new(),
1187 see: Vec::new(),
1188 since: Vec::new(),
1189 }
1190 }
1191
1192 fn named_type(name: &str) -> v2::FieldType {
1193 v2::FieldType {
1194 optional: false,
1195 kind: Some(v2::field_type::Kind::Named(name.to_string())),
1196 }
1197 }
1198
1199 fn stream_of(element: v2::stream_type::Element) -> v2::FieldType {
1200 v2::FieldType {
1201 optional: false,
1202 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
1203 element: Some(element),
1204 })),
1205 }
1206 }
1207
1208 fn fixture() -> v2::Package {
1214 let speed = v2::SignalDef {
1217 payload: "Speed".to_string(),
1218 declared_init: None,
1219 init: Some(v2::InitValue {
1220 derivable: true,
1221 value: Some("0.0".to_string()),
1222 }),
1223 timing: Some(v2::Timing {
1224 mode: v2::TimingMode::StrictPeriodic as i32,
1225 min_us: Some("10000".to_string()),
1226 max_us: Some("10000".to_string()),
1227 default_applied: false,
1228 }),
1229 family: v2::Family::Dispatch as i32,
1230 };
1231
1232 let door_opened = v2::EventDef {
1235 payload: "DoorEvent".to_string(),
1236 timing: Some(v2::Timing {
1237 mode: v2::TimingMode::Range as i32,
1238 min_us: Some("20000".to_string()),
1239 max_us: Some("500000".to_string()),
1240 default_applied: true,
1241 }),
1242 family: v2::Family::Dispatch as i32,
1243 };
1244
1245 let set_target = v2::CommandDef {
1247 params: vec![v2::Param {
1248 name: "target".to_string(),
1249 r#type: Some(named_type("Speed")),
1250 doc: String::new(),
1251 links: Vec::new(),
1252 see: Vec::new(),
1253 since: Vec::new(),
1254 }],
1255 contracts: vec![v2::Contract {
1256 kind: v2::ContractKind::Require as i32,
1257 source: "target >= speed".to_string(),
1258 signal_refs: vec!["speed".to_string()],
1259 param_refs: vec!["target".to_string()],
1260 uses_result: false,
1261 observer_id: "VehicleStatus.setTarget.require[0]".to_string(),
1262 }],
1263 timing: None,
1264 family: v2::Family::Dispatch as i32,
1265 shape: v2::Shape::None as i32,
1266 };
1267
1268 let fetch_faults = v2::QueryDef {
1271 params: vec![v2::Param {
1272 name: "page".to_string(),
1273 r#type: Some(named_type("PageSpec")),
1274 doc: String::new(),
1275 links: Vec::new(),
1276 see: Vec::new(),
1277 since: Vec::new(),
1278 }],
1279 return_type: Some(v2::ReturnType {
1280 kind: Some(v2::return_type::Kind::Fallible(v2::FallibleType {
1281 ok: "FaultPage".to_string(),
1282 err: "DiagError".to_string(),
1283 })),
1284 }),
1285 contracts: vec![v2::Contract {
1286 kind: v2::ContractKind::Ensure as i32,
1287 source: "result.count <= page.limit".to_string(),
1288 signal_refs: Vec::new(),
1289 param_refs: vec!["page".to_string()],
1290 uses_result: true,
1291 observer_id: "VehicleStatus.fetchFaults.ensure[0]".to_string(),
1292 }],
1293 timing: None,
1294 family: v2::Family::Dispatch as i32,
1295 };
1296
1297 let vin = v2::FixedDef {
1299 payload: Some(named_type("Vin")),
1300 };
1301
1302 let vehicle_status = v2::Interface {
1303 name: "VehicleStatus".to_string(),
1304 visibility: v2::Visibility::Public as i32,
1305 doc: "Vehicle status contract".to_string(),
1306 labels: Vec::new(),
1307 deprecated: None,
1308 interactions: vec![
1309 interaction("speed", 1, v2::decl::Kind::SignalDef(speed)),
1310 interaction("doorOpened", 2, v2::decl::Kind::EventDef(door_opened)),
1311 v2::Decl {
1314 ordinal: 3,
1315 kind: Some(v2::decl::Kind::ReservedSlot(v2::Reserved {
1316 ordinal: 3,
1317 name: Some("legacyMode".to_string()),
1318 value: None,
1319 })),
1320 ..interaction("", 3, v2::decl::Kind::ReservedSlot(v2::Reserved::default()))
1321 },
1322 interaction("setTarget", 4, v2::decl::Kind::CommandDef(set_target)),
1323 interaction("fetchFaults", 5, v2::decl::Kind::QueryDef(fetch_faults)),
1324 interaction("vin", 6, v2::decl::Kind::FixedDef(vin)),
1325 ],
1326 number: 0,
1327 provisional: false,
1328 links: Vec::new(),
1329 see: Vec::new(),
1330 since: Vec::new(),
1331 };
1332
1333 let tail_logs = v2::QueryDef {
1336 params: vec![v2::Param {
1337 name: "pattern".to_string(),
1338 r#type: Some(stream_of(v2::stream_type::Element::Primitive(
1339 v2::PrimitiveType::String as i32,
1340 ))),
1341 doc: String::new(),
1342 links: Vec::new(),
1343 see: Vec::new(),
1344 since: Vec::new(),
1345 }],
1346 return_type: Some(v2::ReturnType {
1347 kind: Some(v2::return_type::Kind::Value(stream_of(
1348 v2::stream_type::Element::Named("LogLine".to_string()),
1349 ))),
1350 }),
1351 contracts: Vec::new(),
1352 timing: None,
1353 family: v2::Family::Dispatch as i32,
1354 };
1355
1356 let status_service = v2::Service {
1359 name: "veh.adas.status".to_string(),
1360 visibility: v2::Visibility::Public as i32,
1361 doc: String::new(),
1362 labels: Vec::new(),
1363 deprecated: None,
1364 shapes: vec![v2::ServiceShape {
1365 kind: Some(v2::service_shape::Kind::InterfaceRef(
1366 "VehicleStatus".to_string(),
1367 )),
1368 }],
1369 links: Vec::new(),
1370 see: Vec::new(),
1371 since: Vec::new(),
1372 };
1373 let logs_service = v2::Service {
1376 name: "veh.adas.logs".to_string(),
1377 visibility: v2::Visibility::Public as i32,
1378 doc: String::new(),
1379 labels: Vec::new(),
1380 deprecated: None,
1381 shapes: vec![v2::ServiceShape {
1382 kind: Some(v2::service_shape::Kind::Inline(v2::Interface {
1383 name: String::new(),
1384 visibility: v2::Visibility::Unspecified as i32,
1385 doc: String::new(),
1386 labels: Vec::new(),
1387 deprecated: None,
1388 interactions: vec![interaction(
1389 "tailLogs",
1390 1,
1391 v2::decl::Kind::QueryDef(tail_logs),
1392 )],
1393 number: 0,
1394 provisional: false,
1395 links: Vec::new(),
1396 see: Vec::new(),
1397 since: Vec::new(),
1398 })),
1399 }],
1400 links: Vec::new(),
1401 see: Vec::new(),
1402 since: Vec::new(),
1403 };
1404
1405 v2::Package {
1406 name: "veh.adas".to_string(),
1407 decls: vec![v2::Decl {
1411 limit: None,
1412 name: "Speed".to_string(),
1413 visibility: v2::Visibility::Public as i32,
1414 is_error: false,
1415 doc: String::new(),
1416 labels: Vec::new(),
1417 deprecated: None,
1418 ordinal: 0,
1419 kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
1420 backing: Some(v2::Backing {
1421 kind: Some(v2::backing::Kind::Unit("km/h".to_string())),
1422 }),
1423 constraint: None,
1424 declared_init: None,
1425 init: None,
1426 width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
1427 })),
1428 links: Vec::new(),
1429 see: Vec::new(),
1430 since: Vec::new(),
1431 }],
1432 interfaces: vec![vehicle_status],
1433 services: vec![status_service, logs_service],
1434 retired: Vec::new(),
1435 unit: "veh.adas".to_string(),
1436 }
1437 }
1438
1439 fn vocabulary_fixture() -> v2::Package {
1446 fn decl(name: &str, kind: v2::decl::Kind) -> v2::Decl {
1447 v2::Decl {
1448 limit: None,
1449 name: name.to_string(),
1450 visibility: v2::Visibility::Public as i32,
1451 is_error: false,
1452 doc: String::new(),
1453 labels: Vec::new(),
1454 deprecated: None,
1455 ordinal: 0,
1456 kind: Some(kind),
1457 links: Vec::new(),
1458 see: Vec::new(),
1459 since: Vec::new(),
1460 }
1461 }
1462
1463 fn field(name: &str, ordinal: u32, field_type: v2::FieldType) -> v2::Field {
1464 v2::Field {
1465 name: name.to_string(),
1466 ordinal,
1467 r#type: Some(field_type),
1468 declared_init: None,
1469 init: None,
1470 doc: String::new(),
1471 labels: Vec::new(),
1472 deprecated: None,
1473 links: Vec::new(),
1474 see: Vec::new(),
1475 since: Vec::new(),
1476 }
1477 }
1478
1479 let max_retry = v2::ConstDef {
1481 type_ref: Some("integer".to_string()),
1482 value: "24".to_string(),
1483 regex: None,
1484 };
1485
1486 let gear = v2::EnumDef {
1489 values: vec![
1490 v2::EnumValue {
1491 name: "PARK".to_string(),
1492 value: 1,
1493 doc: String::new(),
1494 links: Vec::new(),
1495 see: Vec::new(),
1496 since: Vec::new(),
1497 },
1498 v2::EnumValue {
1499 name: "DRIVE".to_string(),
1500 value: 2,
1501 doc: String::new(),
1502 links: Vec::new(),
1503 see: Vec::new(),
1504 since: Vec::new(),
1505 },
1506 ],
1507 reserved: vec![v2::Reserved {
1508 ordinal: 0,
1509 name: None,
1510 value: Some(7),
1511 }],
1512 };
1513
1514 let warnings = v2::EnumSetDef {
1517 backing_enum: None,
1518 bits: vec![
1519 v2::EnumValue {
1520 name: "LOW_FUEL".to_string(),
1521 value: 0,
1522 doc: String::new(),
1523 links: Vec::new(),
1524 see: Vec::new(),
1525 since: Vec::new(),
1526 },
1527 v2::EnumValue {
1528 name: "ICE_RISK".to_string(),
1529 value: 33,
1530 doc: String::new(),
1531 links: Vec::new(),
1532 see: Vec::new(),
1533 since: Vec::new(),
1534 },
1535 ],
1536 width: v2::IntWidth::U64 as i32,
1537 };
1538
1539 let plate_text = v2::TypeDef {
1542 backing: Some(v2::Backing {
1543 kind: Some(v2::backing::Kind::Primitive(
1544 v2::PrimitiveType::String as i32,
1545 )),
1546 }),
1547 constraint: Some(v2::Constraint {
1548 min: None,
1549 max: None,
1550 step: None,
1551 len_min: Some(1),
1552 len_max: Some(86),
1553 pattern: None,
1554 pattern_const: None,
1555 }),
1556 declared_init: None,
1557 init: None,
1558 width: None,
1559 };
1560
1561 let sample = v2::UnionDef {
1563 arms: vec![
1564 v2::UnionArm {
1565 name: "speed".to_string(),
1566 ordinal: 1,
1567 type_ref: "Speed".to_string(),
1568 doc: String::new(),
1569 links: Vec::new(),
1570 see: Vec::new(),
1571 since: Vec::new(),
1572 },
1573 v2::UnionArm {
1574 name: "gear".to_string(),
1575 ordinal: 2,
1576 type_ref: "Gear".to_string(),
1577 doc: String::new(),
1578 links: Vec::new(),
1579 see: Vec::new(),
1580 since: Vec::new(),
1581 },
1582 ],
1583 is_result: false,
1584 reserved: Vec::new(),
1585 };
1586
1587 let retries = v2::Field {
1593 declared_init: Some("3".to_string()),
1594 init: Some(v2::InitValue {
1595 derivable: true,
1596 value: Some("3".to_string()),
1597 }),
1598 ..field(
1599 "retries",
1600 1,
1601 v2::FieldType {
1602 optional: false,
1603 kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
1604 backing: Some(v2::Backing {
1605 kind: Some(v2::backing::Kind::Primitive(
1606 v2::PrimitiveType::Integer as i32,
1607 )),
1608 }),
1609 constraint: Some(v2::Constraint {
1610 min: Some("0".to_string()),
1611 max: Some("24".to_string()),
1612 step: None,
1613 len_min: None,
1614 len_max: None,
1615 pattern: None,
1616 pattern_const: None,
1617 }),
1618 declared_init: None,
1619 init: None,
1620 width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
1621 }))),
1622 },
1623 )
1624 };
1625
1626 let position = field(
1629 "position",
1630 2,
1631 v2::FieldType {
1632 optional: false,
1633 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
1634 fields: vec![
1635 v2::TupleField {
1636 name: "x".to_string(),
1637 r#type: Some(named_type("Speed")),
1638 },
1639 v2::TupleField {
1640 name: "y".to_string(),
1641 r#type: Some(named_type("Speed")),
1642 },
1643 ],
1644 })),
1645 },
1646 );
1647
1648 let gears = field(
1650 "gears",
1651 3,
1652 v2::FieldType {
1653 optional: false,
1654 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
1655 element: Some(Box::new(named_type("Gear"))),
1656 min: 1,
1657 max: 4096,
1658 }))),
1659 },
1660 );
1661
1662 let plates = v2::Field {
1666 deprecated: Some("superseded by gears".to_string()),
1667 ..field(
1668 "plates",
1669 4,
1670 v2::FieldType {
1671 optional: false,
1672 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
1673 key: Some(Box::new(named_type("PlateText"))),
1674 value: Some(Box::new(named_type("Gear"))),
1675 min: 0,
1676 max: 53,
1677 }))),
1678 },
1679 )
1680 };
1681
1682 let snapshot = v2::StructDef {
1683 members: [retries, position, gears, plates]
1684 .into_iter()
1685 .map(|field| v2::StructMember {
1686 member: Some(v2::struct_member::Member::Field(Box::new(field))),
1687 })
1688 .collect(),
1689 fixed_layout: false,
1690 };
1691
1692 v2::Package {
1693 name: "veh.vocab".to_string(),
1694 decls: vec![
1695 decl("MAX_RETRY", v2::decl::Kind::ConstDef(max_retry)),
1696 decl("Gear", v2::decl::Kind::EnumDef(gear)),
1697 decl("Warnings", v2::decl::Kind::EnumSetDef(warnings)),
1698 decl("PlateText", v2::decl::Kind::TypeDef(plate_text)),
1699 v2::Decl {
1702 deprecated: Some("use Snapshot".to_string()),
1703 ..decl("Sample", v2::decl::Kind::UnionDef(sample))
1704 },
1705 decl("Snapshot", v2::decl::Kind::StructDef(snapshot)),
1706 ],
1707 interfaces: Vec::new(),
1708 services: Vec::new(),
1709 retired: Vec::new(),
1710 unit: String::new(),
1711 }
1712 }
1713
1714 #[test]
1715 fn protobuf_round_trip_preserves_package() {
1716 let package = fixture();
1717
1718 let buf = v2::to_binary(&package);
1719 let decoded = v2::from_binary(buf.as_slice()).expect("decode must succeed");
1720
1721 assert_eq!(package, decoded);
1722
1723 let vocabulary = vocabulary_fixture();
1725 let decoded_vocabulary =
1726 v2::from_binary(v2::to_binary(&vocabulary).as_slice()).expect("decode must succeed");
1727 assert_eq!(vocabulary, decoded_vocabulary);
1728
1729 let interface = &decoded.interfaces[0];
1730 let ordinals: Vec<u32> = interface.interactions.iter().map(|d| d.ordinal).collect();
1731 assert_eq!(
1732 ordinals,
1733 [1, 2, 3, 4, 5, 6],
1734 "one ordinal sequence, tombstone counted (ridl §11)"
1735 );
1736 let Some(v2::decl::Kind::ReservedSlot(tombstone)) = &interface.interactions[2].kind else {
1737 panic!("ordinal 3 must decode as a reserved tombstone");
1738 };
1739 assert_eq!(tombstone.name.as_deref(), Some("legacyMode"));
1740 let Some(v2::service_shape::Kind::Inline(inline)) = decoded.services[1]
1741 .shapes
1742 .first()
1743 .and_then(|slot| slot.kind.as_ref())
1744 else {
1745 panic!("veh.adas.logs must decode as an inline shape");
1746 };
1747 assert_eq!(inline.name, "", "an inline shape carries no name");
1748 let references: Vec<&str> = decoded.services[0]
1749 .shapes
1750 .iter()
1751 .filter_map(|slot| match &slot.kind {
1752 Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
1753 _ => None,
1754 })
1755 .collect();
1756 assert_eq!(
1757 references,
1758 ["VehicleStatus"],
1759 "a service's set carries its references and nothing else"
1760 );
1761 }
1762
1763 fn package_with_command(family: v2::Family, shape: v2::Shape) -> v2::Package {
1766 v2::Package {
1767 name: "app".to_string(),
1768 interfaces: vec![v2::Interface {
1769 name: "I".to_string(),
1770 visibility: v2::Visibility::Public as i32,
1771 interactions: vec![v2::Decl {
1772 name: "c".to_string(),
1773 visibility: v2::Visibility::Public as i32,
1774 ordinal: 1,
1775 kind: Some(v2::decl::Kind::CommandDef(v2::CommandDef {
1776 family: family as i32,
1777 shape: shape as i32,
1778 ..Default::default()
1779 })),
1780 ..Default::default()
1781 }],
1782 ..Default::default()
1783 }],
1784 ..Default::default()
1785 }
1786 }
1787
1788 #[test]
1791 fn the_readers_reject_an_invalid_interaction_class() {
1792 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1793 let json = v2::render_json(&package).expect("the raw writer renders it");
1794 let err = v2::from_json(&json).expect_err("the JSON reader must refuse it");
1795 assert!(
1796 err.to_string().contains("invalid interaction class"),
1797 "{err}"
1798 );
1799 let bytes = prost::Message::encode_to_vec(&package);
1800 let err = v2::from_binary(&bytes).expect_err("the binary reader must refuse it");
1801 assert!(
1802 err.to_string().contains("invalid interaction class"),
1803 "{err}"
1804 );
1805 }
1806
1807 #[test]
1808 fn the_json_writer_refuses_an_invalid_interaction_class() {
1809 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1810 let err = v2::to_json_pretty(&package).expect_err("the writer must refuse it");
1811 assert!(matches!(err, v2::SerializeError::Class(_)), "{err}");
1812 }
1813
1814 #[test]
1815 fn a_valid_class_round_trips_through_both_readers() {
1816 let package = package_with_command(v2::Family::Intent, v2::Shape::Toggle);
1817 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1818 assert_eq!(v2::from_json(&json).expect("the JSON parses"), package);
1819 assert_eq!(
1820 v2::from_binary(&v2::to_binary(&package)).expect("the binary decodes"),
1821 package
1822 );
1823 }
1824
1825 #[test]
1828 fn a_snapshot_without_family_reads_as_dispatch() {
1829 let package = package_with_command(v2::Family::Dispatch, v2::Shape::None);
1830 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1831 let mut value: serde_json::Value =
1832 serde_json::from_str(&json).expect("the rendering is JSON");
1833 let command = value["interfaces"][0]["interactions"][0]["commandDef"]
1834 .as_object_mut()
1835 .expect("the command object");
1836 assert!(command.remove("family").is_some(), "`family` was rendered");
1837 assert!(command.remove("shape").is_some(), "`shape` was rendered");
1838 let stripped = serde_json::to_string(&value).expect("the stripped value renders");
1839 let read = v2::from_json(&stripped).expect("a 0.9 snapshot reads");
1840 assert_eq!(read, package);
1841 }
1842
1843 #[test]
1844 fn json_round_trip_preserves_package() {
1845 for package in [fixture(), vocabulary_fixture()] {
1846 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1847 let decoded = v2::from_json(&json).expect("json deserialization must succeed");
1848
1849 assert_eq!(package, decoded);
1850 }
1851 }
1852
1853 #[test]
1860 fn number_provisional_and_retired_round_trip_through_json_text_and_binary() {
1861 let mut package = fixture();
1862 package.interfaces[0].number = 4;
1863 package.interfaces[0].provisional = true;
1864 let Some(v2::service_shape::Kind::Inline(inline)) =
1865 package.services[1].shapes[0].kind.as_mut()
1866 else {
1867 panic!("veh.adas.logs holds an inline shape in slot 1");
1868 };
1869 inline.number = 5;
1870 package.retired = vec![
1871 v2::RetiredInterface {
1872 name: "LaneAssist".to_string(),
1873 number: 2,
1874 },
1875 v2::RetiredInterface {
1876 name: "service:veh.hvac.cabin".to_string(),
1877 number: 3,
1878 },
1879 ];
1880
1881 let json = v2::to_json_pretty(&package).expect("the package serializes as IR JSON");
1882 assert_eq!(v2::from_json(&json).expect("the JSON parses back"), package);
1883 let text = v2::to_text_format(&package).expect("the package serializes as prototext");
1884 assert_eq!(
1885 v2::from_text_format(&text).expect("the prototext parses back"),
1886 package
1887 );
1888 assert_eq!(
1889 v2::from_binary(v2::to_binary(&package).as_slice()).expect("the binary decodes"),
1890 package
1891 );
1892
1893 for needle in [
1894 r#""number": 4"#,
1895 r#""provisional": true"#,
1896 r#""number": 5"#,
1897 r#""name": "LaneAssist""#,
1898 r#""name": "service:veh.hvac.cabin""#,
1899 ] {
1900 assert!(
1901 json.contains(needle),
1902 "the JSON must carry {needle}, got: {json}"
1903 );
1904 }
1905
1906 let unnumbered = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1910 for needle in [
1911 r#""number": 0"#,
1912 r#""provisional": false"#,
1913 r#""retired": []"#,
1914 ] {
1915 assert!(
1916 unnumbered.contains(needle),
1917 "a default field must still be written, expected {needle} in: {unnumbered}"
1918 );
1919 }
1920 }
1921
1922 #[test]
1928 fn a_snapshot_lacking_the_number_fields_still_loads() {
1929 let package = v2::from_json(
1930 r#"{"name": "veh.x", "interfaces": [{"name": "LaneKeeping"}], "services": [{"name": "veh.x.s", "shapes": [{"inline": {"name": ""}}]}]}"#,
1931 )
1932 .expect("a pre-lock snapshot loads");
1933
1934 assert_eq!(package.interfaces[0].number, 0);
1935 assert!(!package.interfaces[0].provisional);
1936 let Some(v2::service_shape::Kind::Inline(inline)) =
1937 package.services[0].shapes[0].kind.as_ref()
1938 else {
1939 panic!("the service holds an inline shape");
1940 };
1941 assert_eq!(inline.number, 0);
1942 assert!(!inline.provisional);
1943 assert_eq!(package.retired, Vec::new());
1944 }
1945
1946 #[test]
1951 fn text_format_round_trip_preserves_package() {
1952 for package in [fixture(), vocabulary_fixture()] {
1953 let text = v2::to_text_format(&package).expect("the fixture serializes as prototext");
1954 let decoded = v2::from_text_format(&text).expect("prototext parsing must succeed");
1955
1956 assert_eq!(package, decoded);
1957 }
1958 }
1959
1960 #[test]
1975 fn text_format_is_pretty_with_defaults_in_index_order() {
1976 let text = v2::to_text_format(&fixture()).expect("the fixture serializes as prototext");
1977
1978 assert!(
1980 text.contains("\n "),
1981 "pretty printing must indent nested fields, got: {text}"
1982 );
1983 assert!(
1986 text.contains("is_error: false"),
1987 "a field holding its default must be emitted, got: {text}"
1988 );
1989 assert!(
1992 text.starts_with("name:"),
1993 "fields must print in schema index order, got: {text}"
1994 );
1995 }
1996
1997 fn strict_parse(json: &str) -> v2::Package {
2004 let mut deserializer = serde_json::Deserializer::from_str(json);
2005 let package = <v2::Package as serde::Deserialize>::deserialize(&mut deserializer)
2006 .expect("a strict conformant parser must accept the emitted JSON");
2007 deserializer.end().expect("no trailing input");
2008 package
2009 }
2010
2011 #[test]
2016 fn emitted_json_survives_a_strict_conformant_parse() {
2017 for package in [fixture(), vocabulary_fixture()] {
2018 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
2019 assert_eq!(package, strict_parse(&json));
2020 }
2021 }
2022
2023 #[test]
2024 fn json_renders_timing_bounds_and_fallible_arms_exactly() {
2025 let json = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
2026
2027 assert!(
2031 json.contains(r#""minUs": "10000""#),
2032 "the timing bound must be a JSON string, got: {json}"
2033 );
2034 assert!(
2036 json.contains(r#""ok": "FaultPage""#),
2037 "the ok arm must render, got: {json}"
2038 );
2039 assert!(
2040 json.contains(r#""err": "DiagError""#),
2041 "the err arm must render, got: {json}"
2042 );
2043 }
2044
2045 #[test]
2050 fn json_renders_64_bit_integer_fields_as_strings() {
2051 let json = v2::to_json_pretty(&vocabulary_fixture())
2052 .expect("the vocabulary fixture serializes as IR JSON");
2053
2054 assert!(
2056 json.contains(r#""max": "4096""#),
2057 "an array bound must be a JSON string, got: {json}"
2058 );
2059 assert!(
2061 json.contains(r#""lenMax": "86""#),
2062 "a length bound must be a JSON string, got: {json}"
2063 );
2064 assert!(
2066 json.contains(r#""value": "7""#),
2067 "a retired enum value must be a JSON string, got: {json}"
2068 );
2069 assert!(
2070 json.contains(r#""value": "33""#),
2071 "an enum-set bit position must be a JSON string, got: {json}"
2072 );
2073 }
2074
2075 #[test]
2076 fn fallible_transport_identity_follows_the_derivation_rule() {
2077 let fallible = v2::FallibleType {
2080 ok: "FaultPage".to_string(),
2081 err: "DiagError".to_string(),
2082 };
2083 assert_eq!(
2084 v2::fallible_transport_identity("VehicleStatus", 9, &fallible),
2085 "VehicleStatus#9:FaultPage|DiagError"
2086 );
2087
2088 let package = fixture();
2090 let interface = &package.interfaces[0];
2091 let query_decl = &interface.interactions[4];
2092 let Some(v2::decl::Kind::QueryDef(query)) = &query_decl.kind else {
2093 panic!("ordinal 5 must be the fallible query");
2094 };
2095 let Some(v2::return_type::Kind::Fallible(arms)) = &query.return_type.as_ref().unwrap().kind
2096 else {
2097 panic!("fetchFaults must return a fallible type");
2098 };
2099 assert_eq!(
2100 v2::fallible_transport_identity(&interface.name, query_decl.ordinal, arms),
2101 "VehicleStatus#5:FaultPage|DiagError"
2102 );
2103 }
2104
2105 #[test]
2112 fn shapes_walks_named_interfaces_and_inline_service_shapes() {
2113 let package = fixture();
2114 let walk: Vec<(&str, bool, usize)> = package
2115 .shapes()
2116 .map(|shape| {
2117 (
2118 shape.name,
2119 shape.is_inline(),
2120 shape.interface.interactions.len(),
2121 )
2122 })
2123 .collect();
2124 assert_eq!(
2125 walk,
2126 [("VehicleStatus", false, 6), ("veh.adas.logs", true, 1)],
2127 "the named interface, then the inline shape under the service's \
2128 dotted name",
2129 );
2130
2131 assert_eq!(package.services.len(), 2, "one reference form, one inline");
2135 assert!(
2136 !package
2137 .shapes()
2138 .any(|shape| shape.name == "veh.adas.status"),
2139 "a service naming an interface contributes no shape of its own",
2140 );
2141 }
2142
2143 #[test]
2144 fn relative_name_strips_the_unit_prefix() {
2145 assert_eq!(v2::relative_name("u", "u", "Session"), "Session");
2146 assert_eq!(
2147 v2::relative_name("u", "u.cluster", "Speed"),
2148 "cluster.Speed"
2149 );
2150 assert_eq!(
2151 v2::relative_name("com.example.hmi", "com.example.hmi.cluster.front", "A"),
2152 "cluster.front.A"
2153 );
2154 }
2155
2156 #[test]
2159 fn relative_name_keeps_the_full_name_of_a_package_outside_the_unit() {
2160 assert_eq!(
2161 v2::relative_name("u", "w.cluster", "Speed"),
2162 "w.cluster.Speed"
2163 );
2164 assert_eq!(v2::relative_name("u", "ux", "A"), "ux.A");
2165 assert_eq!(v2::relative_name("u.cluster", "u", "Session"), "u.Session");
2166 }
2167
2168 #[test]
2172 fn unit_retired_reads_a_package_named_twice_once() {
2173 let first = v2::Package {
2174 name: "u.cluster".to_string(),
2175 unit: "u".to_string(),
2176 retired: vec![v2::RetiredInterface {
2177 name: "cluster.Old".to_string(),
2178 number: 3,
2179 }],
2180 ..Default::default()
2181 };
2182 let second = v2::Package {
2183 retired: vec![v2::RetiredInterface {
2184 name: "cluster.Older".to_string(),
2185 number: 4,
2186 }],
2187 ..first.clone()
2188 };
2189 assert_eq!(
2190 v2::unit_retired("u", &[&first, &second]),
2191 first.retired,
2192 "the first package of a name is the one read"
2193 );
2194 }
2195
2196 #[test]
2197 fn an_inline_shape_keeps_its_global_name() {
2198 let mut package = fixture();
2199 package.name = "u.cluster".to_string();
2200 package.unit = "u".to_string();
2201 let inline = package
2202 .shapes()
2203 .find(|shape| shape.is_inline())
2204 .expect("the fixture has an inline shape");
2205 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2206 let named = package
2207 .shapes()
2208 .find(|shape| !shape.is_inline())
2209 .expect("the fixture has a declared interface");
2210 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2211 }
2212
2213 #[test]
2214 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2215 let mut package = fixture();
2216 package.unit = String::new();
2217 let named = package
2218 .shapes()
2219 .find(|shape| !shape.is_inline())
2220 .expect("the fixture has a declared interface");
2221 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2222 }
2223
2224 #[test]
2225 fn unit_of_falls_back_to_the_name() {
2226 let bare = v2::Package {
2227 name: "p".to_string(),
2228 ..Default::default()
2229 };
2230 assert_eq!(v2::unit_of(&bare), "p");
2231 let member = v2::Package {
2232 name: "u.a".to_string(),
2233 unit: "u".to_string(),
2234 ..Default::default()
2235 };
2236 assert_eq!(v2::unit_of(&member), "u");
2237 }
2238
2239 #[test]
2240 fn packages_of_unit_selects_by_unit_of() {
2241 let package = |name: &str, unit: &str| v2::Package {
2242 name: name.to_string(),
2243 unit: unit.to_string(),
2244 ..Default::default()
2245 };
2246 let packages = [
2247 package("u", "u"),
2248 package("u.a", "u"),
2249 package("w", ""),
2250 package("other.b", "other"),
2251 ];
2252 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2253 .map(|p| p.name.as_str())
2254 .collect();
2255 assert_eq!(names, ["u", "u.a"]);
2256 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2257 .map(|p| p.name.as_str())
2258 .collect();
2259 assert_eq!(legacy, ["w"]);
2260 }
2261
2262 #[test]
2267 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2268 let package = fixture();
2269 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2270
2271 let named = shapes[0];
2272 assert!(named.service.is_none());
2273 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2274 assert_eq!(named.visibility(), named.interface.visibility);
2275
2276 let inline = shapes[1];
2277 assert_eq!(
2278 inline.interface.visibility,
2279 v2::Visibility::Unspecified as i32,
2280 "the trap: an inline shape's own visibility field is unset",
2281 );
2282 assert_eq!(
2283 inline.service.expect("an inline shape has an owner").name,
2284 "veh.adas.logs",
2285 );
2286 assert_eq!(
2287 inline.visibility(),
2288 v2::Visibility::Public as i32,
2289 "the accessor reads the owning service's, never the unset field",
2290 );
2291 }
2292
2293 #[test]
2297 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2298 let mut package = fixture();
2299 package.services.clear();
2300 assert_eq!(package.shapes().count(), 1);
2301
2302 package.interfaces.clear();
2303 assert_eq!(package.shapes().count(), 0);
2304 }
2305
2306 #[test]
2313 fn referenced_packages_finds_qualifiers_at_depth() {
2314 fn named(reference: &str) -> v2::FieldType {
2315 v2::FieldType {
2316 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2317 ..Default::default()
2318 }
2319 }
2320
2321 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2322 v2::decl::Kind::FixedDef(v2::FixedDef {
2323 payload: Some(payload),
2324 })
2325 }
2326
2327 let package = v2::Package {
2328 name: "veh.cluster".to_string(),
2329 decls: vec![
2330 v2::Decl {
2331 name: "Local".to_string(),
2332 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2333 payload: "Speed".to_string(),
2334 ..Default::default()
2335 })),
2336 ..Default::default()
2337 },
2338 v2::Decl {
2339 name: "Stamped".to_string(),
2340 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2341 payload: "ridl.std.Timestamp".to_string(),
2342 ..Default::default()
2343 })),
2344 ..Default::default()
2345 },
2346 v2::Decl {
2347 name: "ArrLabels".to_string(),
2348 kind: Some(fixed(v2::FieldType {
2349 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2350 element: Some(Box::new(named("veh.arr.Label"))),
2351 min: 0,
2352 max: 32,
2353 }))),
2354 ..Default::default()
2355 })),
2356 ..Default::default()
2357 },
2358 v2::Decl {
2359 name: "TupThing".to_string(),
2360 kind: Some(fixed(v2::FieldType {
2361 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2362 fields: vec![v2::TupleField {
2363 name: "x".to_string(),
2364 r#type: Some(named("veh.tup.X")),
2365 }],
2366 })),
2367 ..Default::default()
2368 })),
2369 ..Default::default()
2370 },
2371 v2::Decl {
2372 name: "MapThing".to_string(),
2373 kind: Some(fixed(v2::FieldType {
2374 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2375 key: Some(Box::new(named("veh.key.X"))),
2376 value: Some(Box::new(named("veh.val.X"))),
2377 min: 0,
2378 max: 8,
2379 }))),
2380 ..Default::default()
2381 })),
2382 ..Default::default()
2383 },
2384 v2::Decl {
2385 name: "StreamThing".to_string(),
2386 kind: Some(fixed(v2::FieldType {
2387 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2388 element: Some(v2::stream_type::Element::Named(
2389 "veh.strm.X".to_string(),
2390 )),
2391 })),
2392 ..Default::default()
2393 })),
2394 ..Default::default()
2395 },
2396 ],
2397 ..Default::default()
2398 };
2399
2400 let found = v2::referenced_packages(&package);
2401 let expected: std::collections::BTreeSet<String> = [
2402 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2403 ]
2404 .into_iter()
2405 .map(str::to_string)
2406 .collect();
2407 assert_eq!(
2408 found, expected,
2409 "each recursive path must contribute its own distinct qualifier"
2410 );
2411 assert!(
2412 !found.contains("Speed") && !found.contains("veh.cluster"),
2413 "a bare reference contributes no package: {found:?}",
2414 );
2415 }
2416
2417 #[test]
2420 fn referenced_packages_is_empty_without_references() {
2421 let package = v2::Package {
2422 name: "veh.solo".to_string(),
2423 ..Default::default()
2424 };
2425 assert!(v2::referenced_packages(&package).is_empty());
2426 }
2427
2428 const NESTING_BELOW_LIMIT: usize = 45;
2434 const NESTING_PAST_LIMIT: usize = 60;
2438
2439 fn nested_package(depth: usize) -> v2::Package {
2444 let mut payload = v2::FieldType {
2445 optional: false,
2446 kind: Some(v2::field_type::Kind::Primitive(
2447 v2::PrimitiveType::Integer as i32,
2448 )),
2449 };
2450 for _ in 0..depth {
2451 payload = v2::FieldType {
2452 optional: false,
2453 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2454 element: Some(Box::new(payload)),
2455 min: 1,
2456 max: 1,
2457 }))),
2458 };
2459 }
2460 v2::Package {
2461 name: "veh.deep".to_string(),
2462 decls: vec![v2::Decl {
2463 name: "deep".to_string(),
2464 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2465 payload: Some(payload),
2466 })),
2467 ..Default::default()
2468 }],
2469 ..Default::default()
2470 }
2471 }
2472
2473 fn package_at_message_depth(levels: usize) -> v2::Package {
2485 assert!(levels >= 3, "the chain costs three levels before nesting");
2486 let (arrays, tuple) = if levels % 2 == 1 {
2487 ((levels - 3) / 2, false)
2488 } else {
2489 assert!(levels >= 6, "one tuple level costs three");
2490 ((levels - 6) / 2, true)
2491 };
2492
2493 let mut payload = v2::FieldType {
2494 optional: false,
2495 kind: Some(v2::field_type::Kind::Primitive(
2496 v2::PrimitiveType::Integer as i32,
2497 )),
2498 };
2499 for _ in 0..arrays {
2500 payload = v2::FieldType {
2501 optional: false,
2502 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2503 element: Some(Box::new(payload)),
2504 min: 1,
2505 max: 1,
2506 }))),
2507 };
2508 }
2509 if tuple {
2510 payload = v2::FieldType {
2511 optional: false,
2512 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2513 fields: vec![v2::TupleField {
2514 name: "f0".to_string(),
2515 r#type: Some(payload),
2516 }],
2517 })),
2518 };
2519 }
2520 v2::Package {
2521 name: "veh.deep".to_string(),
2522 decls: vec![v2::Decl {
2523 name: "deep".to_string(),
2524 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2525 payload: Some(payload),
2526 })),
2527 ..Default::default()
2528 }],
2529 ..Default::default()
2530 }
2531 }
2532
2533 fn message_nesting(json: &str) -> usize {
2540 let (mut depth, mut max) = (0usize, 0usize);
2541 let (mut in_string, mut escaped) = (false, false);
2542 for byte in json.bytes() {
2543 if in_string {
2544 if escaped {
2545 escaped = false;
2546 } else if byte == b'\\' {
2547 escaped = true;
2548 } else if byte == b'"' {
2549 in_string = false;
2550 }
2551 continue;
2552 }
2553 match byte {
2554 b'"' => in_string = true,
2555 b'{' => {
2556 depth += 1;
2557 max = max.max(depth);
2558 }
2559 b'}' => depth = depth.saturating_sub(1),
2560 _ => {}
2561 }
2562 }
2563 max
2564 }
2565
2566 #[test]
2570 fn package_at_message_depth_builds_the_depth_it_names() {
2571 with_sized_stack(|| {
2572 for levels in [3, 6, 7, 99, 100, 101] {
2573 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2574 .expect("the writer is unrestricted at these depths");
2575 assert_eq!(
2576 message_nesting(&json) - 1,
2577 levels,
2578 "the chain must nest {levels} message levels below the root"
2579 );
2580 }
2581 });
2582 }
2583
2584 #[test]
2593 fn binary_round_trip_at_100_message_levels_succeeds() {
2594 let package = package_at_message_depth(100);
2595 let bytes = v2::to_binary(&package);
2596 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2597 assert_eq!(package, decoded);
2598 }
2599
2600 #[test]
2605 fn binary_decode_at_101_message_levels_returns_an_error() {
2606 let package = package_at_message_depth(101);
2607 let bytes = v2::to_binary(&package);
2608 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2609 assert!(
2610 error.to_string().contains("recursion limit reached"),
2611 "the error must name the limit, got: {error}"
2612 );
2613
2614 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2615 assert_eq!(
2616 package,
2617 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2618 );
2619 }
2620
2621 #[test]
2630 fn json_round_trip_at_400_nested_levels_succeeds() {
2631 with_sized_stack(|| {
2632 let package = nested_package(400);
2633 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2634 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2635 assert_eq!(package, decoded);
2636 });
2637 }
2638
2639 #[test]
2647 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2648 let mut package = fixture();
2649 package.decls[0].visibility = 999;
2650 let err = v2::to_json_pretty(&package)
2651 .expect_err("an out-of-schema discriminant must fail, not panic");
2652 let message = err.to_string();
2653 assert!(
2654 message.contains("canonical protobuf JSON"),
2655 "the error must name the encoding that failed, got: {message}"
2656 );
2657 assert!(
2658 message.contains("discriminant outside the schema"),
2659 "the error must name the known cause, got: {message}"
2660 );
2661 }
2662
2663 #[test]
2668 fn json_parse_rejects_an_unknown_field() {
2669 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2670 .expect_err("an unknown field must be rejected");
2671 assert!(
2672 error.to_string().contains("unknown field"),
2673 "the error must name the defect, got: {error}"
2674 );
2675 }
2676
2677 #[test]
2686 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2687 let with_visibility =
2688 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2689 v2::from_json(&with_visibility("1"))
2690 .expect("an in-range numeric enum value parses, as the mapping expects");
2691 let error = v2::from_json(&with_visibility("77"))
2692 .expect_err("an out-of-range numeric enum value must be rejected");
2693 assert!(
2694 error.to_string().contains("invalid value: integer `77`"),
2695 "the error must name the value, got: {error}"
2696 );
2697 }
2698
2699 #[test]
2705 fn json_parse_rejects_a_float_form_integer() {
2706 for spelling in ["1.0", "1e0"] {
2707 let error = v2::from_json(&format!(
2708 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2709 ))
2710 .expect_err("a float-form integer must be rejected");
2711 assert!(
2712 error.to_string().contains("did not match any variant"),
2713 "the integer field's deserializer must be the one refusing `{spelling}`, \
2714 got: {error}"
2715 );
2716 }
2717 }
2718
2719 #[test]
2726 fn json_parse_rejects_null_for_a_repeated_field() {
2727 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2728 .expect_err("null for a repeated field must be rejected");
2729 assert!(
2730 error.to_string().contains("invalid type: null"),
2731 "the error must name the null, got: {error}"
2732 );
2733 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2734 .expect("null for an optional scalar field still parses");
2735 }
2736
2737 #[test]
2741 fn json_parse_rejects_a_duplicate_key() {
2742 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2743 .expect_err("a duplicate key must be rejected");
2744 assert!(
2745 error.to_string().contains("duplicate field `name`"),
2746 "the error must name the duplicated field, got: {error}"
2747 );
2748 }
2749
2750 #[test]
2757 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2758 with_sized_stack(|| {
2759 let json = v2::to_json_pretty(&nested_package(500))
2760 .expect("the writer is unrestricted at this depth");
2761 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2762 assert!(
2763 error.to_string().contains("1000 JSON levels"),
2764 "the error must name the ceiling, got: {error}"
2765 );
2766 });
2767 }
2768
2769 #[test]
2774 fn json_nesting_ceiling_binds_exactly_at_1000() {
2775 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2776 assert!(
2777 !at.to_string().contains("JSON levels"),
2778 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2779 );
2780
2781 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2782 assert!(
2783 past.to_string().contains("1000 JSON levels"),
2784 "past the ceiling the error must name it, got: {past}"
2785 );
2786 }
2787
2788 #[test]
2793 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2794 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2796 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2798 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2801 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2804 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2806 }
2807
2808 fn nested_text(depth: usize) -> String {
2812 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2813 for _ in 0..depth {
2814 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2815 }
2816 format!(
2817 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2818 )
2819 }
2820
2821 #[test]
2825 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2826 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2827 .expect_err("serialization past the recursion limit must fail, not panic");
2828 let message = err.to_string();
2829 assert!(
2830 message.contains("recursion limit"),
2831 "the error must name the nesting limit as the known cause, got: {message}"
2832 );
2833 assert!(
2834 message.contains("prototext"),
2835 "the error must name the encoding that failed, got: {message}"
2836 );
2837 }
2838
2839 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2852 let outcome = std::thread::Builder::new()
2853 .stack_size(16 * 1024 * 1024)
2854 .spawn(test)
2855 .expect("spawn the large-stack test thread")
2856 .join();
2857 if let Err(payload) = outcome {
2858 std::panic::resume_unwind(payload);
2859 }
2860 }
2861
2862 #[test]
2866 fn text_parse_past_the_nesting_limit_returns_an_error() {
2867 with_sized_stack(|| {
2868 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2869 .expect_err("parsing past the recursion limit must fail, not panic");
2870
2871 let message = error.to_string();
2875 assert!(
2876 message.contains("recursion limit reached"),
2877 "the transcode out of the dynamic message must be the failing \
2878 stage, got: {message}"
2879 );
2880 });
2881 }
2882
2883 #[test]
2886 fn text_round_trip_below_the_nesting_limit_succeeds() {
2887 with_sized_stack(|| {
2888 let package = nested_package(NESTING_BELOW_LIMIT);
2889 let text = v2::to_text_format(&package)
2890 .expect("below the recursion limit, serialization succeeds");
2891 let decoded =
2892 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2893 assert_eq!(package, decoded);
2894 });
2895 }
2896}
2897
2898#[cfg(test)]
2899mod vacuous_constraint {
2900 use crate::v2;
2901
2902 fn constraint() -> v2::Constraint {
2905 v2::Constraint {
2906 min: None,
2907 max: None,
2908 step: None,
2909 len_min: None,
2910 len_max: None,
2911 pattern: None,
2912 pattern_const: None,
2913 }
2914 }
2915
2916 #[test]
2917 fn an_absent_or_empty_constraint_is_vacuous() {
2918 assert!(v2::constraint_is_vacuous(None));
2919 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2920 }
2921
2922 #[test]
2923 fn a_step_constraint_is_non_vacuous() {
2924 let stepped = v2::Constraint {
2925 step: Some("0.5".to_string()),
2926 ..constraint()
2927 };
2928 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2929 }
2930
2931 #[test]
2938 fn any_single_constrained_field_is_non_vacuous() {
2939 let cases = [
2940 (
2941 "min",
2942 v2::Constraint {
2943 min: Some("0.0".to_string()),
2944 ..constraint()
2945 },
2946 ),
2947 (
2948 "max",
2949 v2::Constraint {
2950 max: Some("250.0".to_string()),
2951 ..constraint()
2952 },
2953 ),
2954 (
2955 "len_min",
2956 v2::Constraint {
2957 len_min: Some(1),
2958 ..constraint()
2959 },
2960 ),
2961 (
2962 "len_max",
2963 v2::Constraint {
2964 len_max: Some(256),
2965 ..constraint()
2966 },
2967 ),
2968 (
2969 "pattern",
2970 v2::Constraint {
2971 pattern: Some("^[a-z]+$".to_string()),
2972 ..constraint()
2973 },
2974 ),
2975 (
2976 "pattern_const",
2977 v2::Constraint {
2978 pattern_const: Some("NAME_PATTERN".to_string()),
2979 ..constraint()
2980 },
2981 ),
2982 ];
2983 for (field, case) in cases {
2984 assert!(
2985 !v2::constraint_is_vacuous(Some(&case)),
2986 "`{field}` alone must be non-vacuous"
2987 );
2988 }
2989 }
2990
2991 #[test]
3000 fn a_bound_pair_set_together_is_non_vacuous() {
3001 let ranged = v2::Constraint {
3002 min: Some("0.0".to_string()),
3003 max: Some("250.0".to_string()),
3004 ..constraint()
3005 };
3006 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
3007
3008 let default_length = v2::Constraint {
3009 len_min: Some(0),
3010 len_max: Some(256),
3011 ..constraint()
3012 };
3013 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
3014 }
3015}
3016
3017#[cfg(test)]
3018mod system_round_trip {
3019 use crate::v2;
3020
3021 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
3022 v2::Attribute {
3023 namespace: namespace.to_string(),
3024 key: key.to_string(),
3025 value,
3026 }
3027 }
3028
3029 fn scalar(text: &str) -> v2::AttributeValue {
3030 v2::AttributeValue {
3031 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
3032 }
3033 }
3034
3035 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
3036 v2::AttributeValue {
3037 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
3038 }
3039 }
3040
3041 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
3042 Some(v2::InterfaceRef {
3043 catalog: catalog.to_string(),
3044 name: name.to_string(),
3045 inline,
3046 })
3047 }
3048
3049 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
3050 v2::Endpoint {
3051 component: component.to_string(),
3052 instance: instance.to_string(),
3053 machine: machine.to_string(),
3054 }
3055 }
3056
3057 fn doc_link() -> v2::DocLink {
3065 v2::DocLink {
3066 text: "Cruise".to_string(),
3067 offset: 16,
3068 len: 6,
3069 target: "veh.topology.Cruise".to_string(),
3070 }
3071 }
3072
3073 fn fixture() -> v2::System {
3074 let link = v2::Link {
3075 interface: interface("veh.diag", "veh.diag.access", true),
3076 service: "veh.diag.access".to_string(),
3077 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
3078 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
3079 crossing: v2::Crossing::OffBoard as i32,
3080 };
3081 v2::System {
3082 name: "Vehicle".to_string(),
3083 package: "veh.topology".to_string(),
3084 labels: vec!["ASIL_B".to_string()],
3085 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
3086 members: vec![
3087 v2::MemberLine {
3088 component: "veh.topology.Cruise".to_string(),
3089 attributes: vec![attribute("linux", "pinned", None)],
3090 doc: "Documented, see [Cruise].".to_string(),
3091 links: vec![doc_link()],
3092 see: vec![doc_link()],
3093 since: vec!["1.2".to_string()],
3094 },
3095 v2::MemberLine {
3096 component: "veh.diag.access".to_string(),
3097 attributes: vec![],
3098 doc: "Documented, see [Cruise].".to_string(),
3099 links: vec![doc_link()],
3100 see: vec![doc_link()],
3101 since: vec!["1.2".to_string()],
3102 },
3103 ],
3104 components: vec![
3105 v2::Component {
3106 name: "Cruise".to_string(),
3107 package: "veh.topology".to_string(),
3108 implicit: false,
3109 external: true,
3110 instances: vec!["primary".to_string(), "backup".to_string()],
3111 offers: vec![v2::Offer {
3112 service: "veh.adas.cruise".to_string(),
3113 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
3114 doc: "Documented, see [Cruise].".to_string(),
3115 links: vec![doc_link()],
3116 see: vec![doc_link()],
3117 since: vec!["1.2".to_string()],
3118 }],
3119 requires: vec![v2::Require {
3120 interface: interface("veh.adas", "LaneAssist", false),
3121 service: "veh.adas.lane".to_string(),
3122 producer: "veh.topology.Lane".to_string(),
3123 attributes: vec![attribute(
3124 "linux",
3125 "cpuset",
3126 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
3127 )],
3128 doc: "Documented, see [Cruise].".to_string(),
3129 links: vec![doc_link()],
3130 see: vec![doc_link()],
3131 since: vec!["1.2".to_string()],
3132 }],
3133 labels: vec!["ASIL_B".to_string()],
3134 attributes: vec![attribute("rust", "crate", None)],
3135 doc: "Documented, see [Cruise].".to_string(),
3136 links: vec![doc_link()],
3137 see: vec![doc_link()],
3138 since: vec!["1.2".to_string()],
3139 deprecated: Some("superseded".to_string()),
3140 },
3141 v2::Component {
3142 name: "veh.diag.access".to_string(),
3143 package: String::new(),
3144 implicit: true,
3145 external: false,
3146 instances: vec!["Unit".to_string()],
3147 offers: vec![v2::Offer {
3148 service: "veh.diag.access".to_string(),
3149 attributes: vec![],
3150 doc: "Documented, see [Cruise].".to_string(),
3151 links: vec![doc_link()],
3152 see: vec![doc_link()],
3153 since: vec!["1.2".to_string()],
3154 }],
3155 requires: vec![],
3156 labels: vec![],
3157 attributes: vec![],
3158 doc: "Documented, see [Cruise].".to_string(),
3159 links: vec![doc_link()],
3160 see: vec![doc_link()],
3161 since: vec!["1.2".to_string()],
3162 deprecated: Some("superseded".to_string()),
3163 },
3164 ],
3165 producers: vec![v2::Producer {
3166 service: "veh.adas.cruise".to_string(),
3167 component: "veh.topology.Cruise".to_string(),
3168 instances: vec!["primary".to_string(), "backup".to_string()],
3169 not_yet_realizable: true,
3170 }],
3171 grants: vec![v2::Grant {
3172 component: "veh.topology.Backend".to_string(),
3173 external: true,
3174 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3175 }],
3176 regions: vec![v2::Region {
3177 catalog: "veh.diag".to_string(),
3178 hash: vec![0xab; 32],
3179 interfaces: vec![v2::RegionInterface {
3180 name: "veh.diag.access".to_string(),
3181 inline: true,
3182 number: 2,
3183 provisional: true,
3184 service: "veh.diag.access".to_string(),
3185 }],
3186 }],
3187 distributions: vec![v2::Distribution {
3188 name: "Adas".to_string(),
3189 package: "veh.topology".to_string(),
3190 members: vec![v2::MemberLine {
3191 component: "veh.topology.Cruise".to_string(),
3192 attributes: vec![],
3193 doc: "Documented, see [Cruise].".to_string(),
3194 links: vec![doc_link()],
3195 see: vec![doc_link()],
3196 since: vec!["1.2".to_string()],
3197 }],
3198 depends_on: vec!["veh.topology.Base".to_string()],
3199 labels: vec!["PLATFORM_BUNDLE".to_string()],
3200 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3201 doc: "Documented, see [Cruise].".to_string(),
3202 links: vec![doc_link()],
3203 see: vec![doc_link()],
3204 since: vec!["1.2".to_string()],
3205 tier: v2::Tier::Platform as i32,
3206 deprecated: Some("superseded".to_string()),
3207 }],
3208 deployments: vec![v2::Deployment {
3209 name: "Production".to_string(),
3210 package: "veh.topology".to_string(),
3211 labels: vec!["FLEET".to_string()],
3212 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3213 machines: vec![v2::Machine {
3214 name: "Cloud".to_string(),
3215 external: true,
3216 labels: vec!["OFF_BOARD".to_string()],
3217 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3218 doc: "Documented, see [Cruise].".to_string(),
3219 links: vec![doc_link()],
3220 see: vec![doc_link()],
3221 since: vec!["1.2".to_string()],
3222 deprecated: Some("superseded".to_string()),
3223 }],
3224 placements: vec![v2::Placement {
3225 component: "veh.topology.Cruise".to_string(),
3226 instance: "backup".to_string(),
3227 machine: "Cockpit".to_string(),
3228 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3229 sizing: Some(v2::Sizing {
3230 depth: Some(4_294_967_295),
3231 slots: Some(65_536),
3232 budget: Some(18_446_744_073_709_551_615),
3233 }),
3234 }],
3235 links: vec![link.clone()],
3236 routes: vec![v2::Route {
3237 catalog: "veh.adas".to_string(),
3238 interface_number: 2,
3239 member_ordinal: 1,
3240 interface: "LaneAssist".to_string(),
3241 member: "active".to_string(),
3242 service: "veh.adas.lane".to_string(),
3243 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3244 }],
3245 surface: vec![v2::Surface {
3246 link: Some(link),
3247 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3248 }],
3249 installations: vec![v2::Installation {
3250 distribution: "veh.topology.Adas".to_string(),
3251 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3252 }],
3253 doc: "Documented, see [Cruise].".to_string(),
3254 doc_links: vec![doc_link()],
3255 see: vec![doc_link()],
3256 since: vec!["1.2".to_string()],
3257 sizing: Some(v2::Sizing {
3258 depth: Some(3),
3259 slots: Some(8),
3260 budget: Some(4096),
3261 }),
3262 deprecated: Some("superseded".to_string()),
3263 }],
3264 doc: "Documented, see [Cruise].".to_string(),
3265 see: vec![doc_link()],
3266 since: vec!["1.2".to_string()],
3267 links: vec![doc_link()],
3268 deprecated: Some("superseded".to_string()),
3269 }
3270 }
3271
3272 #[test]
3273 fn system_binary_round_trip_preserves_system() {
3274 let system = fixture();
3275 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3276 .expect("decode must succeed");
3277 assert_eq!(system, decoded);
3278 }
3279
3280 #[test]
3285 fn system_json_round_trip_preserves_system() {
3286 let system = fixture();
3287 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3288 assert!(
3289 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3290 "bytes render as base64, got:\n{json}"
3291 );
3292 assert!(
3293 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3294 "enums render by name, got:\n{json}"
3295 );
3296 assert!(
3297 json.contains("\"notYetRealizable\": true"),
3298 "fields render in lowerCamelCase, got:\n{json}"
3299 );
3300 assert!(
3301 json.contains("\"budget\": \"18446744073709551615\""),
3302 "a 64-bit value renders as a string, got:\n{json}"
3303 );
3304 assert_eq!(
3305 system,
3306 v2::system_from_json(&json).expect("the JSON parses")
3307 );
3308 assert!(
3309 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3310 "an unknown field is rejected"
3311 );
3312 }
3313
3314 #[test]
3318 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3319 let mut absent = fixture();
3320 absent.deployments[0].sizing = None;
3321 absent.deployments[0].placements[0].sizing = None;
3322 let mut empty = absent.clone();
3323 empty.deployments[0].sizing = Some(v2::Sizing::default());
3324 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3325 for system in [&absent, &empty] {
3326 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3327 .expect("decode must succeed");
3328 assert_eq!(system, &binary);
3329 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3330 assert_eq!(
3331 system,
3332 &v2::system_from_json(&json).expect("the JSON parses")
3333 );
3334 }
3335 assert_ne!(
3336 v2::system_to_binary(&absent),
3337 v2::system_to_binary(&empty),
3338 "the binary encodings differ"
3339 );
3340 }
3341
3342 #[test]
3343 fn system_text_format_round_trip_preserves_system() {
3344 let system = fixture();
3345 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3346 assert!(
3347 text.starts_with("name:"),
3348 "fields print in schema index order, got: {text}"
3349 );
3350 assert_eq!(
3351 system,
3352 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3353 );
3354 }
3355
3356 #[test]
3359 fn qualified_names_follow_the_one_derivation() {
3360 let system = fixture();
3361 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3362 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3363 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3364 assert_eq!(
3365 system.distributions[0].qualified_name(),
3366 "veh.topology.Adas"
3367 );
3368 }
3369}
3370
3371#[cfg(test)]
3372mod load_ir_json {
3373 use crate::v2::{self, LoadError, MAX_JSON_NESTING, load_ir_json};
3374
3375 #[test]
3377 fn a_snapshot_file_loads() {
3378 let dir = tempfile::tempdir().unwrap();
3379 let package = v2::Package {
3380 name: "veh.cabin".to_string(),
3381 ..v2::Package::default()
3382 };
3383 let path = dir.path().join("cabin.ir.json");
3384 std::fs::write(&path, v2::to_json_pretty(&package).unwrap()).unwrap();
3385 assert_eq!(load_ir_json(&path).unwrap(), package);
3386 }
3387
3388 #[test]
3392 fn a_missing_file_is_io_and_bad_json_is_parse() {
3393 let dir = tempfile::tempdir().unwrap();
3394
3395 let missing = dir.path().join("missing.ir.json");
3396 let io = std::fs::read_to_string(&missing).unwrap_err();
3397 let error = load_ir_json(&missing).unwrap_err();
3398 assert!(matches!(error, LoadError::Io(_)), "{error:?}");
3399 assert_eq!(
3400 error.to_string(),
3401 format!("cannot read the IR snapshot: {io}")
3402 );
3403
3404 let bad = dir.path().join("bad.ir.json");
3407 std::fs::write(&bad, "\n{").unwrap();
3408 let parse = v2::from_json("\n{").unwrap_err();
3409 let error = load_ir_json(&bad).unwrap_err();
3410 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3411 assert_eq!(
3412 error.to_string(),
3413 format!("the IR snapshot is not valid IR v2 JSON: {parse}")
3414 );
3415 }
3416
3417 #[test]
3420 fn input_past_the_nesting_cap_draws_from_json_s_refusal() {
3421 let dir = tempfile::tempdir().unwrap();
3422 let deep = format!(
3423 "{}{}",
3424 "[".repeat(MAX_JSON_NESTING + 1),
3425 "]".repeat(MAX_JSON_NESTING + 1)
3426 );
3427 let path = dir.path().join("deep.ir.json");
3428 std::fs::write(&path, &deep).unwrap();
3429 let refusal = v2::from_json(&deep).unwrap_err().to_string();
3430 assert!(
3431 refusal.contains("the ceiling this reader enforces"),
3432 "{refusal}"
3433 );
3434 let error = load_ir_json(&path).unwrap_err();
3435 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3436 assert_eq!(
3437 error.to_string(),
3438 format!("the IR snapshot is not valid IR v2 JSON: {refusal}")
3439 );
3440 }
3441}