1use std::borrow::Cow;
29use std::collections::BTreeMap;
30use std::error::Error;
31use std::fmt;
32
33use crate::CodecError;
34
35#[cfg(feature = "schema")]
36use schemars::JsonSchema;
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38
39use crate::text::NonBlankString;
40
41#[derive(Clone, PartialEq, Eq, Hash)]
52#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "String"))]
53pub struct DialectId {
54 value: Cow<'static, str>,
55 namespace_len: usize,
57}
58
59#[derive(Debug, Clone, Copy)]
64pub struct StaticDialectId {
65 value: &'static str,
66 namespace_len: usize,
67}
68
69impl StaticDialectId {
70 #[must_use]
72 pub const fn new(id: &'static str) -> Option<Self> {
73 match dialect_separator(id) {
74 Some(namespace_len) => Some(Self {
75 value: id,
76 namespace_len,
77 }),
78 None => None,
79 }
80 }
81}
82
83impl DialectId {
84 #[must_use]
86 pub const fn from_static(id: StaticDialectId) -> Self {
87 Self {
88 value: Cow::Borrowed(id.value),
89 namespace_len: id.namespace_len,
90 }
91 }
92
93 pub fn parse(id: impl Into<String>) -> Result<Self, DialectIdError> {
95 let id = id.into();
96 match dialect_separator(&id) {
97 Some(namespace_len) => Ok(Self {
98 value: Cow::Owned(id),
99 namespace_len,
100 }),
101 None => Err(DialectIdError(id)),
102 }
103 }
104
105 #[must_use]
107 pub const fn as_str(&self) -> &str {
108 match &self.value {
109 Cow::Borrowed(id) => id,
110 Cow::Owned(id) => id.as_str(),
111 }
112 }
113
114 #[must_use]
116 pub fn namespace(&self) -> &str {
117 self.parts().0
118 }
119
120 pub(crate) fn local(&self) -> &str {
121 self.parts().1
122 }
123
124 fn parts(&self) -> (&str, &str) {
125 let (namespace, qualified_name) = self.as_str().split_at(self.namespace_len);
126 (namespace, &qualified_name[1..])
127 }
128}
129
130#[macro_export]
136macro_rules! dialect_id {
137 ($literal:literal $(,)?) => {{
138 const STATIC_DIALECT_ID: $crate::dialect::StaticDialectId =
139 match $crate::dialect::StaticDialectId::new($literal) {
140 Some(id) => id,
141 None => panic!("invalid pinned dialect id"),
142 };
143 $crate::dialect::DialectId::from_static(STATIC_DIALECT_ID)
144 }};
145}
146
147const fn dialect_separator(id: &str) -> Option<usize> {
148 let bytes = id.as_bytes();
149 let mut index = 0;
150 let mut colon = None;
151 while index < bytes.len() {
152 if bytes[index] == b':' {
153 if colon.is_some() || index == 0 || index + 1 == bytes.len() {
154 return None;
155 }
156 colon = Some(index);
157 }
158 index += 1;
159 }
160 let Some(colon) = colon else {
161 return None;
162 };
163 index = 0;
164 while index < colon {
165 let byte = bytes[index];
166 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() {
167 return None;
168 }
169 index += 1;
170 }
171 if valid_grammar_bytes(bytes, colon + 1) {
172 Some(colon)
173 } else {
174 None
175 }
176}
177
178const fn valid_grammar_bytes(bytes: &[u8], start: usize) -> bool {
184 if start >= bytes.len() {
185 return false;
186 }
187 let mut index = start;
188 while index < bytes.len() {
189 let byte = bytes[index];
190 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && byte != b'-' && byte != b'.' {
191 return false;
192 }
193 index += 1;
194 }
195 bytes[start] != b'-' && bytes[bytes.len() - 1] != b'-'
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct DialectIdError(String);
201
202impl fmt::Display for DialectIdError {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 write!(
205 f,
206 "invalid dialect id {:?}: expected <format>:<name> in lowercase canonical form",
207 self.0
208 )
209 }
210}
211
212impl Error for DialectIdError {}
213
214impl fmt::Display for DialectId {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.write_str(self.as_str())
217 }
218}
219
220impl fmt::Debug for DialectId {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 write!(f, "DialectId({:?})", self.as_str())
223 }
224}
225
226impl Serialize for DialectId {
227 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
228 serializer.serialize_str(self.as_str())
229 }
230}
231
232impl<'de> Deserialize<'de> for DialectId {
233 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
234 Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246#[cfg_attr(feature = "schema", derive(JsonSchema))]
247#[serde(rename_all = "snake_case", deny_unknown_fields)]
248pub enum Admission {
249 Admitted,
251 Unverified {
255 using: Grammar,
257 },
258 Residual,
262 Refused,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278#[cfg_attr(feature = "schema", derive(JsonSchema))]
279#[serde(try_from = "String")]
280pub struct Grammar(String);
281
282impl Grammar {
283 #[must_use]
285 pub fn of(dialect: &DialectId) -> Self {
286 Self(dialect.local().to_owned())
287 }
288
289 pub fn parse(name: impl Into<String>) -> Result<Self, GrammarError> {
293 let name = name.into();
294 if valid_grammar_bytes(name.as_bytes(), 0) {
295 Ok(Self(name))
296 } else {
297 Err(GrammarError(name))
298 }
299 }
300
301 #[must_use]
303 pub fn as_str(&self) -> &str {
304 &self.0
305 }
306}
307
308impl TryFrom<String> for Grammar {
309 type Error = GrammarError;
310
311 fn try_from(name: String) -> Result<Self, Self::Error> {
312 Self::parse(name)
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct GrammarError(String);
319
320impl fmt::Display for GrammarError {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(
323 f,
324 "invalid grammar name {:?}: expected a format-local name in lowercase canonical form",
325 self.0
326 )
327 }
328}
329
330impl Error for GrammarError {}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338#[cfg_attr(feature = "schema", derive(JsonSchema))]
339#[serde(deny_unknown_fields)]
340pub struct DialectMatch {
341 dialect: DialectId,
343 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
349 #[serde(deserialize_with = "crate::distinct_keys::btree_map")]
350 declared: BTreeMap<NonBlankString, String>,
351 #[serde(
357 default,
358 skip_serializing_if = "Option::is_none",
359 deserialize_with = "deserialize_instance"
360 )]
361 instance: Option<String>,
362 admission: Admission,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum LayerInstance {
369 Sole,
371 Tagged,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
386#[cfg_attr(feature = "schema", derive(JsonSchema))]
387pub struct DialectLayers {
388 primary: DialectMatch,
389 extra: Vec<DialectMatch>,
390}
391
392#[derive(Deserialize)]
393#[serde(deny_unknown_fields)]
394struct DialectLayersWire {
395 primary: DialectMatch,
396 extra: Vec<DialectMatch>,
397}
398
399impl<'de> Deserialize<'de> for DialectLayers {
400 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
401 let wire = DialectLayersWire::deserialize(deserializer)?;
402 let mut layers = Self::of(wire.primary);
403 for layer in wire.extra {
404 layers.insert(layer).map_err(|rejected| {
405 <D::Error as serde::de::Error>::custom(format!(
406 "duplicate dialect layer key: {rejected:?}"
407 ))
408 })?;
409 }
410 Ok(layers)
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
416pub enum DialectLayerError {
417 Duplicate(DialectMatch),
419}
420
421impl fmt::Display for DialectLayerError {
422 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
423 match self {
424 Self::Duplicate(layer) => write!(formatter, "duplicate dialect layer key: {layer:?}"),
425 }
426 }
427}
428
429impl Error for DialectLayerError {}
430
431impl From<DialectLayerError> for CodecError {
432 fn from(error: DialectLayerError) -> Self {
433 match error {
434 DialectLayerError::Duplicate(layer) => {
435 Self::malformed(format_args!("duplicate dialect layer key: {layer:?}"))
436 }
437 }
438 }
439}
440
441impl DialectLayers {
442 #[must_use]
444 pub fn of(primary: DialectMatch) -> Self {
445 Self {
446 primary,
447 extra: Vec::new(),
448 }
449 }
450
451 pub fn insert(&mut self, layer: DialectMatch) -> Result<(), DialectLayerError> {
456 if Self::same_key(&self.primary, &layer)
457 || self
458 .extra
459 .iter()
460 .any(|existing| Self::same_key(existing, &layer))
461 {
462 return Err(DialectLayerError::Duplicate(layer));
463 }
464 self.extra.push(layer);
465 Ok(())
466 }
467
468 pub fn with(mut self, layer: DialectMatch) -> Result<Self, DialectLayerError> {
470 self.insert(layer)?;
471 Ok(self)
472 }
473
474 fn same_key(existing: &DialectMatch, layer: &DialectMatch) -> bool {
475 existing.format() == layer.format() && existing.instance == layer.instance
476 }
477
478 #[must_use]
480 pub fn primary(&self) -> &DialectMatch {
481 &self.primary
482 }
483
484 pub fn iter(&self) -> impl Iterator<Item = &DialectMatch> {
486 std::iter::once(&self.primary).chain(&self.extra)
487 }
488}
489
490mod format_identity_sealed {
491 pub trait Sealed {}
492}
493
494pub trait FormatIdentityPayload: format_identity_sealed::Sealed {
499 fn format(&self) -> &str;
501}
502
503impl format_identity_sealed::Sealed for DialectId {}
504
505impl FormatIdentityPayload for DialectId {
506 fn format(&self) -> &str {
507 self.namespace()
508 }
509}
510
511impl format_identity_sealed::Sealed for DialectMatch {}
512
513impl FormatIdentityPayload for DialectMatch {
514 fn format(&self) -> &str {
515 self.format()
516 }
517}
518
519impl format_identity_sealed::Sealed for DialectLayers {}
520
521impl FormatIdentityPayload for DialectLayers {
522 fn format(&self) -> &str {
523 self.primary().format()
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534#[cfg_attr(feature = "schema", derive(JsonSchema))]
535#[serde(tag = "classification", rename_all = "snake_case", deny_unknown_fields)]
536pub enum FormatIdentity<T> {
537 Classified {
539 dialects: T,
541 },
542 Unclassified {
544 format: String,
546 },
547}
548
549impl<T: FormatIdentityPayload> FormatIdentity<T> {
550 #[must_use]
552 pub const fn classified(payload: T) -> Self {
553 Self::Classified { dialects: payload }
554 }
555
556 #[must_use]
558 pub fn unclassified(format: impl Into<String>) -> Self {
559 Self::Unclassified {
560 format: format.into(),
561 }
562 }
563
564 #[must_use]
566 pub fn format(&self) -> &str {
567 match self {
568 Self::Classified { dialects } => dialects.format(),
569 Self::Unclassified { format } => format,
570 }
571 }
572
573 #[must_use]
575 pub const fn classified_payload(&self) -> Option<&T> {
576 match self {
577 Self::Classified { dialects } => Some(dialects),
578 Self::Unclassified { .. } => None,
579 }
580 }
581}
582
583impl DialectMatch {
584 fn with_admission(dialect: DialectId, admission: Admission) -> Self {
585 Self {
586 dialect,
587 declared: BTreeMap::new(),
588 instance: None,
589 admission,
590 }
591 }
592
593 #[must_use]
595 pub fn admitted(dialect: DialectId) -> Self {
596 Self::with_admission(dialect, Admission::Admitted)
597 }
598
599 #[must_use]
604 pub fn unverified(dialect: DialectId, using: Grammar) -> Self {
605 Self::with_admission(dialect, Admission::Unverified { using })
606 }
607
608 #[must_use]
610 pub fn residual(dialect: DialectId) -> Self {
611 Self::with_admission(dialect, Admission::Residual)
612 }
613
614 #[must_use]
616 pub fn refused(dialect: DialectId) -> Self {
617 Self::with_admission(dialect, Admission::Refused)
618 }
619
620 #[must_use]
622 pub fn with_declared(mut self, declared: BTreeMap<NonBlankString, String>) -> Self {
623 self.declared = declared;
624 self
625 }
626
627 #[must_use]
629 pub fn with_declared_entry(mut self, key: NonBlankString, value: &str) -> Self {
630 let _ = self.declared.insert(key, value.to_owned());
632 self
633 }
634
635 #[must_use]
637 pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
638 self.instance = Some(instance.into());
639 self
640 }
641
642 #[must_use]
644 pub fn format(&self) -> &str {
645 self.dialect.namespace()
646 }
647
648 #[must_use]
650 pub fn dialect(&self) -> &DialectId {
651 &self.dialect
652 }
653
654 #[must_use]
656 pub fn declared(&self) -> &BTreeMap<NonBlankString, String> {
657 &self.declared
658 }
659
660 #[must_use]
662 pub fn instance(&self) -> Option<&str> {
663 self.instance.as_deref()
664 }
665
666 #[must_use]
668 pub fn admission(&self) -> &Admission {
669 &self.admission
670 }
671
672 #[must_use]
675 pub fn using(&self) -> Option<DialectId> {
676 match &self.admission {
677 Admission::Unverified { using } => Some(self.grammar_id(using)),
678 Admission::Admitted | Admission::Residual | Admission::Refused => None,
679 }
680 }
681
682 #[must_use]
684 pub fn grammar_id(&self, grammar: &Grammar) -> DialectId {
685 let namespace = self.format();
686 DialectId {
687 value: Cow::Owned(format!("{namespace}:{}", grammar.as_str())),
688 namespace_len: namespace.len(),
689 }
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 #![allow(clippy::unwrap_used)]
696
697 use std::collections::BTreeMap;
698
699 use super::{Admission, DialectId, DialectLayers, DialectMatch, Grammar, StaticDialectId};
700
701 #[derive(serde::Deserialize)]
702 #[serde(deny_unknown_fields)]
703 struct DialectIdConformance {
704 valid: Vec<String>,
705 invalid: Vec<String>,
706 }
707
708 fn layer(format: &str) -> DialectMatch {
709 DialectMatch::admitted(DialectId::parse(format!("{format}:known")).unwrap())
710 }
711
712 #[test]
713 fn a_pinned_id_prints_and_serializes_as_the_plain_string() {
714 let id = crate::dialect_id!("rhino:archive-80");
715
716 assert_eq!(id.to_string(), "rhino:archive-80");
717 assert_eq!(id.as_str(), "rhino:archive-80");
718 assert_eq!(serde_json::to_string(&id).unwrap(), "\"rhino:archive-80\"");
719 assert_eq!(
720 serde_json::from_str::<DialectId>("\"rhino:archive-80\"").unwrap(),
721 id
722 );
723 }
724
725 #[test]
726 fn static_dialect_admission_returns_none_for_invalid_runtime_text() {
727 for text in [
728 "",
729 "rhino",
730 "rhino:",
731 ":known",
732 "Rhino:known",
733 "a:b:c",
734 "a:é",
735 ] {
736 assert!(StaticDialectId::new(text).is_none(), "{text:?}");
737 }
738 let admitted = DialectId::from_static(StaticDialectId::new("rhino:archive-80").unwrap());
739 assert_eq!(admitted.namespace(), "rhino");
740 assert_eq!(admitted.local(), "archive-80");
741 assert_eq!(admitted, DialectId::parse("rhino:archive-80").unwrap());
742 }
743
744 #[test]
745 fn dialect_id_deserialization_rejects_noncanonical_strings() {
746 for id in [
747 "rhino",
748 ":archive-80",
749 "rhino:",
750 "Rhino:archive-80",
751 "rhino:archive_80",
752 "rhino:-archive-80",
753 ] {
754 serde_json::from_value::<DialectId>(serde_json::json!(id))
755 .expect_err("a malformed dialect id must be rejected");
756 }
757 }
758
759 #[test]
760 fn dialect_id_matches_the_shared_conformance_corpus() {
761 let cases: DialectIdConformance =
762 toml::from_str(include_str!("../../../docs/dialect-id-conformance.toml")).unwrap();
763 for id in cases.valid {
764 let admitted =
765 DialectId::parse(id.clone()).unwrap_or_else(|_| panic!("valid dialect id {id:?}"));
766 assert_eq!(format!("{}:{}", admitted.namespace(), admitted.local()), id);
767 }
768 for id in cases.invalid {
769 assert!(
770 DialectId::parse(id.clone()).is_err(),
771 "invalid dialect id {id:?}"
772 );
773 }
774 }
775
776 #[test]
777 fn an_admitted_match_serializes_its_identity() {
778 let admitted = DialectMatch {
779 dialect: crate::dialect_id!("rhino:archive-80"),
780 declared: BTreeMap::new(),
781 instance: None,
782 admission: Admission::Admitted,
783 };
784
785 assert_eq!(
786 serde_json::to_string(&admitted).unwrap(),
787 "{\"dialect\":\"rhino:archive-80\",\"admission\":\"admitted\"}"
788 );
789 }
790
791 #[test]
792 fn a_dialect_match_wire_states_its_format_once_through_its_dialect() {
793 let restated = serde_json::json!({
794 "format": "rhino",
795 "dialect": "rhino:archive-80",
796 "admission": "admitted",
797 });
798
799 let error = serde_json::from_value::<DialectMatch>(restated)
800 .expect_err("the format is read from the dialect, so the wire carries no format key");
801 assert!(
802 error.to_string().contains("unknown field `format`"),
803 "{error}"
804 );
805 assert_eq!(
806 DialectMatch::admitted(crate::dialect_id!("rhino:archive-80")).format(),
807 "rhino"
808 );
809 }
810
811 #[test]
812 fn residual_constructor_records_the_absence_of_a_declared_grammar() {
813 let residual = DialectMatch::residual(crate::dialect_id!("rhino:unknown"));
814
815 assert_eq!(residual.admission(), &Admission::Residual);
816 assert_eq!(residual.using(), None);
817 assert_eq!(
818 serde_json::to_string(&residual).unwrap(),
819 "{\"dialect\":\"rhino:unknown\",\"admission\":\"residual\"}"
820 );
821 }
822
823 #[test]
824 fn an_unverified_admission_names_the_grammar_in_use_by_full_id() {
825 let unverified = DialectMatch::unverified(
826 crate::dialect_id!("acis:save-format-217"),
827 Grammar::of(&crate::dialect_id!("acis:save-format-218")),
828 );
829
830 assert_eq!(
831 unverified.admission(),
832 &Admission::Unverified {
833 using: Grammar::of(&crate::dialect_id!("acis:save-format-218")),
834 }
835 );
836 assert_eq!(
837 unverified.using(),
838 Some(crate::dialect_id!("acis:save-format-218"))
839 );
840 let serialized = serde_json::to_string(&unverified).unwrap();
841 assert_eq!(
842 serialized,
843 "{\"dialect\":\"acis:save-format-217\",\"admission\":{\"unverified\":{\"using\":\"save-format-218\"}}}"
844 );
845 assert_eq!(
846 serde_json::from_str::<DialectMatch>(&serialized).unwrap(),
847 unverified
848 );
849 }
850
851 #[test]
852 fn a_self_named_unverified_grammar_remains_opaque() {
853 let self_named = serde_json::json!({
854 "dialect": "rhino:unknown",
855 "admission": {
856 "unverified": { "using": "unknown" }
857 },
858 });
859 let matched = serde_json::from_value::<DialectMatch>(self_named)
860 .expect("core does not infer grammar semantics from an opaque dialect name");
861 assert_eq!(
862 matched.admission(),
863 &Admission::Unverified {
864 using: Grammar::of(&crate::dialect_id!("rhino:unknown")),
865 }
866 );
867 }
868
869 #[test]
870 fn a_grammar_name_has_no_spelling_for_a_foreign_namespace() {
871 let malformed = serde_json::json!({
872 "dialect": "rhino:unknown",
873 "admission": {
874 "unverified": { "using": "step:ap242-e3" }
875 },
876 });
877 let error = serde_json::from_value::<DialectMatch>(malformed)
878 .expect_err("a grammar substitute belongs to the classified format layer");
879 assert!(
880 error
881 .to_string()
882 .contains("invalid grammar name \"step:ap242-e3\""),
883 "{error}"
884 );
885 }
886
887 #[test]
888 fn grammar_parsing_rejects_a_name_outside_the_format_local_class() {
889 for name in [
890 "",
891 "rhino:archive-80",
892 "Archive-80",
893 "archive_80",
894 "-archive",
895 ] {
896 assert!(
897 Grammar::parse(name).is_err(),
898 "invalid grammar name {name:?}"
899 );
900 }
901 assert_eq!(
902 Grammar::parse("save-format-218").unwrap(),
903 Grammar::of(&crate::dialect_id!("acis:save-format-218"))
904 );
905 }
906
907 #[test]
908 fn identity_does_not_encode_whether_an_unverified_path_used_a_grammar() {
909 let dialect = crate::dialect_id!("rhino:archive-80");
910 let without_grammar = DialectMatch::residual(dialect.clone());
911 assert_eq!(without_grammar.admission(), &Admission::Residual);
912
913 let self_named = DialectMatch::unverified(dialect.clone(), Grammar::of(&dialect));
914 assert_eq!(
915 self_named.admission(),
916 &Admission::Unverified {
917 using: Grammar::of(&crate::dialect_id!("rhino:archive-80")),
918 }
919 );
920 assert_eq!(self_named.using(), Some(dialect));
921 }
922
923 #[test]
924 fn dialect_layers_accept_a_same_format_extra_with_an_instance() {
925 let member = layer("rhino").with_instance("components/member.3dm");
926 let layers = DialectLayers::of(layer("rhino"))
927 .with(member.clone())
928 .expect("distinct dialect layer keys");
929
930 let serialized = serde_json::to_value(&layers).unwrap();
931 assert_eq!(
932 serde_json::from_value::<DialectLayers>(serialized).unwrap(),
933 layers
934 );
935 assert_eq!(layers.extra, [member]);
936 }
937
938 #[test]
939 fn dialect_layers_insert_keeps_the_first_extra_layer_for_a_key() {
940 let first = layer("acis").with_instance("body");
941 let replacement =
942 DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
943 let mut layers = DialectLayers::of(layer("rhino"))
944 .with(first.clone())
945 .expect("distinct dialect layer keys");
946
947 assert_eq!(
948 layers.insert(replacement.clone()),
949 Err(super::DialectLayerError::Duplicate(replacement))
950 );
951 assert_eq!(layers.extra, [first]);
952 }
953
954 #[test]
955 fn dialect_layers_insert_keeps_the_primary_on_its_own_key() {
956 let replacement = DialectMatch::residual(crate::dialect_id!("rhino:other"));
957 let mut layers = DialectLayers::of(layer("rhino"))
958 .with(layer("acis"))
959 .expect("distinct dialect layer keys");
960
961 assert_eq!(
962 layers.insert(replacement.clone()),
963 Err(super::DialectLayerError::Duplicate(replacement))
964 );
965 assert_eq!(layers.primary(), &layer("rhino"));
966 assert_eq!(layers.extra, [layer("acis")]);
967 }
968
969 #[test]
970 fn dialect_layers_builder_returns_the_colliding_layer() {
971 let first = layer("acis").with_instance("body");
972 let replacement =
973 DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
974 let layers = DialectLayers::of(layer("rhino"))
975 .with(first)
976 .expect("distinct dialect layer keys");
977 assert_eq!(
978 layers.with(replacement.clone()),
979 Err(super::DialectLayerError::Duplicate(replacement))
980 );
981 }
982
983 #[test]
984 fn dialect_layers_keep_same_format_extras_with_distinct_instances() {
985 let anonymous = layer("acis");
986 let named = layer("acis").with_instance("body");
987 let mut layers = DialectLayers::of(layer("rhino"));
988
989 assert_eq!(layers.insert(anonymous.clone()), Ok(()));
990 assert_eq!(layers.insert(named.clone()), Ok(()));
991 assert_eq!(layers.extra, [anonymous, named]);
992 }
993
994 #[test]
995 fn dialect_layers_deserialization_rejects_a_repeated_key() {
996 let serialized = serde_json::json!({
997 "primary": layer("rhino"),
998 "extra": [layer("acis"), DialectMatch::residual(crate::dialect_id!("acis:other"))],
999 });
1000
1001 let error = serde_json::from_value::<DialectLayers>(serialized)
1002 .expect_err("a repeated extra layer key is a read error");
1003 assert!(
1004 error.to_string().contains("duplicate dialect layer key"),
1005 "{error}"
1006 );
1007 }
1008
1009 #[test]
1010 fn dialect_layers_deserialization_rejects_an_extra_on_the_primary_key() {
1011 let serialized = serde_json::json!({
1012 "primary": layer("rhino"),
1013 "extra": [DialectMatch::residual(crate::dialect_id!("rhino:other"))],
1014 });
1015
1016 let error = serde_json::from_value::<DialectLayers>(serialized)
1017 .expect_err("an extra layer on the primary key is a read error");
1018 assert!(
1019 error.to_string().contains("duplicate dialect layer key"),
1020 "{error}"
1021 );
1022 }
1023
1024 #[test]
1025 fn dialect_layers_serialize_with_an_explicit_primary() {
1026 let layers = DialectLayers::of(layer("rhino"))
1027 .with(layer("acis"))
1028 .expect("distinct dialect layer keys");
1029 let serialized = serde_json::to_value(&layers).unwrap();
1030
1031 assert_eq!(
1032 serialized,
1033 serde_json::json!({
1034 "primary": layer("rhino"),
1035 "extra": [layer("acis")],
1036 })
1037 );
1038 assert_eq!(
1039 serde_json::from_value::<DialectLayers>(serialized).unwrap(),
1040 layers
1041 );
1042 assert_eq!(
1043 layers.iter().map(DialectMatch::format).collect::<Vec<_>>(),
1044 ["rhino", "acis"]
1045 );
1046 }
1047}
1048
1049crate::named_optional_field!(deserialize_instance, String, "instance");