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 name: name.to_string(),
1178 visibility: v2::Visibility::Unspecified as i32,
1179 is_error: false,
1180 doc: String::new(),
1181 labels: Vec::new(),
1182 deprecated: None,
1183 ordinal,
1184 kind: Some(kind),
1185 links: Vec::new(),
1186 see: Vec::new(),
1187 since: Vec::new(),
1188 }
1189 }
1190
1191 fn named_type(name: &str) -> v2::FieldType {
1192 v2::FieldType {
1193 optional: false,
1194 kind: Some(v2::field_type::Kind::Named(name.to_string())),
1195 }
1196 }
1197
1198 fn stream_of(element: v2::stream_type::Element) -> v2::FieldType {
1199 v2::FieldType {
1200 optional: false,
1201 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
1202 element: Some(element),
1203 })),
1204 }
1205 }
1206
1207 fn fixture() -> v2::Package {
1213 let speed = v2::SignalDef {
1216 payload: "Speed".to_string(),
1217 declared_init: None,
1218 init: Some(v2::InitValue {
1219 derivable: true,
1220 value: Some("0.0".to_string()),
1221 }),
1222 timing: Some(v2::Timing {
1223 mode: v2::TimingMode::StrictPeriodic as i32,
1224 min_us: Some("10000".to_string()),
1225 max_us: Some("10000".to_string()),
1226 default_applied: false,
1227 }),
1228 family: v2::Family::Dispatch as i32,
1229 };
1230
1231 let door_opened = v2::EventDef {
1234 payload: "DoorEvent".to_string(),
1235 timing: Some(v2::Timing {
1236 mode: v2::TimingMode::Range as i32,
1237 min_us: Some("20000".to_string()),
1238 max_us: Some("500000".to_string()),
1239 default_applied: true,
1240 }),
1241 family: v2::Family::Dispatch as i32,
1242 };
1243
1244 let set_target = v2::CommandDef {
1246 params: vec![v2::Param {
1247 name: "target".to_string(),
1248 r#type: Some(named_type("Speed")),
1249 doc: String::new(),
1250 links: Vec::new(),
1251 see: Vec::new(),
1252 since: Vec::new(),
1253 }],
1254 contracts: vec![v2::Contract {
1255 kind: v2::ContractKind::Require as i32,
1256 source: "target >= speed".to_string(),
1257 signal_refs: vec!["speed".to_string()],
1258 param_refs: vec!["target".to_string()],
1259 uses_result: false,
1260 observer_id: "VehicleStatus.setTarget.require[0]".to_string(),
1261 }],
1262 timing: None,
1263 family: v2::Family::Dispatch as i32,
1264 shape: v2::Shape::None as i32,
1265 };
1266
1267 let fetch_faults = v2::QueryDef {
1270 params: vec![v2::Param {
1271 name: "page".to_string(),
1272 r#type: Some(named_type("PageSpec")),
1273 doc: String::new(),
1274 links: Vec::new(),
1275 see: Vec::new(),
1276 since: Vec::new(),
1277 }],
1278 return_type: Some(v2::ReturnType {
1279 kind: Some(v2::return_type::Kind::Fallible(v2::FallibleType {
1280 ok: "FaultPage".to_string(),
1281 err: "DiagError".to_string(),
1282 })),
1283 }),
1284 contracts: vec![v2::Contract {
1285 kind: v2::ContractKind::Ensure as i32,
1286 source: "result.count <= page.limit".to_string(),
1287 signal_refs: Vec::new(),
1288 param_refs: vec!["page".to_string()],
1289 uses_result: true,
1290 observer_id: "VehicleStatus.fetchFaults.ensure[0]".to_string(),
1291 }],
1292 timing: None,
1293 family: v2::Family::Dispatch as i32,
1294 };
1295
1296 let vin = v2::FixedDef {
1298 payload: Some(named_type("Vin")),
1299 };
1300
1301 let vehicle_status = v2::Interface {
1302 name: "VehicleStatus".to_string(),
1303 visibility: v2::Visibility::Public as i32,
1304 doc: "Vehicle status contract".to_string(),
1305 labels: Vec::new(),
1306 deprecated: None,
1307 interactions: vec![
1308 interaction("speed", 1, v2::decl::Kind::SignalDef(speed)),
1309 interaction("doorOpened", 2, v2::decl::Kind::EventDef(door_opened)),
1310 v2::Decl {
1313 ordinal: 3,
1314 kind: Some(v2::decl::Kind::ReservedSlot(v2::Reserved {
1315 ordinal: 3,
1316 name: Some("legacyMode".to_string()),
1317 value: None,
1318 })),
1319 ..interaction("", 3, v2::decl::Kind::ReservedSlot(v2::Reserved::default()))
1320 },
1321 interaction("setTarget", 4, v2::decl::Kind::CommandDef(set_target)),
1322 interaction("fetchFaults", 5, v2::decl::Kind::QueryDef(fetch_faults)),
1323 interaction("vin", 6, v2::decl::Kind::FixedDef(vin)),
1324 ],
1325 number: 0,
1326 provisional: false,
1327 links: Vec::new(),
1328 see: Vec::new(),
1329 since: Vec::new(),
1330 };
1331
1332 let tail_logs = v2::QueryDef {
1335 params: vec![v2::Param {
1336 name: "pattern".to_string(),
1337 r#type: Some(stream_of(v2::stream_type::Element::Primitive(
1338 v2::PrimitiveType::String as i32,
1339 ))),
1340 doc: String::new(),
1341 links: Vec::new(),
1342 see: Vec::new(),
1343 since: Vec::new(),
1344 }],
1345 return_type: Some(v2::ReturnType {
1346 kind: Some(v2::return_type::Kind::Value(stream_of(
1347 v2::stream_type::Element::Named("LogLine".to_string()),
1348 ))),
1349 }),
1350 contracts: Vec::new(),
1351 timing: None,
1352 family: v2::Family::Dispatch as i32,
1353 };
1354
1355 let status_service = v2::Service {
1358 name: "veh.adas.status".to_string(),
1359 visibility: v2::Visibility::Public as i32,
1360 doc: String::new(),
1361 labels: Vec::new(),
1362 deprecated: None,
1363 shapes: vec![v2::ServiceShape {
1364 kind: Some(v2::service_shape::Kind::InterfaceRef(
1365 "VehicleStatus".to_string(),
1366 )),
1367 }],
1368 links: Vec::new(),
1369 see: Vec::new(),
1370 since: Vec::new(),
1371 };
1372 let logs_service = v2::Service {
1375 name: "veh.adas.logs".to_string(),
1376 visibility: v2::Visibility::Public as i32,
1377 doc: String::new(),
1378 labels: Vec::new(),
1379 deprecated: None,
1380 shapes: vec![v2::ServiceShape {
1381 kind: Some(v2::service_shape::Kind::Inline(v2::Interface {
1382 name: String::new(),
1383 visibility: v2::Visibility::Unspecified as i32,
1384 doc: String::new(),
1385 labels: Vec::new(),
1386 deprecated: None,
1387 interactions: vec![interaction(
1388 "tailLogs",
1389 1,
1390 v2::decl::Kind::QueryDef(tail_logs),
1391 )],
1392 number: 0,
1393 provisional: false,
1394 links: Vec::new(),
1395 see: Vec::new(),
1396 since: Vec::new(),
1397 })),
1398 }],
1399 links: Vec::new(),
1400 see: Vec::new(),
1401 since: Vec::new(),
1402 };
1403
1404 v2::Package {
1405 name: "veh.adas".to_string(),
1406 decls: vec![v2::Decl {
1410 name: "Speed".to_string(),
1411 visibility: v2::Visibility::Public as i32,
1412 is_error: false,
1413 doc: String::new(),
1414 labels: Vec::new(),
1415 deprecated: None,
1416 ordinal: 0,
1417 kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
1418 backing: Some(v2::Backing {
1419 kind: Some(v2::backing::Kind::Unit("km/h".to_string())),
1420 }),
1421 constraint: None,
1422 declared_init: None,
1423 init: None,
1424 width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
1425 })),
1426 links: Vec::new(),
1427 see: Vec::new(),
1428 since: Vec::new(),
1429 }],
1430 interfaces: vec![vehicle_status],
1431 services: vec![status_service, logs_service],
1432 retired: Vec::new(),
1433 unit: "veh.adas".to_string(),
1434 }
1435 }
1436
1437 fn vocabulary_fixture() -> v2::Package {
1444 fn decl(name: &str, kind: v2::decl::Kind) -> v2::Decl {
1445 v2::Decl {
1446 name: name.to_string(),
1447 visibility: v2::Visibility::Public as i32,
1448 is_error: false,
1449 doc: String::new(),
1450 labels: Vec::new(),
1451 deprecated: None,
1452 ordinal: 0,
1453 kind: Some(kind),
1454 links: Vec::new(),
1455 see: Vec::new(),
1456 since: Vec::new(),
1457 }
1458 }
1459
1460 fn field(name: &str, ordinal: u32, field_type: v2::FieldType) -> v2::Field {
1461 v2::Field {
1462 name: name.to_string(),
1463 ordinal,
1464 r#type: Some(field_type),
1465 declared_init: None,
1466 init: None,
1467 doc: String::new(),
1468 labels: Vec::new(),
1469 deprecated: None,
1470 links: Vec::new(),
1471 see: Vec::new(),
1472 since: Vec::new(),
1473 }
1474 }
1475
1476 let max_retry = v2::ConstDef {
1478 type_ref: Some("integer".to_string()),
1479 value: "24".to_string(),
1480 regex: None,
1481 };
1482
1483 let gear = v2::EnumDef {
1486 values: vec![
1487 v2::EnumValue {
1488 name: "PARK".to_string(),
1489 value: 1,
1490 doc: String::new(),
1491 links: Vec::new(),
1492 see: Vec::new(),
1493 since: Vec::new(),
1494 },
1495 v2::EnumValue {
1496 name: "DRIVE".to_string(),
1497 value: 2,
1498 doc: String::new(),
1499 links: Vec::new(),
1500 see: Vec::new(),
1501 since: Vec::new(),
1502 },
1503 ],
1504 reserved: vec![v2::Reserved {
1505 ordinal: 0,
1506 name: None,
1507 value: Some(7),
1508 }],
1509 };
1510
1511 let warnings = v2::EnumSetDef {
1514 backing_enum: None,
1515 bits: vec![
1516 v2::EnumValue {
1517 name: "LOW_FUEL".to_string(),
1518 value: 0,
1519 doc: String::new(),
1520 links: Vec::new(),
1521 see: Vec::new(),
1522 since: Vec::new(),
1523 },
1524 v2::EnumValue {
1525 name: "ICE_RISK".to_string(),
1526 value: 33,
1527 doc: String::new(),
1528 links: Vec::new(),
1529 see: Vec::new(),
1530 since: Vec::new(),
1531 },
1532 ],
1533 width: v2::IntWidth::U64 as i32,
1534 };
1535
1536 let plate_text = v2::TypeDef {
1539 backing: Some(v2::Backing {
1540 kind: Some(v2::backing::Kind::Primitive(
1541 v2::PrimitiveType::String as i32,
1542 )),
1543 }),
1544 constraint: Some(v2::Constraint {
1545 min: None,
1546 max: None,
1547 step: None,
1548 len_min: Some(1),
1549 len_max: Some(86),
1550 pattern: None,
1551 pattern_const: None,
1552 }),
1553 declared_init: None,
1554 init: None,
1555 width: None,
1556 };
1557
1558 let sample = v2::UnionDef {
1560 arms: vec![
1561 v2::UnionArm {
1562 name: "speed".to_string(),
1563 ordinal: 1,
1564 type_ref: "Speed".to_string(),
1565 doc: String::new(),
1566 links: Vec::new(),
1567 see: Vec::new(),
1568 since: Vec::new(),
1569 },
1570 v2::UnionArm {
1571 name: "gear".to_string(),
1572 ordinal: 2,
1573 type_ref: "Gear".to_string(),
1574 doc: String::new(),
1575 links: Vec::new(),
1576 see: Vec::new(),
1577 since: Vec::new(),
1578 },
1579 ],
1580 is_result: false,
1581 reserved: Vec::new(),
1582 };
1583
1584 let retries = v2::Field {
1590 declared_init: Some("3".to_string()),
1591 init: Some(v2::InitValue {
1592 derivable: true,
1593 value: Some("3".to_string()),
1594 }),
1595 ..field(
1596 "retries",
1597 1,
1598 v2::FieldType {
1599 optional: false,
1600 kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
1601 backing: Some(v2::Backing {
1602 kind: Some(v2::backing::Kind::Primitive(
1603 v2::PrimitiveType::Integer as i32,
1604 )),
1605 }),
1606 constraint: Some(v2::Constraint {
1607 min: Some("0".to_string()),
1608 max: Some("24".to_string()),
1609 step: None,
1610 len_min: None,
1611 len_max: None,
1612 pattern: None,
1613 pattern_const: None,
1614 }),
1615 declared_init: None,
1616 init: None,
1617 width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
1618 }))),
1619 },
1620 )
1621 };
1622
1623 let position = field(
1626 "position",
1627 2,
1628 v2::FieldType {
1629 optional: false,
1630 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
1631 fields: vec![
1632 v2::TupleField {
1633 name: "x".to_string(),
1634 r#type: Some(named_type("Speed")),
1635 },
1636 v2::TupleField {
1637 name: "y".to_string(),
1638 r#type: Some(named_type("Speed")),
1639 },
1640 ],
1641 })),
1642 },
1643 );
1644
1645 let gears = field(
1647 "gears",
1648 3,
1649 v2::FieldType {
1650 optional: false,
1651 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
1652 element: Some(Box::new(named_type("Gear"))),
1653 min: 1,
1654 max: 4096,
1655 }))),
1656 },
1657 );
1658
1659 let plates = v2::Field {
1663 deprecated: Some("superseded by gears".to_string()),
1664 ..field(
1665 "plates",
1666 4,
1667 v2::FieldType {
1668 optional: false,
1669 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
1670 key: Some(Box::new(named_type("PlateText"))),
1671 value: Some(Box::new(named_type("Gear"))),
1672 min: 0,
1673 max: 53,
1674 }))),
1675 },
1676 )
1677 };
1678
1679 let snapshot = v2::StructDef {
1680 members: [retries, position, gears, plates]
1681 .into_iter()
1682 .map(|field| v2::StructMember {
1683 member: Some(v2::struct_member::Member::Field(Box::new(field))),
1684 })
1685 .collect(),
1686 fixed_layout: false,
1687 };
1688
1689 v2::Package {
1690 name: "veh.vocab".to_string(),
1691 decls: vec![
1692 decl("MAX_RETRY", v2::decl::Kind::ConstDef(max_retry)),
1693 decl("Gear", v2::decl::Kind::EnumDef(gear)),
1694 decl("Warnings", v2::decl::Kind::EnumSetDef(warnings)),
1695 decl("PlateText", v2::decl::Kind::TypeDef(plate_text)),
1696 v2::Decl {
1699 deprecated: Some("use Snapshot".to_string()),
1700 ..decl("Sample", v2::decl::Kind::UnionDef(sample))
1701 },
1702 decl("Snapshot", v2::decl::Kind::StructDef(snapshot)),
1703 ],
1704 interfaces: Vec::new(),
1705 services: Vec::new(),
1706 retired: Vec::new(),
1707 unit: String::new(),
1708 }
1709 }
1710
1711 #[test]
1712 fn protobuf_round_trip_preserves_package() {
1713 let package = fixture();
1714
1715 let buf = v2::to_binary(&package);
1716 let decoded = v2::from_binary(buf.as_slice()).expect("decode must succeed");
1717
1718 assert_eq!(package, decoded);
1719
1720 let vocabulary = vocabulary_fixture();
1722 let decoded_vocabulary =
1723 v2::from_binary(v2::to_binary(&vocabulary).as_slice()).expect("decode must succeed");
1724 assert_eq!(vocabulary, decoded_vocabulary);
1725
1726 let interface = &decoded.interfaces[0];
1727 let ordinals: Vec<u32> = interface.interactions.iter().map(|d| d.ordinal).collect();
1728 assert_eq!(
1729 ordinals,
1730 [1, 2, 3, 4, 5, 6],
1731 "one ordinal sequence, tombstone counted (ridl §11)"
1732 );
1733 let Some(v2::decl::Kind::ReservedSlot(tombstone)) = &interface.interactions[2].kind else {
1734 panic!("ordinal 3 must decode as a reserved tombstone");
1735 };
1736 assert_eq!(tombstone.name.as_deref(), Some("legacyMode"));
1737 let Some(v2::service_shape::Kind::Inline(inline)) = decoded.services[1]
1738 .shapes
1739 .first()
1740 .and_then(|slot| slot.kind.as_ref())
1741 else {
1742 panic!("veh.adas.logs must decode as an inline shape");
1743 };
1744 assert_eq!(inline.name, "", "an inline shape carries no name");
1745 let references: Vec<&str> = decoded.services[0]
1746 .shapes
1747 .iter()
1748 .filter_map(|slot| match &slot.kind {
1749 Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
1750 _ => None,
1751 })
1752 .collect();
1753 assert_eq!(
1754 references,
1755 ["VehicleStatus"],
1756 "a service's set carries its references and nothing else"
1757 );
1758 }
1759
1760 fn package_with_command(family: v2::Family, shape: v2::Shape) -> v2::Package {
1763 v2::Package {
1764 name: "app".to_string(),
1765 interfaces: vec![v2::Interface {
1766 name: "I".to_string(),
1767 visibility: v2::Visibility::Public as i32,
1768 interactions: vec![v2::Decl {
1769 name: "c".to_string(),
1770 visibility: v2::Visibility::Public as i32,
1771 ordinal: 1,
1772 kind: Some(v2::decl::Kind::CommandDef(v2::CommandDef {
1773 family: family as i32,
1774 shape: shape as i32,
1775 ..Default::default()
1776 })),
1777 ..Default::default()
1778 }],
1779 ..Default::default()
1780 }],
1781 ..Default::default()
1782 }
1783 }
1784
1785 #[test]
1788 fn the_readers_reject_an_invalid_interaction_class() {
1789 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1790 let json = v2::render_json(&package).expect("the raw writer renders it");
1791 let err = v2::from_json(&json).expect_err("the JSON reader must refuse it");
1792 assert!(
1793 err.to_string().contains("invalid interaction class"),
1794 "{err}"
1795 );
1796 let bytes = prost::Message::encode_to_vec(&package);
1797 let err = v2::from_binary(&bytes).expect_err("the binary reader must refuse it");
1798 assert!(
1799 err.to_string().contains("invalid interaction class"),
1800 "{err}"
1801 );
1802 }
1803
1804 #[test]
1805 fn the_json_writer_refuses_an_invalid_interaction_class() {
1806 let package = package_with_command(v2::Family::Intent, v2::Shape::None);
1807 let err = v2::to_json_pretty(&package).expect_err("the writer must refuse it");
1808 assert!(matches!(err, v2::SerializeError::Class(_)), "{err}");
1809 }
1810
1811 #[test]
1812 fn a_valid_class_round_trips_through_both_readers() {
1813 let package = package_with_command(v2::Family::Intent, v2::Shape::Toggle);
1814 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1815 assert_eq!(v2::from_json(&json).expect("the JSON parses"), package);
1816 assert_eq!(
1817 v2::from_binary(&v2::to_binary(&package)).expect("the binary decodes"),
1818 package
1819 );
1820 }
1821
1822 #[test]
1825 fn a_snapshot_without_family_reads_as_dispatch() {
1826 let package = package_with_command(v2::Family::Dispatch, v2::Shape::None);
1827 let json = v2::to_json_pretty(&package).expect("the writer renders it");
1828 let mut value: serde_json::Value =
1829 serde_json::from_str(&json).expect("the rendering is JSON");
1830 let command = value["interfaces"][0]["interactions"][0]["commandDef"]
1831 .as_object_mut()
1832 .expect("the command object");
1833 assert!(command.remove("family").is_some(), "`family` was rendered");
1834 assert!(command.remove("shape").is_some(), "`shape` was rendered");
1835 let stripped = serde_json::to_string(&value).expect("the stripped value renders");
1836 let read = v2::from_json(&stripped).expect("a 0.9 snapshot reads");
1837 assert_eq!(read, package);
1838 }
1839
1840 #[test]
1841 fn json_round_trip_preserves_package() {
1842 for package in [fixture(), vocabulary_fixture()] {
1843 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1844 let decoded = v2::from_json(&json).expect("json deserialization must succeed");
1845
1846 assert_eq!(package, decoded);
1847 }
1848 }
1849
1850 #[test]
1857 fn number_provisional_and_retired_round_trip_through_json_text_and_binary() {
1858 let mut package = fixture();
1859 package.interfaces[0].number = 4;
1860 package.interfaces[0].provisional = true;
1861 let Some(v2::service_shape::Kind::Inline(inline)) =
1862 package.services[1].shapes[0].kind.as_mut()
1863 else {
1864 panic!("veh.adas.logs holds an inline shape in slot 1");
1865 };
1866 inline.number = 5;
1867 package.retired = vec![
1868 v2::RetiredInterface {
1869 name: "LaneAssist".to_string(),
1870 number: 2,
1871 },
1872 v2::RetiredInterface {
1873 name: "service:veh.hvac.cabin".to_string(),
1874 number: 3,
1875 },
1876 ];
1877
1878 let json = v2::to_json_pretty(&package).expect("the package serializes as IR JSON");
1879 assert_eq!(v2::from_json(&json).expect("the JSON parses back"), package);
1880 let text = v2::to_text_format(&package).expect("the package serializes as prototext");
1881 assert_eq!(
1882 v2::from_text_format(&text).expect("the prototext parses back"),
1883 package
1884 );
1885 assert_eq!(
1886 v2::from_binary(v2::to_binary(&package).as_slice()).expect("the binary decodes"),
1887 package
1888 );
1889
1890 for needle in [
1891 r#""number": 4"#,
1892 r#""provisional": true"#,
1893 r#""number": 5"#,
1894 r#""name": "LaneAssist""#,
1895 r#""name": "service:veh.hvac.cabin""#,
1896 ] {
1897 assert!(
1898 json.contains(needle),
1899 "the JSON must carry {needle}, got: {json}"
1900 );
1901 }
1902
1903 let unnumbered = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1907 for needle in [
1908 r#""number": 0"#,
1909 r#""provisional": false"#,
1910 r#""retired": []"#,
1911 ] {
1912 assert!(
1913 unnumbered.contains(needle),
1914 "a default field must still be written, expected {needle} in: {unnumbered}"
1915 );
1916 }
1917 }
1918
1919 #[test]
1925 fn a_snapshot_lacking_the_number_fields_still_loads() {
1926 let package = v2::from_json(
1927 r#"{"name": "veh.x", "interfaces": [{"name": "LaneKeeping"}], "services": [{"name": "veh.x.s", "shapes": [{"inline": {"name": ""}}]}]}"#,
1928 )
1929 .expect("a pre-lock snapshot loads");
1930
1931 assert_eq!(package.interfaces[0].number, 0);
1932 assert!(!package.interfaces[0].provisional);
1933 let Some(v2::service_shape::Kind::Inline(inline)) =
1934 package.services[0].shapes[0].kind.as_ref()
1935 else {
1936 panic!("the service holds an inline shape");
1937 };
1938 assert_eq!(inline.number, 0);
1939 assert!(!inline.provisional);
1940 assert_eq!(package.retired, Vec::new());
1941 }
1942
1943 #[test]
1948 fn text_format_round_trip_preserves_package() {
1949 for package in [fixture(), vocabulary_fixture()] {
1950 let text = v2::to_text_format(&package).expect("the fixture serializes as prototext");
1951 let decoded = v2::from_text_format(&text).expect("prototext parsing must succeed");
1952
1953 assert_eq!(package, decoded);
1954 }
1955 }
1956
1957 #[test]
1972 fn text_format_is_pretty_with_defaults_in_index_order() {
1973 let text = v2::to_text_format(&fixture()).expect("the fixture serializes as prototext");
1974
1975 assert!(
1977 text.contains("\n "),
1978 "pretty printing must indent nested fields, got: {text}"
1979 );
1980 assert!(
1983 text.contains("is_error: false"),
1984 "a field holding its default must be emitted, got: {text}"
1985 );
1986 assert!(
1989 text.starts_with("name:"),
1990 "fields must print in schema index order, got: {text}"
1991 );
1992 }
1993
1994 fn strict_parse(json: &str) -> v2::Package {
2001 let mut deserializer = serde_json::Deserializer::from_str(json);
2002 let package = <v2::Package as serde::Deserialize>::deserialize(&mut deserializer)
2003 .expect("a strict conformant parser must accept the emitted JSON");
2004 deserializer.end().expect("no trailing input");
2005 package
2006 }
2007
2008 #[test]
2013 fn emitted_json_survives_a_strict_conformant_parse() {
2014 for package in [fixture(), vocabulary_fixture()] {
2015 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
2016 assert_eq!(package, strict_parse(&json));
2017 }
2018 }
2019
2020 #[test]
2021 fn json_renders_timing_bounds_and_fallible_arms_exactly() {
2022 let json = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
2023
2024 assert!(
2028 json.contains(r#""minUs": "10000""#),
2029 "the timing bound must be a JSON string, got: {json}"
2030 );
2031 assert!(
2033 json.contains(r#""ok": "FaultPage""#),
2034 "the ok arm must render, got: {json}"
2035 );
2036 assert!(
2037 json.contains(r#""err": "DiagError""#),
2038 "the err arm must render, got: {json}"
2039 );
2040 }
2041
2042 #[test]
2047 fn json_renders_64_bit_integer_fields_as_strings() {
2048 let json = v2::to_json_pretty(&vocabulary_fixture())
2049 .expect("the vocabulary fixture serializes as IR JSON");
2050
2051 assert!(
2053 json.contains(r#""max": "4096""#),
2054 "an array bound must be a JSON string, got: {json}"
2055 );
2056 assert!(
2058 json.contains(r#""lenMax": "86""#),
2059 "a length bound must be a JSON string, got: {json}"
2060 );
2061 assert!(
2063 json.contains(r#""value": "7""#),
2064 "a retired enum value must be a JSON string, got: {json}"
2065 );
2066 assert!(
2067 json.contains(r#""value": "33""#),
2068 "an enum-set bit position must be a JSON string, got: {json}"
2069 );
2070 }
2071
2072 #[test]
2073 fn fallible_transport_identity_follows_the_derivation_rule() {
2074 let fallible = v2::FallibleType {
2077 ok: "FaultPage".to_string(),
2078 err: "DiagError".to_string(),
2079 };
2080 assert_eq!(
2081 v2::fallible_transport_identity("VehicleStatus", 9, &fallible),
2082 "VehicleStatus#9:FaultPage|DiagError"
2083 );
2084
2085 let package = fixture();
2087 let interface = &package.interfaces[0];
2088 let query_decl = &interface.interactions[4];
2089 let Some(v2::decl::Kind::QueryDef(query)) = &query_decl.kind else {
2090 panic!("ordinal 5 must be the fallible query");
2091 };
2092 let Some(v2::return_type::Kind::Fallible(arms)) = &query.return_type.as_ref().unwrap().kind
2093 else {
2094 panic!("fetchFaults must return a fallible type");
2095 };
2096 assert_eq!(
2097 v2::fallible_transport_identity(&interface.name, query_decl.ordinal, arms),
2098 "VehicleStatus#5:FaultPage|DiagError"
2099 );
2100 }
2101
2102 #[test]
2109 fn shapes_walks_named_interfaces_and_inline_service_shapes() {
2110 let package = fixture();
2111 let walk: Vec<(&str, bool, usize)> = package
2112 .shapes()
2113 .map(|shape| {
2114 (
2115 shape.name,
2116 shape.is_inline(),
2117 shape.interface.interactions.len(),
2118 )
2119 })
2120 .collect();
2121 assert_eq!(
2122 walk,
2123 [("VehicleStatus", false, 6), ("veh.adas.logs", true, 1)],
2124 "the named interface, then the inline shape under the service's \
2125 dotted name",
2126 );
2127
2128 assert_eq!(package.services.len(), 2, "one reference form, one inline");
2132 assert!(
2133 !package
2134 .shapes()
2135 .any(|shape| shape.name == "veh.adas.status"),
2136 "a service naming an interface contributes no shape of its own",
2137 );
2138 }
2139
2140 #[test]
2141 fn relative_name_strips_the_unit_prefix() {
2142 assert_eq!(v2::relative_name("u", "u", "Session"), "Session");
2143 assert_eq!(
2144 v2::relative_name("u", "u.cluster", "Speed"),
2145 "cluster.Speed"
2146 );
2147 assert_eq!(
2148 v2::relative_name("com.example.hmi", "com.example.hmi.cluster.front", "A"),
2149 "cluster.front.A"
2150 );
2151 }
2152
2153 #[test]
2156 fn relative_name_keeps_the_full_name_of_a_package_outside_the_unit() {
2157 assert_eq!(
2158 v2::relative_name("u", "w.cluster", "Speed"),
2159 "w.cluster.Speed"
2160 );
2161 assert_eq!(v2::relative_name("u", "ux", "A"), "ux.A");
2162 assert_eq!(v2::relative_name("u.cluster", "u", "Session"), "u.Session");
2163 }
2164
2165 #[test]
2169 fn unit_retired_reads_a_package_named_twice_once() {
2170 let first = v2::Package {
2171 name: "u.cluster".to_string(),
2172 unit: "u".to_string(),
2173 retired: vec![v2::RetiredInterface {
2174 name: "cluster.Old".to_string(),
2175 number: 3,
2176 }],
2177 ..Default::default()
2178 };
2179 let second = v2::Package {
2180 retired: vec![v2::RetiredInterface {
2181 name: "cluster.Older".to_string(),
2182 number: 4,
2183 }],
2184 ..first.clone()
2185 };
2186 assert_eq!(
2187 v2::unit_retired("u", &[&first, &second]),
2188 first.retired,
2189 "the first package of a name is the one read"
2190 );
2191 }
2192
2193 #[test]
2194 fn an_inline_shape_keeps_its_global_name() {
2195 let mut package = fixture();
2196 package.name = "u.cluster".to_string();
2197 package.unit = "u".to_string();
2198 let inline = package
2199 .shapes()
2200 .find(|shape| shape.is_inline())
2201 .expect("the fixture has an inline shape");
2202 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2203 let named = package
2204 .shapes()
2205 .find(|shape| !shape.is_inline())
2206 .expect("the fixture has a declared interface");
2207 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2208 }
2209
2210 #[test]
2211 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2212 let mut package = fixture();
2213 package.unit = String::new();
2214 let named = package
2215 .shapes()
2216 .find(|shape| !shape.is_inline())
2217 .expect("the fixture has a declared interface");
2218 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2219 }
2220
2221 #[test]
2222 fn unit_of_falls_back_to_the_name() {
2223 let bare = v2::Package {
2224 name: "p".to_string(),
2225 ..Default::default()
2226 };
2227 assert_eq!(v2::unit_of(&bare), "p");
2228 let member = v2::Package {
2229 name: "u.a".to_string(),
2230 unit: "u".to_string(),
2231 ..Default::default()
2232 };
2233 assert_eq!(v2::unit_of(&member), "u");
2234 }
2235
2236 #[test]
2237 fn packages_of_unit_selects_by_unit_of() {
2238 let package = |name: &str, unit: &str| v2::Package {
2239 name: name.to_string(),
2240 unit: unit.to_string(),
2241 ..Default::default()
2242 };
2243 let packages = [
2244 package("u", "u"),
2245 package("u.a", "u"),
2246 package("w", ""),
2247 package("other.b", "other"),
2248 ];
2249 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2250 .map(|p| p.name.as_str())
2251 .collect();
2252 assert_eq!(names, ["u", "u.a"]);
2253 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2254 .map(|p| p.name.as_str())
2255 .collect();
2256 assert_eq!(legacy, ["w"]);
2257 }
2258
2259 #[test]
2264 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2265 let package = fixture();
2266 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2267
2268 let named = shapes[0];
2269 assert!(named.service.is_none());
2270 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2271 assert_eq!(named.visibility(), named.interface.visibility);
2272
2273 let inline = shapes[1];
2274 assert_eq!(
2275 inline.interface.visibility,
2276 v2::Visibility::Unspecified as i32,
2277 "the trap: an inline shape's own visibility field is unset",
2278 );
2279 assert_eq!(
2280 inline.service.expect("an inline shape has an owner").name,
2281 "veh.adas.logs",
2282 );
2283 assert_eq!(
2284 inline.visibility(),
2285 v2::Visibility::Public as i32,
2286 "the accessor reads the owning service's, never the unset field",
2287 );
2288 }
2289
2290 #[test]
2294 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2295 let mut package = fixture();
2296 package.services.clear();
2297 assert_eq!(package.shapes().count(), 1);
2298
2299 package.interfaces.clear();
2300 assert_eq!(package.shapes().count(), 0);
2301 }
2302
2303 #[test]
2310 fn referenced_packages_finds_qualifiers_at_depth() {
2311 fn named(reference: &str) -> v2::FieldType {
2312 v2::FieldType {
2313 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2314 ..Default::default()
2315 }
2316 }
2317
2318 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2319 v2::decl::Kind::FixedDef(v2::FixedDef {
2320 payload: Some(payload),
2321 })
2322 }
2323
2324 let package = v2::Package {
2325 name: "veh.cluster".to_string(),
2326 decls: vec![
2327 v2::Decl {
2328 name: "Local".to_string(),
2329 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2330 payload: "Speed".to_string(),
2331 ..Default::default()
2332 })),
2333 ..Default::default()
2334 },
2335 v2::Decl {
2336 name: "Stamped".to_string(),
2337 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2338 payload: "ridl.std.Timestamp".to_string(),
2339 ..Default::default()
2340 })),
2341 ..Default::default()
2342 },
2343 v2::Decl {
2344 name: "ArrLabels".to_string(),
2345 kind: Some(fixed(v2::FieldType {
2346 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2347 element: Some(Box::new(named("veh.arr.Label"))),
2348 min: 0,
2349 max: 32,
2350 }))),
2351 ..Default::default()
2352 })),
2353 ..Default::default()
2354 },
2355 v2::Decl {
2356 name: "TupThing".to_string(),
2357 kind: Some(fixed(v2::FieldType {
2358 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2359 fields: vec![v2::TupleField {
2360 name: "x".to_string(),
2361 r#type: Some(named("veh.tup.X")),
2362 }],
2363 })),
2364 ..Default::default()
2365 })),
2366 ..Default::default()
2367 },
2368 v2::Decl {
2369 name: "MapThing".to_string(),
2370 kind: Some(fixed(v2::FieldType {
2371 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2372 key: Some(Box::new(named("veh.key.X"))),
2373 value: Some(Box::new(named("veh.val.X"))),
2374 min: 0,
2375 max: 8,
2376 }))),
2377 ..Default::default()
2378 })),
2379 ..Default::default()
2380 },
2381 v2::Decl {
2382 name: "StreamThing".to_string(),
2383 kind: Some(fixed(v2::FieldType {
2384 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2385 element: Some(v2::stream_type::Element::Named(
2386 "veh.strm.X".to_string(),
2387 )),
2388 })),
2389 ..Default::default()
2390 })),
2391 ..Default::default()
2392 },
2393 ],
2394 ..Default::default()
2395 };
2396
2397 let found = v2::referenced_packages(&package);
2398 let expected: std::collections::BTreeSet<String> = [
2399 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2400 ]
2401 .into_iter()
2402 .map(str::to_string)
2403 .collect();
2404 assert_eq!(
2405 found, expected,
2406 "each recursive path must contribute its own distinct qualifier"
2407 );
2408 assert!(
2409 !found.contains("Speed") && !found.contains("veh.cluster"),
2410 "a bare reference contributes no package: {found:?}",
2411 );
2412 }
2413
2414 #[test]
2417 fn referenced_packages_is_empty_without_references() {
2418 let package = v2::Package {
2419 name: "veh.solo".to_string(),
2420 ..Default::default()
2421 };
2422 assert!(v2::referenced_packages(&package).is_empty());
2423 }
2424
2425 const NESTING_BELOW_LIMIT: usize = 45;
2431 const NESTING_PAST_LIMIT: usize = 60;
2435
2436 fn nested_package(depth: usize) -> v2::Package {
2441 let mut payload = v2::FieldType {
2442 optional: false,
2443 kind: Some(v2::field_type::Kind::Primitive(
2444 v2::PrimitiveType::Integer as i32,
2445 )),
2446 };
2447 for _ in 0..depth {
2448 payload = v2::FieldType {
2449 optional: false,
2450 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2451 element: Some(Box::new(payload)),
2452 min: 1,
2453 max: 1,
2454 }))),
2455 };
2456 }
2457 v2::Package {
2458 name: "veh.deep".to_string(),
2459 decls: vec![v2::Decl {
2460 name: "deep".to_string(),
2461 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2462 payload: Some(payload),
2463 })),
2464 ..Default::default()
2465 }],
2466 ..Default::default()
2467 }
2468 }
2469
2470 fn package_at_message_depth(levels: usize) -> v2::Package {
2482 assert!(levels >= 3, "the chain costs three levels before nesting");
2483 let (arrays, tuple) = if levels % 2 == 1 {
2484 ((levels - 3) / 2, false)
2485 } else {
2486 assert!(levels >= 6, "one tuple level costs three");
2487 ((levels - 6) / 2, true)
2488 };
2489
2490 let mut payload = v2::FieldType {
2491 optional: false,
2492 kind: Some(v2::field_type::Kind::Primitive(
2493 v2::PrimitiveType::Integer as i32,
2494 )),
2495 };
2496 for _ in 0..arrays {
2497 payload = v2::FieldType {
2498 optional: false,
2499 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2500 element: Some(Box::new(payload)),
2501 min: 1,
2502 max: 1,
2503 }))),
2504 };
2505 }
2506 if tuple {
2507 payload = v2::FieldType {
2508 optional: false,
2509 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2510 fields: vec![v2::TupleField {
2511 name: "f0".to_string(),
2512 r#type: Some(payload),
2513 }],
2514 })),
2515 };
2516 }
2517 v2::Package {
2518 name: "veh.deep".to_string(),
2519 decls: vec![v2::Decl {
2520 name: "deep".to_string(),
2521 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2522 payload: Some(payload),
2523 })),
2524 ..Default::default()
2525 }],
2526 ..Default::default()
2527 }
2528 }
2529
2530 fn message_nesting(json: &str) -> usize {
2537 let (mut depth, mut max) = (0usize, 0usize);
2538 let (mut in_string, mut escaped) = (false, false);
2539 for byte in json.bytes() {
2540 if in_string {
2541 if escaped {
2542 escaped = false;
2543 } else if byte == b'\\' {
2544 escaped = true;
2545 } else if byte == b'"' {
2546 in_string = false;
2547 }
2548 continue;
2549 }
2550 match byte {
2551 b'"' => in_string = true,
2552 b'{' => {
2553 depth += 1;
2554 max = max.max(depth);
2555 }
2556 b'}' => depth = depth.saturating_sub(1),
2557 _ => {}
2558 }
2559 }
2560 max
2561 }
2562
2563 #[test]
2567 fn package_at_message_depth_builds_the_depth_it_names() {
2568 with_sized_stack(|| {
2569 for levels in [3, 6, 7, 99, 100, 101] {
2570 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2571 .expect("the writer is unrestricted at these depths");
2572 assert_eq!(
2573 message_nesting(&json) - 1,
2574 levels,
2575 "the chain must nest {levels} message levels below the root"
2576 );
2577 }
2578 });
2579 }
2580
2581 #[test]
2590 fn binary_round_trip_at_100_message_levels_succeeds() {
2591 let package = package_at_message_depth(100);
2592 let bytes = v2::to_binary(&package);
2593 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2594 assert_eq!(package, decoded);
2595 }
2596
2597 #[test]
2602 fn binary_decode_at_101_message_levels_returns_an_error() {
2603 let package = package_at_message_depth(101);
2604 let bytes = v2::to_binary(&package);
2605 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2606 assert!(
2607 error.to_string().contains("recursion limit reached"),
2608 "the error must name the limit, got: {error}"
2609 );
2610
2611 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2612 assert_eq!(
2613 package,
2614 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2615 );
2616 }
2617
2618 #[test]
2627 fn json_round_trip_at_400_nested_levels_succeeds() {
2628 with_sized_stack(|| {
2629 let package = nested_package(400);
2630 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2631 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2632 assert_eq!(package, decoded);
2633 });
2634 }
2635
2636 #[test]
2644 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2645 let mut package = fixture();
2646 package.decls[0].visibility = 999;
2647 let err = v2::to_json_pretty(&package)
2648 .expect_err("an out-of-schema discriminant must fail, not panic");
2649 let message = err.to_string();
2650 assert!(
2651 message.contains("canonical protobuf JSON"),
2652 "the error must name the encoding that failed, got: {message}"
2653 );
2654 assert!(
2655 message.contains("discriminant outside the schema"),
2656 "the error must name the known cause, got: {message}"
2657 );
2658 }
2659
2660 #[test]
2665 fn json_parse_rejects_an_unknown_field() {
2666 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2667 .expect_err("an unknown field must be rejected");
2668 assert!(
2669 error.to_string().contains("unknown field"),
2670 "the error must name the defect, got: {error}"
2671 );
2672 }
2673
2674 #[test]
2683 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2684 let with_visibility =
2685 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2686 v2::from_json(&with_visibility("1"))
2687 .expect("an in-range numeric enum value parses, as the mapping expects");
2688 let error = v2::from_json(&with_visibility("77"))
2689 .expect_err("an out-of-range numeric enum value must be rejected");
2690 assert!(
2691 error.to_string().contains("invalid value: integer `77`"),
2692 "the error must name the value, got: {error}"
2693 );
2694 }
2695
2696 #[test]
2702 fn json_parse_rejects_a_float_form_integer() {
2703 for spelling in ["1.0", "1e0"] {
2704 let error = v2::from_json(&format!(
2705 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2706 ))
2707 .expect_err("a float-form integer must be rejected");
2708 assert!(
2709 error.to_string().contains("did not match any variant"),
2710 "the integer field's deserializer must be the one refusing `{spelling}`, \
2711 got: {error}"
2712 );
2713 }
2714 }
2715
2716 #[test]
2723 fn json_parse_rejects_null_for_a_repeated_field() {
2724 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2725 .expect_err("null for a repeated field must be rejected");
2726 assert!(
2727 error.to_string().contains("invalid type: null"),
2728 "the error must name the null, got: {error}"
2729 );
2730 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2731 .expect("null for an optional scalar field still parses");
2732 }
2733
2734 #[test]
2738 fn json_parse_rejects_a_duplicate_key() {
2739 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2740 .expect_err("a duplicate key must be rejected");
2741 assert!(
2742 error.to_string().contains("duplicate field `name`"),
2743 "the error must name the duplicated field, got: {error}"
2744 );
2745 }
2746
2747 #[test]
2754 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2755 with_sized_stack(|| {
2756 let json = v2::to_json_pretty(&nested_package(500))
2757 .expect("the writer is unrestricted at this depth");
2758 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2759 assert!(
2760 error.to_string().contains("1000 JSON levels"),
2761 "the error must name the ceiling, got: {error}"
2762 );
2763 });
2764 }
2765
2766 #[test]
2771 fn json_nesting_ceiling_binds_exactly_at_1000() {
2772 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2773 assert!(
2774 !at.to_string().contains("JSON levels"),
2775 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2776 );
2777
2778 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2779 assert!(
2780 past.to_string().contains("1000 JSON levels"),
2781 "past the ceiling the error must name it, got: {past}"
2782 );
2783 }
2784
2785 #[test]
2790 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2791 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2793 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2795 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2798 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2801 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2803 }
2804
2805 fn nested_text(depth: usize) -> String {
2809 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2810 for _ in 0..depth {
2811 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2812 }
2813 format!(
2814 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2815 )
2816 }
2817
2818 #[test]
2822 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2823 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2824 .expect_err("serialization past the recursion limit must fail, not panic");
2825 let message = err.to_string();
2826 assert!(
2827 message.contains("recursion limit"),
2828 "the error must name the nesting limit as the known cause, got: {message}"
2829 );
2830 assert!(
2831 message.contains("prototext"),
2832 "the error must name the encoding that failed, got: {message}"
2833 );
2834 }
2835
2836 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2849 let outcome = std::thread::Builder::new()
2850 .stack_size(16 * 1024 * 1024)
2851 .spawn(test)
2852 .expect("spawn the large-stack test thread")
2853 .join();
2854 if let Err(payload) = outcome {
2855 std::panic::resume_unwind(payload);
2856 }
2857 }
2858
2859 #[test]
2863 fn text_parse_past_the_nesting_limit_returns_an_error() {
2864 with_sized_stack(|| {
2865 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2866 .expect_err("parsing past the recursion limit must fail, not panic");
2867
2868 let message = error.to_string();
2872 assert!(
2873 message.contains("recursion limit reached"),
2874 "the transcode out of the dynamic message must be the failing \
2875 stage, got: {message}"
2876 );
2877 });
2878 }
2879
2880 #[test]
2883 fn text_round_trip_below_the_nesting_limit_succeeds() {
2884 with_sized_stack(|| {
2885 let package = nested_package(NESTING_BELOW_LIMIT);
2886 let text = v2::to_text_format(&package)
2887 .expect("below the recursion limit, serialization succeeds");
2888 let decoded =
2889 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2890 assert_eq!(package, decoded);
2891 });
2892 }
2893}
2894
2895#[cfg(test)]
2896mod vacuous_constraint {
2897 use crate::v2;
2898
2899 fn constraint() -> v2::Constraint {
2902 v2::Constraint {
2903 min: None,
2904 max: None,
2905 step: None,
2906 len_min: None,
2907 len_max: None,
2908 pattern: None,
2909 pattern_const: None,
2910 }
2911 }
2912
2913 #[test]
2914 fn an_absent_or_empty_constraint_is_vacuous() {
2915 assert!(v2::constraint_is_vacuous(None));
2916 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2917 }
2918
2919 #[test]
2920 fn a_step_constraint_is_non_vacuous() {
2921 let stepped = v2::Constraint {
2922 step: Some("0.5".to_string()),
2923 ..constraint()
2924 };
2925 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2926 }
2927
2928 #[test]
2935 fn any_single_constrained_field_is_non_vacuous() {
2936 let cases = [
2937 (
2938 "min",
2939 v2::Constraint {
2940 min: Some("0.0".to_string()),
2941 ..constraint()
2942 },
2943 ),
2944 (
2945 "max",
2946 v2::Constraint {
2947 max: Some("250.0".to_string()),
2948 ..constraint()
2949 },
2950 ),
2951 (
2952 "len_min",
2953 v2::Constraint {
2954 len_min: Some(1),
2955 ..constraint()
2956 },
2957 ),
2958 (
2959 "len_max",
2960 v2::Constraint {
2961 len_max: Some(256),
2962 ..constraint()
2963 },
2964 ),
2965 (
2966 "pattern",
2967 v2::Constraint {
2968 pattern: Some("^[a-z]+$".to_string()),
2969 ..constraint()
2970 },
2971 ),
2972 (
2973 "pattern_const",
2974 v2::Constraint {
2975 pattern_const: Some("NAME_PATTERN".to_string()),
2976 ..constraint()
2977 },
2978 ),
2979 ];
2980 for (field, case) in cases {
2981 assert!(
2982 !v2::constraint_is_vacuous(Some(&case)),
2983 "`{field}` alone must be non-vacuous"
2984 );
2985 }
2986 }
2987
2988 #[test]
2997 fn a_bound_pair_set_together_is_non_vacuous() {
2998 let ranged = v2::Constraint {
2999 min: Some("0.0".to_string()),
3000 max: Some("250.0".to_string()),
3001 ..constraint()
3002 };
3003 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
3004
3005 let default_length = v2::Constraint {
3006 len_min: Some(0),
3007 len_max: Some(256),
3008 ..constraint()
3009 };
3010 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
3011 }
3012}
3013
3014#[cfg(test)]
3015mod system_round_trip {
3016 use crate::v2;
3017
3018 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
3019 v2::Attribute {
3020 namespace: namespace.to_string(),
3021 key: key.to_string(),
3022 value,
3023 }
3024 }
3025
3026 fn scalar(text: &str) -> v2::AttributeValue {
3027 v2::AttributeValue {
3028 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
3029 }
3030 }
3031
3032 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
3033 v2::AttributeValue {
3034 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
3035 }
3036 }
3037
3038 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
3039 Some(v2::InterfaceRef {
3040 catalog: catalog.to_string(),
3041 name: name.to_string(),
3042 inline,
3043 })
3044 }
3045
3046 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
3047 v2::Endpoint {
3048 component: component.to_string(),
3049 instance: instance.to_string(),
3050 machine: machine.to_string(),
3051 }
3052 }
3053
3054 fn doc_link() -> v2::DocLink {
3062 v2::DocLink {
3063 text: "Cruise".to_string(),
3064 offset: 16,
3065 len: 6,
3066 target: "veh.topology.Cruise".to_string(),
3067 }
3068 }
3069
3070 fn fixture() -> v2::System {
3071 let link = v2::Link {
3072 interface: interface("veh.diag", "veh.diag.access", true),
3073 service: "veh.diag.access".to_string(),
3074 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
3075 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
3076 crossing: v2::Crossing::OffBoard as i32,
3077 };
3078 v2::System {
3079 name: "Vehicle".to_string(),
3080 package: "veh.topology".to_string(),
3081 labels: vec!["ASIL_B".to_string()],
3082 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
3083 members: vec![
3084 v2::MemberLine {
3085 component: "veh.topology.Cruise".to_string(),
3086 attributes: vec![attribute("linux", "pinned", None)],
3087 doc: "Documented, see [Cruise].".to_string(),
3088 links: vec![doc_link()],
3089 see: vec![doc_link()],
3090 since: vec!["1.2".to_string()],
3091 },
3092 v2::MemberLine {
3093 component: "veh.diag.access".to_string(),
3094 attributes: vec![],
3095 doc: "Documented, see [Cruise].".to_string(),
3096 links: vec![doc_link()],
3097 see: vec![doc_link()],
3098 since: vec!["1.2".to_string()],
3099 },
3100 ],
3101 components: vec![
3102 v2::Component {
3103 name: "Cruise".to_string(),
3104 package: "veh.topology".to_string(),
3105 implicit: false,
3106 external: true,
3107 instances: vec!["primary".to_string(), "backup".to_string()],
3108 offers: vec![v2::Offer {
3109 service: "veh.adas.cruise".to_string(),
3110 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
3111 doc: "Documented, see [Cruise].".to_string(),
3112 links: vec![doc_link()],
3113 see: vec![doc_link()],
3114 since: vec!["1.2".to_string()],
3115 }],
3116 requires: vec![v2::Require {
3117 interface: interface("veh.adas", "LaneAssist", false),
3118 service: "veh.adas.lane".to_string(),
3119 producer: "veh.topology.Lane".to_string(),
3120 attributes: vec![attribute(
3121 "linux",
3122 "cpuset",
3123 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
3124 )],
3125 doc: "Documented, see [Cruise].".to_string(),
3126 links: vec![doc_link()],
3127 see: vec![doc_link()],
3128 since: vec!["1.2".to_string()],
3129 }],
3130 labels: vec!["ASIL_B".to_string()],
3131 attributes: vec![attribute("rust", "crate", None)],
3132 doc: "Documented, see [Cruise].".to_string(),
3133 links: vec![doc_link()],
3134 see: vec![doc_link()],
3135 since: vec!["1.2".to_string()],
3136 deprecated: Some("superseded".to_string()),
3137 },
3138 v2::Component {
3139 name: "veh.diag.access".to_string(),
3140 package: String::new(),
3141 implicit: true,
3142 external: false,
3143 instances: vec!["Unit".to_string()],
3144 offers: vec![v2::Offer {
3145 service: "veh.diag.access".to_string(),
3146 attributes: vec![],
3147 doc: "Documented, see [Cruise].".to_string(),
3148 links: vec![doc_link()],
3149 see: vec![doc_link()],
3150 since: vec!["1.2".to_string()],
3151 }],
3152 requires: vec![],
3153 labels: vec![],
3154 attributes: vec![],
3155 doc: "Documented, see [Cruise].".to_string(),
3156 links: vec![doc_link()],
3157 see: vec![doc_link()],
3158 since: vec!["1.2".to_string()],
3159 deprecated: Some("superseded".to_string()),
3160 },
3161 ],
3162 producers: vec![v2::Producer {
3163 service: "veh.adas.cruise".to_string(),
3164 component: "veh.topology.Cruise".to_string(),
3165 instances: vec!["primary".to_string(), "backup".to_string()],
3166 not_yet_realizable: true,
3167 }],
3168 grants: vec![v2::Grant {
3169 component: "veh.topology.Backend".to_string(),
3170 external: true,
3171 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3172 }],
3173 regions: vec![v2::Region {
3174 catalog: "veh.diag".to_string(),
3175 hash: vec![0xab; 32],
3176 interfaces: vec![v2::RegionInterface {
3177 name: "veh.diag.access".to_string(),
3178 inline: true,
3179 number: 2,
3180 provisional: true,
3181 service: "veh.diag.access".to_string(),
3182 }],
3183 }],
3184 distributions: vec![v2::Distribution {
3185 name: "Adas".to_string(),
3186 package: "veh.topology".to_string(),
3187 members: vec![v2::MemberLine {
3188 component: "veh.topology.Cruise".to_string(),
3189 attributes: vec![],
3190 doc: "Documented, see [Cruise].".to_string(),
3191 links: vec![doc_link()],
3192 see: vec![doc_link()],
3193 since: vec!["1.2".to_string()],
3194 }],
3195 depends_on: vec!["veh.topology.Base".to_string()],
3196 labels: vec!["PLATFORM_BUNDLE".to_string()],
3197 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3198 doc: "Documented, see [Cruise].".to_string(),
3199 links: vec![doc_link()],
3200 see: vec![doc_link()],
3201 since: vec!["1.2".to_string()],
3202 tier: v2::Tier::Platform as i32,
3203 deprecated: Some("superseded".to_string()),
3204 }],
3205 deployments: vec![v2::Deployment {
3206 name: "Production".to_string(),
3207 package: "veh.topology".to_string(),
3208 labels: vec!["FLEET".to_string()],
3209 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3210 machines: vec![v2::Machine {
3211 name: "Cloud".to_string(),
3212 external: true,
3213 labels: vec!["OFF_BOARD".to_string()],
3214 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3215 doc: "Documented, see [Cruise].".to_string(),
3216 links: vec![doc_link()],
3217 see: vec![doc_link()],
3218 since: vec!["1.2".to_string()],
3219 deprecated: Some("superseded".to_string()),
3220 }],
3221 placements: vec![v2::Placement {
3222 component: "veh.topology.Cruise".to_string(),
3223 instance: "backup".to_string(),
3224 machine: "Cockpit".to_string(),
3225 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3226 sizing: Some(v2::Sizing {
3227 depth: Some(4_294_967_295),
3228 slots: Some(65_536),
3229 budget: Some(18_446_744_073_709_551_615),
3230 }),
3231 }],
3232 links: vec![link.clone()],
3233 routes: vec![v2::Route {
3234 catalog: "veh.adas".to_string(),
3235 interface_number: 2,
3236 member_ordinal: 1,
3237 interface: "LaneAssist".to_string(),
3238 member: "active".to_string(),
3239 service: "veh.adas.lane".to_string(),
3240 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3241 }],
3242 surface: vec![v2::Surface {
3243 link: Some(link),
3244 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3245 }],
3246 installations: vec![v2::Installation {
3247 distribution: "veh.topology.Adas".to_string(),
3248 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3249 }],
3250 doc: "Documented, see [Cruise].".to_string(),
3251 doc_links: vec![doc_link()],
3252 see: vec![doc_link()],
3253 since: vec!["1.2".to_string()],
3254 sizing: Some(v2::Sizing {
3255 depth: Some(3),
3256 slots: Some(8),
3257 budget: Some(4096),
3258 }),
3259 deprecated: Some("superseded".to_string()),
3260 }],
3261 doc: "Documented, see [Cruise].".to_string(),
3262 see: vec![doc_link()],
3263 since: vec!["1.2".to_string()],
3264 links: vec![doc_link()],
3265 deprecated: Some("superseded".to_string()),
3266 }
3267 }
3268
3269 #[test]
3270 fn system_binary_round_trip_preserves_system() {
3271 let system = fixture();
3272 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3273 .expect("decode must succeed");
3274 assert_eq!(system, decoded);
3275 }
3276
3277 #[test]
3282 fn system_json_round_trip_preserves_system() {
3283 let system = fixture();
3284 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3285 assert!(
3286 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3287 "bytes render as base64, got:\n{json}"
3288 );
3289 assert!(
3290 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3291 "enums render by name, got:\n{json}"
3292 );
3293 assert!(
3294 json.contains("\"notYetRealizable\": true"),
3295 "fields render in lowerCamelCase, got:\n{json}"
3296 );
3297 assert!(
3298 json.contains("\"budget\": \"18446744073709551615\""),
3299 "a 64-bit value renders as a string, got:\n{json}"
3300 );
3301 assert_eq!(
3302 system,
3303 v2::system_from_json(&json).expect("the JSON parses")
3304 );
3305 assert!(
3306 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3307 "an unknown field is rejected"
3308 );
3309 }
3310
3311 #[test]
3315 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3316 let mut absent = fixture();
3317 absent.deployments[0].sizing = None;
3318 absent.deployments[0].placements[0].sizing = None;
3319 let mut empty = absent.clone();
3320 empty.deployments[0].sizing = Some(v2::Sizing::default());
3321 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3322 for system in [&absent, &empty] {
3323 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3324 .expect("decode must succeed");
3325 assert_eq!(system, &binary);
3326 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3327 assert_eq!(
3328 system,
3329 &v2::system_from_json(&json).expect("the JSON parses")
3330 );
3331 }
3332 assert_ne!(
3333 v2::system_to_binary(&absent),
3334 v2::system_to_binary(&empty),
3335 "the binary encodings differ"
3336 );
3337 }
3338
3339 #[test]
3340 fn system_text_format_round_trip_preserves_system() {
3341 let system = fixture();
3342 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3343 assert!(
3344 text.starts_with("name:"),
3345 "fields print in schema index order, got: {text}"
3346 );
3347 assert_eq!(
3348 system,
3349 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3350 );
3351 }
3352
3353 #[test]
3356 fn qualified_names_follow_the_one_derivation() {
3357 let system = fixture();
3358 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3359 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3360 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3361 assert_eq!(
3362 system.distributions[0].qualified_name(),
3363 "veh.topology.Adas"
3364 );
3365 }
3366}
3367
3368#[cfg(test)]
3369mod load_ir_json {
3370 use crate::v2::{self, LoadError, MAX_JSON_NESTING, load_ir_json};
3371
3372 #[test]
3374 fn a_snapshot_file_loads() {
3375 let dir = tempfile::tempdir().unwrap();
3376 let package = v2::Package {
3377 name: "veh.cabin".to_string(),
3378 ..v2::Package::default()
3379 };
3380 let path = dir.path().join("cabin.ir.json");
3381 std::fs::write(&path, v2::to_json_pretty(&package).unwrap()).unwrap();
3382 assert_eq!(load_ir_json(&path).unwrap(), package);
3383 }
3384
3385 #[test]
3389 fn a_missing_file_is_io_and_bad_json_is_parse() {
3390 let dir = tempfile::tempdir().unwrap();
3391
3392 let missing = dir.path().join("missing.ir.json");
3393 let io = std::fs::read_to_string(&missing).unwrap_err();
3394 let error = load_ir_json(&missing).unwrap_err();
3395 assert!(matches!(error, LoadError::Io(_)), "{error:?}");
3396 assert_eq!(
3397 error.to_string(),
3398 format!("cannot read the IR snapshot: {io}")
3399 );
3400
3401 let bad = dir.path().join("bad.ir.json");
3404 std::fs::write(&bad, "\n{").unwrap();
3405 let parse = v2::from_json("\n{").unwrap_err();
3406 let error = load_ir_json(&bad).unwrap_err();
3407 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3408 assert_eq!(
3409 error.to_string(),
3410 format!("the IR snapshot is not valid IR v2 JSON: {parse}")
3411 );
3412 }
3413
3414 #[test]
3417 fn input_past_the_nesting_cap_draws_from_json_s_refusal() {
3418 let dir = tempfile::tempdir().unwrap();
3419 let deep = format!(
3420 "{}{}",
3421 "[".repeat(MAX_JSON_NESTING + 1),
3422 "]".repeat(MAX_JSON_NESTING + 1)
3423 );
3424 let path = dir.path().join("deep.ir.json");
3425 std::fs::write(&path, &deep).unwrap();
3426 let refusal = v2::from_json(&deep).unwrap_err().to_string();
3427 assert!(
3428 refusal.contains("the ceiling this reader enforces"),
3429 "{refusal}"
3430 );
3431 let error = load_ir_json(&path).unwrap_err();
3432 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3433 assert_eq!(
3434 error.to_string(),
3435 format!("the IR snapshot is not valid IR v2 JSON: {refusal}")
3436 );
3437 }
3438}