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]
2031 fn unit_retired_reads_a_package_named_twice_once() {
2032 let package = v2::Package {
2033 name: "u.cluster".to_string(),
2034 unit: "u".to_string(),
2035 retired: vec![v2::RetiredInterface {
2036 name: "cluster.Old".to_string(),
2037 number: 3,
2038 }],
2039 ..Default::default()
2040 };
2041 let once = v2::unit_retired("u", &[&package]);
2042 assert_eq!(once.len(), 1);
2043 assert_eq!(v2::unit_retired("u", &[&package, &package]), once);
2044 }
2045
2046 #[test]
2047 fn an_inline_shape_keeps_its_global_name() {
2048 let mut package = fixture();
2049 package.name = "u.cluster".to_string();
2050 package.unit = "u".to_string();
2051 let inline = package
2052 .shapes()
2053 .find(|shape| shape.is_inline())
2054 .expect("the fixture has an inline shape");
2055 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2056 let named = package
2057 .shapes()
2058 .find(|shape| !shape.is_inline())
2059 .expect("the fixture has a declared interface");
2060 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2061 }
2062
2063 #[test]
2064 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2065 let mut package = fixture();
2066 package.unit = String::new();
2067 let named = package
2068 .shapes()
2069 .find(|shape| !shape.is_inline())
2070 .expect("the fixture has a declared interface");
2071 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2072 }
2073
2074 #[test]
2075 fn unit_of_falls_back_to_the_name() {
2076 let bare = v2::Package {
2077 name: "p".to_string(),
2078 ..Default::default()
2079 };
2080 assert_eq!(v2::unit_of(&bare), "p");
2081 let member = v2::Package {
2082 name: "u.a".to_string(),
2083 unit: "u".to_string(),
2084 ..Default::default()
2085 };
2086 assert_eq!(v2::unit_of(&member), "u");
2087 }
2088
2089 #[test]
2090 fn packages_of_unit_selects_by_unit_of() {
2091 let package = |name: &str, unit: &str| v2::Package {
2092 name: name.to_string(),
2093 unit: unit.to_string(),
2094 ..Default::default()
2095 };
2096 let packages = [
2097 package("u", "u"),
2098 package("u.a", "u"),
2099 package("w", ""),
2100 package("other.b", "other"),
2101 ];
2102 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2103 .map(|p| p.name.as_str())
2104 .collect();
2105 assert_eq!(names, ["u", "u.a"]);
2106 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2107 .map(|p| p.name.as_str())
2108 .collect();
2109 assert_eq!(legacy, ["w"]);
2110 }
2111
2112 #[test]
2117 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2118 let package = fixture();
2119 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2120
2121 let named = shapes[0];
2122 assert!(named.service.is_none());
2123 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2124 assert_eq!(named.visibility(), named.interface.visibility);
2125
2126 let inline = shapes[1];
2127 assert_eq!(
2128 inline.interface.visibility,
2129 v2::Visibility::Unspecified as i32,
2130 "the trap: an inline shape's own visibility field is unset",
2131 );
2132 assert_eq!(
2133 inline.service.expect("an inline shape has an owner").name,
2134 "veh.adas.logs",
2135 );
2136 assert_eq!(
2137 inline.visibility(),
2138 v2::Visibility::Public as i32,
2139 "the accessor reads the owning service's, never the unset field",
2140 );
2141 }
2142
2143 #[test]
2147 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2148 let mut package = fixture();
2149 package.services.clear();
2150 assert_eq!(package.shapes().count(), 1);
2151
2152 package.interfaces.clear();
2153 assert_eq!(package.shapes().count(), 0);
2154 }
2155
2156 #[test]
2163 fn referenced_packages_finds_qualifiers_at_depth() {
2164 fn named(reference: &str) -> v2::FieldType {
2165 v2::FieldType {
2166 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2167 ..Default::default()
2168 }
2169 }
2170
2171 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2172 v2::decl::Kind::FixedDef(v2::FixedDef {
2173 payload: Some(payload),
2174 })
2175 }
2176
2177 let package = v2::Package {
2178 name: "veh.cluster".to_string(),
2179 decls: vec![
2180 v2::Decl {
2181 name: "Local".to_string(),
2182 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2183 payload: "Speed".to_string(),
2184 ..Default::default()
2185 })),
2186 ..Default::default()
2187 },
2188 v2::Decl {
2189 name: "Stamped".to_string(),
2190 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2191 payload: "ridl.std.Timestamp".to_string(),
2192 ..Default::default()
2193 })),
2194 ..Default::default()
2195 },
2196 v2::Decl {
2197 name: "ArrLabels".to_string(),
2198 kind: Some(fixed(v2::FieldType {
2199 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2200 element: Some(Box::new(named("veh.arr.Label"))),
2201 min: 0,
2202 max: 32,
2203 }))),
2204 ..Default::default()
2205 })),
2206 ..Default::default()
2207 },
2208 v2::Decl {
2209 name: "TupThing".to_string(),
2210 kind: Some(fixed(v2::FieldType {
2211 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2212 fields: vec![v2::TupleField {
2213 name: "x".to_string(),
2214 r#type: Some(named("veh.tup.X")),
2215 }],
2216 })),
2217 ..Default::default()
2218 })),
2219 ..Default::default()
2220 },
2221 v2::Decl {
2222 name: "MapThing".to_string(),
2223 kind: Some(fixed(v2::FieldType {
2224 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2225 key: Some(Box::new(named("veh.key.X"))),
2226 value: Some(Box::new(named("veh.val.X"))),
2227 min: 0,
2228 max: 8,
2229 }))),
2230 ..Default::default()
2231 })),
2232 ..Default::default()
2233 },
2234 v2::Decl {
2235 name: "StreamThing".to_string(),
2236 kind: Some(fixed(v2::FieldType {
2237 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2238 element: Some(v2::stream_type::Element::Named(
2239 "veh.strm.X".to_string(),
2240 )),
2241 })),
2242 ..Default::default()
2243 })),
2244 ..Default::default()
2245 },
2246 ],
2247 ..Default::default()
2248 };
2249
2250 let found = v2::referenced_packages(&package);
2251 let expected: std::collections::BTreeSet<String> = [
2252 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2253 ]
2254 .into_iter()
2255 .map(str::to_string)
2256 .collect();
2257 assert_eq!(
2258 found, expected,
2259 "each recursive path must contribute its own distinct qualifier"
2260 );
2261 assert!(
2262 !found.contains("Speed") && !found.contains("veh.cluster"),
2263 "a bare reference contributes no package: {found:?}",
2264 );
2265 }
2266
2267 #[test]
2270 fn referenced_packages_is_empty_without_references() {
2271 let package = v2::Package {
2272 name: "veh.solo".to_string(),
2273 ..Default::default()
2274 };
2275 assert!(v2::referenced_packages(&package).is_empty());
2276 }
2277
2278 const NESTING_BELOW_LIMIT: usize = 45;
2284 const NESTING_PAST_LIMIT: usize = 60;
2288
2289 fn nested_package(depth: usize) -> v2::Package {
2294 let mut payload = v2::FieldType {
2295 optional: false,
2296 kind: Some(v2::field_type::Kind::Primitive(
2297 v2::PrimitiveType::Integer as i32,
2298 )),
2299 };
2300 for _ in 0..depth {
2301 payload = v2::FieldType {
2302 optional: false,
2303 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2304 element: Some(Box::new(payload)),
2305 min: 1,
2306 max: 1,
2307 }))),
2308 };
2309 }
2310 v2::Package {
2311 name: "veh.deep".to_string(),
2312 decls: vec![v2::Decl {
2313 name: "deep".to_string(),
2314 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2315 payload: Some(payload),
2316 })),
2317 ..Default::default()
2318 }],
2319 ..Default::default()
2320 }
2321 }
2322
2323 fn package_at_message_depth(levels: usize) -> v2::Package {
2335 assert!(levels >= 3, "the chain costs three levels before nesting");
2336 let (arrays, tuple) = if levels % 2 == 1 {
2337 ((levels - 3) / 2, false)
2338 } else {
2339 assert!(levels >= 6, "one tuple level costs three");
2340 ((levels - 6) / 2, true)
2341 };
2342
2343 let mut payload = v2::FieldType {
2344 optional: false,
2345 kind: Some(v2::field_type::Kind::Primitive(
2346 v2::PrimitiveType::Integer as i32,
2347 )),
2348 };
2349 for _ in 0..arrays {
2350 payload = v2::FieldType {
2351 optional: false,
2352 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2353 element: Some(Box::new(payload)),
2354 min: 1,
2355 max: 1,
2356 }))),
2357 };
2358 }
2359 if tuple {
2360 payload = v2::FieldType {
2361 optional: false,
2362 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2363 fields: vec![v2::TupleField {
2364 name: "f0".to_string(),
2365 r#type: Some(payload),
2366 }],
2367 })),
2368 };
2369 }
2370 v2::Package {
2371 name: "veh.deep".to_string(),
2372 decls: vec![v2::Decl {
2373 name: "deep".to_string(),
2374 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2375 payload: Some(payload),
2376 })),
2377 ..Default::default()
2378 }],
2379 ..Default::default()
2380 }
2381 }
2382
2383 fn message_nesting(json: &str) -> usize {
2390 let (mut depth, mut max) = (0usize, 0usize);
2391 let (mut in_string, mut escaped) = (false, false);
2392 for byte in json.bytes() {
2393 if in_string {
2394 if escaped {
2395 escaped = false;
2396 } else if byte == b'\\' {
2397 escaped = true;
2398 } else if byte == b'"' {
2399 in_string = false;
2400 }
2401 continue;
2402 }
2403 match byte {
2404 b'"' => in_string = true,
2405 b'{' => {
2406 depth += 1;
2407 max = max.max(depth);
2408 }
2409 b'}' => depth = depth.saturating_sub(1),
2410 _ => {}
2411 }
2412 }
2413 max
2414 }
2415
2416 #[test]
2420 fn package_at_message_depth_builds_the_depth_it_names() {
2421 with_sized_stack(|| {
2422 for levels in [3, 6, 7, 99, 100, 101] {
2423 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2424 .expect("the writer is unrestricted at these depths");
2425 assert_eq!(
2426 message_nesting(&json) - 1,
2427 levels,
2428 "the chain must nest {levels} message levels below the root"
2429 );
2430 }
2431 });
2432 }
2433
2434 #[test]
2443 fn binary_round_trip_at_100_message_levels_succeeds() {
2444 let package = package_at_message_depth(100);
2445 let bytes = v2::to_binary(&package);
2446 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2447 assert_eq!(package, decoded);
2448 }
2449
2450 #[test]
2455 fn binary_decode_at_101_message_levels_returns_an_error() {
2456 let package = package_at_message_depth(101);
2457 let bytes = v2::to_binary(&package);
2458 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2459 assert!(
2460 error.to_string().contains("recursion limit reached"),
2461 "the error must name the limit, got: {error}"
2462 );
2463
2464 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2465 assert_eq!(
2466 package,
2467 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2468 );
2469 }
2470
2471 #[test]
2480 fn json_round_trip_at_400_nested_levels_succeeds() {
2481 with_sized_stack(|| {
2482 let package = nested_package(400);
2483 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2484 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2485 assert_eq!(package, decoded);
2486 });
2487 }
2488
2489 #[test]
2497 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2498 let mut package = fixture();
2499 package.decls[0].visibility = 999;
2500 let err = v2::to_json_pretty(&package)
2501 .expect_err("an out-of-schema discriminant must fail, not panic");
2502 let message = err.to_string();
2503 assert!(
2504 message.contains("canonical protobuf JSON"),
2505 "the error must name the encoding that failed, got: {message}"
2506 );
2507 assert!(
2508 message.contains("discriminant outside the schema"),
2509 "the error must name the known cause, got: {message}"
2510 );
2511 }
2512
2513 #[test]
2518 fn json_parse_rejects_an_unknown_field() {
2519 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2520 .expect_err("an unknown field must be rejected");
2521 assert!(
2522 error.to_string().contains("unknown field"),
2523 "the error must name the defect, got: {error}"
2524 );
2525 }
2526
2527 #[test]
2536 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2537 let with_visibility =
2538 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2539 v2::from_json(&with_visibility("1"))
2540 .expect("an in-range numeric enum value parses, as the mapping expects");
2541 let error = v2::from_json(&with_visibility("77"))
2542 .expect_err("an out-of-range numeric enum value must be rejected");
2543 assert!(
2544 error.to_string().contains("invalid value: integer `77`"),
2545 "the error must name the value, got: {error}"
2546 );
2547 }
2548
2549 #[test]
2555 fn json_parse_rejects_a_float_form_integer() {
2556 for spelling in ["1.0", "1e0"] {
2557 let error = v2::from_json(&format!(
2558 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2559 ))
2560 .expect_err("a float-form integer must be rejected");
2561 assert!(
2562 error.to_string().contains("did not match any variant"),
2563 "the integer field's deserializer must be the one refusing `{spelling}`, \
2564 got: {error}"
2565 );
2566 }
2567 }
2568
2569 #[test]
2576 fn json_parse_rejects_null_for_a_repeated_field() {
2577 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2578 .expect_err("null for a repeated field must be rejected");
2579 assert!(
2580 error.to_string().contains("invalid type: null"),
2581 "the error must name the null, got: {error}"
2582 );
2583 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2584 .expect("null for an optional scalar field still parses");
2585 }
2586
2587 #[test]
2591 fn json_parse_rejects_a_duplicate_key() {
2592 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2593 .expect_err("a duplicate key must be rejected");
2594 assert!(
2595 error.to_string().contains("duplicate field `name`"),
2596 "the error must name the duplicated field, got: {error}"
2597 );
2598 }
2599
2600 #[test]
2607 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2608 with_sized_stack(|| {
2609 let json = v2::to_json_pretty(&nested_package(500))
2610 .expect("the writer is unrestricted at this depth");
2611 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2612 assert!(
2613 error.to_string().contains("1000 JSON levels"),
2614 "the error must name the ceiling, got: {error}"
2615 );
2616 });
2617 }
2618
2619 #[test]
2624 fn json_nesting_ceiling_binds_exactly_at_1000() {
2625 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2626 assert!(
2627 !at.to_string().contains("JSON levels"),
2628 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2629 );
2630
2631 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2632 assert!(
2633 past.to_string().contains("1000 JSON levels"),
2634 "past the ceiling the error must name it, got: {past}"
2635 );
2636 }
2637
2638 #[test]
2643 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2644 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2646 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2648 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2651 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2654 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2656 }
2657
2658 fn nested_text(depth: usize) -> String {
2662 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2663 for _ in 0..depth {
2664 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2665 }
2666 format!(
2667 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2668 )
2669 }
2670
2671 #[test]
2675 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2676 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2677 .expect_err("serialization past the recursion limit must fail, not panic");
2678 let message = err.to_string();
2679 assert!(
2680 message.contains("recursion limit"),
2681 "the error must name the nesting limit as the known cause, got: {message}"
2682 );
2683 assert!(
2684 message.contains("prototext"),
2685 "the error must name the encoding that failed, got: {message}"
2686 );
2687 }
2688
2689 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2702 let outcome = std::thread::Builder::new()
2703 .stack_size(16 * 1024 * 1024)
2704 .spawn(test)
2705 .expect("spawn the large-stack test thread")
2706 .join();
2707 if let Err(payload) = outcome {
2708 std::panic::resume_unwind(payload);
2709 }
2710 }
2711
2712 #[test]
2716 fn text_parse_past_the_nesting_limit_returns_an_error() {
2717 with_sized_stack(|| {
2718 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2719 .expect_err("parsing past the recursion limit must fail, not panic");
2720
2721 let message = error.to_string();
2725 assert!(
2726 message.contains("recursion limit reached"),
2727 "the transcode out of the dynamic message must be the failing \
2728 stage, got: {message}"
2729 );
2730 });
2731 }
2732
2733 #[test]
2736 fn text_round_trip_below_the_nesting_limit_succeeds() {
2737 with_sized_stack(|| {
2738 let package = nested_package(NESTING_BELOW_LIMIT);
2739 let text = v2::to_text_format(&package)
2740 .expect("below the recursion limit, serialization succeeds");
2741 let decoded =
2742 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2743 assert_eq!(package, decoded);
2744 });
2745 }
2746}
2747
2748#[cfg(test)]
2749mod vacuous_constraint {
2750 use crate::v2;
2751
2752 fn constraint() -> v2::Constraint {
2755 v2::Constraint {
2756 min: None,
2757 max: None,
2758 step: None,
2759 len_min: None,
2760 len_max: None,
2761 pattern: None,
2762 pattern_const: None,
2763 }
2764 }
2765
2766 #[test]
2767 fn an_absent_or_empty_constraint_is_vacuous() {
2768 assert!(v2::constraint_is_vacuous(None));
2769 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2770 }
2771
2772 #[test]
2773 fn a_step_constraint_is_non_vacuous() {
2774 let stepped = v2::Constraint {
2775 step: Some("0.5".to_string()),
2776 ..constraint()
2777 };
2778 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2779 }
2780
2781 #[test]
2788 fn any_single_constrained_field_is_non_vacuous() {
2789 let cases = [
2790 (
2791 "min",
2792 v2::Constraint {
2793 min: Some("0.0".to_string()),
2794 ..constraint()
2795 },
2796 ),
2797 (
2798 "max",
2799 v2::Constraint {
2800 max: Some("250.0".to_string()),
2801 ..constraint()
2802 },
2803 ),
2804 (
2805 "len_min",
2806 v2::Constraint {
2807 len_min: Some(1),
2808 ..constraint()
2809 },
2810 ),
2811 (
2812 "len_max",
2813 v2::Constraint {
2814 len_max: Some(256),
2815 ..constraint()
2816 },
2817 ),
2818 (
2819 "pattern",
2820 v2::Constraint {
2821 pattern: Some("^[a-z]+$".to_string()),
2822 ..constraint()
2823 },
2824 ),
2825 (
2826 "pattern_const",
2827 v2::Constraint {
2828 pattern_const: Some("NAME_PATTERN".to_string()),
2829 ..constraint()
2830 },
2831 ),
2832 ];
2833 for (field, case) in cases {
2834 assert!(
2835 !v2::constraint_is_vacuous(Some(&case)),
2836 "`{field}` alone must be non-vacuous"
2837 );
2838 }
2839 }
2840
2841 #[test]
2850 fn a_bound_pair_set_together_is_non_vacuous() {
2851 let ranged = v2::Constraint {
2852 min: Some("0.0".to_string()),
2853 max: Some("250.0".to_string()),
2854 ..constraint()
2855 };
2856 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
2857
2858 let default_length = v2::Constraint {
2859 len_min: Some(0),
2860 len_max: Some(256),
2861 ..constraint()
2862 };
2863 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
2864 }
2865}
2866
2867#[cfg(test)]
2868mod system_round_trip {
2869 use crate::v2;
2870
2871 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
2872 v2::Attribute {
2873 namespace: namespace.to_string(),
2874 key: key.to_string(),
2875 value,
2876 }
2877 }
2878
2879 fn scalar(text: &str) -> v2::AttributeValue {
2880 v2::AttributeValue {
2881 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
2882 }
2883 }
2884
2885 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
2886 v2::AttributeValue {
2887 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
2888 }
2889 }
2890
2891 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
2892 Some(v2::InterfaceRef {
2893 catalog: catalog.to_string(),
2894 name: name.to_string(),
2895 inline,
2896 })
2897 }
2898
2899 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
2900 v2::Endpoint {
2901 component: component.to_string(),
2902 instance: instance.to_string(),
2903 machine: machine.to_string(),
2904 }
2905 }
2906
2907 fn doc_link() -> v2::DocLink {
2915 v2::DocLink {
2916 text: "Cruise".to_string(),
2917 offset: 16,
2918 len: 6,
2919 target: "veh.topology.Cruise".to_string(),
2920 }
2921 }
2922
2923 fn fixture() -> v2::System {
2924 let link = v2::Link {
2925 interface: interface("veh.diag", "veh.diag.access", true),
2926 service: "veh.diag.access".to_string(),
2927 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
2928 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
2929 crossing: v2::Crossing::OffBoard as i32,
2930 };
2931 v2::System {
2932 name: "Vehicle".to_string(),
2933 package: "veh.topology".to_string(),
2934 labels: vec!["ASIL_B".to_string()],
2935 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
2936 members: vec![
2937 v2::MemberLine {
2938 component: "veh.topology.Cruise".to_string(),
2939 attributes: vec![attribute("linux", "pinned", None)],
2940 doc: "Documented, see [Cruise].".to_string(),
2941 links: vec![doc_link()],
2942 see: vec![doc_link()],
2943 since: vec!["1.2".to_string()],
2944 },
2945 v2::MemberLine {
2946 component: "veh.diag.access".to_string(),
2947 attributes: vec![],
2948 doc: "Documented, see [Cruise].".to_string(),
2949 links: vec![doc_link()],
2950 see: vec![doc_link()],
2951 since: vec!["1.2".to_string()],
2952 },
2953 ],
2954 components: vec![
2955 v2::Component {
2956 name: "Cruise".to_string(),
2957 package: "veh.topology".to_string(),
2958 implicit: false,
2959 external: true,
2960 instances: vec!["primary".to_string(), "backup".to_string()],
2961 offers: vec![v2::Offer {
2962 service: "veh.adas.cruise".to_string(),
2963 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
2964 doc: "Documented, see [Cruise].".to_string(),
2965 links: vec![doc_link()],
2966 see: vec![doc_link()],
2967 since: vec!["1.2".to_string()],
2968 }],
2969 requires: vec![v2::Require {
2970 interface: interface("veh.adas", "LaneAssist", false),
2971 service: "veh.adas.lane".to_string(),
2972 producer: "veh.topology.Lane".to_string(),
2973 attributes: vec![attribute(
2974 "linux",
2975 "cpuset",
2976 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
2977 )],
2978 doc: "Documented, see [Cruise].".to_string(),
2979 links: vec![doc_link()],
2980 see: vec![doc_link()],
2981 since: vec!["1.2".to_string()],
2982 }],
2983 labels: vec!["ASIL_B".to_string()],
2984 attributes: vec![attribute("rust", "crate", None)],
2985 doc: "Documented, see [Cruise].".to_string(),
2986 links: vec![doc_link()],
2987 see: vec![doc_link()],
2988 since: vec!["1.2".to_string()],
2989 },
2990 v2::Component {
2991 name: "veh.diag.access".to_string(),
2992 package: String::new(),
2993 implicit: true,
2994 external: false,
2995 instances: vec!["Unit".to_string()],
2996 offers: vec![v2::Offer {
2997 service: "veh.diag.access".to_string(),
2998 attributes: vec![],
2999 doc: "Documented, see [Cruise].".to_string(),
3000 links: vec![doc_link()],
3001 see: vec![doc_link()],
3002 since: vec!["1.2".to_string()],
3003 }],
3004 requires: vec![],
3005 labels: vec![],
3006 attributes: vec![],
3007 doc: "Documented, see [Cruise].".to_string(),
3008 links: vec![doc_link()],
3009 see: vec![doc_link()],
3010 since: vec!["1.2".to_string()],
3011 },
3012 ],
3013 producers: vec![v2::Producer {
3014 service: "veh.adas.cruise".to_string(),
3015 component: "veh.topology.Cruise".to_string(),
3016 instances: vec!["primary".to_string(), "backup".to_string()],
3017 not_yet_realizable: true,
3018 }],
3019 grants: vec![v2::Grant {
3020 component: "veh.topology.Backend".to_string(),
3021 external: true,
3022 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3023 }],
3024 regions: vec![v2::Region {
3025 catalog: "veh.diag".to_string(),
3026 hash: vec![0xab; 32],
3027 interfaces: vec![v2::RegionInterface {
3028 name: "veh.diag.access".to_string(),
3029 inline: true,
3030 number: 2,
3031 provisional: true,
3032 service: "veh.diag.access".to_string(),
3033 }],
3034 }],
3035 distributions: vec![v2::Distribution {
3036 name: "Adas".to_string(),
3037 package: "veh.topology".to_string(),
3038 members: vec![v2::MemberLine {
3039 component: "veh.topology.Cruise".to_string(),
3040 attributes: vec![],
3041 doc: "Documented, see [Cruise].".to_string(),
3042 links: vec![doc_link()],
3043 see: vec![doc_link()],
3044 since: vec!["1.2".to_string()],
3045 }],
3046 depends_on: vec!["veh.topology.Base".to_string()],
3047 labels: vec!["PLATFORM_BUNDLE".to_string()],
3048 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3049 doc: "Documented, see [Cruise].".to_string(),
3050 links: vec![doc_link()],
3051 see: vec![doc_link()],
3052 since: vec!["1.2".to_string()],
3053 }],
3054 deployments: vec![v2::Deployment {
3055 name: "Production".to_string(),
3056 package: "veh.topology".to_string(),
3057 labels: vec!["FLEET".to_string()],
3058 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3059 machines: vec![v2::Machine {
3060 name: "Cloud".to_string(),
3061 external: true,
3062 labels: vec!["OFF_BOARD".to_string()],
3063 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3064 doc: "Documented, see [Cruise].".to_string(),
3065 links: vec![doc_link()],
3066 see: vec![doc_link()],
3067 since: vec!["1.2".to_string()],
3068 }],
3069 placements: vec![v2::Placement {
3070 component: "veh.topology.Cruise".to_string(),
3071 instance: "backup".to_string(),
3072 machine: "Cockpit".to_string(),
3073 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3074 sizing: Some(v2::Sizing {
3075 depth: Some(4_294_967_295),
3076 slots: Some(65_536),
3077 budget: Some(18_446_744_073_709_551_615),
3078 }),
3079 }],
3080 links: vec![link.clone()],
3081 routes: vec![v2::Route {
3082 catalog: "veh.adas".to_string(),
3083 interface_number: 2,
3084 member_ordinal: 1,
3085 interface: "LaneAssist".to_string(),
3086 member: "active".to_string(),
3087 service: "veh.adas.lane".to_string(),
3088 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3089 }],
3090 surface: vec![v2::Surface {
3091 link: Some(link),
3092 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3093 }],
3094 installations: vec![v2::Installation {
3095 distribution: "veh.topology.Adas".to_string(),
3096 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3097 }],
3098 doc: "Documented, see [Cruise].".to_string(),
3099 doc_links: vec![doc_link()],
3100 see: vec![doc_link()],
3101 since: vec!["1.2".to_string()],
3102 sizing: Some(v2::Sizing {
3103 depth: Some(3),
3104 slots: Some(8),
3105 budget: Some(4096),
3106 }),
3107 }],
3108 doc: "Documented, see [Cruise].".to_string(),
3109 see: vec![doc_link()],
3110 since: vec!["1.2".to_string()],
3111 links: vec![doc_link()],
3112 }
3113 }
3114
3115 #[test]
3116 fn system_binary_round_trip_preserves_system() {
3117 let system = fixture();
3118 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3119 .expect("decode must succeed");
3120 assert_eq!(system, decoded);
3121 }
3122
3123 #[test]
3128 fn system_json_round_trip_preserves_system() {
3129 let system = fixture();
3130 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3131 assert!(
3132 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3133 "bytes render as base64, got:\n{json}"
3134 );
3135 assert!(
3136 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3137 "enums render by name, got:\n{json}"
3138 );
3139 assert!(
3140 json.contains("\"notYetRealizable\": true"),
3141 "fields render in lowerCamelCase, got:\n{json}"
3142 );
3143 assert!(
3144 json.contains("\"budget\": \"18446744073709551615\""),
3145 "a 64-bit value renders as a string, got:\n{json}"
3146 );
3147 assert_eq!(
3148 system,
3149 v2::system_from_json(&json).expect("the JSON parses")
3150 );
3151 assert!(
3152 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3153 "an unknown field is rejected"
3154 );
3155 }
3156
3157 #[test]
3161 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3162 let mut absent = fixture();
3163 absent.deployments[0].sizing = None;
3164 absent.deployments[0].placements[0].sizing = None;
3165 let mut empty = absent.clone();
3166 empty.deployments[0].sizing = Some(v2::Sizing::default());
3167 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3168 for system in [&absent, &empty] {
3169 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3170 .expect("decode must succeed");
3171 assert_eq!(system, &binary);
3172 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3173 assert_eq!(
3174 system,
3175 &v2::system_from_json(&json).expect("the JSON parses")
3176 );
3177 }
3178 assert_ne!(
3179 v2::system_to_binary(&absent),
3180 v2::system_to_binary(&empty),
3181 "the binary encodings differ"
3182 );
3183 }
3184
3185 #[test]
3186 fn system_text_format_round_trip_preserves_system() {
3187 let system = fixture();
3188 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3189 assert!(
3190 text.starts_with("name:"),
3191 "fields print in schema index order, got: {text}"
3192 );
3193 assert_eq!(
3194 system,
3195 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3196 );
3197 }
3198
3199 #[test]
3202 fn qualified_names_follow_the_one_derivation() {
3203 let system = fixture();
3204 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3205 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3206 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3207 assert_eq!(
3208 system.distributions[0].qualified_name(),
3209 "veh.topology.Adas"
3210 );
3211 }
3212}