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 open: false,
1513 };
1514
1515 let warnings = v2::EnumSetDef {
1518 backing_enum: None,
1519 bits: vec![
1520 v2::EnumValue {
1521 name: "LOW_FUEL".to_string(),
1522 value: 0,
1523 doc: String::new(),
1524 links: Vec::new(),
1525 see: Vec::new(),
1526 since: Vec::new(),
1527 },
1528 v2::EnumValue {
1529 name: "ICE_RISK".to_string(),
1530 value: 33,
1531 doc: String::new(),
1532 links: Vec::new(),
1533 see: Vec::new(),
1534 since: Vec::new(),
1535 },
1536 ],
1537 width: v2::IntWidth::U64 as i32,
1538 open: false,
1539 };
1540
1541 let plate_text = v2::TypeDef {
1544 backing: Some(v2::Backing {
1545 kind: Some(v2::backing::Kind::Primitive(
1546 v2::PrimitiveType::String as i32,
1547 )),
1548 }),
1549 constraint: Some(v2::Constraint {
1550 min: None,
1551 max: None,
1552 step: None,
1553 len_min: Some(1),
1554 len_max: Some(86),
1555 pattern: None,
1556 pattern_const: None,
1557 }),
1558 declared_init: None,
1559 init: None,
1560 width: None,
1561 };
1562
1563 let sample = v2::UnionDef {
1565 arms: vec![
1566 v2::UnionArm {
1567 name: "speed".to_string(),
1568 ordinal: 1,
1569 type_ref: "Speed".to_string(),
1570 doc: String::new(),
1571 links: Vec::new(),
1572 see: Vec::new(),
1573 since: Vec::new(),
1574 },
1575 v2::UnionArm {
1576 name: "gear".to_string(),
1577 ordinal: 2,
1578 type_ref: "Gear".to_string(),
1579 doc: String::new(),
1580 links: Vec::new(),
1581 see: Vec::new(),
1582 since: Vec::new(),
1583 },
1584 ],
1585 is_result: false,
1586 reserved: Vec::new(),
1587 };
1588
1589 let retries = v2::Field {
1595 declared_init: Some("3".to_string()),
1596 init: Some(v2::InitValue {
1597 derivable: true,
1598 value: Some("3".to_string()),
1599 }),
1600 ..field(
1601 "retries",
1602 1,
1603 v2::FieldType {
1604 optional: false,
1605 kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
1606 backing: Some(v2::Backing {
1607 kind: Some(v2::backing::Kind::Primitive(
1608 v2::PrimitiveType::Integer as i32,
1609 )),
1610 }),
1611 constraint: Some(v2::Constraint {
1612 min: Some("0".to_string()),
1613 max: Some("24".to_string()),
1614 step: None,
1615 len_min: None,
1616 len_max: None,
1617 pattern: None,
1618 pattern_const: None,
1619 }),
1620 declared_init: None,
1621 init: None,
1622 width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
1623 }))),
1624 },
1625 )
1626 };
1627
1628 let position = field(
1631 "position",
1632 2,
1633 v2::FieldType {
1634 optional: false,
1635 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
1636 fields: vec![
1637 v2::TupleField {
1638 name: "x".to_string(),
1639 r#type: Some(named_type("Speed")),
1640 },
1641 v2::TupleField {
1642 name: "y".to_string(),
1643 r#type: Some(named_type("Speed")),
1644 },
1645 ],
1646 })),
1647 },
1648 );
1649
1650 let gears = field(
1652 "gears",
1653 3,
1654 v2::FieldType {
1655 optional: false,
1656 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
1657 element: Some(Box::new(named_type("Gear"))),
1658 min: 1,
1659 max: 4096,
1660 }))),
1661 },
1662 );
1663
1664 let plates = v2::Field {
1668 deprecated: Some("superseded by gears".to_string()),
1669 ..field(
1670 "plates",
1671 4,
1672 v2::FieldType {
1673 optional: false,
1674 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
1675 key: Some(Box::new(named_type("PlateText"))),
1676 value: Some(Box::new(named_type("Gear"))),
1677 min: 0,
1678 max: 53,
1679 }))),
1680 },
1681 )
1682 };
1683
1684 let snapshot = v2::StructDef {
1685 members: [retries, position, gears, plates]
1686 .into_iter()
1687 .map(|field| v2::StructMember {
1688 member: Some(v2::struct_member::Member::Field(Box::new(field))),
1689 })
1690 .collect(),
1691 fixed_layout: false,
1692 };
1693
1694 v2::Package {
1695 name: "veh.vocab".to_string(),
1696 decls: vec![
1697 decl("MAX_RETRY", v2::decl::Kind::ConstDef(max_retry)),
1698 decl("Gear", v2::decl::Kind::EnumDef(gear)),
1699 decl("Warnings", v2::decl::Kind::EnumSetDef(warnings)),
1700 decl("PlateText", v2::decl::Kind::TypeDef(plate_text)),
1701 v2::Decl {
1704 deprecated: Some("use Snapshot".to_string()),
1705 ..decl("Sample", v2::decl::Kind::UnionDef(sample))
1706 },
1707 decl("Snapshot", v2::decl::Kind::StructDef(snapshot)),
1708 ],
1709 interfaces: Vec::new(),
1710 services: Vec::new(),
1711 retired: Vec::new(),
1712 unit: String::new(),
1713 }
1714 }
1715
1716 #[test]
1717 fn protobuf_round_trip_preserves_package() {
1718 let package = fixture();
1719
1720 let buf = v2::to_binary(&package);
1721 let decoded = v2::from_binary(buf.as_slice()).expect("decode must succeed");
1722
1723 assert_eq!(package, decoded);
1724
1725 let vocabulary = vocabulary_fixture();
1727 let decoded_vocabulary =
1728 v2::from_binary(v2::to_binary(&vocabulary).as_slice()).expect("decode must succeed");
1729 assert_eq!(vocabulary, decoded_vocabulary);
1730
1731 let interface = &decoded.interfaces[0];
1732 let ordinals: Vec<u32> = interface.interactions.iter().map(|d| d.ordinal).collect();
1733 assert_eq!(
1734 ordinals,
1735 [1, 2, 3, 4, 5, 6],
1736 "one ordinal sequence, tombstone counted (ridl §11)"
1737 );
1738 let Some(v2::decl::Kind::ReservedSlot(tombstone)) = &interface.interactions[2].kind else {
1739 panic!("ordinal 3 must decode as a reserved tombstone");
1740 };
1741 assert_eq!(tombstone.name.as_deref(), Some("legacyMode"));
1742 let Some(v2::service_shape::Kind::Inline(inline)) = decoded.services[1]
1743 .shapes
1744 .first()
1745 .and_then(|slot| slot.kind.as_ref())
1746 else {
1747 panic!("veh.adas.logs must decode as an inline shape");
1748 };
1749 assert_eq!(inline.name, "", "an inline shape carries no name");
1750 let references: Vec<&str> = decoded.services[0]
1751 .shapes
1752 .iter()
1753 .filter_map(|slot| match &slot.kind {
1754 Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
1755 _ => None,
1756 })
1757 .collect();
1758 assert_eq!(
1759 references,
1760 ["VehicleStatus"],
1761 "a service's set carries its references and nothing else"
1762 );
1763 }
1764
1765 fn package_with_command(family: v2::Family, shape: v2::Shape) -> v2::Package {
1768 v2::Package {
1769 name: "app".to_string(),
1770 interfaces: vec![v2::Interface {
1771 name: "I".to_string(),
1772 visibility: v2::Visibility::Public as i32,
1773 interactions: vec![v2::Decl {
1774 name: "c".to_string(),
1775 visibility: v2::Visibility::Public as i32,
1776 ordinal: 1,
1777 kind: Some(v2::decl::Kind::CommandDef(v2::CommandDef {
1778 family: family as i32,
1779 shape: shape as i32,
1780 ..Default::default()
1781 })),
1782 ..Default::default()
1783 }],
1784 ..Default::default()
1785 }],
1786 ..Default::default()
1787 }
1788 }
1789
1790 #[test]
1793 fn the_readers_reject_an_invalid_interaction_class() {
1794 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1795 let json = v2::render_json(&package).expect("the raw writer renders it");
1796 let err = v2::from_json(&json).expect_err("the JSON reader must refuse it");
1797 assert!(
1798 err.to_string().contains("invalid interaction class"),
1799 "{err}"
1800 );
1801 let bytes = prost::Message::encode_to_vec(&package);
1802 let err = v2::from_binary(&bytes).expect_err("the binary reader must refuse it");
1803 assert!(
1804 err.to_string().contains("invalid interaction class"),
1805 "{err}"
1806 );
1807 }
1808
1809 #[test]
1810 fn the_json_writer_refuses_an_invalid_interaction_class() {
1811 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1812 let err = v2::to_json_pretty(&package).expect_err("the writer must refuse it");
1813 assert!(matches!(err, v2::SerializeError::Class(_)), "{err}");
1814 }
1815
1816 #[test]
1817 fn a_valid_class_round_trips_through_both_readers() {
1818 let package = package_with_command(v2::Family::Intent, v2::Shape::Toggle);
1819 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1820 assert_eq!(v2::from_json(&json).expect("the JSON parses"), package);
1821 assert_eq!(
1822 v2::from_binary(&v2::to_binary(&package)).expect("the binary decodes"),
1823 package
1824 );
1825 }
1826
1827 #[test]
1830 fn a_snapshot_without_family_reads_as_dispatch() {
1831 let package = package_with_command(v2::Family::Dispatch, v2::Shape::None);
1832 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1833 let mut value: serde_json::Value =
1834 serde_json::from_str(&json).expect("the rendering is JSON");
1835 let command = value["interfaces"][0]["interactions"][0]["commandDef"]
1836 .as_object_mut()
1837 .expect("the command object");
1838 assert!(command.remove("family").is_some(), "`family` was rendered");
1839 assert!(command.remove("shape").is_some(), "`shape` was rendered");
1840 let stripped = serde_json::to_string(&value).expect("the stripped value renders");
1841 let read = v2::from_json(&stripped).expect("a 0.9 snapshot reads");
1842 assert_eq!(read, package);
1843 }
1844
1845 #[test]
1846 fn json_round_trip_preserves_package() {
1847 for package in [fixture(), vocabulary_fixture()] {
1848 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1849 let decoded = v2::from_json(&json).expect("json deserialization must succeed");
1850
1851 assert_eq!(package, decoded);
1852 }
1853 }
1854
1855 #[test]
1862 fn number_provisional_and_retired_round_trip_through_json_text_and_binary() {
1863 let mut package = fixture();
1864 package.interfaces[0].number = 4;
1865 package.interfaces[0].provisional = true;
1866 let Some(v2::service_shape::Kind::Inline(inline)) =
1867 package.services[1].shapes[0].kind.as_mut()
1868 else {
1869 panic!("veh.adas.logs holds an inline shape in slot 1");
1870 };
1871 inline.number = 5;
1872 package.retired = vec![
1873 v2::RetiredInterface {
1874 name: "LaneAssist".to_string(),
1875 number: 2,
1876 },
1877 v2::RetiredInterface {
1878 name: "service:veh.hvac.cabin".to_string(),
1879 number: 3,
1880 },
1881 ];
1882
1883 let json = v2::to_json_pretty(&package).expect("the package serializes as IR JSON");
1884 assert_eq!(v2::from_json(&json).expect("the JSON parses back"), package);
1885 let text = v2::to_text_format(&package).expect("the package serializes as prototext");
1886 assert_eq!(
1887 v2::from_text_format(&text).expect("the prototext parses back"),
1888 package
1889 );
1890 assert_eq!(
1891 v2::from_binary(v2::to_binary(&package).as_slice()).expect("the binary decodes"),
1892 package
1893 );
1894
1895 for needle in [
1896 r#""number": 4"#,
1897 r#""provisional": true"#,
1898 r#""number": 5"#,
1899 r#""name": "LaneAssist""#,
1900 r#""name": "service:veh.hvac.cabin""#,
1901 ] {
1902 assert!(
1903 json.contains(needle),
1904 "the JSON must carry {needle}, got: {json}"
1905 );
1906 }
1907
1908 let unnumbered = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1912 for needle in [
1913 r#""number": 0"#,
1914 r#""provisional": false"#,
1915 r#""retired": []"#,
1916 ] {
1917 assert!(
1918 unnumbered.contains(needle),
1919 "a default field must still be written, expected {needle} in: {unnumbered}"
1920 );
1921 }
1922 }
1923
1924 #[test]
1930 fn a_snapshot_lacking_the_number_fields_still_loads() {
1931 let package = v2::from_json(
1932 r#"{"name": "veh.x", "interfaces": [{"name": "LaneKeeping"}], "services": [{"name": "veh.x.s", "shapes": [{"inline": {"name": ""}}]}]}"#,
1933 )
1934 .expect("a pre-lock snapshot loads");
1935
1936 assert_eq!(package.interfaces[0].number, 0);
1937 assert!(!package.interfaces[0].provisional);
1938 let Some(v2::service_shape::Kind::Inline(inline)) =
1939 package.services[0].shapes[0].kind.as_ref()
1940 else {
1941 panic!("the service holds an inline shape");
1942 };
1943 assert_eq!(inline.number, 0);
1944 assert!(!inline.provisional);
1945 assert_eq!(package.retired, Vec::new());
1946 }
1947
1948 #[test]
1953 fn text_format_round_trip_preserves_package() {
1954 for package in [fixture(), vocabulary_fixture()] {
1955 let text = v2::to_text_format(&package).expect("the fixture serializes as prototext");
1956 let decoded = v2::from_text_format(&text).expect("prototext parsing must succeed");
1957
1958 assert_eq!(package, decoded);
1959 }
1960 }
1961
1962 #[test]
1977 fn text_format_is_pretty_with_defaults_in_index_order() {
1978 let text = v2::to_text_format(&fixture()).expect("the fixture serializes as prototext");
1979
1980 assert!(
1982 text.contains("\n "),
1983 "pretty printing must indent nested fields, got: {text}"
1984 );
1985 assert!(
1988 text.contains("is_error: false"),
1989 "a field holding its default must be emitted, got: {text}"
1990 );
1991 assert!(
1994 text.starts_with("name:"),
1995 "fields must print in schema index order, got: {text}"
1996 );
1997 }
1998
1999 fn strict_parse(json: &str) -> v2::Package {
2006 let mut deserializer = serde_json::Deserializer::from_str(json);
2007 let package = <v2::Package as serde::Deserialize>::deserialize(&mut deserializer)
2008 .expect("a strict conformant parser must accept the emitted JSON");
2009 deserializer.end().expect("no trailing input");
2010 package
2011 }
2012
2013 #[test]
2018 fn emitted_json_survives_a_strict_conformant_parse() {
2019 for package in [fixture(), vocabulary_fixture()] {
2020 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
2021 assert_eq!(package, strict_parse(&json));
2022 }
2023 }
2024
2025 #[test]
2026 fn json_renders_timing_bounds_and_fallible_arms_exactly() {
2027 let json = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
2028
2029 assert!(
2033 json.contains(r#""minUs": "10000""#),
2034 "the timing bound must be a JSON string, got: {json}"
2035 );
2036 assert!(
2038 json.contains(r#""ok": "FaultPage""#),
2039 "the ok arm must render, got: {json}"
2040 );
2041 assert!(
2042 json.contains(r#""err": "DiagError""#),
2043 "the err arm must render, got: {json}"
2044 );
2045 }
2046
2047 #[test]
2052 fn json_renders_64_bit_integer_fields_as_strings() {
2053 let json = v2::to_json_pretty(&vocabulary_fixture())
2054 .expect("the vocabulary fixture serializes as IR JSON");
2055
2056 assert!(
2058 json.contains(r#""max": "4096""#),
2059 "an array bound must be a JSON string, got: {json}"
2060 );
2061 assert!(
2063 json.contains(r#""lenMax": "86""#),
2064 "a length bound must be a JSON string, got: {json}"
2065 );
2066 assert!(
2068 json.contains(r#""value": "7""#),
2069 "a retired enum value must be a JSON string, got: {json}"
2070 );
2071 assert!(
2072 json.contains(r#""value": "33""#),
2073 "an enum-set bit position must be a JSON string, got: {json}"
2074 );
2075 }
2076
2077 #[test]
2078 fn fallible_transport_identity_follows_the_derivation_rule() {
2079 let fallible = v2::FallibleType {
2082 ok: "FaultPage".to_string(),
2083 err: "DiagError".to_string(),
2084 };
2085 assert_eq!(
2086 v2::fallible_transport_identity("VehicleStatus", 9, &fallible),
2087 "VehicleStatus#9:FaultPage|DiagError"
2088 );
2089
2090 let package = fixture();
2092 let interface = &package.interfaces[0];
2093 let query_decl = &interface.interactions[4];
2094 let Some(v2::decl::Kind::QueryDef(query)) = &query_decl.kind else {
2095 panic!("ordinal 5 must be the fallible query");
2096 };
2097 let Some(v2::return_type::Kind::Fallible(arms)) = &query.return_type.as_ref().unwrap().kind
2098 else {
2099 panic!("fetchFaults must return a fallible type");
2100 };
2101 assert_eq!(
2102 v2::fallible_transport_identity(&interface.name, query_decl.ordinal, arms),
2103 "VehicleStatus#5:FaultPage|DiagError"
2104 );
2105 }
2106
2107 #[test]
2114 fn shapes_walks_named_interfaces_and_inline_service_shapes() {
2115 let package = fixture();
2116 let walk: Vec<(&str, bool, usize)> = package
2117 .shapes()
2118 .map(|shape| {
2119 (
2120 shape.name,
2121 shape.is_inline(),
2122 shape.interface.interactions.len(),
2123 )
2124 })
2125 .collect();
2126 assert_eq!(
2127 walk,
2128 [("VehicleStatus", false, 6), ("veh.adas.logs", true, 1)],
2129 "the named interface, then the inline shape under the service's \
2130 dotted name",
2131 );
2132
2133 assert_eq!(package.services.len(), 2, "one reference form, one inline");
2137 assert!(
2138 !package
2139 .shapes()
2140 .any(|shape| shape.name == "veh.adas.status"),
2141 "a service naming an interface contributes no shape of its own",
2142 );
2143 }
2144
2145 #[test]
2146 fn relative_name_strips_the_unit_prefix() {
2147 assert_eq!(v2::relative_name("u", "u", "Session"), "Session");
2148 assert_eq!(
2149 v2::relative_name("u", "u.cluster", "Speed"),
2150 "cluster.Speed"
2151 );
2152 assert_eq!(
2153 v2::relative_name("com.example.hmi", "com.example.hmi.cluster.front", "A"),
2154 "cluster.front.A"
2155 );
2156 }
2157
2158 #[test]
2161 fn relative_name_keeps_the_full_name_of_a_package_outside_the_unit() {
2162 assert_eq!(
2163 v2::relative_name("u", "w.cluster", "Speed"),
2164 "w.cluster.Speed"
2165 );
2166 assert_eq!(v2::relative_name("u", "ux", "A"), "ux.A");
2167 assert_eq!(v2::relative_name("u.cluster", "u", "Session"), "u.Session");
2168 }
2169
2170 #[test]
2174 fn unit_retired_reads_a_package_named_twice_once() {
2175 let first = v2::Package {
2176 name: "u.cluster".to_string(),
2177 unit: "u".to_string(),
2178 retired: vec![v2::RetiredInterface {
2179 name: "cluster.Old".to_string(),
2180 number: 3,
2181 }],
2182 ..Default::default()
2183 };
2184 let second = v2::Package {
2185 retired: vec![v2::RetiredInterface {
2186 name: "cluster.Older".to_string(),
2187 number: 4,
2188 }],
2189 ..first.clone()
2190 };
2191 assert_eq!(
2192 v2::unit_retired("u", &[&first, &second]),
2193 first.retired,
2194 "the first package of a name is the one read"
2195 );
2196 }
2197
2198 #[test]
2199 fn an_inline_shape_keeps_its_global_name() {
2200 let mut package = fixture();
2201 package.name = "u.cluster".to_string();
2202 package.unit = "u".to_string();
2203 let inline = package
2204 .shapes()
2205 .find(|shape| shape.is_inline())
2206 .expect("the fixture has an inline shape");
2207 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2208 let named = package
2209 .shapes()
2210 .find(|shape| !shape.is_inline())
2211 .expect("the fixture has a declared interface");
2212 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2213 }
2214
2215 #[test]
2216 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2217 let mut package = fixture();
2218 package.unit = String::new();
2219 let named = package
2220 .shapes()
2221 .find(|shape| !shape.is_inline())
2222 .expect("the fixture has a declared interface");
2223 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2224 }
2225
2226 #[test]
2227 fn unit_of_falls_back_to_the_name() {
2228 let bare = v2::Package {
2229 name: "p".to_string(),
2230 ..Default::default()
2231 };
2232 assert_eq!(v2::unit_of(&bare), "p");
2233 let member = v2::Package {
2234 name: "u.a".to_string(),
2235 unit: "u".to_string(),
2236 ..Default::default()
2237 };
2238 assert_eq!(v2::unit_of(&member), "u");
2239 }
2240
2241 #[test]
2242 fn packages_of_unit_selects_by_unit_of() {
2243 let package = |name: &str, unit: &str| v2::Package {
2244 name: name.to_string(),
2245 unit: unit.to_string(),
2246 ..Default::default()
2247 };
2248 let packages = [
2249 package("u", "u"),
2250 package("u.a", "u"),
2251 package("w", ""),
2252 package("other.b", "other"),
2253 ];
2254 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2255 .map(|p| p.name.as_str())
2256 .collect();
2257 assert_eq!(names, ["u", "u.a"]);
2258 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2259 .map(|p| p.name.as_str())
2260 .collect();
2261 assert_eq!(legacy, ["w"]);
2262 }
2263
2264 #[test]
2269 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2270 let package = fixture();
2271 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2272
2273 let named = shapes[0];
2274 assert!(named.service.is_none());
2275 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2276 assert_eq!(named.visibility(), named.interface.visibility);
2277
2278 let inline = shapes[1];
2279 assert_eq!(
2280 inline.interface.visibility,
2281 v2::Visibility::Unspecified as i32,
2282 "the trap: an inline shape's own visibility field is unset",
2283 );
2284 assert_eq!(
2285 inline.service.expect("an inline shape has an owner").name,
2286 "veh.adas.logs",
2287 );
2288 assert_eq!(
2289 inline.visibility(),
2290 v2::Visibility::Public as i32,
2291 "the accessor reads the owning service's, never the unset field",
2292 );
2293 }
2294
2295 #[test]
2299 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2300 let mut package = fixture();
2301 package.services.clear();
2302 assert_eq!(package.shapes().count(), 1);
2303
2304 package.interfaces.clear();
2305 assert_eq!(package.shapes().count(), 0);
2306 }
2307
2308 #[test]
2315 fn referenced_packages_finds_qualifiers_at_depth() {
2316 fn named(reference: &str) -> v2::FieldType {
2317 v2::FieldType {
2318 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2319 ..Default::default()
2320 }
2321 }
2322
2323 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2324 v2::decl::Kind::FixedDef(v2::FixedDef {
2325 payload: Some(payload),
2326 })
2327 }
2328
2329 let package = v2::Package {
2330 name: "veh.cluster".to_string(),
2331 decls: vec![
2332 v2::Decl {
2333 name: "Local".to_string(),
2334 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2335 payload: "Speed".to_string(),
2336 ..Default::default()
2337 })),
2338 ..Default::default()
2339 },
2340 v2::Decl {
2341 name: "Stamped".to_string(),
2342 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2343 payload: "ridl.std.Timestamp".to_string(),
2344 ..Default::default()
2345 })),
2346 ..Default::default()
2347 },
2348 v2::Decl {
2349 name: "ArrLabels".to_string(),
2350 kind: Some(fixed(v2::FieldType {
2351 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2352 element: Some(Box::new(named("veh.arr.Label"))),
2353 min: 0,
2354 max: 32,
2355 }))),
2356 ..Default::default()
2357 })),
2358 ..Default::default()
2359 },
2360 v2::Decl {
2361 name: "TupThing".to_string(),
2362 kind: Some(fixed(v2::FieldType {
2363 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2364 fields: vec![v2::TupleField {
2365 name: "x".to_string(),
2366 r#type: Some(named("veh.tup.X")),
2367 }],
2368 })),
2369 ..Default::default()
2370 })),
2371 ..Default::default()
2372 },
2373 v2::Decl {
2374 name: "MapThing".to_string(),
2375 kind: Some(fixed(v2::FieldType {
2376 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2377 key: Some(Box::new(named("veh.key.X"))),
2378 value: Some(Box::new(named("veh.val.X"))),
2379 min: 0,
2380 max: 8,
2381 }))),
2382 ..Default::default()
2383 })),
2384 ..Default::default()
2385 },
2386 v2::Decl {
2387 name: "StreamThing".to_string(),
2388 kind: Some(fixed(v2::FieldType {
2389 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2390 element: Some(v2::stream_type::Element::Named(
2391 "veh.strm.X".to_string(),
2392 )),
2393 })),
2394 ..Default::default()
2395 })),
2396 ..Default::default()
2397 },
2398 ],
2399 ..Default::default()
2400 };
2401
2402 let found = v2::referenced_packages(&package);
2403 let expected: std::collections::BTreeSet<String> = [
2404 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2405 ]
2406 .into_iter()
2407 .map(str::to_string)
2408 .collect();
2409 assert_eq!(
2410 found, expected,
2411 "each recursive path must contribute its own distinct qualifier"
2412 );
2413 assert!(
2414 !found.contains("Speed") && !found.contains("veh.cluster"),
2415 "a bare reference contributes no package: {found:?}",
2416 );
2417 }
2418
2419 #[test]
2422 fn referenced_packages_is_empty_without_references() {
2423 let package = v2::Package {
2424 name: "veh.solo".to_string(),
2425 ..Default::default()
2426 };
2427 assert!(v2::referenced_packages(&package).is_empty());
2428 }
2429
2430 const NESTING_BELOW_LIMIT: usize = 45;
2436 const NESTING_PAST_LIMIT: usize = 60;
2440
2441 fn nested_package(depth: usize) -> v2::Package {
2446 let mut payload = v2::FieldType {
2447 optional: false,
2448 kind: Some(v2::field_type::Kind::Primitive(
2449 v2::PrimitiveType::Integer as i32,
2450 )),
2451 };
2452 for _ in 0..depth {
2453 payload = v2::FieldType {
2454 optional: false,
2455 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2456 element: Some(Box::new(payload)),
2457 min: 1,
2458 max: 1,
2459 }))),
2460 };
2461 }
2462 v2::Package {
2463 name: "veh.deep".to_string(),
2464 decls: vec![v2::Decl {
2465 name: "deep".to_string(),
2466 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2467 payload: Some(payload),
2468 })),
2469 ..Default::default()
2470 }],
2471 ..Default::default()
2472 }
2473 }
2474
2475 fn package_at_message_depth(levels: usize) -> v2::Package {
2487 assert!(levels >= 3, "the chain costs three levels before nesting");
2488 let (arrays, tuple) = if levels % 2 == 1 {
2489 ((levels - 3) / 2, false)
2490 } else {
2491 assert!(levels >= 6, "one tuple level costs three");
2492 ((levels - 6) / 2, true)
2493 };
2494
2495 let mut payload = v2::FieldType {
2496 optional: false,
2497 kind: Some(v2::field_type::Kind::Primitive(
2498 v2::PrimitiveType::Integer as i32,
2499 )),
2500 };
2501 for _ in 0..arrays {
2502 payload = v2::FieldType {
2503 optional: false,
2504 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2505 element: Some(Box::new(payload)),
2506 min: 1,
2507 max: 1,
2508 }))),
2509 };
2510 }
2511 if tuple {
2512 payload = v2::FieldType {
2513 optional: false,
2514 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2515 fields: vec![v2::TupleField {
2516 name: "f0".to_string(),
2517 r#type: Some(payload),
2518 }],
2519 })),
2520 };
2521 }
2522 v2::Package {
2523 name: "veh.deep".to_string(),
2524 decls: vec![v2::Decl {
2525 name: "deep".to_string(),
2526 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2527 payload: Some(payload),
2528 })),
2529 ..Default::default()
2530 }],
2531 ..Default::default()
2532 }
2533 }
2534
2535 fn message_nesting(json: &str) -> usize {
2542 let (mut depth, mut max) = (0usize, 0usize);
2543 let (mut in_string, mut escaped) = (false, false);
2544 for byte in json.bytes() {
2545 if in_string {
2546 if escaped {
2547 escaped = false;
2548 } else if byte == b'\\' {
2549 escaped = true;
2550 } else if byte == b'"' {
2551 in_string = false;
2552 }
2553 continue;
2554 }
2555 match byte {
2556 b'"' => in_string = true,
2557 b'{' => {
2558 depth += 1;
2559 max = max.max(depth);
2560 }
2561 b'}' => depth = depth.saturating_sub(1),
2562 _ => {}
2563 }
2564 }
2565 max
2566 }
2567
2568 #[test]
2572 fn package_at_message_depth_builds_the_depth_it_names() {
2573 with_sized_stack(|| {
2574 for levels in [3, 6, 7, 99, 100, 101] {
2575 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2576 .expect("the writer is unrestricted at these depths");
2577 assert_eq!(
2578 message_nesting(&json) - 1,
2579 levels,
2580 "the chain must nest {levels} message levels below the root"
2581 );
2582 }
2583 });
2584 }
2585
2586 #[test]
2595 fn binary_round_trip_at_100_message_levels_succeeds() {
2596 let package = package_at_message_depth(100);
2597 let bytes = v2::to_binary(&package);
2598 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2599 assert_eq!(package, decoded);
2600 }
2601
2602 #[test]
2607 fn binary_decode_at_101_message_levels_returns_an_error() {
2608 let package = package_at_message_depth(101);
2609 let bytes = v2::to_binary(&package);
2610 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2611 assert!(
2612 error.to_string().contains("recursion limit reached"),
2613 "the error must name the limit, got: {error}"
2614 );
2615
2616 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2617 assert_eq!(
2618 package,
2619 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2620 );
2621 }
2622
2623 #[test]
2632 fn json_round_trip_at_400_nested_levels_succeeds() {
2633 with_sized_stack(|| {
2634 let package = nested_package(400);
2635 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2636 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2637 assert_eq!(package, decoded);
2638 });
2639 }
2640
2641 #[test]
2649 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2650 let mut package = fixture();
2651 package.decls[0].visibility = 999;
2652 let err = v2::to_json_pretty(&package)
2653 .expect_err("an out-of-schema discriminant must fail, not panic");
2654 let message = err.to_string();
2655 assert!(
2656 message.contains("canonical protobuf JSON"),
2657 "the error must name the encoding that failed, got: {message}"
2658 );
2659 assert!(
2660 message.contains("discriminant outside the schema"),
2661 "the error must name the known cause, got: {message}"
2662 );
2663 }
2664
2665 #[test]
2670 fn json_parse_rejects_an_unknown_field() {
2671 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2672 .expect_err("an unknown field must be rejected");
2673 assert!(
2674 error.to_string().contains("unknown field"),
2675 "the error must name the defect, got: {error}"
2676 );
2677 }
2678
2679 #[test]
2688 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2689 let with_visibility =
2690 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2691 v2::from_json(&with_visibility("1"))
2692 .expect("an in-range numeric enum value parses, as the mapping expects");
2693 let error = v2::from_json(&with_visibility("77"))
2694 .expect_err("an out-of-range numeric enum value must be rejected");
2695 assert!(
2696 error.to_string().contains("invalid value: integer `77`"),
2697 "the error must name the value, got: {error}"
2698 );
2699 }
2700
2701 #[test]
2707 fn json_parse_rejects_a_float_form_integer() {
2708 for spelling in ["1.0", "1e0"] {
2709 let error = v2::from_json(&format!(
2710 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2711 ))
2712 .expect_err("a float-form integer must be rejected");
2713 assert!(
2714 error.to_string().contains("did not match any variant"),
2715 "the integer field's deserializer must be the one refusing `{spelling}`, \
2716 got: {error}"
2717 );
2718 }
2719 }
2720
2721 #[test]
2728 fn json_parse_rejects_null_for_a_repeated_field() {
2729 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2730 .expect_err("null for a repeated field must be rejected");
2731 assert!(
2732 error.to_string().contains("invalid type: null"),
2733 "the error must name the null, got: {error}"
2734 );
2735 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2736 .expect("null for an optional scalar field still parses");
2737 }
2738
2739 #[test]
2743 fn json_parse_rejects_a_duplicate_key() {
2744 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2745 .expect_err("a duplicate key must be rejected");
2746 assert!(
2747 error.to_string().contains("duplicate field `name`"),
2748 "the error must name the duplicated field, got: {error}"
2749 );
2750 }
2751
2752 #[test]
2759 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2760 with_sized_stack(|| {
2761 let json = v2::to_json_pretty(&nested_package(500))
2762 .expect("the writer is unrestricted at this depth");
2763 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2764 assert!(
2765 error.to_string().contains("1000 JSON levels"),
2766 "the error must name the ceiling, got: {error}"
2767 );
2768 });
2769 }
2770
2771 #[test]
2776 fn json_nesting_ceiling_binds_exactly_at_1000() {
2777 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2778 assert!(
2779 !at.to_string().contains("JSON levels"),
2780 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2781 );
2782
2783 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2784 assert!(
2785 past.to_string().contains("1000 JSON levels"),
2786 "past the ceiling the error must name it, got: {past}"
2787 );
2788 }
2789
2790 #[test]
2795 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2796 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2798 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2800 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2803 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2806 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2808 }
2809
2810 fn nested_text(depth: usize) -> String {
2814 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2815 for _ in 0..depth {
2816 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2817 }
2818 format!(
2819 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2820 )
2821 }
2822
2823 #[test]
2827 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2828 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2829 .expect_err("serialization past the recursion limit must fail, not panic");
2830 let message = err.to_string();
2831 assert!(
2832 message.contains("recursion limit"),
2833 "the error must name the nesting limit as the known cause, got: {message}"
2834 );
2835 assert!(
2836 message.contains("prototext"),
2837 "the error must name the encoding that failed, got: {message}"
2838 );
2839 }
2840
2841 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2854 let outcome = std::thread::Builder::new()
2855 .stack_size(16 * 1024 * 1024)
2856 .spawn(test)
2857 .expect("spawn the large-stack test thread")
2858 .join();
2859 if let Err(payload) = outcome {
2860 std::panic::resume_unwind(payload);
2861 }
2862 }
2863
2864 #[test]
2868 fn text_parse_past_the_nesting_limit_returns_an_error() {
2869 with_sized_stack(|| {
2870 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2871 .expect_err("parsing past the recursion limit must fail, not panic");
2872
2873 let message = error.to_string();
2877 assert!(
2878 message.contains("recursion limit reached"),
2879 "the transcode out of the dynamic message must be the failing \
2880 stage, got: {message}"
2881 );
2882 });
2883 }
2884
2885 #[test]
2888 fn text_round_trip_below_the_nesting_limit_succeeds() {
2889 with_sized_stack(|| {
2890 let package = nested_package(NESTING_BELOW_LIMIT);
2891 let text = v2::to_text_format(&package)
2892 .expect("below the recursion limit, serialization succeeds");
2893 let decoded =
2894 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2895 assert_eq!(package, decoded);
2896 });
2897 }
2898}
2899
2900#[cfg(test)]
2901mod vacuous_constraint {
2902 use crate::v2;
2903
2904 fn constraint() -> v2::Constraint {
2907 v2::Constraint {
2908 min: None,
2909 max: None,
2910 step: None,
2911 len_min: None,
2912 len_max: None,
2913 pattern: None,
2914 pattern_const: None,
2915 }
2916 }
2917
2918 #[test]
2919 fn an_absent_or_empty_constraint_is_vacuous() {
2920 assert!(v2::constraint_is_vacuous(None));
2921 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2922 }
2923
2924 #[test]
2925 fn a_step_constraint_is_non_vacuous() {
2926 let stepped = v2::Constraint {
2927 step: Some("0.5".to_string()),
2928 ..constraint()
2929 };
2930 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2931 }
2932
2933 #[test]
2940 fn any_single_constrained_field_is_non_vacuous() {
2941 let cases = [
2942 (
2943 "min",
2944 v2::Constraint {
2945 min: Some("0.0".to_string()),
2946 ..constraint()
2947 },
2948 ),
2949 (
2950 "max",
2951 v2::Constraint {
2952 max: Some("250.0".to_string()),
2953 ..constraint()
2954 },
2955 ),
2956 (
2957 "len_min",
2958 v2::Constraint {
2959 len_min: Some(1),
2960 ..constraint()
2961 },
2962 ),
2963 (
2964 "len_max",
2965 v2::Constraint {
2966 len_max: Some(256),
2967 ..constraint()
2968 },
2969 ),
2970 (
2971 "pattern",
2972 v2::Constraint {
2973 pattern: Some("^[a-z]+$".to_string()),
2974 ..constraint()
2975 },
2976 ),
2977 (
2978 "pattern_const",
2979 v2::Constraint {
2980 pattern_const: Some("NAME_PATTERN".to_string()),
2981 ..constraint()
2982 },
2983 ),
2984 ];
2985 for (field, case) in cases {
2986 assert!(
2987 !v2::constraint_is_vacuous(Some(&case)),
2988 "`{field}` alone must be non-vacuous"
2989 );
2990 }
2991 }
2992
2993 #[test]
3002 fn a_bound_pair_set_together_is_non_vacuous() {
3003 let ranged = v2::Constraint {
3004 min: Some("0.0".to_string()),
3005 max: Some("250.0".to_string()),
3006 ..constraint()
3007 };
3008 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
3009
3010 let default_length = v2::Constraint {
3011 len_min: Some(0),
3012 len_max: Some(256),
3013 ..constraint()
3014 };
3015 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
3016 }
3017}
3018
3019#[cfg(test)]
3020mod system_round_trip {
3021 use crate::v2;
3022
3023 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
3024 v2::Attribute {
3025 namespace: namespace.to_string(),
3026 key: key.to_string(),
3027 value,
3028 }
3029 }
3030
3031 fn scalar(text: &str) -> v2::AttributeValue {
3032 v2::AttributeValue {
3033 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
3034 }
3035 }
3036
3037 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
3038 v2::AttributeValue {
3039 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
3040 }
3041 }
3042
3043 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
3044 Some(v2::InterfaceRef {
3045 catalog: catalog.to_string(),
3046 name: name.to_string(),
3047 inline,
3048 })
3049 }
3050
3051 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
3052 v2::Endpoint {
3053 component: component.to_string(),
3054 instance: instance.to_string(),
3055 machine: machine.to_string(),
3056 }
3057 }
3058
3059 fn doc_link() -> v2::DocLink {
3067 v2::DocLink {
3068 text: "Cruise".to_string(),
3069 offset: 16,
3070 len: 6,
3071 target: "veh.topology.Cruise".to_string(),
3072 }
3073 }
3074
3075 fn fixture() -> v2::System {
3076 let link = v2::Link {
3077 interface: interface("veh.diag", "veh.diag.access", true),
3078 service: "veh.diag.access".to_string(),
3079 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
3080 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
3081 crossing: v2::Crossing::OffBoard as i32,
3082 };
3083 v2::System {
3084 name: "Vehicle".to_string(),
3085 package: "veh.topology".to_string(),
3086 labels: vec!["ASIL_B".to_string()],
3087 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
3088 members: vec![
3089 v2::MemberLine {
3090 component: "veh.topology.Cruise".to_string(),
3091 attributes: vec![attribute("linux", "pinned", None)],
3092 doc: "Documented, see [Cruise].".to_string(),
3093 links: vec![doc_link()],
3094 see: vec![doc_link()],
3095 since: vec!["1.2".to_string()],
3096 },
3097 v2::MemberLine {
3098 component: "veh.diag.access".to_string(),
3099 attributes: vec![],
3100 doc: "Documented, see [Cruise].".to_string(),
3101 links: vec![doc_link()],
3102 see: vec![doc_link()],
3103 since: vec!["1.2".to_string()],
3104 },
3105 ],
3106 components: vec![
3107 v2::Component {
3108 name: "Cruise".to_string(),
3109 package: "veh.topology".to_string(),
3110 implicit: false,
3111 external: true,
3112 instances: vec!["primary".to_string(), "backup".to_string()],
3113 numbers: vec![
3114 v2::InstanceNumber {
3115 instance: "primary".to_string(),
3116 number: 1,
3117 provisional: false,
3118 },
3119 v2::InstanceNumber {
3120 instance: "backup".to_string(),
3121 number: 2,
3122 provisional: true,
3123 },
3124 ],
3125 offers: vec![v2::Offer {
3126 service: "veh.adas.cruise".to_string(),
3127 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
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 requires: vec![v2::Require {
3134 interface: interface("veh.adas", "LaneAssist", false),
3135 service: "veh.adas.lane".to_string(),
3136 producer: "veh.topology.Lane".to_string(),
3137 attributes: vec![attribute(
3138 "linux",
3139 "cpuset",
3140 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
3141 )],
3142 doc: "Documented, see [Cruise].".to_string(),
3143 links: vec![doc_link()],
3144 see: vec![doc_link()],
3145 since: vec!["1.2".to_string()],
3146 }],
3147 labels: vec!["ASIL_B".to_string()],
3148 attributes: vec![attribute("rust", "crate", None)],
3149 doc: "Documented, see [Cruise].".to_string(),
3150 links: vec![doc_link()],
3151 see: vec![doc_link()],
3152 since: vec!["1.2".to_string()],
3153 deprecated: Some("superseded".to_string()),
3154 },
3155 v2::Component {
3156 name: "veh.diag.access".to_string(),
3157 package: String::new(),
3158 implicit: true,
3159 external: false,
3160 instances: vec!["Unit".to_string()],
3161 numbers: vec![v2::InstanceNumber {
3162 instance: "Unit".to_string(),
3163 number: 3,
3164 provisional: false,
3165 }],
3166 offers: vec![v2::Offer {
3167 service: "veh.diag.access".to_string(),
3168 attributes: vec![],
3169 doc: "Documented, see [Cruise].".to_string(),
3170 links: vec![doc_link()],
3171 see: vec![doc_link()],
3172 since: vec!["1.2".to_string()],
3173 }],
3174 requires: vec![],
3175 labels: vec![],
3176 attributes: vec![],
3177 doc: "Documented, see [Cruise].".to_string(),
3178 links: vec![doc_link()],
3179 see: vec![doc_link()],
3180 since: vec!["1.2".to_string()],
3181 deprecated: Some("superseded".to_string()),
3182 },
3183 ],
3184 producers: vec![v2::Producer {
3185 service: "veh.adas.cruise".to_string(),
3186 component: "veh.topology.Cruise".to_string(),
3187 instances: vec!["primary".to_string(), "backup".to_string()],
3188 not_yet_realizable: true,
3189 }],
3190 grants: vec![v2::Grant {
3191 component: "veh.topology.Backend".to_string(),
3192 external: true,
3193 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3194 }],
3195 regions: vec![v2::Region {
3196 catalog: "veh.diag".to_string(),
3197 hash: vec![0xab; 32],
3198 interfaces: vec![v2::RegionInterface {
3199 name: "veh.diag.access".to_string(),
3200 inline: true,
3201 number: 2,
3202 provisional: true,
3203 service: "veh.diag.access".to_string(),
3204 }],
3205 }],
3206 distributions: vec![v2::Distribution {
3207 name: "Adas".to_string(),
3208 package: "veh.topology".to_string(),
3209 members: vec![v2::MemberLine {
3210 component: "veh.topology.Cruise".to_string(),
3211 attributes: vec![],
3212 doc: "Documented, see [Cruise].".to_string(),
3213 links: vec![doc_link()],
3214 see: vec![doc_link()],
3215 since: vec!["1.2".to_string()],
3216 }],
3217 depends_on: vec!["veh.topology.Base".to_string()],
3218 labels: vec!["PLATFORM_BUNDLE".to_string()],
3219 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3220 doc: "Documented, see [Cruise].".to_string(),
3221 links: vec![doc_link()],
3222 see: vec![doc_link()],
3223 since: vec!["1.2".to_string()],
3224 tier: v2::Tier::Platform as i32,
3225 deprecated: Some("superseded".to_string()),
3226 }],
3227 deployments: vec![v2::Deployment {
3228 name: "Production".to_string(),
3229 package: "veh.topology".to_string(),
3230 labels: vec!["FLEET".to_string()],
3231 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3232 machines: vec![v2::Machine {
3233 name: "Cloud".to_string(),
3234 external: true,
3235 labels: vec!["OFF_BOARD".to_string()],
3236 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3237 doc: "Documented, see [Cruise].".to_string(),
3238 links: vec![doc_link()],
3239 see: vec![doc_link()],
3240 since: vec!["1.2".to_string()],
3241 deprecated: Some("superseded".to_string()),
3242 }],
3243 placements: vec![v2::Placement {
3244 component: "veh.topology.Cruise".to_string(),
3245 instance: "backup".to_string(),
3246 machine: "Cockpit".to_string(),
3247 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3248 sizing: Some(v2::Sizing {
3249 depth: Some(4_294_967_295),
3250 slots: Some(65_536),
3251 budget: Some(18_446_744_073_709_551_615),
3252 }),
3253 }],
3254 links: vec![link.clone()],
3255 routes: vec![v2::Route {
3256 catalog: "veh.adas".to_string(),
3257 interface_number: 2,
3258 member_ordinal: 1,
3259 interface: "LaneAssist".to_string(),
3260 member: "active".to_string(),
3261 service: "veh.adas.lane".to_string(),
3262 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3263 }],
3264 surface: vec![v2::Surface {
3265 link: Some(link),
3266 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3267 }],
3268 installations: vec![v2::Installation {
3269 distribution: "veh.topology.Adas".to_string(),
3270 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3271 }],
3272 doc: "Documented, see [Cruise].".to_string(),
3273 doc_links: vec![doc_link()],
3274 see: vec![doc_link()],
3275 since: vec!["1.2".to_string()],
3276 sizing: Some(v2::Sizing {
3277 depth: Some(3),
3278 slots: Some(8),
3279 budget: Some(4096),
3280 }),
3281 deprecated: Some("superseded".to_string()),
3282 }],
3283 doc: "Documented, see [Cruise].".to_string(),
3284 see: vec![doc_link()],
3285 since: vec!["1.2".to_string()],
3286 links: vec![doc_link()],
3287 deprecated: Some("superseded".to_string()),
3288 }
3289 }
3290
3291 #[test]
3292 fn system_binary_round_trip_preserves_system() {
3293 let system = fixture();
3294 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3295 .expect("decode must succeed");
3296 assert_eq!(system, decoded);
3297 }
3298
3299 #[test]
3304 fn system_json_round_trip_preserves_system() {
3305 let system = fixture();
3306 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3307 assert!(
3308 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3309 "bytes render as base64, got:\n{json}"
3310 );
3311 assert!(
3312 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3313 "enums render by name, got:\n{json}"
3314 );
3315 assert!(
3316 json.contains("\"notYetRealizable\": true"),
3317 "fields render in lowerCamelCase, got:\n{json}"
3318 );
3319 assert!(
3320 json.contains("\"budget\": \"18446744073709551615\""),
3321 "a 64-bit value renders as a string, got:\n{json}"
3322 );
3323 assert_eq!(
3324 system,
3325 v2::system_from_json(&json).expect("the JSON parses")
3326 );
3327 assert!(
3328 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3329 "an unknown field is rejected"
3330 );
3331 }
3332
3333 #[test]
3337 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3338 let mut absent = fixture();
3339 absent.deployments[0].sizing = None;
3340 absent.deployments[0].placements[0].sizing = None;
3341 let mut empty = absent.clone();
3342 empty.deployments[0].sizing = Some(v2::Sizing::default());
3343 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3344 for system in [&absent, &empty] {
3345 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3346 .expect("decode must succeed");
3347 assert_eq!(system, &binary);
3348 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3349 assert_eq!(
3350 system,
3351 &v2::system_from_json(&json).expect("the JSON parses")
3352 );
3353 }
3354 assert_ne!(
3355 v2::system_to_binary(&absent),
3356 v2::system_to_binary(&empty),
3357 "the binary encodings differ"
3358 );
3359 }
3360
3361 #[test]
3362 fn system_text_format_round_trip_preserves_system() {
3363 let system = fixture();
3364 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3365 assert!(
3366 text.starts_with("name:"),
3367 "fields print in schema index order, got: {text}"
3368 );
3369 assert_eq!(
3370 system,
3371 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3372 );
3373 }
3374
3375 #[test]
3378 fn qualified_names_follow_the_one_derivation() {
3379 let system = fixture();
3380 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3381 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3382 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3383 assert_eq!(
3384 system.distributions[0].qualified_name(),
3385 "veh.topology.Adas"
3386 );
3387 }
3388}
3389
3390#[cfg(test)]
3391mod load_ir_json {
3392 use crate::v2::{self, LoadError, MAX_JSON_NESTING, load_ir_json};
3393
3394 #[test]
3396 fn a_snapshot_file_loads() {
3397 let dir = tempfile::tempdir().unwrap();
3398 let package = v2::Package {
3399 name: "veh.cabin".to_string(),
3400 ..v2::Package::default()
3401 };
3402 let path = dir.path().join("cabin.ir.json");
3403 std::fs::write(&path, v2::to_json_pretty(&package).unwrap()).unwrap();
3404 assert_eq!(load_ir_json(&path).unwrap(), package);
3405 }
3406
3407 #[test]
3411 fn a_missing_file_is_io_and_bad_json_is_parse() {
3412 let dir = tempfile::tempdir().unwrap();
3413
3414 let missing = dir.path().join("missing.ir.json");
3415 let io = std::fs::read_to_string(&missing).unwrap_err();
3416 let error = load_ir_json(&missing).unwrap_err();
3417 assert!(matches!(error, LoadError::Io(_)), "{error:?}");
3418 assert_eq!(
3419 error.to_string(),
3420 format!("cannot read the IR snapshot: {io}")
3421 );
3422
3423 let bad = dir.path().join("bad.ir.json");
3426 std::fs::write(&bad, "\n{").unwrap();
3427 let parse = v2::from_json("\n{").unwrap_err();
3428 let error = load_ir_json(&bad).unwrap_err();
3429 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3430 assert_eq!(
3431 error.to_string(),
3432 format!("the IR snapshot is not valid IR v2 JSON: {parse}")
3433 );
3434 }
3435
3436 #[test]
3439 fn input_past_the_nesting_cap_draws_from_json_s_refusal() {
3440 let dir = tempfile::tempdir().unwrap();
3441 let deep = format!(
3442 "{}{}",
3443 "[".repeat(MAX_JSON_NESTING + 1),
3444 "]".repeat(MAX_JSON_NESTING + 1)
3445 );
3446 let path = dir.path().join("deep.ir.json");
3447 std::fs::write(&path, &deep).unwrap();
3448 let refusal = v2::from_json(&deep).unwrap_err().to_string();
3449 assert!(
3450 refusal.contains("the ceiling this reader enforces"),
3451 "{refusal}"
3452 );
3453 let error = load_ir_json(&path).unwrap_err();
3454 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3455 assert_eq!(
3456 error.to_string(),
3457 format!("the IR snapshot is not valid IR v2 JSON: {refusal}")
3458 );
3459 }
3460}