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 }
138
139 impl std::fmt::Display for SerializeError {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 match self {
142 Self::Json(source) => write!(
143 f,
144 "cannot render the package as canonical protobuf JSON: {source}; the known \
145 cause is an enum field holding a discriminant outside the schema"
146 ),
147 Self::Text(source) => write!(
148 f,
149 "cannot render the package as prototext: {source}; the known cause \
150 is composite nesting deeper than the transcoding decoder's recursion limit"
151 ),
152 }
153 }
154 }
155
156 impl std::error::Error for SerializeError {
157 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
158 match self {
159 Self::Json(source) => Some(source),
160 Self::Text(source) => Some(source),
161 }
162 }
163 }
164
165 pub fn to_json_pretty(package: &Package) -> Result<String, SerializeError> {
186 render_json(package)
187 }
188
189 pub fn system_to_json_pretty(system: &System) -> Result<String, SerializeError> {
193 render_json(system)
194 }
195
196 pub(crate) fn render_json<M: serde::Serialize>(message: &M) -> Result<String, SerializeError> {
200 let mut buf = Vec::new();
201 let mut serializer = serde_json::Serializer::pretty(&mut buf);
202 serde::Serialize::serialize(message, &mut serializer).map_err(SerializeError::Json)?;
203 Ok(String::from_utf8(buf).expect("serde_json emits UTF-8"))
204 }
205
206 pub(crate) const MAX_JSON_NESTING: usize = 1_000;
225
226 const JSON_PARSE_STACK: usize = 16 * 1024 * 1024;
231
232 pub(crate) fn max_json_nesting(text: &str) -> usize {
240 let mut depth = 0usize;
241 let mut deepest = 0usize;
242 let mut in_string = false;
243 let mut escaped = false;
244 for byte in text.bytes() {
245 if in_string {
246 if escaped {
247 escaped = false;
248 } else if byte == b'\\' {
249 escaped = true;
250 } else if byte == b'"' {
251 in_string = false;
252 }
253 } else {
254 match byte {
255 b'"' => in_string = true,
256 b'{' | b'[' => {
257 depth += 1;
258 deepest = deepest.max(depth);
259 }
260 b'}' | b']' => depth = depth.saturating_sub(1),
261 _ => {}
262 }
263 }
264 }
265 deepest
266 }
267
268 pub fn from_json(text: &str) -> Result<Package, serde_json::Error> {
290 read_json(text)
291 }
292
293 pub fn system_from_json(text: &str) -> Result<System, serde_json::Error> {
297 read_json(text)
298 }
299
300 pub(crate) fn read_json<M>(text: &str) -> Result<M, serde_json::Error>
303 where
304 M: serde::de::DeserializeOwned + Send,
305 {
306 check_nesting(text)?;
307 on_parse_stack(|| parse_json(text))
308 }
309
310 pub(crate) fn read_json_ignoring_unknown<M>(
328 descriptor: prost_reflect::MessageDescriptor,
329 text: &str,
330 ) -> Result<M, serde_json::Error>
331 where
332 M: serde::de::DeserializeOwned + Send,
333 {
334 use serde::de::DeserializeSeed as _;
335 check_nesting(text)?;
336 on_parse_stack(|| {
337 let mut deserializer = serde_json::Deserializer::from_str(text);
338 deserializer.disable_recursion_limit();
339 let known = Shape::Message(descriptor).deserialize(&mut deserializer)?;
340 deserializer.end()?;
341 serde::Deserialize::deserialize(known)
342 })
343 }
344
345 #[derive(Clone)]
348 enum Shape {
349 Message(prost_reflect::MessageDescriptor),
351 List(Box<Shape>),
353 Map(Box<Shape>),
355 Opaque,
357 }
358
359 impl Shape {
360 fn of(field: &prost_reflect::FieldDescriptor) -> Shape {
362 use prost_reflect::Kind;
363 if field.is_map() {
364 let Kind::Message(entry) = field.kind() else {
365 return Shape::Opaque;
366 };
367 return Shape::Map(Box::new(Shape::of_kind(
368 &entry.map_entry_value_field().kind(),
369 )));
370 }
371 let item = Shape::of_kind(&field.kind());
372 if field.is_list() {
373 Shape::List(Box::new(item))
374 } else {
375 item
376 }
377 }
378
379 fn of_kind(kind: &prost_reflect::Kind) -> Shape {
380 match kind {
381 prost_reflect::Kind::Message(message)
382 if !message.full_name().starts_with("google.protobuf.") =>
383 {
384 Shape::Message(message.clone())
385 }
386 _ => Shape::Opaque,
387 }
388 }
389 }
390
391 impl<'de> serde::de::DeserializeSeed<'de> for Shape {
392 type Value = serde_json::Value;
393
394 fn deserialize<D: serde::Deserializer<'de>>(
395 self,
396 deserializer: D,
397 ) -> Result<Self::Value, D::Error> {
398 match self {
399 Shape::Opaque => serde::Deserialize::deserialize(deserializer),
400 shape => deserializer.deserialize_any(ShapeVisitor(shape)),
401 }
402 }
403 }
404
405 struct ShapeVisitor(Shape);
406
407 impl<'de> serde::de::Visitor<'de> for ShapeVisitor {
408 type Value = serde_json::Value;
409
410 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 formatter.write_str(match self.0 {
412 Shape::List(_) => "an array",
413 _ => "an object",
414 })
415 }
416
417 fn visit_unit<E>(self) -> Result<Self::Value, E> {
419 Ok(serde_json::Value::Null)
420 }
421
422 fn visit_seq<A: serde::de::SeqAccess<'de>>(
423 self,
424 mut seq: A,
425 ) -> Result<Self::Value, A::Error> {
426 let Shape::List(item) = self.0 else {
427 return Err(serde::de::Error::invalid_type(
428 serde::de::Unexpected::Seq,
429 &self,
430 ));
431 };
432 let mut items = Vec::new();
433 while let Some(value) = seq.next_element_seed((*item).clone())? {
434 items.push(value);
435 }
436 Ok(serde_json::Value::Array(items))
437 }
438
439 fn visit_map<A: serde::de::MapAccess<'de>>(
440 self,
441 mut map: A,
442 ) -> Result<Self::Value, A::Error> {
443 use serde::de::Error as _;
444 let mut out = serde_json::Map::new();
445 match &self.0 {
446 Shape::Message(descriptor) => {
447 let mut seen = std::collections::HashSet::new();
448 while let Some(key) = map.next_key::<String>()? {
449 let field = descriptor
450 .get_field_by_json_name(&key)
451 .or_else(|| descriptor.get_field_by_name(&key));
452 match field {
453 Some(field) => {
454 if !seen.insert(field.number()) {
455 return Err(A::Error::custom(format!(
456 "duplicate field `{key}`"
457 )));
458 }
459 let value = map.next_value_seed(Shape::of(&field))?;
460 out.insert(key, value);
461 }
462 None => {
463 map.next_value::<serde::de::IgnoredAny>()?;
464 }
465 }
466 }
467 }
468 Shape::Map(value_shape) => {
469 while let Some(key) = map.next_key::<String>()? {
470 let value = map.next_value_seed((**value_shape).clone())?;
471 if out.insert(key.clone(), value).is_some() {
472 return Err(A::Error::custom(format!("duplicate key `{key}`")));
473 }
474 }
475 }
476 Shape::List(_) | Shape::Opaque => {
477 return Err(A::Error::invalid_type(serde::de::Unexpected::Map, &self));
478 }
479 }
480 Ok(serde_json::Value::Object(out))
481 }
482 }
483
484 fn check_nesting(text: &str) -> Result<(), serde_json::Error> {
486 if max_json_nesting(text) > MAX_JSON_NESTING {
487 return Err(<serde_json::Error as serde::de::Error>::custom(format!(
488 "the input nests deeper than {MAX_JSON_NESTING} JSON levels, the ceiling this \
489 reader enforces (ADR-0014 decision 14); real IR nests orders of magnitude \
490 shallower"
491 )));
492 }
493 Ok(())
494 }
495
496 fn on_parse_stack<R: Send>(parse: impl FnOnce() -> R + Send) -> R {
499 if cfg!(target_family = "wasm") {
500 parse()
509 } else {
510 std::thread::scope(|scope| {
511 let handle = std::thread::Builder::new()
512 .stack_size(JSON_PARSE_STACK)
513 .spawn_scoped(scope, parse)
514 .expect("the JSON parse thread spawns");
515 match handle.join() {
516 Ok(result) => result,
517 Err(payload) => std::panic::resume_unwind(payload),
518 }
519 })
520 }
521 }
522
523 fn parse_json<M: serde::de::DeserializeOwned>(text: &str) -> Result<M, serde_json::Error> {
528 let mut deserializer = serde_json::Deserializer::from_str(text);
529 deserializer.disable_recursion_limit();
530 let message: M = serde::Deserialize::deserialize(&mut deserializer)?;
531 deserializer.end()?;
532 Ok(message)
533 }
534
535 pub fn to_text_format(package: &Package) -> Result<String, SerializeError> {
549 render_text_for(package_descriptor(), package)
550 }
551
552 pub fn system_to_text_format(system: &System) -> Result<String, SerializeError> {
556 render_text_for(system_descriptor(), system)
557 }
558
559 pub(crate) fn render_text_for<M: prost::Message>(
562 descriptor: prost_reflect::MessageDescriptor,
563 message: &M,
564 ) -> Result<String, SerializeError> {
565 let dynamic = transcode(descriptor, message).map_err(SerializeError::Text)?;
566 Ok(dynamic.to_text_format_with_options(
567 &prost_reflect::text_format::FormatOptions::new()
568 .pretty(true)
569 .skip_default_fields(false)
570 .print_message_fields_in_index_order(true),
571 ))
572 }
573
574 #[cfg(test)]
580 #[derive(Debug)]
581 pub(crate) enum TextFormatError {
582 Parse(prost_reflect::text_format::ParseError),
584 Transcode(prost::DecodeError),
586 }
587
588 #[cfg(test)]
589 impl std::fmt::Display for TextFormatError {
590 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591 match self {
592 Self::Parse(source) => {
593 write!(f, "cannot parse the text as an IR package: {source}")
594 }
595 Self::Transcode(source) => write!(
596 f,
597 "cannot rebuild the parsed prototext as a package: {source}; the known cause \
598 is composite nesting deeper than the transcoding decoder's recursion limit"
599 ),
600 }
601 }
602 }
603
604 #[cfg(test)]
605 impl std::error::Error for TextFormatError {
606 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
607 match self {
608 Self::Parse(source) => Some(source),
609 Self::Transcode(source) => Some(source),
610 }
611 }
612 }
613
614 #[cfg(test)]
635 pub(crate) fn from_text_format(text: &str) -> Result<Package, TextFormatError> {
636 parse_text(package_descriptor(), text)
637 }
638
639 #[cfg(test)]
643 pub(crate) fn system_from_text_format(text: &str) -> Result<System, TextFormatError> {
644 parse_text(system_descriptor(), text)
645 }
646
647 #[cfg(test)]
648 fn parse_text<M: prost::Message + Default>(
649 descriptor: prost_reflect::MessageDescriptor,
650 text: &str,
651 ) -> Result<M, TextFormatError> {
652 let dynamic = prost_reflect::DynamicMessage::parse_text_format(descriptor, text)
653 .map_err(TextFormatError::Parse)?;
654 dynamic.transcode_to().map_err(TextFormatError::Transcode)
655 }
656
657 pub fn to_binary(package: &Package) -> Vec<u8> {
664 prost::Message::encode_to_vec(package)
665 }
666
667 pub fn from_binary(bytes: &[u8]) -> Result<Package, prost::DecodeError> {
670 prost::Message::decode(bytes)
671 }
672
673 pub fn system_to_binary(system: &System) -> Vec<u8> {
676 prost::Message::encode_to_vec(system)
677 }
678
679 pub fn system_from_binary(bytes: &[u8]) -> Result<System, prost::DecodeError> {
682 prost::Message::decode(bytes)
683 }
684
685 fn qualified(package: &str, name: &str) -> String {
690 if package.is_empty() {
691 name.to_string()
692 } else {
693 format!("{package}.{name}")
694 }
695 }
696
697 impl System {
698 pub fn qualified_name(&self) -> String {
701 qualified(&self.package, &self.name)
702 }
703 }
704
705 impl Component {
706 pub fn qualified_name(&self) -> String {
709 qualified(&self.package, &self.name)
710 }
711 }
712
713 impl Distribution {
714 pub fn qualified_name(&self) -> String {
717 qualified(&self.package, &self.name)
718 }
719 }
720
721 #[derive(Debug, Clone, Copy, PartialEq)]
752 pub struct InterfaceShape<'a> {
753 pub name: &'a str,
757 pub interface: &'a Interface,
759 pub service: Option<&'a Service>,
762 }
763
764 impl InterfaceShape<'_> {
765 pub fn visibility(&self) -> i32 {
770 match self.service {
771 Some(service) => service.visibility,
772 None => self.interface.visibility,
773 }
774 }
775
776 pub fn is_inline(&self) -> bool {
778 self.service.is_some()
779 }
780 }
781
782 impl Package {
783 pub fn shapes(&self) -> impl Iterator<Item = InterfaceShape<'_>> {
798 let named = self.interfaces.iter().map(|interface| InterfaceShape {
799 name: &interface.name,
800 interface,
801 service: None,
802 });
803 let inline = self.services.iter().flat_map(|service| {
804 service
805 .shapes
806 .iter()
807 .filter_map(move |slot| match slot.kind.as_ref()? {
808 service_shape::Kind::Inline(interface) => Some(InterfaceShape {
809 name: &service.name,
810 interface,
811 service: Some(service),
812 }),
813 service_shape::Kind::InterfaceRef(_) => None,
814 })
815 });
816 named.chain(inline)
817 }
818 }
819
820 pub fn unit_of(package: &Package) -> &str {
825 if package.unit.is_empty() {
826 &package.name
827 } else {
828 &package.unit
829 }
830 }
831
832 pub fn packages_of_unit<'a>(
834 unit: &str,
835 packages: &'a [Package],
836 ) -> impl Iterator<Item = &'a Package> {
837 select_unit(unit, packages.iter())
838 }
839
840 pub fn members_of_unit<'a>(
843 unit: &str,
844 packages: &[&'a Package],
845 ) -> impl Iterator<Item = &'a Package> {
846 select_unit(unit, packages.iter().copied())
847 }
848
849 fn select_unit<'a>(
852 unit: &str,
853 packages: impl Iterator<Item = &'a Package>,
854 ) -> impl Iterator<Item = &'a Package> {
855 let unit = unit.to_owned();
856 packages.filter(move |p| unit_of(p) == unit)
857 }
858
859 pub fn relative_name(unit: &str, package: &str, name: &str) -> String {
863 if package == unit {
864 return name.to_owned();
865 }
866 match package
870 .strip_prefix(unit)
871 .and_then(|rest| rest.strip_prefix('.'))
872 {
873 Some(below) => format!("{below}.{name}"),
874 None => format!("{package}.{name}"),
875 }
876 }
877
878 impl Package {
879 pub fn catalog_name(&self, shape: &InterfaceShape<'_>) -> String {
883 if shape.is_inline() {
884 shape.name.to_owned()
885 } else {
886 relative_name(unit_of(self), &self.name, shape.name)
887 }
888 }
889 }
890
891 pub fn unit_retired(unit: &str, packages: &[&Package]) -> Vec<RetiredInterface> {
898 let mut seen: Vec<&str> = Vec::new();
899 let mut retired: Vec<RetiredInterface> = members_of_unit(unit, packages)
900 .filter(|package| {
901 let first = !seen.contains(&package.name.as_str());
902 if first {
903 seen.push(&package.name);
904 }
905 first
906 })
907 .flat_map(|package| package.retired.iter().cloned())
908 .collect();
909 retired.sort_by_key(|entry| entry.number);
910 retired
911 }
912
913 pub fn referenced_packages(package: &Package) -> std::collections::BTreeSet<String> {
926 let mut found = std::collections::BTreeSet::new();
927 for decl in &package.decls {
928 walk_decl(decl, &mut found);
929 }
930 for interface in &package.interfaces {
931 for interaction in &interface.interactions {
932 walk_decl(interaction, &mut found);
933 }
934 }
935 for service in &package.services {
936 for slot in &service.shapes {
937 match &slot.kind {
938 Some(service_shape::Kind::InterfaceRef(reference)) => {
939 qualifier(reference, &mut found);
940 }
941 Some(service_shape::Kind::Inline(interface)) => {
942 for interaction in &interface.interactions {
943 walk_decl(interaction, &mut found);
944 }
945 }
946 None => {}
947 }
948 }
949 }
950 found
951 }
952
953 fn qualifier(reference: &str, found: &mut std::collections::BTreeSet<String>) {
956 if let Some((package, _)) = reference.rsplit_once('.') {
957 found.insert(package.to_string());
958 }
959 }
960
961 fn walk_decl(decl: &Decl, found: &mut std::collections::BTreeSet<String>) {
964 match &decl.kind {
965 Some(decl::Kind::TypeDef(type_def)) => walk_type_def(type_def, found),
966 Some(decl::Kind::ConstDef(const_def)) => {
967 if let Some(reference) = &const_def.type_ref {
968 qualifier(reference, found);
969 }
970 }
971 Some(decl::Kind::StructDef(struct_def)) => {
972 for member in &struct_def.members {
973 match &member.member {
974 Some(struct_member::Member::Field(field)) => {
975 if let Some(field_type) = &field.r#type {
976 walk_field_type(field_type, found);
977 }
978 }
979 Some(struct_member::Member::Reserved(_)) | None => {}
981 }
982 }
983 }
984 Some(decl::Kind::EnumDef(_)) => {}
986 Some(decl::Kind::EnumSetDef(enum_set)) => {
987 if let Some(reference) = &enum_set.backing_enum {
988 qualifier(reference, found);
989 }
990 }
991 Some(decl::Kind::UnionDef(union_def)) => {
992 for arm in &union_def.arms {
993 qualifier(&arm.type_ref, found);
994 }
995 }
996 Some(decl::Kind::SignalDef(signal)) => qualifier(&signal.payload, found),
997 Some(decl::Kind::EventDef(event)) => qualifier(&event.payload, found),
998 Some(decl::Kind::CommandDef(command)) => {
999 for param in &command.params {
1000 if let Some(field_type) = ¶m.r#type {
1001 walk_field_type(field_type, found);
1002 }
1003 }
1004 }
1005 Some(decl::Kind::QueryDef(query)) => {
1006 for param in &query.params {
1007 if let Some(field_type) = ¶m.r#type {
1008 walk_field_type(field_type, found);
1009 }
1010 }
1011 if let Some(return_type) = &query.return_type {
1012 walk_return_type(return_type, found);
1013 }
1014 }
1015 Some(decl::Kind::FixedDef(fixed)) => {
1016 if let Some(field_type) = &fixed.payload {
1017 walk_field_type(field_type, found);
1018 }
1019 }
1020 Some(decl::Kind::ReservedSlot(_)) | None => {}
1022 }
1023 }
1024
1025 fn walk_field_type(field_type: &FieldType, found: &mut std::collections::BTreeSet<String>) {
1028 match &field_type.kind {
1029 Some(field_type::Kind::Named(reference)) => qualifier(reference, found),
1030 Some(field_type::Kind::Primitive(_)) => {}
1032 Some(field_type::Kind::InlineScalar(type_def)) => walk_type_def(type_def, found),
1033 Some(field_type::Kind::Tuple(tuple)) => {
1034 for field in &tuple.fields {
1035 if let Some(inner) = &field.r#type {
1036 walk_field_type(inner, found);
1037 }
1038 }
1039 }
1040 Some(field_type::Kind::Array(array)) => {
1041 if let Some(element) = &array.element {
1042 walk_field_type(element, found);
1043 }
1044 }
1045 Some(field_type::Kind::Map(map)) => {
1046 if let Some(key) = &map.key {
1047 walk_field_type(key, found);
1048 }
1049 if let Some(value) = &map.value {
1050 walk_field_type(value, found);
1051 }
1052 }
1053 Some(field_type::Kind::Stream(stream)) => match &stream.element {
1054 Some(stream_type::Element::Named(reference)) => qualifier(reference, found),
1055 Some(stream_type::Element::Primitive(_)) | None => {}
1057 },
1058 None => {}
1059 }
1060 }
1061
1062 fn walk_type_def(type_def: &TypeDef, found: &mut std::collections::BTreeSet<String>) {
1064 if let Some(constraint) = &type_def.constraint
1065 && let Some(reference) = &constraint.pattern_const
1066 {
1067 qualifier(reference, found);
1068 }
1069 }
1070
1071 fn walk_return_type(return_type: &ReturnType, found: &mut std::collections::BTreeSet<String>) {
1072 match &return_type.kind {
1073 Some(return_type::Kind::Value(field_type)) => walk_field_type(field_type, found),
1074 Some(return_type::Kind::Fallible(fallible)) => {
1075 qualifier(&fallible.ok, found);
1076 qualifier(&fallible.err, found);
1077 }
1078 None => {}
1079 }
1080 }
1081
1082 pub fn constraint_is_vacuous(constraint: Option<&Constraint>) -> bool {
1098 let Some(c) = constraint else { return true };
1099 c.min.is_none()
1100 && c.max.is_none()
1101 && c.step.is_none()
1102 && c.len_min.is_none()
1103 && c.len_max.is_none()
1104 && c.pattern.is_none()
1105 && c.pattern_const.is_none()
1106 }
1107}
1108
1109pub mod catalog_hash;
1110pub mod codegen;
1111pub mod name;
1112pub mod projection;
1113pub mod rules;
1114pub mod zero;
1115
1116#[cfg(test)]
1117mod v2_round_trip {
1118 use crate::v2;
1119
1120 fn interaction(name: &str, ordinal: u32, kind: v2::decl::Kind) -> v2::Decl {
1125 v2::Decl {
1126 name: name.to_string(),
1127 visibility: v2::Visibility::Unspecified as i32,
1128 is_error: false,
1129 doc: String::new(),
1130 labels: Vec::new(),
1131 deprecated: None,
1132 ordinal,
1133 kind: Some(kind),
1134 links: Vec::new(),
1135 see: Vec::new(),
1136 since: Vec::new(),
1137 }
1138 }
1139
1140 fn named_type(name: &str) -> v2::FieldType {
1141 v2::FieldType {
1142 optional: false,
1143 kind: Some(v2::field_type::Kind::Named(name.to_string())),
1144 }
1145 }
1146
1147 fn stream_of(element: v2::stream_type::Element) -> v2::FieldType {
1148 v2::FieldType {
1149 optional: false,
1150 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
1151 element: Some(element),
1152 })),
1153 }
1154 }
1155
1156 fn fixture() -> v2::Package {
1162 let speed = v2::SignalDef {
1165 payload: "Speed".to_string(),
1166 declared_init: None,
1167 init: Some(v2::InitValue {
1168 derivable: true,
1169 value: Some("0.0".to_string()),
1170 }),
1171 timing: Some(v2::Timing {
1172 mode: v2::TimingMode::StrictPeriodic as i32,
1173 min_us: Some("10000".to_string()),
1174 max_us: Some("10000".to_string()),
1175 default_applied: false,
1176 }),
1177 };
1178
1179 let door_opened = v2::EventDef {
1182 payload: "DoorEvent".to_string(),
1183 timing: Some(v2::Timing {
1184 mode: v2::TimingMode::Range as i32,
1185 min_us: Some("20000".to_string()),
1186 max_us: Some("500000".to_string()),
1187 default_applied: true,
1188 }),
1189 };
1190
1191 let set_target = v2::CommandDef {
1193 params: vec![v2::Param {
1194 name: "target".to_string(),
1195 r#type: Some(named_type("Speed")),
1196 doc: String::new(),
1197 links: Vec::new(),
1198 see: Vec::new(),
1199 since: Vec::new(),
1200 }],
1201 contracts: vec![v2::Contract {
1202 kind: v2::ContractKind::Require as i32,
1203 source: "target >= speed".to_string(),
1204 signal_refs: vec!["speed".to_string()],
1205 param_refs: vec!["target".to_string()],
1206 uses_result: false,
1207 observer_id: "VehicleStatus.setTarget.require[0]".to_string(),
1208 }],
1209 timing: None,
1210 };
1211
1212 let fetch_faults = v2::QueryDef {
1215 params: vec![v2::Param {
1216 name: "page".to_string(),
1217 r#type: Some(named_type("PageSpec")),
1218 doc: String::new(),
1219 links: Vec::new(),
1220 see: Vec::new(),
1221 since: Vec::new(),
1222 }],
1223 return_type: Some(v2::ReturnType {
1224 kind: Some(v2::return_type::Kind::Fallible(v2::FallibleType {
1225 ok: "FaultPage".to_string(),
1226 err: "DiagError".to_string(),
1227 })),
1228 }),
1229 contracts: vec![v2::Contract {
1230 kind: v2::ContractKind::Ensure as i32,
1231 source: "result.count <= page.limit".to_string(),
1232 signal_refs: Vec::new(),
1233 param_refs: vec!["page".to_string()],
1234 uses_result: true,
1235 observer_id: "VehicleStatus.fetchFaults.ensure[0]".to_string(),
1236 }],
1237 timing: None,
1238 };
1239
1240 let vin = v2::FixedDef {
1242 payload: Some(named_type("Vin")),
1243 };
1244
1245 let vehicle_status = v2::Interface {
1246 name: "VehicleStatus".to_string(),
1247 visibility: v2::Visibility::Public as i32,
1248 doc: "Vehicle status contract".to_string(),
1249 labels: Vec::new(),
1250 deprecated: None,
1251 interactions: vec![
1252 interaction("speed", 1, v2::decl::Kind::SignalDef(speed)),
1253 interaction("doorOpened", 2, v2::decl::Kind::EventDef(door_opened)),
1254 v2::Decl {
1257 ordinal: 3,
1258 kind: Some(v2::decl::Kind::ReservedSlot(v2::Reserved {
1259 ordinal: 3,
1260 name: Some("legacyMode".to_string()),
1261 value: None,
1262 })),
1263 ..interaction("", 3, v2::decl::Kind::ReservedSlot(v2::Reserved::default()))
1264 },
1265 interaction("setTarget", 4, v2::decl::Kind::CommandDef(set_target)),
1266 interaction("fetchFaults", 5, v2::decl::Kind::QueryDef(fetch_faults)),
1267 interaction("vin", 6, v2::decl::Kind::FixedDef(vin)),
1268 ],
1269 number: 0,
1270 provisional: false,
1271 links: Vec::new(),
1272 see: Vec::new(),
1273 since: Vec::new(),
1274 };
1275
1276 let tail_logs = v2::QueryDef {
1279 params: vec![v2::Param {
1280 name: "pattern".to_string(),
1281 r#type: Some(stream_of(v2::stream_type::Element::Primitive(
1282 v2::PrimitiveType::String as i32,
1283 ))),
1284 doc: String::new(),
1285 links: Vec::new(),
1286 see: Vec::new(),
1287 since: Vec::new(),
1288 }],
1289 return_type: Some(v2::ReturnType {
1290 kind: Some(v2::return_type::Kind::Value(stream_of(
1291 v2::stream_type::Element::Named("LogLine".to_string()),
1292 ))),
1293 }),
1294 contracts: Vec::new(),
1295 timing: None,
1296 };
1297
1298 let status_service = v2::Service {
1301 name: "veh.adas.status".to_string(),
1302 visibility: v2::Visibility::Public as i32,
1303 doc: String::new(),
1304 labels: Vec::new(),
1305 deprecated: None,
1306 shapes: vec![v2::ServiceShape {
1307 kind: Some(v2::service_shape::Kind::InterfaceRef(
1308 "VehicleStatus".to_string(),
1309 )),
1310 }],
1311 links: Vec::new(),
1312 see: Vec::new(),
1313 since: Vec::new(),
1314 };
1315 let logs_service = v2::Service {
1318 name: "veh.adas.logs".to_string(),
1319 visibility: v2::Visibility::Public as i32,
1320 doc: String::new(),
1321 labels: Vec::new(),
1322 deprecated: None,
1323 shapes: vec![v2::ServiceShape {
1324 kind: Some(v2::service_shape::Kind::Inline(v2::Interface {
1325 name: String::new(),
1326 visibility: v2::Visibility::Unspecified as i32,
1327 doc: String::new(),
1328 labels: Vec::new(),
1329 deprecated: None,
1330 interactions: vec![interaction(
1331 "tailLogs",
1332 1,
1333 v2::decl::Kind::QueryDef(tail_logs),
1334 )],
1335 number: 0,
1336 provisional: false,
1337 links: Vec::new(),
1338 see: Vec::new(),
1339 since: Vec::new(),
1340 })),
1341 }],
1342 links: Vec::new(),
1343 see: Vec::new(),
1344 since: Vec::new(),
1345 };
1346
1347 v2::Package {
1348 name: "veh.adas".to_string(),
1349 decls: vec![v2::Decl {
1353 name: "Speed".to_string(),
1354 visibility: v2::Visibility::Public as i32,
1355 is_error: false,
1356 doc: String::new(),
1357 labels: Vec::new(),
1358 deprecated: None,
1359 ordinal: 0,
1360 kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
1361 backing: Some(v2::Backing {
1362 kind: Some(v2::backing::Kind::Unit("km/h".to_string())),
1363 }),
1364 constraint: None,
1365 declared_init: None,
1366 init: None,
1367 width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
1368 })),
1369 links: Vec::new(),
1370 see: Vec::new(),
1371 since: Vec::new(),
1372 }],
1373 interfaces: vec![vehicle_status],
1374 services: vec![status_service, logs_service],
1375 retired: Vec::new(),
1376 unit: "veh.adas".to_string(),
1377 }
1378 }
1379
1380 fn vocabulary_fixture() -> v2::Package {
1387 fn decl(name: &str, kind: v2::decl::Kind) -> v2::Decl {
1388 v2::Decl {
1389 name: name.to_string(),
1390 visibility: v2::Visibility::Public as i32,
1391 is_error: false,
1392 doc: String::new(),
1393 labels: Vec::new(),
1394 deprecated: None,
1395 ordinal: 0,
1396 kind: Some(kind),
1397 links: Vec::new(),
1398 see: Vec::new(),
1399 since: Vec::new(),
1400 }
1401 }
1402
1403 fn field(name: &str, ordinal: u32, field_type: v2::FieldType) -> v2::Field {
1404 v2::Field {
1405 name: name.to_string(),
1406 ordinal,
1407 r#type: Some(field_type),
1408 declared_init: None,
1409 init: None,
1410 doc: String::new(),
1411 labels: Vec::new(),
1412 deprecated: None,
1413 links: Vec::new(),
1414 see: Vec::new(),
1415 since: Vec::new(),
1416 }
1417 }
1418
1419 let max_retry = v2::ConstDef {
1421 type_ref: Some("integer".to_string()),
1422 value: "24".to_string(),
1423 regex: None,
1424 };
1425
1426 let gear = v2::EnumDef {
1429 values: vec![
1430 v2::EnumValue {
1431 name: "PARK".to_string(),
1432 value: 1,
1433 doc: String::new(),
1434 links: Vec::new(),
1435 see: Vec::new(),
1436 since: Vec::new(),
1437 },
1438 v2::EnumValue {
1439 name: "DRIVE".to_string(),
1440 value: 2,
1441 doc: String::new(),
1442 links: Vec::new(),
1443 see: Vec::new(),
1444 since: Vec::new(),
1445 },
1446 ],
1447 reserved: vec![v2::Reserved {
1448 ordinal: 0,
1449 name: None,
1450 value: Some(7),
1451 }],
1452 };
1453
1454 let warnings = v2::EnumSetDef {
1457 backing_enum: None,
1458 bits: vec![
1459 v2::EnumValue {
1460 name: "LOW_FUEL".to_string(),
1461 value: 0,
1462 doc: String::new(),
1463 links: Vec::new(),
1464 see: Vec::new(),
1465 since: Vec::new(),
1466 },
1467 v2::EnumValue {
1468 name: "ICE_RISK".to_string(),
1469 value: 33,
1470 doc: String::new(),
1471 links: Vec::new(),
1472 see: Vec::new(),
1473 since: Vec::new(),
1474 },
1475 ],
1476 width: v2::IntWidth::U64 as i32,
1477 };
1478
1479 let plate_text = v2::TypeDef {
1482 backing: Some(v2::Backing {
1483 kind: Some(v2::backing::Kind::Primitive(
1484 v2::PrimitiveType::String as i32,
1485 )),
1486 }),
1487 constraint: Some(v2::Constraint {
1488 min: None,
1489 max: None,
1490 step: None,
1491 len_min: Some(1),
1492 len_max: Some(86),
1493 pattern: None,
1494 pattern_const: None,
1495 }),
1496 declared_init: None,
1497 init: None,
1498 width: None,
1499 };
1500
1501 let sample = v2::UnionDef {
1503 arms: vec![
1504 v2::UnionArm {
1505 name: "speed".to_string(),
1506 ordinal: 1,
1507 type_ref: "Speed".to_string(),
1508 doc: String::new(),
1509 links: Vec::new(),
1510 see: Vec::new(),
1511 since: Vec::new(),
1512 },
1513 v2::UnionArm {
1514 name: "gear".to_string(),
1515 ordinal: 2,
1516 type_ref: "Gear".to_string(),
1517 doc: String::new(),
1518 links: Vec::new(),
1519 see: Vec::new(),
1520 since: Vec::new(),
1521 },
1522 ],
1523 is_result: false,
1524 reserved: Vec::new(),
1525 };
1526
1527 let retries = v2::Field {
1533 declared_init: Some("3".to_string()),
1534 init: Some(v2::InitValue {
1535 derivable: true,
1536 value: Some("3".to_string()),
1537 }),
1538 ..field(
1539 "retries",
1540 1,
1541 v2::FieldType {
1542 optional: false,
1543 kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
1544 backing: Some(v2::Backing {
1545 kind: Some(v2::backing::Kind::Primitive(
1546 v2::PrimitiveType::Integer as i32,
1547 )),
1548 }),
1549 constraint: Some(v2::Constraint {
1550 min: Some("0".to_string()),
1551 max: Some("24".to_string()),
1552 step: None,
1553 len_min: None,
1554 len_max: None,
1555 pattern: None,
1556 pattern_const: None,
1557 }),
1558 declared_init: None,
1559 init: None,
1560 width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
1561 }))),
1562 },
1563 )
1564 };
1565
1566 let position = field(
1569 "position",
1570 2,
1571 v2::FieldType {
1572 optional: false,
1573 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
1574 fields: vec![
1575 v2::TupleField {
1576 name: "x".to_string(),
1577 r#type: Some(named_type("Speed")),
1578 },
1579 v2::TupleField {
1580 name: "y".to_string(),
1581 r#type: Some(named_type("Speed")),
1582 },
1583 ],
1584 })),
1585 },
1586 );
1587
1588 let gears = field(
1590 "gears",
1591 3,
1592 v2::FieldType {
1593 optional: false,
1594 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
1595 element: Some(Box::new(named_type("Gear"))),
1596 min: 1,
1597 max: 4096,
1598 }))),
1599 },
1600 );
1601
1602 let plates = v2::Field {
1606 deprecated: Some("superseded by gears".to_string()),
1607 ..field(
1608 "plates",
1609 4,
1610 v2::FieldType {
1611 optional: false,
1612 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
1613 key: Some(Box::new(named_type("PlateText"))),
1614 value: Some(Box::new(named_type("Gear"))),
1615 min: 0,
1616 max: 53,
1617 }))),
1618 },
1619 )
1620 };
1621
1622 let snapshot = v2::StructDef {
1623 members: [retries, position, gears, plates]
1624 .into_iter()
1625 .map(|field| v2::StructMember {
1626 member: Some(v2::struct_member::Member::Field(Box::new(field))),
1627 })
1628 .collect(),
1629 fixed_layout: false,
1630 };
1631
1632 v2::Package {
1633 name: "veh.vocab".to_string(),
1634 decls: vec![
1635 decl("MAX_RETRY", v2::decl::Kind::ConstDef(max_retry)),
1636 decl("Gear", v2::decl::Kind::EnumDef(gear)),
1637 decl("Warnings", v2::decl::Kind::EnumSetDef(warnings)),
1638 decl("PlateText", v2::decl::Kind::TypeDef(plate_text)),
1639 v2::Decl {
1642 deprecated: Some("use Snapshot".to_string()),
1643 ..decl("Sample", v2::decl::Kind::UnionDef(sample))
1644 },
1645 decl("Snapshot", v2::decl::Kind::StructDef(snapshot)),
1646 ],
1647 interfaces: Vec::new(),
1648 services: Vec::new(),
1649 retired: Vec::new(),
1650 unit: String::new(),
1651 }
1652 }
1653
1654 #[test]
1655 fn protobuf_round_trip_preserves_package() {
1656 let package = fixture();
1657
1658 let buf = v2::to_binary(&package);
1659 let decoded = v2::from_binary(buf.as_slice()).expect("decode must succeed");
1660
1661 assert_eq!(package, decoded);
1662
1663 let vocabulary = vocabulary_fixture();
1665 let decoded_vocabulary =
1666 v2::from_binary(v2::to_binary(&vocabulary).as_slice()).expect("decode must succeed");
1667 assert_eq!(vocabulary, decoded_vocabulary);
1668
1669 let interface = &decoded.interfaces[0];
1670 let ordinals: Vec<u32> = interface.interactions.iter().map(|d| d.ordinal).collect();
1671 assert_eq!(
1672 ordinals,
1673 [1, 2, 3, 4, 5, 6],
1674 "one ordinal sequence, tombstone counted (ridl §11)"
1675 );
1676 let Some(v2::decl::Kind::ReservedSlot(tombstone)) = &interface.interactions[2].kind else {
1677 panic!("ordinal 3 must decode as a reserved tombstone");
1678 };
1679 assert_eq!(tombstone.name.as_deref(), Some("legacyMode"));
1680 let Some(v2::service_shape::Kind::Inline(inline)) = decoded.services[1]
1681 .shapes
1682 .first()
1683 .and_then(|slot| slot.kind.as_ref())
1684 else {
1685 panic!("veh.adas.logs must decode as an inline shape");
1686 };
1687 assert_eq!(inline.name, "", "an inline shape carries no name");
1688 let references: Vec<&str> = decoded.services[0]
1689 .shapes
1690 .iter()
1691 .filter_map(|slot| match &slot.kind {
1692 Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
1693 _ => None,
1694 })
1695 .collect();
1696 assert_eq!(
1697 references,
1698 ["VehicleStatus"],
1699 "a service's set carries its references and nothing else"
1700 );
1701 }
1702
1703 #[test]
1704 fn json_round_trip_preserves_package() {
1705 for package in [fixture(), vocabulary_fixture()] {
1706 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1707 let decoded = v2::from_json(&json).expect("json deserialization must succeed");
1708
1709 assert_eq!(package, decoded);
1710 }
1711 }
1712
1713 #[test]
1720 fn number_provisional_and_retired_round_trip_through_json_text_and_binary() {
1721 let mut package = fixture();
1722 package.interfaces[0].number = 4;
1723 package.interfaces[0].provisional = true;
1724 let Some(v2::service_shape::Kind::Inline(inline)) =
1725 package.services[1].shapes[0].kind.as_mut()
1726 else {
1727 panic!("veh.adas.logs holds an inline shape in slot 1");
1728 };
1729 inline.number = 5;
1730 package.retired = vec![
1731 v2::RetiredInterface {
1732 name: "LaneAssist".to_string(),
1733 number: 2,
1734 },
1735 v2::RetiredInterface {
1736 name: "service:veh.hvac.cabin".to_string(),
1737 number: 3,
1738 },
1739 ];
1740
1741 let json = v2::to_json_pretty(&package).expect("the package serializes as IR JSON");
1742 assert_eq!(v2::from_json(&json).expect("the JSON parses back"), package);
1743 let text = v2::to_text_format(&package).expect("the package serializes as prototext");
1744 assert_eq!(
1745 v2::from_text_format(&text).expect("the prototext parses back"),
1746 package
1747 );
1748 assert_eq!(
1749 v2::from_binary(v2::to_binary(&package).as_slice()).expect("the binary decodes"),
1750 package
1751 );
1752
1753 for needle in [
1754 r#""number": 4"#,
1755 r#""provisional": true"#,
1756 r#""number": 5"#,
1757 r#""name": "LaneAssist""#,
1758 r#""name": "service:veh.hvac.cabin""#,
1759 ] {
1760 assert!(
1761 json.contains(needle),
1762 "the JSON must carry {needle}, got: {json}"
1763 );
1764 }
1765
1766 let unnumbered = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1770 for needle in [
1771 r#""number": 0"#,
1772 r#""provisional": false"#,
1773 r#""retired": []"#,
1774 ] {
1775 assert!(
1776 unnumbered.contains(needle),
1777 "a default field must still be written, expected {needle} in: {unnumbered}"
1778 );
1779 }
1780 }
1781
1782 #[test]
1788 fn a_snapshot_lacking_the_number_fields_still_loads() {
1789 let package = v2::from_json(
1790 r#"{"name": "veh.x", "interfaces": [{"name": "LaneKeeping"}], "services": [{"name": "veh.x.s", "shapes": [{"inline": {"name": ""}}]}]}"#,
1791 )
1792 .expect("a pre-lock snapshot loads");
1793
1794 assert_eq!(package.interfaces[0].number, 0);
1795 assert!(!package.interfaces[0].provisional);
1796 let Some(v2::service_shape::Kind::Inline(inline)) =
1797 package.services[0].shapes[0].kind.as_ref()
1798 else {
1799 panic!("the service holds an inline shape");
1800 };
1801 assert_eq!(inline.number, 0);
1802 assert!(!inline.provisional);
1803 assert_eq!(package.retired, Vec::new());
1804 }
1805
1806 #[test]
1811 fn text_format_round_trip_preserves_package() {
1812 for package in [fixture(), vocabulary_fixture()] {
1813 let text = v2::to_text_format(&package).expect("the fixture serializes as prototext");
1814 let decoded = v2::from_text_format(&text).expect("prototext parsing must succeed");
1815
1816 assert_eq!(package, decoded);
1817 }
1818 }
1819
1820 #[test]
1835 fn text_format_is_pretty_with_defaults_in_index_order() {
1836 let text = v2::to_text_format(&fixture()).expect("the fixture serializes as prototext");
1837
1838 assert!(
1840 text.contains("\n "),
1841 "pretty printing must indent nested fields, got: {text}"
1842 );
1843 assert!(
1846 text.contains("is_error: false"),
1847 "a field holding its default must be emitted, got: {text}"
1848 );
1849 assert!(
1852 text.starts_with("name:"),
1853 "fields must print in schema index order, got: {text}"
1854 );
1855 }
1856
1857 fn strict_parse(json: &str) -> v2::Package {
1864 let mut deserializer = serde_json::Deserializer::from_str(json);
1865 let package = <v2::Package as serde::Deserialize>::deserialize(&mut deserializer)
1866 .expect("a strict conformant parser must accept the emitted JSON");
1867 deserializer.end().expect("no trailing input");
1868 package
1869 }
1870
1871 #[test]
1876 fn emitted_json_survives_a_strict_conformant_parse() {
1877 for package in [fixture(), vocabulary_fixture()] {
1878 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1879 assert_eq!(package, strict_parse(&json));
1880 }
1881 }
1882
1883 #[test]
1884 fn json_renders_timing_bounds_and_fallible_arms_exactly() {
1885 let json = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1886
1887 assert!(
1891 json.contains(r#""minUs": "10000""#),
1892 "the timing bound must be a JSON string, got: {json}"
1893 );
1894 assert!(
1896 json.contains(r#""ok": "FaultPage""#),
1897 "the ok arm must render, got: {json}"
1898 );
1899 assert!(
1900 json.contains(r#""err": "DiagError""#),
1901 "the err arm must render, got: {json}"
1902 );
1903 }
1904
1905 #[test]
1910 fn json_renders_64_bit_integer_fields_as_strings() {
1911 let json = v2::to_json_pretty(&vocabulary_fixture())
1912 .expect("the vocabulary fixture serializes as IR JSON");
1913
1914 assert!(
1916 json.contains(r#""max": "4096""#),
1917 "an array bound must be a JSON string, got: {json}"
1918 );
1919 assert!(
1921 json.contains(r#""lenMax": "86""#),
1922 "a length bound must be a JSON string, got: {json}"
1923 );
1924 assert!(
1926 json.contains(r#""value": "7""#),
1927 "a retired enum value must be a JSON string, got: {json}"
1928 );
1929 assert!(
1930 json.contains(r#""value": "33""#),
1931 "an enum-set bit position must be a JSON string, got: {json}"
1932 );
1933 }
1934
1935 #[test]
1936 fn fallible_transport_identity_follows_the_derivation_rule() {
1937 let fallible = v2::FallibleType {
1940 ok: "FaultPage".to_string(),
1941 err: "DiagError".to_string(),
1942 };
1943 assert_eq!(
1944 v2::fallible_transport_identity("VehicleStatus", 9, &fallible),
1945 "VehicleStatus#9:FaultPage|DiagError"
1946 );
1947
1948 let package = fixture();
1950 let interface = &package.interfaces[0];
1951 let query_decl = &interface.interactions[4];
1952 let Some(v2::decl::Kind::QueryDef(query)) = &query_decl.kind else {
1953 panic!("ordinal 5 must be the fallible query");
1954 };
1955 let Some(v2::return_type::Kind::Fallible(arms)) = &query.return_type.as_ref().unwrap().kind
1956 else {
1957 panic!("fetchFaults must return a fallible type");
1958 };
1959 assert_eq!(
1960 v2::fallible_transport_identity(&interface.name, query_decl.ordinal, arms),
1961 "VehicleStatus#5:FaultPage|DiagError"
1962 );
1963 }
1964
1965 #[test]
1972 fn shapes_walks_named_interfaces_and_inline_service_shapes() {
1973 let package = fixture();
1974 let walk: Vec<(&str, bool, usize)> = package
1975 .shapes()
1976 .map(|shape| {
1977 (
1978 shape.name,
1979 shape.is_inline(),
1980 shape.interface.interactions.len(),
1981 )
1982 })
1983 .collect();
1984 assert_eq!(
1985 walk,
1986 [("VehicleStatus", false, 6), ("veh.adas.logs", true, 1)],
1987 "the named interface, then the inline shape under the service's \
1988 dotted name",
1989 );
1990
1991 assert_eq!(package.services.len(), 2, "one reference form, one inline");
1995 assert!(
1996 !package
1997 .shapes()
1998 .any(|shape| shape.name == "veh.adas.status"),
1999 "a service naming an interface contributes no shape of its own",
2000 );
2001 }
2002
2003 #[test]
2004 fn relative_name_strips_the_unit_prefix() {
2005 assert_eq!(v2::relative_name("u", "u", "Session"), "Session");
2006 assert_eq!(
2007 v2::relative_name("u", "u.cluster", "Speed"),
2008 "cluster.Speed"
2009 );
2010 assert_eq!(
2011 v2::relative_name("com.example.hmi", "com.example.hmi.cluster.front", "A"),
2012 "cluster.front.A"
2013 );
2014 }
2015
2016 #[test]
2019 fn relative_name_keeps_the_full_name_of_a_package_outside_the_unit() {
2020 assert_eq!(
2021 v2::relative_name("u", "w.cluster", "Speed"),
2022 "w.cluster.Speed"
2023 );
2024 assert_eq!(v2::relative_name("u", "ux", "A"), "ux.A");
2025 assert_eq!(v2::relative_name("u.cluster", "u", "Session"), "u.Session");
2026 }
2027
2028 #[test]
2032 fn unit_retired_reads_a_package_named_twice_once() {
2033 let first = v2::Package {
2034 name: "u.cluster".to_string(),
2035 unit: "u".to_string(),
2036 retired: vec![v2::RetiredInterface {
2037 name: "cluster.Old".to_string(),
2038 number: 3,
2039 }],
2040 ..Default::default()
2041 };
2042 let second = v2::Package {
2043 retired: vec![v2::RetiredInterface {
2044 name: "cluster.Older".to_string(),
2045 number: 4,
2046 }],
2047 ..first.clone()
2048 };
2049 assert_eq!(
2050 v2::unit_retired("u", &[&first, &second]),
2051 first.retired,
2052 "the first package of a name is the one read"
2053 );
2054 }
2055
2056 #[test]
2057 fn an_inline_shape_keeps_its_global_name() {
2058 let mut package = fixture();
2059 package.name = "u.cluster".to_string();
2060 package.unit = "u".to_string();
2061 let inline = package
2062 .shapes()
2063 .find(|shape| shape.is_inline())
2064 .expect("the fixture has an inline shape");
2065 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2066 let named = package
2067 .shapes()
2068 .find(|shape| !shape.is_inline())
2069 .expect("the fixture has a declared interface");
2070 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2071 }
2072
2073 #[test]
2074 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2075 let mut package = fixture();
2076 package.unit = String::new();
2077 let named = package
2078 .shapes()
2079 .find(|shape| !shape.is_inline())
2080 .expect("the fixture has a declared interface");
2081 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2082 }
2083
2084 #[test]
2085 fn unit_of_falls_back_to_the_name() {
2086 let bare = v2::Package {
2087 name: "p".to_string(),
2088 ..Default::default()
2089 };
2090 assert_eq!(v2::unit_of(&bare), "p");
2091 let member = v2::Package {
2092 name: "u.a".to_string(),
2093 unit: "u".to_string(),
2094 ..Default::default()
2095 };
2096 assert_eq!(v2::unit_of(&member), "u");
2097 }
2098
2099 #[test]
2100 fn packages_of_unit_selects_by_unit_of() {
2101 let package = |name: &str, unit: &str| v2::Package {
2102 name: name.to_string(),
2103 unit: unit.to_string(),
2104 ..Default::default()
2105 };
2106 let packages = [
2107 package("u", "u"),
2108 package("u.a", "u"),
2109 package("w", ""),
2110 package("other.b", "other"),
2111 ];
2112 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2113 .map(|p| p.name.as_str())
2114 .collect();
2115 assert_eq!(names, ["u", "u.a"]);
2116 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2117 .map(|p| p.name.as_str())
2118 .collect();
2119 assert_eq!(legacy, ["w"]);
2120 }
2121
2122 #[test]
2127 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2128 let package = fixture();
2129 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2130
2131 let named = shapes[0];
2132 assert!(named.service.is_none());
2133 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2134 assert_eq!(named.visibility(), named.interface.visibility);
2135
2136 let inline = shapes[1];
2137 assert_eq!(
2138 inline.interface.visibility,
2139 v2::Visibility::Unspecified as i32,
2140 "the trap: an inline shape's own visibility field is unset",
2141 );
2142 assert_eq!(
2143 inline.service.expect("an inline shape has an owner").name,
2144 "veh.adas.logs",
2145 );
2146 assert_eq!(
2147 inline.visibility(),
2148 v2::Visibility::Public as i32,
2149 "the accessor reads the owning service's, never the unset field",
2150 );
2151 }
2152
2153 #[test]
2157 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2158 let mut package = fixture();
2159 package.services.clear();
2160 assert_eq!(package.shapes().count(), 1);
2161
2162 package.interfaces.clear();
2163 assert_eq!(package.shapes().count(), 0);
2164 }
2165
2166 #[test]
2173 fn referenced_packages_finds_qualifiers_at_depth() {
2174 fn named(reference: &str) -> v2::FieldType {
2175 v2::FieldType {
2176 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2177 ..Default::default()
2178 }
2179 }
2180
2181 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2182 v2::decl::Kind::FixedDef(v2::FixedDef {
2183 payload: Some(payload),
2184 })
2185 }
2186
2187 let package = v2::Package {
2188 name: "veh.cluster".to_string(),
2189 decls: vec![
2190 v2::Decl {
2191 name: "Local".to_string(),
2192 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2193 payload: "Speed".to_string(),
2194 ..Default::default()
2195 })),
2196 ..Default::default()
2197 },
2198 v2::Decl {
2199 name: "Stamped".to_string(),
2200 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2201 payload: "ridl.std.Timestamp".to_string(),
2202 ..Default::default()
2203 })),
2204 ..Default::default()
2205 },
2206 v2::Decl {
2207 name: "ArrLabels".to_string(),
2208 kind: Some(fixed(v2::FieldType {
2209 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2210 element: Some(Box::new(named("veh.arr.Label"))),
2211 min: 0,
2212 max: 32,
2213 }))),
2214 ..Default::default()
2215 })),
2216 ..Default::default()
2217 },
2218 v2::Decl {
2219 name: "TupThing".to_string(),
2220 kind: Some(fixed(v2::FieldType {
2221 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2222 fields: vec![v2::TupleField {
2223 name: "x".to_string(),
2224 r#type: Some(named("veh.tup.X")),
2225 }],
2226 })),
2227 ..Default::default()
2228 })),
2229 ..Default::default()
2230 },
2231 v2::Decl {
2232 name: "MapThing".to_string(),
2233 kind: Some(fixed(v2::FieldType {
2234 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2235 key: Some(Box::new(named("veh.key.X"))),
2236 value: Some(Box::new(named("veh.val.X"))),
2237 min: 0,
2238 max: 8,
2239 }))),
2240 ..Default::default()
2241 })),
2242 ..Default::default()
2243 },
2244 v2::Decl {
2245 name: "StreamThing".to_string(),
2246 kind: Some(fixed(v2::FieldType {
2247 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2248 element: Some(v2::stream_type::Element::Named(
2249 "veh.strm.X".to_string(),
2250 )),
2251 })),
2252 ..Default::default()
2253 })),
2254 ..Default::default()
2255 },
2256 ],
2257 ..Default::default()
2258 };
2259
2260 let found = v2::referenced_packages(&package);
2261 let expected: std::collections::BTreeSet<String> = [
2262 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2263 ]
2264 .into_iter()
2265 .map(str::to_string)
2266 .collect();
2267 assert_eq!(
2268 found, expected,
2269 "each recursive path must contribute its own distinct qualifier"
2270 );
2271 assert!(
2272 !found.contains("Speed") && !found.contains("veh.cluster"),
2273 "a bare reference contributes no package: {found:?}",
2274 );
2275 }
2276
2277 #[test]
2280 fn referenced_packages_is_empty_without_references() {
2281 let package = v2::Package {
2282 name: "veh.solo".to_string(),
2283 ..Default::default()
2284 };
2285 assert!(v2::referenced_packages(&package).is_empty());
2286 }
2287
2288 const NESTING_BELOW_LIMIT: usize = 45;
2294 const NESTING_PAST_LIMIT: usize = 60;
2298
2299 fn nested_package(depth: usize) -> v2::Package {
2304 let mut payload = v2::FieldType {
2305 optional: false,
2306 kind: Some(v2::field_type::Kind::Primitive(
2307 v2::PrimitiveType::Integer as i32,
2308 )),
2309 };
2310 for _ in 0..depth {
2311 payload = v2::FieldType {
2312 optional: false,
2313 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2314 element: Some(Box::new(payload)),
2315 min: 1,
2316 max: 1,
2317 }))),
2318 };
2319 }
2320 v2::Package {
2321 name: "veh.deep".to_string(),
2322 decls: vec![v2::Decl {
2323 name: "deep".to_string(),
2324 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2325 payload: Some(payload),
2326 })),
2327 ..Default::default()
2328 }],
2329 ..Default::default()
2330 }
2331 }
2332
2333 fn package_at_message_depth(levels: usize) -> v2::Package {
2345 assert!(levels >= 3, "the chain costs three levels before nesting");
2346 let (arrays, tuple) = if levels % 2 == 1 {
2347 ((levels - 3) / 2, false)
2348 } else {
2349 assert!(levels >= 6, "one tuple level costs three");
2350 ((levels - 6) / 2, true)
2351 };
2352
2353 let mut payload = v2::FieldType {
2354 optional: false,
2355 kind: Some(v2::field_type::Kind::Primitive(
2356 v2::PrimitiveType::Integer as i32,
2357 )),
2358 };
2359 for _ in 0..arrays {
2360 payload = v2::FieldType {
2361 optional: false,
2362 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2363 element: Some(Box::new(payload)),
2364 min: 1,
2365 max: 1,
2366 }))),
2367 };
2368 }
2369 if tuple {
2370 payload = v2::FieldType {
2371 optional: false,
2372 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2373 fields: vec![v2::TupleField {
2374 name: "f0".to_string(),
2375 r#type: Some(payload),
2376 }],
2377 })),
2378 };
2379 }
2380 v2::Package {
2381 name: "veh.deep".to_string(),
2382 decls: vec![v2::Decl {
2383 name: "deep".to_string(),
2384 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2385 payload: Some(payload),
2386 })),
2387 ..Default::default()
2388 }],
2389 ..Default::default()
2390 }
2391 }
2392
2393 fn message_nesting(json: &str) -> usize {
2400 let (mut depth, mut max) = (0usize, 0usize);
2401 let (mut in_string, mut escaped) = (false, false);
2402 for byte in json.bytes() {
2403 if in_string {
2404 if escaped {
2405 escaped = false;
2406 } else if byte == b'\\' {
2407 escaped = true;
2408 } else if byte == b'"' {
2409 in_string = false;
2410 }
2411 continue;
2412 }
2413 match byte {
2414 b'"' => in_string = true,
2415 b'{' => {
2416 depth += 1;
2417 max = max.max(depth);
2418 }
2419 b'}' => depth = depth.saturating_sub(1),
2420 _ => {}
2421 }
2422 }
2423 max
2424 }
2425
2426 #[test]
2430 fn package_at_message_depth_builds_the_depth_it_names() {
2431 with_sized_stack(|| {
2432 for levels in [3, 6, 7, 99, 100, 101] {
2433 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2434 .expect("the writer is unrestricted at these depths");
2435 assert_eq!(
2436 message_nesting(&json) - 1,
2437 levels,
2438 "the chain must nest {levels} message levels below the root"
2439 );
2440 }
2441 });
2442 }
2443
2444 #[test]
2453 fn binary_round_trip_at_100_message_levels_succeeds() {
2454 let package = package_at_message_depth(100);
2455 let bytes = v2::to_binary(&package);
2456 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2457 assert_eq!(package, decoded);
2458 }
2459
2460 #[test]
2465 fn binary_decode_at_101_message_levels_returns_an_error() {
2466 let package = package_at_message_depth(101);
2467 let bytes = v2::to_binary(&package);
2468 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2469 assert!(
2470 error.to_string().contains("recursion limit reached"),
2471 "the error must name the limit, got: {error}"
2472 );
2473
2474 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2475 assert_eq!(
2476 package,
2477 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2478 );
2479 }
2480
2481 #[test]
2490 fn json_round_trip_at_400_nested_levels_succeeds() {
2491 with_sized_stack(|| {
2492 let package = nested_package(400);
2493 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2494 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2495 assert_eq!(package, decoded);
2496 });
2497 }
2498
2499 #[test]
2507 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2508 let mut package = fixture();
2509 package.decls[0].visibility = 999;
2510 let err = v2::to_json_pretty(&package)
2511 .expect_err("an out-of-schema discriminant must fail, not panic");
2512 let message = err.to_string();
2513 assert!(
2514 message.contains("canonical protobuf JSON"),
2515 "the error must name the encoding that failed, got: {message}"
2516 );
2517 assert!(
2518 message.contains("discriminant outside the schema"),
2519 "the error must name the known cause, got: {message}"
2520 );
2521 }
2522
2523 #[test]
2528 fn json_parse_rejects_an_unknown_field() {
2529 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2530 .expect_err("an unknown field must be rejected");
2531 assert!(
2532 error.to_string().contains("unknown field"),
2533 "the error must name the defect, got: {error}"
2534 );
2535 }
2536
2537 #[test]
2546 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2547 let with_visibility =
2548 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2549 v2::from_json(&with_visibility("1"))
2550 .expect("an in-range numeric enum value parses, as the mapping expects");
2551 let error = v2::from_json(&with_visibility("77"))
2552 .expect_err("an out-of-range numeric enum value must be rejected");
2553 assert!(
2554 error.to_string().contains("invalid value: integer `77`"),
2555 "the error must name the value, got: {error}"
2556 );
2557 }
2558
2559 #[test]
2565 fn json_parse_rejects_a_float_form_integer() {
2566 for spelling in ["1.0", "1e0"] {
2567 let error = v2::from_json(&format!(
2568 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2569 ))
2570 .expect_err("a float-form integer must be rejected");
2571 assert!(
2572 error.to_string().contains("did not match any variant"),
2573 "the integer field's deserializer must be the one refusing `{spelling}`, \
2574 got: {error}"
2575 );
2576 }
2577 }
2578
2579 #[test]
2586 fn json_parse_rejects_null_for_a_repeated_field() {
2587 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2588 .expect_err("null for a repeated field must be rejected");
2589 assert!(
2590 error.to_string().contains("invalid type: null"),
2591 "the error must name the null, got: {error}"
2592 );
2593 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2594 .expect("null for an optional scalar field still parses");
2595 }
2596
2597 #[test]
2601 fn json_parse_rejects_a_duplicate_key() {
2602 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2603 .expect_err("a duplicate key must be rejected");
2604 assert!(
2605 error.to_string().contains("duplicate field `name`"),
2606 "the error must name the duplicated field, got: {error}"
2607 );
2608 }
2609
2610 #[test]
2617 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2618 with_sized_stack(|| {
2619 let json = v2::to_json_pretty(&nested_package(500))
2620 .expect("the writer is unrestricted at this depth");
2621 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2622 assert!(
2623 error.to_string().contains("1000 JSON levels"),
2624 "the error must name the ceiling, got: {error}"
2625 );
2626 });
2627 }
2628
2629 #[test]
2634 fn json_nesting_ceiling_binds_exactly_at_1000() {
2635 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2636 assert!(
2637 !at.to_string().contains("JSON levels"),
2638 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2639 );
2640
2641 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2642 assert!(
2643 past.to_string().contains("1000 JSON levels"),
2644 "past the ceiling the error must name it, got: {past}"
2645 );
2646 }
2647
2648 #[test]
2653 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2654 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2656 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2658 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2661 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2664 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2666 }
2667
2668 fn nested_text(depth: usize) -> String {
2672 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2673 for _ in 0..depth {
2674 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2675 }
2676 format!(
2677 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2678 )
2679 }
2680
2681 #[test]
2685 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2686 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2687 .expect_err("serialization past the recursion limit must fail, not panic");
2688 let message = err.to_string();
2689 assert!(
2690 message.contains("recursion limit"),
2691 "the error must name the nesting limit as the known cause, got: {message}"
2692 );
2693 assert!(
2694 message.contains("prototext"),
2695 "the error must name the encoding that failed, got: {message}"
2696 );
2697 }
2698
2699 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2712 let outcome = std::thread::Builder::new()
2713 .stack_size(16 * 1024 * 1024)
2714 .spawn(test)
2715 .expect("spawn the large-stack test thread")
2716 .join();
2717 if let Err(payload) = outcome {
2718 std::panic::resume_unwind(payload);
2719 }
2720 }
2721
2722 #[test]
2726 fn text_parse_past_the_nesting_limit_returns_an_error() {
2727 with_sized_stack(|| {
2728 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2729 .expect_err("parsing past the recursion limit must fail, not panic");
2730
2731 let message = error.to_string();
2735 assert!(
2736 message.contains("recursion limit reached"),
2737 "the transcode out of the dynamic message must be the failing \
2738 stage, got: {message}"
2739 );
2740 });
2741 }
2742
2743 #[test]
2746 fn text_round_trip_below_the_nesting_limit_succeeds() {
2747 with_sized_stack(|| {
2748 let package = nested_package(NESTING_BELOW_LIMIT);
2749 let text = v2::to_text_format(&package)
2750 .expect("below the recursion limit, serialization succeeds");
2751 let decoded =
2752 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2753 assert_eq!(package, decoded);
2754 });
2755 }
2756}
2757
2758#[cfg(test)]
2759mod vacuous_constraint {
2760 use crate::v2;
2761
2762 fn constraint() -> v2::Constraint {
2765 v2::Constraint {
2766 min: None,
2767 max: None,
2768 step: None,
2769 len_min: None,
2770 len_max: None,
2771 pattern: None,
2772 pattern_const: None,
2773 }
2774 }
2775
2776 #[test]
2777 fn an_absent_or_empty_constraint_is_vacuous() {
2778 assert!(v2::constraint_is_vacuous(None));
2779 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2780 }
2781
2782 #[test]
2783 fn a_step_constraint_is_non_vacuous() {
2784 let stepped = v2::Constraint {
2785 step: Some("0.5".to_string()),
2786 ..constraint()
2787 };
2788 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2789 }
2790
2791 #[test]
2798 fn any_single_constrained_field_is_non_vacuous() {
2799 let cases = [
2800 (
2801 "min",
2802 v2::Constraint {
2803 min: Some("0.0".to_string()),
2804 ..constraint()
2805 },
2806 ),
2807 (
2808 "max",
2809 v2::Constraint {
2810 max: Some("250.0".to_string()),
2811 ..constraint()
2812 },
2813 ),
2814 (
2815 "len_min",
2816 v2::Constraint {
2817 len_min: Some(1),
2818 ..constraint()
2819 },
2820 ),
2821 (
2822 "len_max",
2823 v2::Constraint {
2824 len_max: Some(256),
2825 ..constraint()
2826 },
2827 ),
2828 (
2829 "pattern",
2830 v2::Constraint {
2831 pattern: Some("^[a-z]+$".to_string()),
2832 ..constraint()
2833 },
2834 ),
2835 (
2836 "pattern_const",
2837 v2::Constraint {
2838 pattern_const: Some("NAME_PATTERN".to_string()),
2839 ..constraint()
2840 },
2841 ),
2842 ];
2843 for (field, case) in cases {
2844 assert!(
2845 !v2::constraint_is_vacuous(Some(&case)),
2846 "`{field}` alone must be non-vacuous"
2847 );
2848 }
2849 }
2850
2851 #[test]
2860 fn a_bound_pair_set_together_is_non_vacuous() {
2861 let ranged = v2::Constraint {
2862 min: Some("0.0".to_string()),
2863 max: Some("250.0".to_string()),
2864 ..constraint()
2865 };
2866 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
2867
2868 let default_length = v2::Constraint {
2869 len_min: Some(0),
2870 len_max: Some(256),
2871 ..constraint()
2872 };
2873 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
2874 }
2875}
2876
2877#[cfg(test)]
2878mod system_round_trip {
2879 use crate::v2;
2880
2881 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
2882 v2::Attribute {
2883 namespace: namespace.to_string(),
2884 key: key.to_string(),
2885 value,
2886 }
2887 }
2888
2889 fn scalar(text: &str) -> v2::AttributeValue {
2890 v2::AttributeValue {
2891 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
2892 }
2893 }
2894
2895 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
2896 v2::AttributeValue {
2897 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
2898 }
2899 }
2900
2901 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
2902 Some(v2::InterfaceRef {
2903 catalog: catalog.to_string(),
2904 name: name.to_string(),
2905 inline,
2906 })
2907 }
2908
2909 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
2910 v2::Endpoint {
2911 component: component.to_string(),
2912 instance: instance.to_string(),
2913 machine: machine.to_string(),
2914 }
2915 }
2916
2917 fn doc_link() -> v2::DocLink {
2925 v2::DocLink {
2926 text: "Cruise".to_string(),
2927 offset: 16,
2928 len: 6,
2929 target: "veh.topology.Cruise".to_string(),
2930 }
2931 }
2932
2933 fn fixture() -> v2::System {
2934 let link = v2::Link {
2935 interface: interface("veh.diag", "veh.diag.access", true),
2936 service: "veh.diag.access".to_string(),
2937 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
2938 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
2939 crossing: v2::Crossing::OffBoard as i32,
2940 };
2941 v2::System {
2942 name: "Vehicle".to_string(),
2943 package: "veh.topology".to_string(),
2944 labels: vec!["ASIL_B".to_string()],
2945 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
2946 members: vec![
2947 v2::MemberLine {
2948 component: "veh.topology.Cruise".to_string(),
2949 attributes: vec![attribute("linux", "pinned", None)],
2950 doc: "Documented, see [Cruise].".to_string(),
2951 links: vec![doc_link()],
2952 see: vec![doc_link()],
2953 since: vec!["1.2".to_string()],
2954 },
2955 v2::MemberLine {
2956 component: "veh.diag.access".to_string(),
2957 attributes: vec![],
2958 doc: "Documented, see [Cruise].".to_string(),
2959 links: vec![doc_link()],
2960 see: vec![doc_link()],
2961 since: vec!["1.2".to_string()],
2962 },
2963 ],
2964 components: vec![
2965 v2::Component {
2966 name: "Cruise".to_string(),
2967 package: "veh.topology".to_string(),
2968 implicit: false,
2969 external: true,
2970 instances: vec!["primary".to_string(), "backup".to_string()],
2971 offers: vec![v2::Offer {
2972 service: "veh.adas.cruise".to_string(),
2973 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
2974 doc: "Documented, see [Cruise].".to_string(),
2975 links: vec![doc_link()],
2976 see: vec![doc_link()],
2977 since: vec!["1.2".to_string()],
2978 }],
2979 requires: vec![v2::Require {
2980 interface: interface("veh.adas", "LaneAssist", false),
2981 service: "veh.adas.lane".to_string(),
2982 producer: "veh.topology.Lane".to_string(),
2983 attributes: vec![attribute(
2984 "linux",
2985 "cpuset",
2986 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
2987 )],
2988 doc: "Documented, see [Cruise].".to_string(),
2989 links: vec![doc_link()],
2990 see: vec![doc_link()],
2991 since: vec!["1.2".to_string()],
2992 }],
2993 labels: vec!["ASIL_B".to_string()],
2994 attributes: vec![attribute("rust", "crate", None)],
2995 doc: "Documented, see [Cruise].".to_string(),
2996 links: vec![doc_link()],
2997 see: vec![doc_link()],
2998 since: vec!["1.2".to_string()],
2999 },
3000 v2::Component {
3001 name: "veh.diag.access".to_string(),
3002 package: String::new(),
3003 implicit: true,
3004 external: false,
3005 instances: vec!["Unit".to_string()],
3006 offers: vec![v2::Offer {
3007 service: "veh.diag.access".to_string(),
3008 attributes: vec![],
3009 doc: "Documented, see [Cruise].".to_string(),
3010 links: vec![doc_link()],
3011 see: vec![doc_link()],
3012 since: vec!["1.2".to_string()],
3013 }],
3014 requires: vec![],
3015 labels: vec![],
3016 attributes: vec![],
3017 doc: "Documented, see [Cruise].".to_string(),
3018 links: vec![doc_link()],
3019 see: vec![doc_link()],
3020 since: vec!["1.2".to_string()],
3021 },
3022 ],
3023 producers: vec![v2::Producer {
3024 service: "veh.adas.cruise".to_string(),
3025 component: "veh.topology.Cruise".to_string(),
3026 instances: vec!["primary".to_string(), "backup".to_string()],
3027 not_yet_realizable: true,
3028 }],
3029 grants: vec![v2::Grant {
3030 component: "veh.topology.Backend".to_string(),
3031 external: true,
3032 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3033 }],
3034 regions: vec![v2::Region {
3035 catalog: "veh.diag".to_string(),
3036 hash: vec![0xab; 32],
3037 interfaces: vec![v2::RegionInterface {
3038 name: "veh.diag.access".to_string(),
3039 inline: true,
3040 number: 2,
3041 provisional: true,
3042 service: "veh.diag.access".to_string(),
3043 }],
3044 }],
3045 distributions: vec![v2::Distribution {
3046 name: "Adas".to_string(),
3047 package: "veh.topology".to_string(),
3048 members: vec![v2::MemberLine {
3049 component: "veh.topology.Cruise".to_string(),
3050 attributes: vec![],
3051 doc: "Documented, see [Cruise].".to_string(),
3052 links: vec![doc_link()],
3053 see: vec![doc_link()],
3054 since: vec!["1.2".to_string()],
3055 }],
3056 depends_on: vec!["veh.topology.Base".to_string()],
3057 labels: vec!["PLATFORM_BUNDLE".to_string()],
3058 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3059 doc: "Documented, see [Cruise].".to_string(),
3060 links: vec![doc_link()],
3061 see: vec![doc_link()],
3062 since: vec!["1.2".to_string()],
3063 }],
3064 deployments: vec![v2::Deployment {
3065 name: "Production".to_string(),
3066 package: "veh.topology".to_string(),
3067 labels: vec!["FLEET".to_string()],
3068 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3069 machines: vec![v2::Machine {
3070 name: "Cloud".to_string(),
3071 external: true,
3072 labels: vec!["OFF_BOARD".to_string()],
3073 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3074 doc: "Documented, see [Cruise].".to_string(),
3075 links: vec![doc_link()],
3076 see: vec![doc_link()],
3077 since: vec!["1.2".to_string()],
3078 }],
3079 placements: vec![v2::Placement {
3080 component: "veh.topology.Cruise".to_string(),
3081 instance: "backup".to_string(),
3082 machine: "Cockpit".to_string(),
3083 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3084 sizing: Some(v2::Sizing {
3085 depth: Some(4_294_967_295),
3086 slots: Some(65_536),
3087 budget: Some(18_446_744_073_709_551_615),
3088 }),
3089 }],
3090 links: vec![link.clone()],
3091 routes: vec![v2::Route {
3092 catalog: "veh.adas".to_string(),
3093 interface_number: 2,
3094 member_ordinal: 1,
3095 interface: "LaneAssist".to_string(),
3096 member: "active".to_string(),
3097 service: "veh.adas.lane".to_string(),
3098 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3099 }],
3100 surface: vec![v2::Surface {
3101 link: Some(link),
3102 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3103 }],
3104 installations: vec![v2::Installation {
3105 distribution: "veh.topology.Adas".to_string(),
3106 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3107 }],
3108 doc: "Documented, see [Cruise].".to_string(),
3109 doc_links: vec![doc_link()],
3110 see: vec![doc_link()],
3111 since: vec!["1.2".to_string()],
3112 sizing: Some(v2::Sizing {
3113 depth: Some(3),
3114 slots: Some(8),
3115 budget: Some(4096),
3116 }),
3117 }],
3118 doc: "Documented, see [Cruise].".to_string(),
3119 see: vec![doc_link()],
3120 since: vec!["1.2".to_string()],
3121 links: vec![doc_link()],
3122 }
3123 }
3124
3125 #[test]
3126 fn system_binary_round_trip_preserves_system() {
3127 let system = fixture();
3128 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3129 .expect("decode must succeed");
3130 assert_eq!(system, decoded);
3131 }
3132
3133 #[test]
3138 fn system_json_round_trip_preserves_system() {
3139 let system = fixture();
3140 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3141 assert!(
3142 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3143 "bytes render as base64, got:\n{json}"
3144 );
3145 assert!(
3146 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3147 "enums render by name, got:\n{json}"
3148 );
3149 assert!(
3150 json.contains("\"notYetRealizable\": true"),
3151 "fields render in lowerCamelCase, got:\n{json}"
3152 );
3153 assert!(
3154 json.contains("\"budget\": \"18446744073709551615\""),
3155 "a 64-bit value renders as a string, got:\n{json}"
3156 );
3157 assert_eq!(
3158 system,
3159 v2::system_from_json(&json).expect("the JSON parses")
3160 );
3161 assert!(
3162 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3163 "an unknown field is rejected"
3164 );
3165 }
3166
3167 #[test]
3171 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3172 let mut absent = fixture();
3173 absent.deployments[0].sizing = None;
3174 absent.deployments[0].placements[0].sizing = None;
3175 let mut empty = absent.clone();
3176 empty.deployments[0].sizing = Some(v2::Sizing::default());
3177 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3178 for system in [&absent, &empty] {
3179 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3180 .expect("decode must succeed");
3181 assert_eq!(system, &binary);
3182 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3183 assert_eq!(
3184 system,
3185 &v2::system_from_json(&json).expect("the JSON parses")
3186 );
3187 }
3188 assert_ne!(
3189 v2::system_to_binary(&absent),
3190 v2::system_to_binary(&empty),
3191 "the binary encodings differ"
3192 );
3193 }
3194
3195 #[test]
3196 fn system_text_format_round_trip_preserves_system() {
3197 let system = fixture();
3198 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3199 assert!(
3200 text.starts_with("name:"),
3201 "fields print in schema index order, got: {text}"
3202 );
3203 assert_eq!(
3204 system,
3205 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3206 );
3207 }
3208
3209 #[test]
3212 fn qualified_names_follow_the_one_derivation() {
3213 let system = fixture();
3214 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3215 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3216 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3217 assert_eq!(
3218 system.distributions[0].qualified_name(),
3219 "veh.topology.Adas"
3220 );
3221 }
3222}