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 #[derive(Debug)]
295 pub enum LoadError {
296 Io(std::io::Error),
298 Parse(String),
300 }
301
302 impl std::fmt::Display for LoadError {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 match self {
305 LoadError::Io(err) => write!(f, "cannot read the IR snapshot: {err}"),
306 LoadError::Parse(err) => {
307 write!(f, "the IR snapshot is not valid IR v2 JSON: {err}")
308 }
309 }
310 }
311 }
312
313 impl std::error::Error for LoadError {}
314
315 pub fn load_ir_json(path: &std::path::Path) -> Result<Package, LoadError> {
319 let text = std::fs::read_to_string(path).map_err(LoadError::Io)?;
320 from_json(&text).map_err(|err| LoadError::Parse(err.to_string()))
321 }
322
323 pub fn system_from_json(text: &str) -> Result<System, serde_json::Error> {
327 read_json(text)
328 }
329
330 pub(crate) fn read_json<M>(text: &str) -> Result<M, serde_json::Error>
333 where
334 M: serde::de::DeserializeOwned + Send,
335 {
336 check_nesting(text)?;
337 on_parse_stack(|| parse_json(text))
338 }
339
340 pub(crate) fn read_json_ignoring_unknown<M>(
358 descriptor: prost_reflect::MessageDescriptor,
359 text: &str,
360 ) -> Result<M, serde_json::Error>
361 where
362 M: serde::de::DeserializeOwned + Send,
363 {
364 use serde::de::DeserializeSeed as _;
365 check_nesting(text)?;
366 on_parse_stack(|| {
367 let mut deserializer = serde_json::Deserializer::from_str(text);
368 deserializer.disable_recursion_limit();
369 let known = Shape::Message(descriptor).deserialize(&mut deserializer)?;
370 deserializer.end()?;
371 serde::Deserialize::deserialize(known)
372 })
373 }
374
375 #[derive(Clone)]
378 enum Shape {
379 Message(prost_reflect::MessageDescriptor),
381 List(Box<Shape>),
383 Map(Box<Shape>),
385 Opaque,
387 }
388
389 impl Shape {
390 fn of(field: &prost_reflect::FieldDescriptor) -> Shape {
392 use prost_reflect::Kind;
393 if field.is_map() {
394 let Kind::Message(entry) = field.kind() else {
395 return Shape::Opaque;
396 };
397 return Shape::Map(Box::new(Shape::of_kind(
398 &entry.map_entry_value_field().kind(),
399 )));
400 }
401 let item = Shape::of_kind(&field.kind());
402 if field.is_list() {
403 Shape::List(Box::new(item))
404 } else {
405 item
406 }
407 }
408
409 fn of_kind(kind: &prost_reflect::Kind) -> Shape {
410 match kind {
411 prost_reflect::Kind::Message(message)
412 if !message.full_name().starts_with("google.protobuf.") =>
413 {
414 Shape::Message(message.clone())
415 }
416 _ => Shape::Opaque,
417 }
418 }
419 }
420
421 impl<'de> serde::de::DeserializeSeed<'de> for Shape {
422 type Value = serde_json::Value;
423
424 fn deserialize<D: serde::Deserializer<'de>>(
425 self,
426 deserializer: D,
427 ) -> Result<Self::Value, D::Error> {
428 match self {
429 Shape::Opaque => serde::Deserialize::deserialize(deserializer),
430 shape => deserializer.deserialize_any(ShapeVisitor(shape)),
431 }
432 }
433 }
434
435 struct ShapeVisitor(Shape);
436
437 impl<'de> serde::de::Visitor<'de> for ShapeVisitor {
438 type Value = serde_json::Value;
439
440 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 formatter.write_str(match self.0 {
442 Shape::List(_) => "an array",
443 _ => "an object",
444 })
445 }
446
447 fn visit_unit<E>(self) -> Result<Self::Value, E> {
449 Ok(serde_json::Value::Null)
450 }
451
452 fn visit_seq<A: serde::de::SeqAccess<'de>>(
453 self,
454 mut seq: A,
455 ) -> Result<Self::Value, A::Error> {
456 let Shape::List(item) = self.0 else {
457 return Err(serde::de::Error::invalid_type(
458 serde::de::Unexpected::Seq,
459 &self,
460 ));
461 };
462 let mut items = Vec::new();
463 while let Some(value) = seq.next_element_seed((*item).clone())? {
464 items.push(value);
465 }
466 Ok(serde_json::Value::Array(items))
467 }
468
469 fn visit_map<A: serde::de::MapAccess<'de>>(
470 self,
471 mut map: A,
472 ) -> Result<Self::Value, A::Error> {
473 use serde::de::Error as _;
474 let mut out = serde_json::Map::new();
475 match &self.0 {
476 Shape::Message(descriptor) => {
477 let mut seen = std::collections::HashSet::new();
478 while let Some(key) = map.next_key::<String>()? {
479 let field = descriptor
480 .get_field_by_json_name(&key)
481 .or_else(|| descriptor.get_field_by_name(&key));
482 match field {
483 Some(field) => {
484 if !seen.insert(field.number()) {
485 return Err(A::Error::custom(format!(
486 "duplicate field `{key}`"
487 )));
488 }
489 let value = map.next_value_seed(Shape::of(&field))?;
490 out.insert(key, value);
491 }
492 None => {
493 map.next_value::<serde::de::IgnoredAny>()?;
494 }
495 }
496 }
497 }
498 Shape::Map(value_shape) => {
499 while let Some(key) = map.next_key::<String>()? {
500 let value = map.next_value_seed((**value_shape).clone())?;
501 if out.insert(key.clone(), value).is_some() {
502 return Err(A::Error::custom(format!("duplicate key `{key}`")));
503 }
504 }
505 }
506 Shape::List(_) | Shape::Opaque => {
507 return Err(A::Error::invalid_type(serde::de::Unexpected::Map, &self));
508 }
509 }
510 Ok(serde_json::Value::Object(out))
511 }
512 }
513
514 fn check_nesting(text: &str) -> Result<(), serde_json::Error> {
516 if max_json_nesting(text) > MAX_JSON_NESTING {
517 return Err(<serde_json::Error as serde::de::Error>::custom(format!(
518 "the input nests deeper than {MAX_JSON_NESTING} JSON levels, the ceiling this \
519 reader enforces (ADR-0014 decision 14); real IR nests orders of magnitude \
520 shallower"
521 )));
522 }
523 Ok(())
524 }
525
526 fn on_parse_stack<R: Send>(parse: impl FnOnce() -> R + Send) -> R {
529 if cfg!(target_family = "wasm") {
530 parse()
539 } else {
540 std::thread::scope(|scope| {
541 let handle = std::thread::Builder::new()
542 .stack_size(JSON_PARSE_STACK)
543 .spawn_scoped(scope, parse)
544 .expect("the JSON parse thread spawns");
545 match handle.join() {
546 Ok(result) => result,
547 Err(payload) => std::panic::resume_unwind(payload),
548 }
549 })
550 }
551 }
552
553 fn parse_json<M: serde::de::DeserializeOwned>(text: &str) -> Result<M, serde_json::Error> {
558 let mut deserializer = serde_json::Deserializer::from_str(text);
559 deserializer.disable_recursion_limit();
560 let message: M = serde::Deserialize::deserialize(&mut deserializer)?;
561 deserializer.end()?;
562 Ok(message)
563 }
564
565 pub fn to_text_format(package: &Package) -> Result<String, SerializeError> {
579 render_text_for(package_descriptor(), package)
580 }
581
582 pub fn system_to_text_format(system: &System) -> Result<String, SerializeError> {
586 render_text_for(system_descriptor(), system)
587 }
588
589 pub(crate) fn render_text_for<M: prost::Message>(
592 descriptor: prost_reflect::MessageDescriptor,
593 message: &M,
594 ) -> Result<String, SerializeError> {
595 let dynamic = transcode(descriptor, message).map_err(SerializeError::Text)?;
596 Ok(dynamic.to_text_format_with_options(
597 &prost_reflect::text_format::FormatOptions::new()
598 .pretty(true)
599 .skip_default_fields(false)
600 .print_message_fields_in_index_order(true),
601 ))
602 }
603
604 #[cfg(test)]
610 #[derive(Debug)]
611 pub(crate) enum TextFormatError {
612 Parse(prost_reflect::text_format::ParseError),
614 Transcode(prost::DecodeError),
616 }
617
618 #[cfg(test)]
619 impl std::fmt::Display for TextFormatError {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 match self {
622 Self::Parse(source) => {
623 write!(f, "cannot parse the text as an IR package: {source}")
624 }
625 Self::Transcode(source) => write!(
626 f,
627 "cannot rebuild the parsed prototext as a package: {source}; the known cause \
628 is composite nesting deeper than the transcoding decoder's recursion limit"
629 ),
630 }
631 }
632 }
633
634 #[cfg(test)]
635 impl std::error::Error for TextFormatError {
636 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
637 match self {
638 Self::Parse(source) => Some(source),
639 Self::Transcode(source) => Some(source),
640 }
641 }
642 }
643
644 #[cfg(test)]
665 pub(crate) fn from_text_format(text: &str) -> Result<Package, TextFormatError> {
666 parse_text(package_descriptor(), text)
667 }
668
669 #[cfg(test)]
673 pub(crate) fn system_from_text_format(text: &str) -> Result<System, TextFormatError> {
674 parse_text(system_descriptor(), text)
675 }
676
677 #[cfg(test)]
678 fn parse_text<M: prost::Message + Default>(
679 descriptor: prost_reflect::MessageDescriptor,
680 text: &str,
681 ) -> Result<M, TextFormatError> {
682 let dynamic = prost_reflect::DynamicMessage::parse_text_format(descriptor, text)
683 .map_err(TextFormatError::Parse)?;
684 dynamic.transcode_to().map_err(TextFormatError::Transcode)
685 }
686
687 pub fn to_binary(package: &Package) -> Vec<u8> {
694 prost::Message::encode_to_vec(package)
695 }
696
697 pub fn from_binary(bytes: &[u8]) -> Result<Package, prost::DecodeError> {
700 prost::Message::decode(bytes)
701 }
702
703 pub fn system_to_binary(system: &System) -> Vec<u8> {
706 prost::Message::encode_to_vec(system)
707 }
708
709 pub fn system_from_binary(bytes: &[u8]) -> Result<System, prost::DecodeError> {
712 prost::Message::decode(bytes)
713 }
714
715 fn qualified(package: &str, name: &str) -> String {
720 if package.is_empty() {
721 name.to_string()
722 } else {
723 format!("{package}.{name}")
724 }
725 }
726
727 impl System {
728 pub fn qualified_name(&self) -> String {
731 qualified(&self.package, &self.name)
732 }
733 }
734
735 impl Component {
736 pub fn qualified_name(&self) -> String {
739 qualified(&self.package, &self.name)
740 }
741 }
742
743 impl Distribution {
744 pub fn qualified_name(&self) -> String {
747 qualified(&self.package, &self.name)
748 }
749 }
750
751 #[derive(Debug, Clone, Copy, PartialEq)]
782 pub struct InterfaceShape<'a> {
783 pub name: &'a str,
787 pub interface: &'a Interface,
789 pub service: Option<&'a Service>,
792 }
793
794 impl InterfaceShape<'_> {
795 pub fn visibility(&self) -> i32 {
800 match self.service {
801 Some(service) => service.visibility,
802 None => self.interface.visibility,
803 }
804 }
805
806 pub fn is_inline(&self) -> bool {
808 self.service.is_some()
809 }
810 }
811
812 impl Package {
813 pub fn shapes(&self) -> impl Iterator<Item = InterfaceShape<'_>> {
828 let named = self.interfaces.iter().map(|interface| InterfaceShape {
829 name: &interface.name,
830 interface,
831 service: None,
832 });
833 let inline = self.services.iter().flat_map(|service| {
834 service
835 .shapes
836 .iter()
837 .filter_map(move |slot| match slot.kind.as_ref()? {
838 service_shape::Kind::Inline(interface) => Some(InterfaceShape {
839 name: &service.name,
840 interface,
841 service: Some(service),
842 }),
843 service_shape::Kind::InterfaceRef(_) => None,
844 })
845 });
846 named.chain(inline)
847 }
848 }
849
850 pub fn unit_of(package: &Package) -> &str {
855 if package.unit.is_empty() {
856 &package.name
857 } else {
858 &package.unit
859 }
860 }
861
862 pub fn packages_of_unit<'a>(
864 unit: &str,
865 packages: &'a [Package],
866 ) -> impl Iterator<Item = &'a Package> {
867 select_unit(unit, packages.iter())
868 }
869
870 pub fn members_of_unit<'a>(
873 unit: &str,
874 packages: &[&'a Package],
875 ) -> impl Iterator<Item = &'a Package> {
876 select_unit(unit, packages.iter().copied())
877 }
878
879 fn select_unit<'a>(
882 unit: &str,
883 packages: impl Iterator<Item = &'a Package>,
884 ) -> impl Iterator<Item = &'a Package> {
885 let unit = unit.to_owned();
886 packages.filter(move |p| unit_of(p) == unit)
887 }
888
889 pub fn relative_name(unit: &str, package: &str, name: &str) -> String {
893 if package == unit {
894 return name.to_owned();
895 }
896 match package
900 .strip_prefix(unit)
901 .and_then(|rest| rest.strip_prefix('.'))
902 {
903 Some(below) => format!("{below}.{name}"),
904 None => format!("{package}.{name}"),
905 }
906 }
907
908 impl Package {
909 pub fn catalog_name(&self, shape: &InterfaceShape<'_>) -> String {
913 if shape.is_inline() {
914 shape.name.to_owned()
915 } else {
916 relative_name(unit_of(self), &self.name, shape.name)
917 }
918 }
919 }
920
921 pub fn unit_retired(unit: &str, packages: &[&Package]) -> Vec<RetiredInterface> {
928 let mut seen: Vec<&str> = Vec::new();
929 let mut retired: Vec<RetiredInterface> = members_of_unit(unit, packages)
930 .filter(|package| {
931 let first = !seen.contains(&package.name.as_str());
932 if first {
933 seen.push(&package.name);
934 }
935 first
936 })
937 .flat_map(|package| package.retired.iter().cloned())
938 .collect();
939 retired.sort_by_key(|entry| entry.number);
940 retired
941 }
942
943 pub fn referenced_packages(package: &Package) -> std::collections::BTreeSet<String> {
956 let mut found = std::collections::BTreeSet::new();
957 for decl in &package.decls {
958 walk_decl(decl, &mut found);
959 }
960 for interface in &package.interfaces {
961 for interaction in &interface.interactions {
962 walk_decl(interaction, &mut found);
963 }
964 }
965 for service in &package.services {
966 for slot in &service.shapes {
967 match &slot.kind {
968 Some(service_shape::Kind::InterfaceRef(reference)) => {
969 qualifier(reference, &mut found);
970 }
971 Some(service_shape::Kind::Inline(interface)) => {
972 for interaction in &interface.interactions {
973 walk_decl(interaction, &mut found);
974 }
975 }
976 None => {}
977 }
978 }
979 }
980 found
981 }
982
983 fn qualifier(reference: &str, found: &mut std::collections::BTreeSet<String>) {
986 if let Some((package, _)) = reference.rsplit_once('.') {
987 found.insert(package.to_string());
988 }
989 }
990
991 fn walk_decl(decl: &Decl, found: &mut std::collections::BTreeSet<String>) {
994 match &decl.kind {
995 Some(decl::Kind::TypeDef(type_def)) => walk_type_def(type_def, found),
996 Some(decl::Kind::ConstDef(const_def)) => {
997 if let Some(reference) = &const_def.type_ref {
998 qualifier(reference, found);
999 }
1000 }
1001 Some(decl::Kind::StructDef(struct_def)) => {
1002 for member in &struct_def.members {
1003 match &member.member {
1004 Some(struct_member::Member::Field(field)) => {
1005 if let Some(field_type) = &field.r#type {
1006 walk_field_type(field_type, found);
1007 }
1008 }
1009 Some(struct_member::Member::Reserved(_)) | None => {}
1011 }
1012 }
1013 }
1014 Some(decl::Kind::EnumDef(_)) => {}
1016 Some(decl::Kind::EnumSetDef(enum_set)) => {
1017 if let Some(reference) = &enum_set.backing_enum {
1018 qualifier(reference, found);
1019 }
1020 }
1021 Some(decl::Kind::UnionDef(union_def)) => {
1022 for arm in &union_def.arms {
1023 qualifier(&arm.type_ref, found);
1024 }
1025 }
1026 Some(decl::Kind::SignalDef(signal)) => qualifier(&signal.payload, found),
1027 Some(decl::Kind::EventDef(event)) => qualifier(&event.payload, found),
1028 Some(decl::Kind::CommandDef(command)) => {
1029 for param in &command.params {
1030 if let Some(field_type) = ¶m.r#type {
1031 walk_field_type(field_type, found);
1032 }
1033 }
1034 }
1035 Some(decl::Kind::QueryDef(query)) => {
1036 for param in &query.params {
1037 if let Some(field_type) = ¶m.r#type {
1038 walk_field_type(field_type, found);
1039 }
1040 }
1041 if let Some(return_type) = &query.return_type {
1042 walk_return_type(return_type, found);
1043 }
1044 }
1045 Some(decl::Kind::FixedDef(fixed)) => {
1046 if let Some(field_type) = &fixed.payload {
1047 walk_field_type(field_type, found);
1048 }
1049 }
1050 Some(decl::Kind::ReservedSlot(_)) | None => {}
1052 }
1053 }
1054
1055 fn walk_field_type(field_type: &FieldType, found: &mut std::collections::BTreeSet<String>) {
1058 match &field_type.kind {
1059 Some(field_type::Kind::Named(reference)) => qualifier(reference, found),
1060 Some(field_type::Kind::Primitive(_)) => {}
1062 Some(field_type::Kind::InlineScalar(type_def)) => walk_type_def(type_def, found),
1063 Some(field_type::Kind::Tuple(tuple)) => {
1064 for field in &tuple.fields {
1065 if let Some(inner) = &field.r#type {
1066 walk_field_type(inner, found);
1067 }
1068 }
1069 }
1070 Some(field_type::Kind::Array(array)) => {
1071 if let Some(element) = &array.element {
1072 walk_field_type(element, found);
1073 }
1074 }
1075 Some(field_type::Kind::Map(map)) => {
1076 if let Some(key) = &map.key {
1077 walk_field_type(key, found);
1078 }
1079 if let Some(value) = &map.value {
1080 walk_field_type(value, found);
1081 }
1082 }
1083 Some(field_type::Kind::Stream(stream)) => match &stream.element {
1084 Some(stream_type::Element::Named(reference)) => qualifier(reference, found),
1085 Some(stream_type::Element::Primitive(_)) | None => {}
1087 },
1088 None => {}
1089 }
1090 }
1091
1092 fn walk_type_def(type_def: &TypeDef, found: &mut std::collections::BTreeSet<String>) {
1094 if let Some(constraint) = &type_def.constraint
1095 && let Some(reference) = &constraint.pattern_const
1096 {
1097 qualifier(reference, found);
1098 }
1099 }
1100
1101 fn walk_return_type(return_type: &ReturnType, found: &mut std::collections::BTreeSet<String>) {
1102 match &return_type.kind {
1103 Some(return_type::Kind::Value(field_type)) => walk_field_type(field_type, found),
1104 Some(return_type::Kind::Fallible(fallible)) => {
1105 qualifier(&fallible.ok, found);
1106 qualifier(&fallible.err, found);
1107 }
1108 None => {}
1109 }
1110 }
1111
1112 pub fn constraint_is_vacuous(constraint: Option<&Constraint>) -> bool {
1128 let Some(c) = constraint else { return true };
1129 c.min.is_none()
1130 && c.max.is_none()
1131 && c.step.is_none()
1132 && c.len_min.is_none()
1133 && c.len_max.is_none()
1134 && c.pattern.is_none()
1135 && c.pattern_const.is_none()
1136 }
1137}
1138
1139pub mod catalog_hash;
1140pub mod codegen;
1141pub mod name;
1142pub mod projection;
1143pub mod rules;
1144pub mod zero;
1145
1146#[cfg(test)]
1147mod v2_round_trip {
1148 use crate::v2;
1149
1150 fn interaction(name: &str, ordinal: u32, kind: v2::decl::Kind) -> v2::Decl {
1155 v2::Decl {
1156 name: name.to_string(),
1157 visibility: v2::Visibility::Unspecified as i32,
1158 is_error: false,
1159 doc: String::new(),
1160 labels: Vec::new(),
1161 deprecated: None,
1162 ordinal,
1163 kind: Some(kind),
1164 links: Vec::new(),
1165 see: Vec::new(),
1166 since: Vec::new(),
1167 }
1168 }
1169
1170 fn named_type(name: &str) -> v2::FieldType {
1171 v2::FieldType {
1172 optional: false,
1173 kind: Some(v2::field_type::Kind::Named(name.to_string())),
1174 }
1175 }
1176
1177 fn stream_of(element: v2::stream_type::Element) -> v2::FieldType {
1178 v2::FieldType {
1179 optional: false,
1180 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
1181 element: Some(element),
1182 })),
1183 }
1184 }
1185
1186 fn fixture() -> v2::Package {
1192 let speed = v2::SignalDef {
1195 payload: "Speed".to_string(),
1196 declared_init: None,
1197 init: Some(v2::InitValue {
1198 derivable: true,
1199 value: Some("0.0".to_string()),
1200 }),
1201 timing: Some(v2::Timing {
1202 mode: v2::TimingMode::StrictPeriodic as i32,
1203 min_us: Some("10000".to_string()),
1204 max_us: Some("10000".to_string()),
1205 default_applied: false,
1206 }),
1207 };
1208
1209 let door_opened = v2::EventDef {
1212 payload: "DoorEvent".to_string(),
1213 timing: Some(v2::Timing {
1214 mode: v2::TimingMode::Range as i32,
1215 min_us: Some("20000".to_string()),
1216 max_us: Some("500000".to_string()),
1217 default_applied: true,
1218 }),
1219 };
1220
1221 let set_target = v2::CommandDef {
1223 params: vec![v2::Param {
1224 name: "target".to_string(),
1225 r#type: Some(named_type("Speed")),
1226 doc: String::new(),
1227 links: Vec::new(),
1228 see: Vec::new(),
1229 since: Vec::new(),
1230 }],
1231 contracts: vec![v2::Contract {
1232 kind: v2::ContractKind::Require as i32,
1233 source: "target >= speed".to_string(),
1234 signal_refs: vec!["speed".to_string()],
1235 param_refs: vec!["target".to_string()],
1236 uses_result: false,
1237 observer_id: "VehicleStatus.setTarget.require[0]".to_string(),
1238 }],
1239 timing: None,
1240 };
1241
1242 let fetch_faults = v2::QueryDef {
1245 params: vec![v2::Param {
1246 name: "page".to_string(),
1247 r#type: Some(named_type("PageSpec")),
1248 doc: String::new(),
1249 links: Vec::new(),
1250 see: Vec::new(),
1251 since: Vec::new(),
1252 }],
1253 return_type: Some(v2::ReturnType {
1254 kind: Some(v2::return_type::Kind::Fallible(v2::FallibleType {
1255 ok: "FaultPage".to_string(),
1256 err: "DiagError".to_string(),
1257 })),
1258 }),
1259 contracts: vec![v2::Contract {
1260 kind: v2::ContractKind::Ensure as i32,
1261 source: "result.count <= page.limit".to_string(),
1262 signal_refs: Vec::new(),
1263 param_refs: vec!["page".to_string()],
1264 uses_result: true,
1265 observer_id: "VehicleStatus.fetchFaults.ensure[0]".to_string(),
1266 }],
1267 timing: None,
1268 };
1269
1270 let vin = v2::FixedDef {
1272 payload: Some(named_type("Vin")),
1273 };
1274
1275 let vehicle_status = v2::Interface {
1276 name: "VehicleStatus".to_string(),
1277 visibility: v2::Visibility::Public as i32,
1278 doc: "Vehicle status contract".to_string(),
1279 labels: Vec::new(),
1280 deprecated: None,
1281 interactions: vec![
1282 interaction("speed", 1, v2::decl::Kind::SignalDef(speed)),
1283 interaction("doorOpened", 2, v2::decl::Kind::EventDef(door_opened)),
1284 v2::Decl {
1287 ordinal: 3,
1288 kind: Some(v2::decl::Kind::ReservedSlot(v2::Reserved {
1289 ordinal: 3,
1290 name: Some("legacyMode".to_string()),
1291 value: None,
1292 })),
1293 ..interaction("", 3, v2::decl::Kind::ReservedSlot(v2::Reserved::default()))
1294 },
1295 interaction("setTarget", 4, v2::decl::Kind::CommandDef(set_target)),
1296 interaction("fetchFaults", 5, v2::decl::Kind::QueryDef(fetch_faults)),
1297 interaction("vin", 6, v2::decl::Kind::FixedDef(vin)),
1298 ],
1299 number: 0,
1300 provisional: false,
1301 links: Vec::new(),
1302 see: Vec::new(),
1303 since: Vec::new(),
1304 };
1305
1306 let tail_logs = v2::QueryDef {
1309 params: vec![v2::Param {
1310 name: "pattern".to_string(),
1311 r#type: Some(stream_of(v2::stream_type::Element::Primitive(
1312 v2::PrimitiveType::String as i32,
1313 ))),
1314 doc: String::new(),
1315 links: Vec::new(),
1316 see: Vec::new(),
1317 since: Vec::new(),
1318 }],
1319 return_type: Some(v2::ReturnType {
1320 kind: Some(v2::return_type::Kind::Value(stream_of(
1321 v2::stream_type::Element::Named("LogLine".to_string()),
1322 ))),
1323 }),
1324 contracts: Vec::new(),
1325 timing: None,
1326 };
1327
1328 let status_service = v2::Service {
1331 name: "veh.adas.status".to_string(),
1332 visibility: v2::Visibility::Public as i32,
1333 doc: String::new(),
1334 labels: Vec::new(),
1335 deprecated: None,
1336 shapes: vec![v2::ServiceShape {
1337 kind: Some(v2::service_shape::Kind::InterfaceRef(
1338 "VehicleStatus".to_string(),
1339 )),
1340 }],
1341 links: Vec::new(),
1342 see: Vec::new(),
1343 since: Vec::new(),
1344 };
1345 let logs_service = v2::Service {
1348 name: "veh.adas.logs".to_string(),
1349 visibility: v2::Visibility::Public as i32,
1350 doc: String::new(),
1351 labels: Vec::new(),
1352 deprecated: None,
1353 shapes: vec![v2::ServiceShape {
1354 kind: Some(v2::service_shape::Kind::Inline(v2::Interface {
1355 name: String::new(),
1356 visibility: v2::Visibility::Unspecified as i32,
1357 doc: String::new(),
1358 labels: Vec::new(),
1359 deprecated: None,
1360 interactions: vec![interaction(
1361 "tailLogs",
1362 1,
1363 v2::decl::Kind::QueryDef(tail_logs),
1364 )],
1365 number: 0,
1366 provisional: false,
1367 links: Vec::new(),
1368 see: Vec::new(),
1369 since: Vec::new(),
1370 })),
1371 }],
1372 links: Vec::new(),
1373 see: Vec::new(),
1374 since: Vec::new(),
1375 };
1376
1377 v2::Package {
1378 name: "veh.adas".to_string(),
1379 decls: vec![v2::Decl {
1383 name: "Speed".to_string(),
1384 visibility: v2::Visibility::Public as i32,
1385 is_error: false,
1386 doc: String::new(),
1387 labels: Vec::new(),
1388 deprecated: None,
1389 ordinal: 0,
1390 kind: Some(v2::decl::Kind::TypeDef(v2::TypeDef {
1391 backing: Some(v2::Backing {
1392 kind: Some(v2::backing::Kind::Unit("km/h".to_string())),
1393 }),
1394 constraint: None,
1395 declared_init: None,
1396 init: None,
1397 width: Some(v2::type_def::Width::FloatWidth(v2::FloatWidth::F32 as i32)),
1398 })),
1399 links: Vec::new(),
1400 see: Vec::new(),
1401 since: Vec::new(),
1402 }],
1403 interfaces: vec![vehicle_status],
1404 services: vec![status_service, logs_service],
1405 retired: Vec::new(),
1406 unit: "veh.adas".to_string(),
1407 }
1408 }
1409
1410 fn vocabulary_fixture() -> v2::Package {
1417 fn decl(name: &str, kind: v2::decl::Kind) -> v2::Decl {
1418 v2::Decl {
1419 name: name.to_string(),
1420 visibility: v2::Visibility::Public as i32,
1421 is_error: false,
1422 doc: String::new(),
1423 labels: Vec::new(),
1424 deprecated: None,
1425 ordinal: 0,
1426 kind: Some(kind),
1427 links: Vec::new(),
1428 see: Vec::new(),
1429 since: Vec::new(),
1430 }
1431 }
1432
1433 fn field(name: &str, ordinal: u32, field_type: v2::FieldType) -> v2::Field {
1434 v2::Field {
1435 name: name.to_string(),
1436 ordinal,
1437 r#type: Some(field_type),
1438 declared_init: None,
1439 init: None,
1440 doc: String::new(),
1441 labels: Vec::new(),
1442 deprecated: None,
1443 links: Vec::new(),
1444 see: Vec::new(),
1445 since: Vec::new(),
1446 }
1447 }
1448
1449 let max_retry = v2::ConstDef {
1451 type_ref: Some("integer".to_string()),
1452 value: "24".to_string(),
1453 regex: None,
1454 };
1455
1456 let gear = v2::EnumDef {
1459 values: vec![
1460 v2::EnumValue {
1461 name: "PARK".to_string(),
1462 value: 1,
1463 doc: String::new(),
1464 links: Vec::new(),
1465 see: Vec::new(),
1466 since: Vec::new(),
1467 },
1468 v2::EnumValue {
1469 name: "DRIVE".to_string(),
1470 value: 2,
1471 doc: String::new(),
1472 links: Vec::new(),
1473 see: Vec::new(),
1474 since: Vec::new(),
1475 },
1476 ],
1477 reserved: vec![v2::Reserved {
1478 ordinal: 0,
1479 name: None,
1480 value: Some(7),
1481 }],
1482 };
1483
1484 let warnings = v2::EnumSetDef {
1487 backing_enum: None,
1488 bits: vec![
1489 v2::EnumValue {
1490 name: "LOW_FUEL".to_string(),
1491 value: 0,
1492 doc: String::new(),
1493 links: Vec::new(),
1494 see: Vec::new(),
1495 since: Vec::new(),
1496 },
1497 v2::EnumValue {
1498 name: "ICE_RISK".to_string(),
1499 value: 33,
1500 doc: String::new(),
1501 links: Vec::new(),
1502 see: Vec::new(),
1503 since: Vec::new(),
1504 },
1505 ],
1506 width: v2::IntWidth::U64 as i32,
1507 };
1508
1509 let plate_text = v2::TypeDef {
1512 backing: Some(v2::Backing {
1513 kind: Some(v2::backing::Kind::Primitive(
1514 v2::PrimitiveType::String as i32,
1515 )),
1516 }),
1517 constraint: Some(v2::Constraint {
1518 min: None,
1519 max: None,
1520 step: None,
1521 len_min: Some(1),
1522 len_max: Some(86),
1523 pattern: None,
1524 pattern_const: None,
1525 }),
1526 declared_init: None,
1527 init: None,
1528 width: None,
1529 };
1530
1531 let sample = v2::UnionDef {
1533 arms: vec![
1534 v2::UnionArm {
1535 name: "speed".to_string(),
1536 ordinal: 1,
1537 type_ref: "Speed".to_string(),
1538 doc: String::new(),
1539 links: Vec::new(),
1540 see: Vec::new(),
1541 since: Vec::new(),
1542 },
1543 v2::UnionArm {
1544 name: "gear".to_string(),
1545 ordinal: 2,
1546 type_ref: "Gear".to_string(),
1547 doc: String::new(),
1548 links: Vec::new(),
1549 see: Vec::new(),
1550 since: Vec::new(),
1551 },
1552 ],
1553 is_result: false,
1554 reserved: Vec::new(),
1555 };
1556
1557 let retries = v2::Field {
1563 declared_init: Some("3".to_string()),
1564 init: Some(v2::InitValue {
1565 derivable: true,
1566 value: Some("3".to_string()),
1567 }),
1568 ..field(
1569 "retries",
1570 1,
1571 v2::FieldType {
1572 optional: false,
1573 kind: Some(v2::field_type::Kind::InlineScalar(Box::new(v2::TypeDef {
1574 backing: Some(v2::Backing {
1575 kind: Some(v2::backing::Kind::Primitive(
1576 v2::PrimitiveType::Integer as i32,
1577 )),
1578 }),
1579 constraint: Some(v2::Constraint {
1580 min: Some("0".to_string()),
1581 max: Some("24".to_string()),
1582 step: None,
1583 len_min: None,
1584 len_max: None,
1585 pattern: None,
1586 pattern_const: None,
1587 }),
1588 declared_init: None,
1589 init: None,
1590 width: Some(v2::type_def::Width::IntWidth(v2::IntWidth::U8 as i32)),
1591 }))),
1592 },
1593 )
1594 };
1595
1596 let position = field(
1599 "position",
1600 2,
1601 v2::FieldType {
1602 optional: false,
1603 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
1604 fields: vec![
1605 v2::TupleField {
1606 name: "x".to_string(),
1607 r#type: Some(named_type("Speed")),
1608 },
1609 v2::TupleField {
1610 name: "y".to_string(),
1611 r#type: Some(named_type("Speed")),
1612 },
1613 ],
1614 })),
1615 },
1616 );
1617
1618 let gears = field(
1620 "gears",
1621 3,
1622 v2::FieldType {
1623 optional: false,
1624 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
1625 element: Some(Box::new(named_type("Gear"))),
1626 min: 1,
1627 max: 4096,
1628 }))),
1629 },
1630 );
1631
1632 let plates = v2::Field {
1636 deprecated: Some("superseded by gears".to_string()),
1637 ..field(
1638 "plates",
1639 4,
1640 v2::FieldType {
1641 optional: false,
1642 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
1643 key: Some(Box::new(named_type("PlateText"))),
1644 value: Some(Box::new(named_type("Gear"))),
1645 min: 0,
1646 max: 53,
1647 }))),
1648 },
1649 )
1650 };
1651
1652 let snapshot = v2::StructDef {
1653 members: [retries, position, gears, plates]
1654 .into_iter()
1655 .map(|field| v2::StructMember {
1656 member: Some(v2::struct_member::Member::Field(Box::new(field))),
1657 })
1658 .collect(),
1659 fixed_layout: false,
1660 };
1661
1662 v2::Package {
1663 name: "veh.vocab".to_string(),
1664 decls: vec![
1665 decl("MAX_RETRY", v2::decl::Kind::ConstDef(max_retry)),
1666 decl("Gear", v2::decl::Kind::EnumDef(gear)),
1667 decl("Warnings", v2::decl::Kind::EnumSetDef(warnings)),
1668 decl("PlateText", v2::decl::Kind::TypeDef(plate_text)),
1669 v2::Decl {
1672 deprecated: Some("use Snapshot".to_string()),
1673 ..decl("Sample", v2::decl::Kind::UnionDef(sample))
1674 },
1675 decl("Snapshot", v2::decl::Kind::StructDef(snapshot)),
1676 ],
1677 interfaces: Vec::new(),
1678 services: Vec::new(),
1679 retired: Vec::new(),
1680 unit: String::new(),
1681 }
1682 }
1683
1684 #[test]
1685 fn protobuf_round_trip_preserves_package() {
1686 let package = fixture();
1687
1688 let buf = v2::to_binary(&package);
1689 let decoded = v2::from_binary(buf.as_slice()).expect("decode must succeed");
1690
1691 assert_eq!(package, decoded);
1692
1693 let vocabulary = vocabulary_fixture();
1695 let decoded_vocabulary =
1696 v2::from_binary(v2::to_binary(&vocabulary).as_slice()).expect("decode must succeed");
1697 assert_eq!(vocabulary, decoded_vocabulary);
1698
1699 let interface = &decoded.interfaces[0];
1700 let ordinals: Vec<u32> = interface.interactions.iter().map(|d| d.ordinal).collect();
1701 assert_eq!(
1702 ordinals,
1703 [1, 2, 3, 4, 5, 6],
1704 "one ordinal sequence, tombstone counted (ridl §11)"
1705 );
1706 let Some(v2::decl::Kind::ReservedSlot(tombstone)) = &interface.interactions[2].kind else {
1707 panic!("ordinal 3 must decode as a reserved tombstone");
1708 };
1709 assert_eq!(tombstone.name.as_deref(), Some("legacyMode"));
1710 let Some(v2::service_shape::Kind::Inline(inline)) = decoded.services[1]
1711 .shapes
1712 .first()
1713 .and_then(|slot| slot.kind.as_ref())
1714 else {
1715 panic!("veh.adas.logs must decode as an inline shape");
1716 };
1717 assert_eq!(inline.name, "", "an inline shape carries no name");
1718 let references: Vec<&str> = decoded.services[0]
1719 .shapes
1720 .iter()
1721 .filter_map(|slot| match &slot.kind {
1722 Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
1723 _ => None,
1724 })
1725 .collect();
1726 assert_eq!(
1727 references,
1728 ["VehicleStatus"],
1729 "a service's set carries its references and nothing else"
1730 );
1731 }
1732
1733 #[test]
1734 fn json_round_trip_preserves_package() {
1735 for package in [fixture(), vocabulary_fixture()] {
1736 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1737 let decoded = v2::from_json(&json).expect("json deserialization must succeed");
1738
1739 assert_eq!(package, decoded);
1740 }
1741 }
1742
1743 #[test]
1750 fn number_provisional_and_retired_round_trip_through_json_text_and_binary() {
1751 let mut package = fixture();
1752 package.interfaces[0].number = 4;
1753 package.interfaces[0].provisional = true;
1754 let Some(v2::service_shape::Kind::Inline(inline)) =
1755 package.services[1].shapes[0].kind.as_mut()
1756 else {
1757 panic!("veh.adas.logs holds an inline shape in slot 1");
1758 };
1759 inline.number = 5;
1760 package.retired = vec![
1761 v2::RetiredInterface {
1762 name: "LaneAssist".to_string(),
1763 number: 2,
1764 },
1765 v2::RetiredInterface {
1766 name: "service:veh.hvac.cabin".to_string(),
1767 number: 3,
1768 },
1769 ];
1770
1771 let json = v2::to_json_pretty(&package).expect("the package serializes as IR JSON");
1772 assert_eq!(v2::from_json(&json).expect("the JSON parses back"), package);
1773 let text = v2::to_text_format(&package).expect("the package serializes as prototext");
1774 assert_eq!(
1775 v2::from_text_format(&text).expect("the prototext parses back"),
1776 package
1777 );
1778 assert_eq!(
1779 v2::from_binary(v2::to_binary(&package).as_slice()).expect("the binary decodes"),
1780 package
1781 );
1782
1783 for needle in [
1784 r#""number": 4"#,
1785 r#""provisional": true"#,
1786 r#""number": 5"#,
1787 r#""name": "LaneAssist""#,
1788 r#""name": "service:veh.hvac.cabin""#,
1789 ] {
1790 assert!(
1791 json.contains(needle),
1792 "the JSON must carry {needle}, got: {json}"
1793 );
1794 }
1795
1796 let unnumbered = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1800 for needle in [
1801 r#""number": 0"#,
1802 r#""provisional": false"#,
1803 r#""retired": []"#,
1804 ] {
1805 assert!(
1806 unnumbered.contains(needle),
1807 "a default field must still be written, expected {needle} in: {unnumbered}"
1808 );
1809 }
1810 }
1811
1812 #[test]
1818 fn a_snapshot_lacking_the_number_fields_still_loads() {
1819 let package = v2::from_json(
1820 r#"{"name": "veh.x", "interfaces": [{"name": "LaneKeeping"}], "services": [{"name": "veh.x.s", "shapes": [{"inline": {"name": ""}}]}]}"#,
1821 )
1822 .expect("a pre-lock snapshot loads");
1823
1824 assert_eq!(package.interfaces[0].number, 0);
1825 assert!(!package.interfaces[0].provisional);
1826 let Some(v2::service_shape::Kind::Inline(inline)) =
1827 package.services[0].shapes[0].kind.as_ref()
1828 else {
1829 panic!("the service holds an inline shape");
1830 };
1831 assert_eq!(inline.number, 0);
1832 assert!(!inline.provisional);
1833 assert_eq!(package.retired, Vec::new());
1834 }
1835
1836 #[test]
1841 fn text_format_round_trip_preserves_package() {
1842 for package in [fixture(), vocabulary_fixture()] {
1843 let text = v2::to_text_format(&package).expect("the fixture serializes as prototext");
1844 let decoded = v2::from_text_format(&text).expect("prototext parsing must succeed");
1845
1846 assert_eq!(package, decoded);
1847 }
1848 }
1849
1850 #[test]
1865 fn text_format_is_pretty_with_defaults_in_index_order() {
1866 let text = v2::to_text_format(&fixture()).expect("the fixture serializes as prototext");
1867
1868 assert!(
1870 text.contains("\n "),
1871 "pretty printing must indent nested fields, got: {text}"
1872 );
1873 assert!(
1876 text.contains("is_error: false"),
1877 "a field holding its default must be emitted, got: {text}"
1878 );
1879 assert!(
1882 text.starts_with("name:"),
1883 "fields must print in schema index order, got: {text}"
1884 );
1885 }
1886
1887 fn strict_parse(json: &str) -> v2::Package {
1894 let mut deserializer = serde_json::Deserializer::from_str(json);
1895 let package = <v2::Package as serde::Deserialize>::deserialize(&mut deserializer)
1896 .expect("a strict conformant parser must accept the emitted JSON");
1897 deserializer.end().expect("no trailing input");
1898 package
1899 }
1900
1901 #[test]
1906 fn emitted_json_survives_a_strict_conformant_parse() {
1907 for package in [fixture(), vocabulary_fixture()] {
1908 let json = v2::to_json_pretty(&package).expect("the fixture serializes as IR JSON");
1909 assert_eq!(package, strict_parse(&json));
1910 }
1911 }
1912
1913 #[test]
1914 fn json_renders_timing_bounds_and_fallible_arms_exactly() {
1915 let json = v2::to_json_pretty(&fixture()).expect("the fixture serializes as IR JSON");
1916
1917 assert!(
1921 json.contains(r#""minUs": "10000""#),
1922 "the timing bound must be a JSON string, got: {json}"
1923 );
1924 assert!(
1926 json.contains(r#""ok": "FaultPage""#),
1927 "the ok arm must render, got: {json}"
1928 );
1929 assert!(
1930 json.contains(r#""err": "DiagError""#),
1931 "the err arm must render, got: {json}"
1932 );
1933 }
1934
1935 #[test]
1940 fn json_renders_64_bit_integer_fields_as_strings() {
1941 let json = v2::to_json_pretty(&vocabulary_fixture())
1942 .expect("the vocabulary fixture serializes as IR JSON");
1943
1944 assert!(
1946 json.contains(r#""max": "4096""#),
1947 "an array bound must be a JSON string, got: {json}"
1948 );
1949 assert!(
1951 json.contains(r#""lenMax": "86""#),
1952 "a length bound must be a JSON string, got: {json}"
1953 );
1954 assert!(
1956 json.contains(r#""value": "7""#),
1957 "a retired enum value must be a JSON string, got: {json}"
1958 );
1959 assert!(
1960 json.contains(r#""value": "33""#),
1961 "an enum-set bit position must be a JSON string, got: {json}"
1962 );
1963 }
1964
1965 #[test]
1966 fn fallible_transport_identity_follows_the_derivation_rule() {
1967 let fallible = v2::FallibleType {
1970 ok: "FaultPage".to_string(),
1971 err: "DiagError".to_string(),
1972 };
1973 assert_eq!(
1974 v2::fallible_transport_identity("VehicleStatus", 9, &fallible),
1975 "VehicleStatus#9:FaultPage|DiagError"
1976 );
1977
1978 let package = fixture();
1980 let interface = &package.interfaces[0];
1981 let query_decl = &interface.interactions[4];
1982 let Some(v2::decl::Kind::QueryDef(query)) = &query_decl.kind else {
1983 panic!("ordinal 5 must be the fallible query");
1984 };
1985 let Some(v2::return_type::Kind::Fallible(arms)) = &query.return_type.as_ref().unwrap().kind
1986 else {
1987 panic!("fetchFaults must return a fallible type");
1988 };
1989 assert_eq!(
1990 v2::fallible_transport_identity(&interface.name, query_decl.ordinal, arms),
1991 "VehicleStatus#5:FaultPage|DiagError"
1992 );
1993 }
1994
1995 #[test]
2002 fn shapes_walks_named_interfaces_and_inline_service_shapes() {
2003 let package = fixture();
2004 let walk: Vec<(&str, bool, usize)> = package
2005 .shapes()
2006 .map(|shape| {
2007 (
2008 shape.name,
2009 shape.is_inline(),
2010 shape.interface.interactions.len(),
2011 )
2012 })
2013 .collect();
2014 assert_eq!(
2015 walk,
2016 [("VehicleStatus", false, 6), ("veh.adas.logs", true, 1)],
2017 "the named interface, then the inline shape under the service's \
2018 dotted name",
2019 );
2020
2021 assert_eq!(package.services.len(), 2, "one reference form, one inline");
2025 assert!(
2026 !package
2027 .shapes()
2028 .any(|shape| shape.name == "veh.adas.status"),
2029 "a service naming an interface contributes no shape of its own",
2030 );
2031 }
2032
2033 #[test]
2034 fn relative_name_strips_the_unit_prefix() {
2035 assert_eq!(v2::relative_name("u", "u", "Session"), "Session");
2036 assert_eq!(
2037 v2::relative_name("u", "u.cluster", "Speed"),
2038 "cluster.Speed"
2039 );
2040 assert_eq!(
2041 v2::relative_name("com.example.hmi", "com.example.hmi.cluster.front", "A"),
2042 "cluster.front.A"
2043 );
2044 }
2045
2046 #[test]
2049 fn relative_name_keeps_the_full_name_of_a_package_outside_the_unit() {
2050 assert_eq!(
2051 v2::relative_name("u", "w.cluster", "Speed"),
2052 "w.cluster.Speed"
2053 );
2054 assert_eq!(v2::relative_name("u", "ux", "A"), "ux.A");
2055 assert_eq!(v2::relative_name("u.cluster", "u", "Session"), "u.Session");
2056 }
2057
2058 #[test]
2062 fn unit_retired_reads_a_package_named_twice_once() {
2063 let first = v2::Package {
2064 name: "u.cluster".to_string(),
2065 unit: "u".to_string(),
2066 retired: vec![v2::RetiredInterface {
2067 name: "cluster.Old".to_string(),
2068 number: 3,
2069 }],
2070 ..Default::default()
2071 };
2072 let second = v2::Package {
2073 retired: vec![v2::RetiredInterface {
2074 name: "cluster.Older".to_string(),
2075 number: 4,
2076 }],
2077 ..first.clone()
2078 };
2079 assert_eq!(
2080 v2::unit_retired("u", &[&first, &second]),
2081 first.retired,
2082 "the first package of a name is the one read"
2083 );
2084 }
2085
2086 #[test]
2087 fn an_inline_shape_keeps_its_global_name() {
2088 let mut package = fixture();
2089 package.name = "u.cluster".to_string();
2090 package.unit = "u".to_string();
2091 let inline = package
2092 .shapes()
2093 .find(|shape| shape.is_inline())
2094 .expect("the fixture has an inline shape");
2095 assert_eq!(package.catalog_name(&inline), "veh.adas.logs");
2096 let named = package
2097 .shapes()
2098 .find(|shape| !shape.is_inline())
2099 .expect("the fixture has a declared interface");
2100 assert_eq!(package.catalog_name(&named), "cluster.VehicleStatus");
2101 }
2102
2103 #[test]
2104 fn catalog_name_treats_an_empty_unit_as_the_package_name() {
2105 let mut package = fixture();
2106 package.unit = String::new();
2107 let named = package
2108 .shapes()
2109 .find(|shape| !shape.is_inline())
2110 .expect("the fixture has a declared interface");
2111 assert_eq!(package.catalog_name(&named), "VehicleStatus");
2112 }
2113
2114 #[test]
2115 fn unit_of_falls_back_to_the_name() {
2116 let bare = v2::Package {
2117 name: "p".to_string(),
2118 ..Default::default()
2119 };
2120 assert_eq!(v2::unit_of(&bare), "p");
2121 let member = v2::Package {
2122 name: "u.a".to_string(),
2123 unit: "u".to_string(),
2124 ..Default::default()
2125 };
2126 assert_eq!(v2::unit_of(&member), "u");
2127 }
2128
2129 #[test]
2130 fn packages_of_unit_selects_by_unit_of() {
2131 let package = |name: &str, unit: &str| v2::Package {
2132 name: name.to_string(),
2133 unit: unit.to_string(),
2134 ..Default::default()
2135 };
2136 let packages = [
2137 package("u", "u"),
2138 package("u.a", "u"),
2139 package("w", ""),
2140 package("other.b", "other"),
2141 ];
2142 let names: Vec<&str> = v2::packages_of_unit("u", &packages)
2143 .map(|p| p.name.as_str())
2144 .collect();
2145 assert_eq!(names, ["u", "u.a"]);
2146 let legacy: Vec<&str> = v2::packages_of_unit("w", &packages)
2147 .map(|p| p.name.as_str())
2148 .collect();
2149 assert_eq!(legacy, ["w"]);
2150 }
2151
2152 #[test]
2157 fn shape_visibility_reads_the_owning_service_for_an_inline_shape() {
2158 let package = fixture();
2159 let shapes: Vec<v2::InterfaceShape<'_>> = package.shapes().collect();
2160
2161 let named = shapes[0];
2162 assert!(named.service.is_none());
2163 assert_eq!(named.visibility(), v2::Visibility::Public as i32);
2164 assert_eq!(named.visibility(), named.interface.visibility);
2165
2166 let inline = shapes[1];
2167 assert_eq!(
2168 inline.interface.visibility,
2169 v2::Visibility::Unspecified as i32,
2170 "the trap: an inline shape's own visibility field is unset",
2171 );
2172 assert_eq!(
2173 inline.service.expect("an inline shape has an owner").name,
2174 "veh.adas.logs",
2175 );
2176 assert_eq!(
2177 inline.visibility(),
2178 v2::Visibility::Public as i32,
2179 "the accessor reads the owning service's, never the unset field",
2180 );
2181 }
2182
2183 #[test]
2187 fn shapes_is_empty_only_when_the_package_declares_no_shape() {
2188 let mut package = fixture();
2189 package.services.clear();
2190 assert_eq!(package.shapes().count(), 1);
2191
2192 package.interfaces.clear();
2193 assert_eq!(package.shapes().count(), 0);
2194 }
2195
2196 #[test]
2203 fn referenced_packages_finds_qualifiers_at_depth() {
2204 fn named(reference: &str) -> v2::FieldType {
2205 v2::FieldType {
2206 kind: Some(v2::field_type::Kind::Named(reference.to_string())),
2207 ..Default::default()
2208 }
2209 }
2210
2211 fn fixed(payload: v2::FieldType) -> v2::decl::Kind {
2212 v2::decl::Kind::FixedDef(v2::FixedDef {
2213 payload: Some(payload),
2214 })
2215 }
2216
2217 let package = v2::Package {
2218 name: "veh.cluster".to_string(),
2219 decls: vec![
2220 v2::Decl {
2221 name: "Local".to_string(),
2222 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2223 payload: "Speed".to_string(),
2224 ..Default::default()
2225 })),
2226 ..Default::default()
2227 },
2228 v2::Decl {
2229 name: "Stamped".to_string(),
2230 kind: Some(v2::decl::Kind::SignalDef(v2::SignalDef {
2231 payload: "ridl.std.Timestamp".to_string(),
2232 ..Default::default()
2233 })),
2234 ..Default::default()
2235 },
2236 v2::Decl {
2237 name: "ArrLabels".to_string(),
2238 kind: Some(fixed(v2::FieldType {
2239 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2240 element: Some(Box::new(named("veh.arr.Label"))),
2241 min: 0,
2242 max: 32,
2243 }))),
2244 ..Default::default()
2245 })),
2246 ..Default::default()
2247 },
2248 v2::Decl {
2249 name: "TupThing".to_string(),
2250 kind: Some(fixed(v2::FieldType {
2251 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2252 fields: vec![v2::TupleField {
2253 name: "x".to_string(),
2254 r#type: Some(named("veh.tup.X")),
2255 }],
2256 })),
2257 ..Default::default()
2258 })),
2259 ..Default::default()
2260 },
2261 v2::Decl {
2262 name: "MapThing".to_string(),
2263 kind: Some(fixed(v2::FieldType {
2264 kind: Some(v2::field_type::Kind::Map(Box::new(v2::MapType {
2265 key: Some(Box::new(named("veh.key.X"))),
2266 value: Some(Box::new(named("veh.val.X"))),
2267 min: 0,
2268 max: 8,
2269 }))),
2270 ..Default::default()
2271 })),
2272 ..Default::default()
2273 },
2274 v2::Decl {
2275 name: "StreamThing".to_string(),
2276 kind: Some(fixed(v2::FieldType {
2277 kind: Some(v2::field_type::Kind::Stream(v2::StreamType {
2278 element: Some(v2::stream_type::Element::Named(
2279 "veh.strm.X".to_string(),
2280 )),
2281 })),
2282 ..Default::default()
2283 })),
2284 ..Default::default()
2285 },
2286 ],
2287 ..Default::default()
2288 };
2289
2290 let found = v2::referenced_packages(&package);
2291 let expected: std::collections::BTreeSet<String> = [
2292 "ridl.std", "veh.arr", "veh.tup", "veh.key", "veh.val", "veh.strm",
2293 ]
2294 .into_iter()
2295 .map(str::to_string)
2296 .collect();
2297 assert_eq!(
2298 found, expected,
2299 "each recursive path must contribute its own distinct qualifier"
2300 );
2301 assert!(
2302 !found.contains("Speed") && !found.contains("veh.cluster"),
2303 "a bare reference contributes no package: {found:?}",
2304 );
2305 }
2306
2307 #[test]
2310 fn referenced_packages_is_empty_without_references() {
2311 let package = v2::Package {
2312 name: "veh.solo".to_string(),
2313 ..Default::default()
2314 };
2315 assert!(v2::referenced_packages(&package).is_empty());
2316 }
2317
2318 const NESTING_BELOW_LIMIT: usize = 45;
2324 const NESTING_PAST_LIMIT: usize = 60;
2328
2329 fn nested_package(depth: usize) -> v2::Package {
2334 let mut payload = v2::FieldType {
2335 optional: false,
2336 kind: Some(v2::field_type::Kind::Primitive(
2337 v2::PrimitiveType::Integer as i32,
2338 )),
2339 };
2340 for _ in 0..depth {
2341 payload = v2::FieldType {
2342 optional: false,
2343 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2344 element: Some(Box::new(payload)),
2345 min: 1,
2346 max: 1,
2347 }))),
2348 };
2349 }
2350 v2::Package {
2351 name: "veh.deep".to_string(),
2352 decls: vec![v2::Decl {
2353 name: "deep".to_string(),
2354 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2355 payload: Some(payload),
2356 })),
2357 ..Default::default()
2358 }],
2359 ..Default::default()
2360 }
2361 }
2362
2363 fn package_at_message_depth(levels: usize) -> v2::Package {
2375 assert!(levels >= 3, "the chain costs three levels before nesting");
2376 let (arrays, tuple) = if levels % 2 == 1 {
2377 ((levels - 3) / 2, false)
2378 } else {
2379 assert!(levels >= 6, "one tuple level costs three");
2380 ((levels - 6) / 2, true)
2381 };
2382
2383 let mut payload = v2::FieldType {
2384 optional: false,
2385 kind: Some(v2::field_type::Kind::Primitive(
2386 v2::PrimitiveType::Integer as i32,
2387 )),
2388 };
2389 for _ in 0..arrays {
2390 payload = v2::FieldType {
2391 optional: false,
2392 kind: Some(v2::field_type::Kind::Array(Box::new(v2::ArrayType {
2393 element: Some(Box::new(payload)),
2394 min: 1,
2395 max: 1,
2396 }))),
2397 };
2398 }
2399 if tuple {
2400 payload = v2::FieldType {
2401 optional: false,
2402 kind: Some(v2::field_type::Kind::Tuple(v2::TupleType {
2403 fields: vec![v2::TupleField {
2404 name: "f0".to_string(),
2405 r#type: Some(payload),
2406 }],
2407 })),
2408 };
2409 }
2410 v2::Package {
2411 name: "veh.deep".to_string(),
2412 decls: vec![v2::Decl {
2413 name: "deep".to_string(),
2414 kind: Some(v2::decl::Kind::FixedDef(v2::FixedDef {
2415 payload: Some(payload),
2416 })),
2417 ..Default::default()
2418 }],
2419 ..Default::default()
2420 }
2421 }
2422
2423 fn message_nesting(json: &str) -> usize {
2430 let (mut depth, mut max) = (0usize, 0usize);
2431 let (mut in_string, mut escaped) = (false, false);
2432 for byte in json.bytes() {
2433 if in_string {
2434 if escaped {
2435 escaped = false;
2436 } else if byte == b'\\' {
2437 escaped = true;
2438 } else if byte == b'"' {
2439 in_string = false;
2440 }
2441 continue;
2442 }
2443 match byte {
2444 b'"' => in_string = true,
2445 b'{' => {
2446 depth += 1;
2447 max = max.max(depth);
2448 }
2449 b'}' => depth = depth.saturating_sub(1),
2450 _ => {}
2451 }
2452 }
2453 max
2454 }
2455
2456 #[test]
2460 fn package_at_message_depth_builds_the_depth_it_names() {
2461 with_sized_stack(|| {
2462 for levels in [3, 6, 7, 99, 100, 101] {
2463 let json = v2::to_json_pretty(&package_at_message_depth(levels))
2464 .expect("the writer is unrestricted at these depths");
2465 assert_eq!(
2466 message_nesting(&json) - 1,
2467 levels,
2468 "the chain must nest {levels} message levels below the root"
2469 );
2470 }
2471 });
2472 }
2473
2474 #[test]
2483 fn binary_round_trip_at_100_message_levels_succeeds() {
2484 let package = package_at_message_depth(100);
2485 let bytes = v2::to_binary(&package);
2486 let decoded = v2::from_binary(&bytes).expect("100 message levels decode");
2487 assert_eq!(package, decoded);
2488 }
2489
2490 #[test]
2495 fn binary_decode_at_101_message_levels_returns_an_error() {
2496 let package = package_at_message_depth(101);
2497 let bytes = v2::to_binary(&package);
2498 let error = v2::from_binary(&bytes).expect_err("101 message levels must fail, not panic");
2499 assert!(
2500 error.to_string().contains("recursion limit reached"),
2501 "the error must name the limit, got: {error}"
2502 );
2503
2504 let json = v2::to_json_pretty(&package).expect("the canonical encoding has no such bound");
2505 assert_eq!(
2506 package,
2507 v2::from_json(&json).expect("the canonical encoding round-trips the same package")
2508 );
2509 }
2510
2511 #[test]
2520 fn json_round_trip_at_400_nested_levels_succeeds() {
2521 with_sized_stack(|| {
2522 let package = nested_package(400);
2523 let json = v2::to_json_pretty(&package).expect("the writer has no message-level limit");
2524 let decoded = v2::from_json(&json).expect("the reader parses within its ceiling");
2525 assert_eq!(package, decoded);
2526 });
2527 }
2528
2529 #[test]
2537 fn json_serialization_of_an_out_of_schema_discriminant_returns_an_error() {
2538 let mut package = fixture();
2539 package.decls[0].visibility = 999;
2540 let err = v2::to_json_pretty(&package)
2541 .expect_err("an out-of-schema discriminant must fail, not panic");
2542 let message = err.to_string();
2543 assert!(
2544 message.contains("canonical protobuf JSON"),
2545 "the error must name the encoding that failed, got: {message}"
2546 );
2547 assert!(
2548 message.contains("discriminant outside the schema"),
2549 "the error must name the known cause, got: {message}"
2550 );
2551 }
2552
2553 #[test]
2558 fn json_parse_rejects_an_unknown_field() {
2559 let error = v2::from_json(r#"{"name": "veh.deep", "notAField": 1}"#)
2560 .expect_err("an unknown field must be rejected");
2561 assert!(
2562 error.to_string().contains("unknown field"),
2563 "the error must name the defect, got: {error}"
2564 );
2565 }
2566
2567 #[test]
2576 fn json_parse_rejects_an_out_of_range_numeric_enum_value() {
2577 let with_visibility =
2578 |value: &str| format!(r#"{{"name": "veh.x", "decls": [{{"visibility": {value}}}]}}"#);
2579 v2::from_json(&with_visibility("1"))
2580 .expect("an in-range numeric enum value parses, as the mapping expects");
2581 let error = v2::from_json(&with_visibility("77"))
2582 .expect_err("an out-of-range numeric enum value must be rejected");
2583 assert!(
2584 error.to_string().contains("invalid value: integer `77`"),
2585 "the error must name the value, got: {error}"
2586 );
2587 }
2588
2589 #[test]
2595 fn json_parse_rejects_a_float_form_integer() {
2596 for spelling in ["1.0", "1e0"] {
2597 let error = v2::from_json(&format!(
2598 r#"{{"name": "veh.x", "decls": [{{"fixedDef": {{"payload": {{"array": {{"min": {spelling}}}}}}}}}]}}"#,
2599 ))
2600 .expect_err("a float-form integer must be rejected");
2601 assert!(
2602 error.to_string().contains("did not match any variant"),
2603 "the integer field's deserializer must be the one refusing `{spelling}`, \
2604 got: {error}"
2605 );
2606 }
2607 }
2608
2609 #[test]
2616 fn json_parse_rejects_null_for_a_repeated_field() {
2617 let error = v2::from_json(r#"{"name": "veh.x", "decls": null}"#)
2618 .expect_err("null for a repeated field must be rejected");
2619 assert!(
2620 error.to_string().contains("invalid type: null"),
2621 "the error must name the null, got: {error}"
2622 );
2623 v2::from_json(r#"{"name": "veh.x", "decls": [{"deprecated": null}]}"#)
2624 .expect("null for an optional scalar field still parses");
2625 }
2626
2627 #[test]
2631 fn json_parse_rejects_a_duplicate_key() {
2632 let error = v2::from_json(r#"{"name": "a", "name": "b"}"#)
2633 .expect_err("a duplicate key must be rejected");
2634 assert!(
2635 error.to_string().contains("duplicate field `name`"),
2636 "the error must name the duplicated field, got: {error}"
2637 );
2638 }
2639
2640 #[test]
2647 fn json_parse_past_the_nesting_ceiling_returns_an_error() {
2648 with_sized_stack(|| {
2649 let json = v2::to_json_pretty(&nested_package(500))
2650 .expect("the writer is unrestricted at this depth");
2651 let error = v2::from_json(&json).expect_err("the reader must refuse past its ceiling");
2652 assert!(
2653 error.to_string().contains("1000 JSON levels"),
2654 "the error must name the ceiling, got: {error}"
2655 );
2656 });
2657 }
2658
2659 #[test]
2664 fn json_nesting_ceiling_binds_exactly_at_1000() {
2665 let at = v2::from_json(&"[".repeat(1_000)).expect_err("an array is not a package");
2666 assert!(
2667 !at.to_string().contains("JSON levels"),
2668 "at the ceiling the parser, not the scan, must be the one refusing, got: {at}"
2669 );
2670
2671 let past = v2::from_json(&"[".repeat(1_001)).expect_err("past the ceiling, the scan");
2672 assert!(
2673 past.to_string().contains("1000 JSON levels"),
2674 "past the ceiling the error must name it, got: {past}"
2675 );
2676 }
2677
2678 #[test]
2683 fn nesting_scan_counts_brackets_outside_string_literals_only() {
2684 assert_eq!(v2::max_json_nesting(r#"{"a": [{"b": []}]}"#), 4);
2686 assert_eq!(v2::max_json_nesting(r#"{"doc": "{[[[{"}"#), 1);
2688 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\"[[[", "x": []}"#), 2);
2691 assert_eq!(v2::max_json_nesting(r#"{"doc": "a\\", "x": [[]]}"#), 3);
2694 assert_eq!(v2::max_json_nesting("]]]{"), 1);
2696 }
2697
2698 fn nested_text(depth: usize) -> String {
2702 let mut payload = "primitive: PRIMITIVE_TYPE_INTEGER".to_string();
2703 for _ in 0..depth {
2704 payload = format!("array {{ element {{ {payload} }} min: 1 max: 1 }}");
2705 }
2706 format!(
2707 r#"name: "veh.deep" decls {{ name: "deep" fixed_def {{ payload {{ {payload} }} }} }}"#
2708 )
2709 }
2710
2711 #[test]
2715 fn text_serialization_past_the_nesting_limit_returns_an_error() {
2716 let err = v2::to_text_format(&nested_package(NESTING_PAST_LIMIT))
2717 .expect_err("serialization past the recursion limit must fail, not panic");
2718 let message = err.to_string();
2719 assert!(
2720 message.contains("recursion limit"),
2721 "the error must name the nesting limit as the known cause, got: {message}"
2722 );
2723 assert!(
2724 message.contains("prototext"),
2725 "the error must name the encoding that failed, got: {message}"
2726 );
2727 }
2728
2729 fn with_sized_stack(test: impl FnOnce() + Send + 'static) {
2742 let outcome = std::thread::Builder::new()
2743 .stack_size(16 * 1024 * 1024)
2744 .spawn(test)
2745 .expect("spawn the large-stack test thread")
2746 .join();
2747 if let Err(payload) = outcome {
2748 std::panic::resume_unwind(payload);
2749 }
2750 }
2751
2752 #[test]
2756 fn text_parse_past_the_nesting_limit_returns_an_error() {
2757 with_sized_stack(|| {
2758 let error = v2::from_text_format(&nested_text(NESTING_PAST_LIMIT))
2759 .expect_err("parsing past the recursion limit must fail, not panic");
2760
2761 let message = error.to_string();
2765 assert!(
2766 message.contains("recursion limit reached"),
2767 "the transcode out of the dynamic message must be the failing \
2768 stage, got: {message}"
2769 );
2770 });
2771 }
2772
2773 #[test]
2776 fn text_round_trip_below_the_nesting_limit_succeeds() {
2777 with_sized_stack(|| {
2778 let package = nested_package(NESTING_BELOW_LIMIT);
2779 let text = v2::to_text_format(&package)
2780 .expect("below the recursion limit, serialization succeeds");
2781 let decoded =
2782 v2::from_text_format(&text).expect("below the recursion limit, parsing succeeds");
2783 assert_eq!(package, decoded);
2784 });
2785 }
2786}
2787
2788#[cfg(test)]
2789mod vacuous_constraint {
2790 use crate::v2;
2791
2792 fn constraint() -> v2::Constraint {
2795 v2::Constraint {
2796 min: None,
2797 max: None,
2798 step: None,
2799 len_min: None,
2800 len_max: None,
2801 pattern: None,
2802 pattern_const: None,
2803 }
2804 }
2805
2806 #[test]
2807 fn an_absent_or_empty_constraint_is_vacuous() {
2808 assert!(v2::constraint_is_vacuous(None));
2809 assert!(v2::constraint_is_vacuous(Some(&constraint())));
2810 }
2811
2812 #[test]
2813 fn a_step_constraint_is_non_vacuous() {
2814 let stepped = v2::Constraint {
2815 step: Some("0.5".to_string()),
2816 ..constraint()
2817 };
2818 assert!(!v2::constraint_is_vacuous(Some(&stepped)));
2819 }
2820
2821 #[test]
2828 fn any_single_constrained_field_is_non_vacuous() {
2829 let cases = [
2830 (
2831 "min",
2832 v2::Constraint {
2833 min: Some("0.0".to_string()),
2834 ..constraint()
2835 },
2836 ),
2837 (
2838 "max",
2839 v2::Constraint {
2840 max: Some("250.0".to_string()),
2841 ..constraint()
2842 },
2843 ),
2844 (
2845 "len_min",
2846 v2::Constraint {
2847 len_min: Some(1),
2848 ..constraint()
2849 },
2850 ),
2851 (
2852 "len_max",
2853 v2::Constraint {
2854 len_max: Some(256),
2855 ..constraint()
2856 },
2857 ),
2858 (
2859 "pattern",
2860 v2::Constraint {
2861 pattern: Some("^[a-z]+$".to_string()),
2862 ..constraint()
2863 },
2864 ),
2865 (
2866 "pattern_const",
2867 v2::Constraint {
2868 pattern_const: Some("NAME_PATTERN".to_string()),
2869 ..constraint()
2870 },
2871 ),
2872 ];
2873 for (field, case) in cases {
2874 assert!(
2875 !v2::constraint_is_vacuous(Some(&case)),
2876 "`{field}` alone must be non-vacuous"
2877 );
2878 }
2879 }
2880
2881 #[test]
2890 fn a_bound_pair_set_together_is_non_vacuous() {
2891 let ranged = v2::Constraint {
2892 min: Some("0.0".to_string()),
2893 max: Some("250.0".to_string()),
2894 ..constraint()
2895 };
2896 assert!(!v2::constraint_is_vacuous(Some(&ranged)));
2897
2898 let default_length = v2::Constraint {
2899 len_min: Some(0),
2900 len_max: Some(256),
2901 ..constraint()
2902 };
2903 assert!(!v2::constraint_is_vacuous(Some(&default_length)));
2904 }
2905}
2906
2907#[cfg(test)]
2908mod system_round_trip {
2909 use crate::v2;
2910
2911 fn attribute(namespace: &str, key: &str, value: Option<v2::AttributeValue>) -> v2::Attribute {
2912 v2::Attribute {
2913 namespace: namespace.to_string(),
2914 key: key.to_string(),
2915 value,
2916 }
2917 }
2918
2919 fn scalar(text: &str) -> v2::AttributeValue {
2920 v2::AttributeValue {
2921 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
2922 }
2923 }
2924
2925 fn list(items: Vec<v2::AttributeValue>) -> v2::AttributeValue {
2926 v2::AttributeValue {
2927 kind: Some(v2::attribute_value::Kind::List(v2::AttributeList { items })),
2928 }
2929 }
2930
2931 fn interface(catalog: &str, name: &str, inline: bool) -> Option<v2::InterfaceRef> {
2932 Some(v2::InterfaceRef {
2933 catalog: catalog.to_string(),
2934 name: name.to_string(),
2935 inline,
2936 })
2937 }
2938
2939 fn endpoint(component: &str, instance: &str, machine: &str) -> v2::Endpoint {
2940 v2::Endpoint {
2941 component: component.to_string(),
2942 instance: instance.to_string(),
2943 machine: machine.to_string(),
2944 }
2945 }
2946
2947 fn doc_link() -> v2::DocLink {
2955 v2::DocLink {
2956 text: "Cruise".to_string(),
2957 offset: 16,
2958 len: 6,
2959 target: "veh.topology.Cruise".to_string(),
2960 }
2961 }
2962
2963 fn fixture() -> v2::System {
2964 let link = v2::Link {
2965 interface: interface("veh.diag", "veh.diag.access", true),
2966 service: "veh.diag.access".to_string(),
2967 consumer: Some(endpoint("veh.topology.Backend", "Unit", "Cloud")),
2968 producer: Some(endpoint("veh.diag.access", "Unit", "AdasHpc")),
2969 crossing: v2::Crossing::OffBoard as i32,
2970 };
2971 v2::System {
2972 name: "Vehicle".to_string(),
2973 package: "veh.topology".to_string(),
2974 labels: vec!["ASIL_B".to_string()],
2975 attributes: vec![attribute("rust", "crate", Some(scalar("\"vehicle\"")))],
2976 members: vec![
2977 v2::MemberLine {
2978 component: "veh.topology.Cruise".to_string(),
2979 attributes: vec![attribute("linux", "pinned", None)],
2980 doc: "Documented, see [Cruise].".to_string(),
2981 links: vec![doc_link()],
2982 see: vec![doc_link()],
2983 since: vec!["1.2".to_string()],
2984 },
2985 v2::MemberLine {
2986 component: "veh.diag.access".to_string(),
2987 attributes: vec![],
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 ],
2994 components: vec![
2995 v2::Component {
2996 name: "Cruise".to_string(),
2997 package: "veh.topology".to_string(),
2998 implicit: false,
2999 external: true,
3000 instances: vec!["primary".to_string(), "backup".to_string()],
3001 offers: vec![v2::Offer {
3002 service: "veh.adas.cruise".to_string(),
3003 attributes: vec![attribute("someip", "serviceId", Some(scalar("4097")))],
3004 doc: "Documented, see [Cruise].".to_string(),
3005 links: vec![doc_link()],
3006 see: vec![doc_link()],
3007 since: vec!["1.2".to_string()],
3008 }],
3009 requires: vec![v2::Require {
3010 interface: interface("veh.adas", "LaneAssist", false),
3011 service: "veh.adas.lane".to_string(),
3012 producer: "veh.topology.Lane".to_string(),
3013 attributes: vec![attribute(
3014 "linux",
3015 "cpuset",
3016 Some(list(vec![scalar("2"), list(vec![scalar("3")])])),
3017 )],
3018 doc: "Documented, see [Cruise].".to_string(),
3019 links: vec![doc_link()],
3020 see: vec![doc_link()],
3021 since: vec!["1.2".to_string()],
3022 }],
3023 labels: vec!["ASIL_B".to_string()],
3024 attributes: vec![attribute("rust", "crate", None)],
3025 doc: "Documented, see [Cruise].".to_string(),
3026 links: vec![doc_link()],
3027 see: vec![doc_link()],
3028 since: vec!["1.2".to_string()],
3029 },
3030 v2::Component {
3031 name: "veh.diag.access".to_string(),
3032 package: String::new(),
3033 implicit: true,
3034 external: false,
3035 instances: vec!["Unit".to_string()],
3036 offers: vec![v2::Offer {
3037 service: "veh.diag.access".to_string(),
3038 attributes: vec![],
3039 doc: "Documented, see [Cruise].".to_string(),
3040 links: vec![doc_link()],
3041 see: vec![doc_link()],
3042 since: vec!["1.2".to_string()],
3043 }],
3044 requires: vec![],
3045 labels: vec![],
3046 attributes: vec![],
3047 doc: "Documented, see [Cruise].".to_string(),
3048 links: vec![doc_link()],
3049 see: vec![doc_link()],
3050 since: vec!["1.2".to_string()],
3051 },
3052 ],
3053 producers: vec![v2::Producer {
3054 service: "veh.adas.cruise".to_string(),
3055 component: "veh.topology.Cruise".to_string(),
3056 instances: vec!["primary".to_string(), "backup".to_string()],
3057 not_yet_realizable: true,
3058 }],
3059 grants: vec![v2::Grant {
3060 component: "veh.topology.Backend".to_string(),
3061 external: true,
3062 regions: vec!["veh.adas".to_string(), "veh.diag".to_string()],
3063 }],
3064 regions: vec![v2::Region {
3065 catalog: "veh.diag".to_string(),
3066 hash: vec![0xab; 32],
3067 interfaces: vec![v2::RegionInterface {
3068 name: "veh.diag.access".to_string(),
3069 inline: true,
3070 number: 2,
3071 provisional: true,
3072 service: "veh.diag.access".to_string(),
3073 }],
3074 }],
3075 distributions: vec![v2::Distribution {
3076 name: "Adas".to_string(),
3077 package: "veh.topology".to_string(),
3078 members: vec![v2::MemberLine {
3079 component: "veh.topology.Cruise".to_string(),
3080 attributes: vec![],
3081 doc: "Documented, see [Cruise].".to_string(),
3082 links: vec![doc_link()],
3083 see: vec![doc_link()],
3084 since: vec!["1.2".to_string()],
3085 }],
3086 depends_on: vec!["veh.topology.Base".to_string()],
3087 labels: vec!["PLATFORM_BUNDLE".to_string()],
3088 attributes: vec![attribute("deb", "section", Some(scalar("net")))],
3089 doc: "Documented, see [Cruise].".to_string(),
3090 links: vec![doc_link()],
3091 see: vec![doc_link()],
3092 since: vec!["1.2".to_string()],
3093 }],
3094 deployments: vec![v2::Deployment {
3095 name: "Production".to_string(),
3096 package: "veh.topology".to_string(),
3097 labels: vec!["FLEET".to_string()],
3098 attributes: vec![attribute("ota", "channel", Some(scalar("stable")))],
3099 machines: vec![v2::Machine {
3100 name: "Cloud".to_string(),
3101 external: true,
3102 labels: vec!["OFF_BOARD".to_string()],
3103 attributes: vec![attribute("net", "zone", Some(scalar("wan")))],
3104 doc: "Documented, see [Cruise].".to_string(),
3105 links: vec![doc_link()],
3106 see: vec![doc_link()],
3107 since: vec!["1.2".to_string()],
3108 }],
3109 placements: vec![v2::Placement {
3110 component: "veh.topology.Cruise".to_string(),
3111 instance: "backup".to_string(),
3112 machine: "Cockpit".to_string(),
3113 attributes: vec![attribute("linux", "cpuset", Some(list(vec![])))],
3114 sizing: Some(v2::Sizing {
3115 depth: Some(4_294_967_295),
3116 slots: Some(65_536),
3117 budget: Some(18_446_744_073_709_551_615),
3118 }),
3119 }],
3120 links: vec![link.clone()],
3121 routes: vec![v2::Route {
3122 catalog: "veh.adas".to_string(),
3123 interface_number: 2,
3124 member_ordinal: 1,
3125 interface: "LaneAssist".to_string(),
3126 member: "active".to_string(),
3127 service: "veh.adas.lane".to_string(),
3128 producers: vec![endpoint("veh.topology.Lane", "Unit", "AdasHpc")],
3129 }],
3130 surface: vec![v2::Surface {
3131 link: Some(link),
3132 direction: v2::SurfaceDirection::ExternalConsumes as i32,
3133 }],
3134 installations: vec![v2::Installation {
3135 distribution: "veh.topology.Adas".to_string(),
3136 machines: vec!["AdasHpc".to_string(), "Cockpit".to_string()],
3137 }],
3138 doc: "Documented, see [Cruise].".to_string(),
3139 doc_links: vec![doc_link()],
3140 see: vec![doc_link()],
3141 since: vec!["1.2".to_string()],
3142 sizing: Some(v2::Sizing {
3143 depth: Some(3),
3144 slots: Some(8),
3145 budget: Some(4096),
3146 }),
3147 }],
3148 doc: "Documented, see [Cruise].".to_string(),
3149 see: vec![doc_link()],
3150 since: vec!["1.2".to_string()],
3151 links: vec![doc_link()],
3152 }
3153 }
3154
3155 #[test]
3156 fn system_binary_round_trip_preserves_system() {
3157 let system = fixture();
3158 let decoded = v2::system_from_binary(v2::system_to_binary(&system).as_slice())
3159 .expect("decode must succeed");
3160 assert_eq!(system, decoded);
3161 }
3162
3163 #[test]
3168 fn system_json_round_trip_preserves_system() {
3169 let system = fixture();
3170 let json = v2::system_to_json_pretty(&system).expect("the fixture serializes as JSON");
3171 assert!(
3172 json.contains("\"hash\": \"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\""),
3173 "bytes render as base64, got:\n{json}"
3174 );
3175 assert!(
3176 json.contains("\"crossing\": \"CROSSING_OFF_BOARD\""),
3177 "enums render by name, got:\n{json}"
3178 );
3179 assert!(
3180 json.contains("\"notYetRealizable\": true"),
3181 "fields render in lowerCamelCase, got:\n{json}"
3182 );
3183 assert!(
3184 json.contains("\"budget\": \"18446744073709551615\""),
3185 "a 64-bit value renders as a string, got:\n{json}"
3186 );
3187 assert_eq!(
3188 system,
3189 v2::system_from_json(&json).expect("the JSON parses")
3190 );
3191 assert!(
3192 v2::system_from_json(&json.replacen("\"name\"", "\"nam\"", 1)).is_err(),
3193 "an unknown field is rejected"
3194 );
3195 }
3196
3197 #[test]
3201 fn an_absent_sizing_and_an_empty_one_stay_distinct_through_the_encodings() {
3202 let mut absent = fixture();
3203 absent.deployments[0].sizing = None;
3204 absent.deployments[0].placements[0].sizing = None;
3205 let mut empty = absent.clone();
3206 empty.deployments[0].sizing = Some(v2::Sizing::default());
3207 empty.deployments[0].placements[0].sizing = Some(v2::Sizing::default());
3208 for system in [&absent, &empty] {
3209 let binary = v2::system_from_binary(v2::system_to_binary(system).as_slice())
3210 .expect("decode must succeed");
3211 assert_eq!(system, &binary);
3212 let json = v2::system_to_json_pretty(system).expect("serializes as JSON");
3213 assert_eq!(
3214 system,
3215 &v2::system_from_json(&json).expect("the JSON parses")
3216 );
3217 }
3218 assert_ne!(
3219 v2::system_to_binary(&absent),
3220 v2::system_to_binary(&empty),
3221 "the binary encodings differ"
3222 );
3223 }
3224
3225 #[test]
3226 fn system_text_format_round_trip_preserves_system() {
3227 let system = fixture();
3228 let text = v2::system_to_text_format(&system).expect("the fixture serializes as prototext");
3229 assert!(
3230 text.starts_with("name:"),
3231 "fields print in schema index order, got: {text}"
3232 );
3233 assert_eq!(
3234 system,
3235 v2::system_from_text_format(&text).expect("prototext parsing must succeed")
3236 );
3237 }
3238
3239 #[test]
3242 fn qualified_names_follow_the_one_derivation() {
3243 let system = fixture();
3244 assert_eq!(system.qualified_name(), "veh.topology.Vehicle");
3245 assert_eq!(system.components[0].qualified_name(), "veh.topology.Cruise");
3246 assert_eq!(system.components[1].qualified_name(), "veh.diag.access");
3247 assert_eq!(
3248 system.distributions[0].qualified_name(),
3249 "veh.topology.Adas"
3250 );
3251 }
3252}
3253
3254#[cfg(test)]
3255mod load_ir_json {
3256 use crate::v2::{self, LoadError, MAX_JSON_NESTING, load_ir_json};
3257
3258 #[test]
3260 fn a_snapshot_file_loads() {
3261 let dir = tempfile::tempdir().unwrap();
3262 let package = v2::Package {
3263 name: "veh.cabin".to_string(),
3264 ..v2::Package::default()
3265 };
3266 let path = dir.path().join("cabin.ir.json");
3267 std::fs::write(&path, v2::to_json_pretty(&package).unwrap()).unwrap();
3268 assert_eq!(load_ir_json(&path).unwrap(), package);
3269 }
3270
3271 #[test]
3275 fn a_missing_file_is_io_and_bad_json_is_parse() {
3276 let dir = tempfile::tempdir().unwrap();
3277
3278 let missing = dir.path().join("missing.ir.json");
3279 let io = std::fs::read_to_string(&missing).unwrap_err();
3280 let error = load_ir_json(&missing).unwrap_err();
3281 assert!(matches!(error, LoadError::Io(_)), "{error:?}");
3282 assert_eq!(
3283 error.to_string(),
3284 format!("cannot read the IR snapshot: {io}")
3285 );
3286
3287 let bad = dir.path().join("bad.ir.json");
3290 std::fs::write(&bad, "\n{").unwrap();
3291 let parse = v2::from_json("\n{").unwrap_err();
3292 let error = load_ir_json(&bad).unwrap_err();
3293 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3294 assert_eq!(
3295 error.to_string(),
3296 format!("the IR snapshot is not valid IR v2 JSON: {parse}")
3297 );
3298 }
3299
3300 #[test]
3303 fn input_past_the_nesting_cap_draws_from_json_s_refusal() {
3304 let dir = tempfile::tempdir().unwrap();
3305 let deep = format!(
3306 "{}{}",
3307 "[".repeat(MAX_JSON_NESTING + 1),
3308 "]".repeat(MAX_JSON_NESTING + 1)
3309 );
3310 let path = dir.path().join("deep.ir.json");
3311 std::fs::write(&path, &deep).unwrap();
3312 let refusal = v2::from_json(&deep).unwrap_err().to_string();
3313 assert!(
3314 refusal.contains("the ceiling this reader enforces"),
3315 "{refusal}"
3316 );
3317 let error = load_ir_json(&path).unwrap_err();
3318 assert!(matches!(error, LoadError::Parse(_)), "{error:?}");
3319 assert_eq!(
3320 error.to_string(),
3321 format!("the IR snapshot is not valid IR v2 JSON: {refusal}")
3322 );
3323 }
3324}