Skip to main content

rusty_alto/
codec.rs

1//! Typed input and output codecs.
2//!
3//! An [`InputCodec`] is keyed on the type it produces. [`InputCodecRegistry`]
4//! can therefore offer several formats for one semantic result, such as
5//! `.irtg` and `.tag` for [`Irtg`], without mixing in codecs for other types.
6//!
7//! An [`OutputCodec`] is keyed on the public value type. Public values are
8//! self-contained (no interner ids), so codecs need no
9//! [`Signature`](crate::Signature). An algebra owns one codec that produces
10//! its preferred [`VisualRepresentation`]; independent textual codecs are
11//! stored in an [`OutputCodecRegistry`] and are available to every algebra
12//! with the same public value type.
13//!
14//! Registry lookup is by exact Rust type. Listing codecs only returns their metadata and does not
15//! encode a value. A GUI can therefore evaluate a derivation on demand, call
16//! [`Algebra::visualize`](crate::Algebra::visualize) for display, list the matching textual codecs
17//! for a Copy menu, and invoke only the codec selected by the user.
18
19use crate::{
20    ExplicitWithSignature, FeatureStructure, Irtg, TagStringValue, TreeValue,
21    codecs::TulipacInputCodec,
22};
23use std::{
24    any::{Any, TypeId},
25    collections::HashMap,
26    error::Error,
27    fmt, fs,
28    io::{Cursor, Read},
29    path::Path,
30};
31
32/// Failure while selecting or invoking an input codec.
33#[derive(Debug)]
34pub enum InputCodecError {
35    /// Reading the input failed.
36    Io(std::io::Error),
37    /// Input bytes were not valid UTF-8.
38    Utf8(std::string::FromUtf8Error),
39    /// A concrete codec rejected the input.
40    Codec(Box<dyn Error + Send + Sync>),
41    /// A path has no usable filename extension.
42    MissingExtension,
43    /// No codec for the requested result type has this name or extension.
44    UnknownCodec(String),
45}
46
47impl InputCodecError {
48    /// Wrap a concrete codec error while retaining it as the error source.
49    pub fn codec(error: impl Error + Send + Sync + 'static) -> Self {
50        Self::Codec(Box::new(error))
51    }
52}
53
54impl fmt::Display for InputCodecError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Io(error) => write!(f, "failed to read codec input: {error}"),
58            Self::Utf8(error) => write!(f, "codec input is not valid UTF-8: {error}"),
59            Self::Codec(error) => error.fmt(f),
60            Self::MissingExtension => f.write_str("input path has no filename extension"),
61            Self::UnknownCodec(format) => write!(f, "no input codec registered for {format:?}"),
62        }
63    }
64}
65
66impl Error for InputCodecError {
67    fn source(&self) -> Option<&(dyn Error + 'static)> {
68        match self {
69            Self::Io(error) => Some(error),
70            Self::Utf8(error) => Some(error),
71            Self::Codec(error) => Some(&**error),
72            Self::MissingExtension | Self::UnknownCodec(_) => None,
73        }
74    }
75}
76
77impl From<std::io::Error> for InputCodecError {
78    fn from(error: std::io::Error) -> Self {
79        Self::Io(error)
80    }
81}
82
83impl From<std::string::FromUtf8Error> for InputCodecError {
84    fn from(error: std::string::FromUtf8Error) -> Self {
85        Self::Utf8(error)
86    }
87}
88
89/// Decode a byte stream into a value of type `T`.
90pub trait InputCodec<T>: Send + Sync {
91    /// Return static codec metadata. This must not read or parse input.
92    fn metadata(&self) -> &'static CodecMetadata;
93
94    /// Decode a byte stream.
95    fn read(&self, reader: &mut dyn Read) -> Result<T, InputCodecError>;
96
97    /// Decode a file. Codecs with path-relative semantics may override this method.
98    fn read_path(&self, path: &Path) -> Result<T, InputCodecError> {
99        let mut file = fs::File::open(path)?;
100        self.read(&mut file)
101    }
102
103    /// Decode UTF-8 text.
104    fn decode(&self, input: &str) -> Result<T, InputCodecError> {
105        self.read(&mut Cursor::new(input.as_bytes()))
106    }
107
108    /// Decode an in-memory byte sequence.
109    fn read_bytes(&self, input: &[u8]) -> Result<T, InputCodecError> {
110        self.read(&mut Cursor::new(input))
111    }
112}
113
114/// Input codec for Alto's textual `.irtg` format.
115#[derive(Clone, Copy, Debug, Default)]
116pub struct IrtgInputCodec;
117
118impl InputCodec<crate::Irtg> for IrtgInputCodec {
119    fn metadata(&self) -> &'static CodecMetadata {
120        static METADATA: CodecMetadata = CodecMetadata {
121            name: "irtg",
122            description: "IRTG grammar",
123            extension: Some("irtg"),
124        };
125        &METADATA
126    }
127
128    fn read(&self, reader: &mut dyn Read) -> Result<crate::Irtg, InputCodecError> {
129        crate::parse_irtg(reader).map_err(InputCodecError::codec)
130    }
131}
132
133/// Input codec for Alto's textual `.auto` tree-automaton format.
134#[derive(Clone, Copy, Debug, Default)]
135pub struct AltoTreeAutomatonInputCodec;
136
137impl InputCodec<ExplicitWithSignature> for AltoTreeAutomatonInputCodec {
138    fn metadata(&self) -> &'static CodecMetadata {
139        static METADATA: CodecMetadata = CodecMetadata {
140            name: "auto",
141            description: "Tree automaton",
142            extension: Some("auto"),
143        };
144        &METADATA
145    }
146
147    fn read(&self, reader: &mut dyn Read) -> Result<ExplicitWithSignature, InputCodecError> {
148        let input = read_utf8(reader)?;
149        crate::parse_alto(&input).map_err(InputCodecError::codec)
150    }
151}
152
153fn read_utf8(reader: &mut dyn Read) -> Result<String, InputCodecError> {
154    let mut bytes = Vec::new();
155    reader.read_to_end(&mut bytes)?;
156    String::from_utf8(bytes).map_err(InputCodecError::Utf8)
157}
158
159/// Static information describing an output codec.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub struct CodecMetadata {
162    /// Stable programmatic codec name.
163    pub name: &'static str,
164    /// Human-readable description suitable for menus.
165    pub description: &'static str,
166    /// Conventional filename extension, without a leading dot.
167    pub extension: Option<&'static str>,
168}
169
170/// Thread-safe input codec trait object stored by [`InputCodecRegistry`].
171pub type RegisteredInputCodec<T> = dyn InputCodec<T> + Send + Sync;
172
173/// Error returned when an input-codec registration conflicts with an existing codec.
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub enum InputCodecRegistryError {
176    /// Two codecs for the same result type use the same normalized name.
177    DuplicateName(String),
178    /// Two codecs for the same result type use the same normalized extension.
179    DuplicateExtension(String),
180}
181
182impl fmt::Display for InputCodecRegistryError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            Self::DuplicateName(name) => {
186                write!(f, "duplicate input codec name {name:?} for one result type")
187            }
188            Self::DuplicateExtension(extension) => write!(
189                f,
190                "duplicate input codec extension {extension:?} for one result type"
191            ),
192        }
193    }
194}
195
196impl Error for InputCodecRegistryError {}
197
198/// Registry of input codecs, keyed first by exact result type and then by codec metadata.
199#[derive(Default)]
200pub struct InputCodecRegistry {
201    codecs: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
202}
203
204impl InputCodecRegistry {
205    /// Create an empty registry.
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Create the registry containing rusty-alto's built-in file formats.
211    pub fn standard() -> Self {
212        let mut registry = Self::new();
213        registry
214            .register::<Irtg, _>(IrtgInputCodec)
215            .expect("built-in input codec metadata is unique");
216        registry
217            .register::<Irtg, _>(TulipacInputCodec)
218            .expect("built-in input codec metadata is unique");
219        registry
220            .register::<ExplicitWithSignature, _>(AltoTreeAutomatonInputCodec)
221            .expect("built-in input codec metadata is unique");
222        registry
223    }
224
225    /// Register a codec for the exact result type `T`.
226    pub fn register<T: 'static, C>(&mut self, codec: C) -> Result<(), InputCodecRegistryError>
227    where
228        C: InputCodec<T> + 'static,
229    {
230        let codecs = self
231            .codecs
232            .entry(TypeId::of::<T>())
233            .or_insert_with(|| Box::new(Vec::<Box<RegisteredInputCodec<T>>>::new()))
234            .downcast_mut::<Vec<Box<RegisteredInputCodec<T>>>>()
235            .expect("a TypeId uniquely identifies its codec vector");
236        let metadata = codec.metadata();
237        let name = normalize_name(metadata.name);
238        if codecs
239            .iter()
240            .any(|registered| normalize_name(registered.metadata().name) == name)
241        {
242            return Err(InputCodecRegistryError::DuplicateName(name));
243        }
244        if let Some(extension) = metadata.extension.map(normalize_extension)
245            && codecs.iter().any(|registered| {
246                registered
247                    .metadata()
248                    .extension
249                    .map(normalize_extension)
250                    .as_deref()
251                    == Some(extension.as_str())
252            })
253        {
254            return Err(InputCodecRegistryError::DuplicateExtension(extension));
255        }
256        codecs.push(Box::new(codec));
257        Ok(())
258    }
259
260    /// Return all codecs registered for the exact result type `T`.
261    pub fn codecs_for<T: 'static>(&self) -> &[Box<RegisteredInputCodec<T>>] {
262        self.codecs
263            .get(&TypeId::of::<T>())
264            .and_then(|codecs| codecs.downcast_ref::<Vec<Box<RegisteredInputCodec<T>>>>())
265            .map_or(&[], Vec::as_slice)
266    }
267
268    /// Find a codec for `T` by its case-insensitive metadata name.
269    pub fn codec_for_name<T: 'static>(
270        &self,
271        name: &str,
272    ) -> Result<&RegisteredInputCodec<T>, InputCodecError> {
273        let normalized = normalize_name(name);
274        self.codecs_for::<T>()
275            .iter()
276            .find(|codec| normalize_name(codec.metadata().name) == normalized)
277            .map(Box::as_ref)
278            .ok_or_else(|| InputCodecError::UnknownCodec(name.to_owned()))
279    }
280
281    /// Find a codec for `T` by filename extension.
282    pub fn codec_for_extension<T: 'static>(
283        &self,
284        extension: &str,
285    ) -> Result<&RegisteredInputCodec<T>, InputCodecError> {
286        let normalized = normalize_extension(extension);
287        self.codecs_for::<T>()
288            .iter()
289            .find(|codec| {
290                codec
291                    .metadata()
292                    .extension
293                    .map(normalize_extension)
294                    .as_deref()
295                    == Some(normalized.as_str())
296            })
297            .map(Box::as_ref)
298            .ok_or_else(|| InputCodecError::UnknownCodec(extension.to_owned()))
299    }
300
301    /// Find a codec for `T` from the extension of `path`.
302    pub fn codec_for_path<T: 'static>(
303        &self,
304        path: &Path,
305    ) -> Result<&RegisteredInputCodec<T>, InputCodecError> {
306        let extension = path
307            .extension()
308            .and_then(|extension| extension.to_str())
309            .ok_or(InputCodecError::MissingExtension)?;
310        self.codec_for_extension(extension)
311    }
312}
313
314fn normalize_name(name: &str) -> String {
315    name.trim().to_ascii_lowercase()
316}
317
318fn normalize_extension(extension: &str) -> String {
319    extension
320        .trim()
321        .trim_start_matches('.')
322        .to_ascii_lowercase()
323}
324
325/// Encode a public algebra value of type `V`.
326///
327/// Textual codecs use `Output = String`; algebra-owned display codecs use
328/// `Output = VisualRepresentation`.
329pub trait OutputCodec<V: ?Sized> {
330    /// The representation produced by this codec.
331    type Output;
332
333    /// Return static codec metadata. This must not inspect or encode a value.
334    fn metadata(&self) -> &'static CodecMetadata;
335
336    /// Encode `value`.
337    fn encode(&self, value: &V) -> Self::Output;
338}
339
340/// Codec for self-describing values: renders via the value's [`Display`](fmt::Display) impl.
341///
342/// Suitable for values that already carry their own labels (e.g. a tree algebra's value).
343#[derive(Clone, Copy, Debug, Default)]
344pub struct DisplayCodec;
345
346impl<V: fmt::Display + ?Sized> OutputCodec<V> for DisplayCodec {
347    type Output = String;
348
349    fn metadata(&self) -> &'static CodecMetadata {
350        static METADATA: CodecMetadata = CodecMetadata {
351            name: "display",
352            description: "Default string representation",
353            extension: Some("txt"),
354        };
355        &METADATA
356    }
357
358    fn encode(&self, value: &V) -> Self::Output {
359        value.to_string()
360    }
361}
362
363/// Codec for a word sequence: joins the words with single spaces.
364///
365/// Used by [`StringAlgebra`](crate::StringAlgebra) (public value `Vec<String>`).
366#[derive(Clone, Copy, Debug, Default)]
367pub struct SpaceJoinCodec;
368
369impl OutputCodec<Vec<String>> for SpaceJoinCodec {
370    type Output = String;
371
372    fn metadata(&self) -> &'static CodecMetadata {
373        static METADATA: CodecMetadata = CodecMetadata {
374            name: "string",
375            description: "Space-separated string",
376            extension: Some("txt"),
377        };
378        &METADATA
379    }
380
381    #[allow(clippy::ptr_arg)] // value type is fixed by the `OutputCodec<Vec<String>>` impl
382    fn encode(&self, value: &Vec<String>) -> Self::Output {
383        value.join(" ")
384    }
385}
386
387/// GUI-neutral preferred representation of an algebra value.
388#[derive(Debug)]
389pub enum VisualRepresentation {
390    /// Plain textual display.
391    Text(String),
392    /// A structured tree.
393    Tree(TreeValue),
394    /// A structured feature structure.
395    FeatureStructure(FeatureStructure),
396}
397
398/// Adapt any textual codec into a display codec.
399#[derive(Clone, Copy, Debug, Default)]
400pub struct TextVisualizationCodec<C> {
401    codec: C,
402}
403
404impl<C> TextVisualizationCodec<C> {
405    /// Wrap `codec` as a text visualization codec.
406    pub fn new(codec: C) -> Self {
407        Self { codec }
408    }
409}
410
411impl<V: ?Sized, C> OutputCodec<V> for TextVisualizationCodec<C>
412where
413    C: OutputCodec<V, Output = String>,
414{
415    type Output = VisualRepresentation;
416
417    fn metadata(&self) -> &'static CodecMetadata {
418        self.codec.metadata()
419    }
420
421    fn encode(&self, value: &V) -> Self::Output {
422        VisualRepresentation::Text(self.codec.encode(value))
423    }
424}
425
426/// Display codec that preserves tree structure.
427#[derive(Clone, Copy, Debug, Default)]
428pub struct TreeVisualizationCodec;
429
430impl OutputCodec<TreeValue> for TreeVisualizationCodec {
431    type Output = VisualRepresentation;
432
433    fn metadata(&self) -> &'static CodecMetadata {
434        static METADATA: CodecMetadata = CodecMetadata {
435            name: "tree-visualization",
436            description: "Tree visualization",
437            extension: None,
438        };
439        &METADATA
440    }
441
442    fn encode(&self, value: &TreeValue) -> Self::Output {
443        VisualRepresentation::Tree(value.clone())
444    }
445}
446
447/// Display codec that preserves feature-structure topology.
448#[derive(Clone, Copy, Debug, Default)]
449pub struct FeatureStructureVisualizationCodec;
450
451impl OutputCodec<FeatureStructure> for FeatureStructureVisualizationCodec {
452    type Output = VisualRepresentation;
453
454    fn metadata(&self) -> &'static CodecMetadata {
455        static METADATA: CodecMetadata = CodecMetadata {
456            name: "feature-structure-visualization",
457            description: "Feature-structure visualization",
458            extension: None,
459        };
460        &METADATA
461    }
462
463    fn encode(&self, value: &FeatureStructure) -> Self::Output {
464        VisualRepresentation::FeatureStructure(value.clone())
465    }
466}
467
468/// Thread-safe textual codec trait object stored by [`OutputCodecRegistry`].
469pub type TextOutputCodec<V> = dyn OutputCodec<V, Output = String> + Send + Sync;
470
471/// Failure while selecting a textual output codec.
472#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum OutputCodecError {
474    /// No codec for the value's exact public type has this name.
475    UnknownCodec(String),
476}
477
478impl fmt::Display for OutputCodecError {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        match self {
481            Self::UnknownCodec(name) => write!(f, "no output codec registered with name {name:?}"),
482        }
483    }
484}
485
486impl Error for OutputCodecError {}
487
488/// Registry of independent textual output codecs, keyed by exact public value type.
489///
490/// The registry's `Any` storage is only a heterogeneous type map. After lookup,
491/// callers receive ordinary typed [`OutputCodec<V>`] trait objects.
492#[derive(Default)]
493pub struct OutputCodecRegistry {
494    codecs: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
495}
496
497impl OutputCodecRegistry {
498    /// Create an empty registry.
499    pub fn new() -> Self {
500        Self::default()
501    }
502
503    /// Create the standard registry for rusty-alto's built-in public value types.
504    pub fn standard() -> Self {
505        let mut registry = Self::new();
506        registry.register::<Vec<String>, _>(SpaceJoinCodec);
507        registry.register::<TagStringValue<String>, _>(DisplayCodec);
508        registry.register::<TreeValue, _>(DisplayCodec);
509        registry.register::<FeatureStructure, _>(DisplayCodec);
510        registry
511    }
512
513    /// Register a textual codec for the exact public value type `V`.
514    pub fn register<V: 'static, C>(&mut self, codec: C)
515    where
516        C: OutputCodec<V, Output = String> + Send + Sync + 'static,
517    {
518        self.codecs
519            .entry(TypeId::of::<V>())
520            .or_insert_with(|| Box::new(Vec::<Box<TextOutputCodec<V>>>::new()))
521            .downcast_mut::<Vec<Box<TextOutputCodec<V>>>>()
522            .expect("a TypeId uniquely identifies its codec vector")
523            .push(Box::new(codec));
524    }
525
526    /// Return textual codecs registered for the exact public value type `V`.
527    ///
528    /// Accessing this slice or codec metadata does not invoke encoding.
529    pub fn codecs_for<V: 'static>(&self) -> &[Box<TextOutputCodec<V>>] {
530        self.codecs
531            .get(&TypeId::of::<V>())
532            .and_then(|codecs| codecs.downcast_ref::<Vec<Box<TextOutputCodec<V>>>>())
533            .map_or(&[], Vec::as_slice)
534    }
535
536    /// Find a textual codec by its case-insensitive metadata name.
537    pub fn codec_for_name<V: 'static>(
538        &self,
539        name: &str,
540    ) -> Result<&TextOutputCodec<V>, OutputCodecError> {
541        let normalized = normalize_name(name);
542        self.codecs_for::<V>()
543            .iter()
544            .find(|codec| normalize_name(codec.metadata().name) == normalized)
545            .map(Box::as_ref)
546            .ok_or_else(|| OutputCodecError::UnknownCodec(name.to_owned()))
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use std::sync::{
554        Arc,
555        atomic::{AtomicUsize, Ordering},
556    };
557
558    #[derive(Clone)]
559    struct CountingInputCodec {
560        calls: Arc<AtomicUsize>,
561        metadata: &'static CodecMetadata,
562    }
563
564    impl InputCodec<u32> for CountingInputCodec {
565        fn metadata(&self) -> &'static CodecMetadata {
566            self.metadata
567        }
568
569        fn read(&self, reader: &mut dyn Read) -> Result<u32, InputCodecError> {
570            self.calls.fetch_add(1, Ordering::Relaxed);
571            let input = read_utf8(reader)?;
572            input.trim().parse().map_err(InputCodecError::codec)
573        }
574    }
575
576    static COUNTING_METADATA: CodecMetadata = CodecMetadata {
577        name: "count",
578        description: "Count",
579        extension: Some("count"),
580    };
581
582    static DUPLICATE_NAME_METADATA: CodecMetadata = CodecMetadata {
583        name: "COUNT",
584        description: "Duplicate count",
585        extension: Some("other"),
586    };
587
588    static DUPLICATE_EXTENSION_METADATA: CodecMetadata = CodecMetadata {
589        name: "other",
590        description: "Duplicate extension",
591        extension: Some(".COUNT"),
592    };
593
594    #[test]
595    fn standard_input_registry_is_partitioned_by_exact_result_type() {
596        let registry = InputCodecRegistry::standard();
597        let irtg_extensions = registry
598            .codecs_for::<Irtg>()
599            .iter()
600            .filter_map(|codec| codec.metadata().extension)
601            .collect::<Vec<_>>();
602        assert_eq!(irtg_extensions, vec!["irtg", "tag"]);
603
604        let auto_extensions = registry
605            .codecs_for::<ExplicitWithSignature>()
606            .iter()
607            .filter_map(|codec| codec.metadata().extension)
608            .collect::<Vec<_>>();
609        assert_eq!(auto_extensions, vec!["auto"]);
610        assert!(registry.codecs_for::<u32>().is_empty());
611    }
612
613    #[test]
614    fn input_registry_lookup_is_normalized_and_lazy() {
615        let calls = Arc::new(AtomicUsize::new(0));
616        let mut registry = InputCodecRegistry::new();
617        registry
618            .register::<u32, _>(CountingInputCodec {
619                calls: calls.clone(),
620                metadata: &COUNTING_METADATA,
621            })
622            .unwrap();
623
624        assert_eq!(
625            registry
626                .codec_for_name::<u32>("COUNT")
627                .unwrap()
628                .metadata()
629                .name,
630            "count"
631        );
632        assert_eq!(
633            registry
634                .codec_for_extension::<u32>(".CoUnT")
635                .unwrap()
636                .metadata()
637                .extension,
638            Some("count")
639        );
640        assert_eq!(
641            registry
642                .codec_for_path::<u32>(Path::new("value.COUNT"))
643                .unwrap()
644                .metadata()
645                .name,
646            "count"
647        );
648        assert_eq!(calls.load(Ordering::Relaxed), 0);
649        assert_eq!(
650            registry
651                .codec_for_extension::<u32>("count")
652                .unwrap()
653                .decode("17")
654                .unwrap(),
655            17
656        );
657        assert_eq!(calls.load(Ordering::Relaxed), 1);
658        assert!(matches!(
659            registry.codec_for_path::<u32>(Path::new("no-extension")),
660            Err(InputCodecError::MissingExtension)
661        ));
662        assert!(matches!(
663            registry.codec_for_extension::<u32>("unknown"),
664            Err(InputCodecError::UnknownCodec(_))
665        ));
666    }
667
668    #[test]
669    fn input_registry_rejects_duplicate_names_and_extensions_per_type() {
670        let calls = Arc::new(AtomicUsize::new(0));
671        let mut registry = InputCodecRegistry::new();
672        registry
673            .register::<u32, _>(CountingInputCodec {
674                calls: calls.clone(),
675                metadata: &COUNTING_METADATA,
676            })
677            .unwrap();
678        assert_eq!(
679            registry.register::<u32, _>(CountingInputCodec {
680                calls: calls.clone(),
681                metadata: &DUPLICATE_NAME_METADATA,
682            }),
683            Err(InputCodecRegistryError::DuplicateName("count".to_owned()))
684        );
685        assert_eq!(
686            registry.register::<u32, _>(CountingInputCodec {
687                calls,
688                metadata: &DUPLICATE_EXTENSION_METADATA,
689            }),
690            Err(InputCodecRegistryError::DuplicateExtension(
691                "count".to_owned()
692            ))
693        );
694        assert!(
695            registry.register::<u64, _>(U64InputCodec).is_ok(),
696            "the same metadata is allowed for another result type"
697        );
698    }
699
700    struct U64InputCodec;
701
702    impl InputCodec<u64> for U64InputCodec {
703        fn metadata(&self) -> &'static CodecMetadata {
704            &COUNTING_METADATA
705        }
706
707        fn read(&self, reader: &mut dyn Read) -> Result<u64, InputCodecError> {
708            read_utf8(reader)?
709                .trim()
710                .parse()
711                .map_err(InputCodecError::codec)
712        }
713    }
714
715    #[test]
716    fn built_in_input_codecs_decode_expected_types() {
717        let irtg = IrtgInputCodec
718            .decode(
719                "interpretation string: de.up.ling.irtg.algebra.StringAlgebra\n\
720                 S! -> word\n[string] hello\n",
721            )
722            .unwrap();
723        assert!(irtg.interpretation_ref("string").is_some());
724
725        let auto = AltoTreeAutomatonInputCodec
726            .decode("S! -> f(A)\nA -> a")
727            .unwrap();
728        assert_eq!(auto.states.resolve(crate::StateId(0)), "S");
729        assert!(auto.signature.get("f").is_some());
730        assert_eq!(auto.automaton.rules().count(), 2);
731    }
732
733    #[test]
734    fn display_codec_uses_display() {
735        assert_eq!(DisplayCodec.encode(&2u8), "2");
736        assert_eq!(DisplayCodec.encode("hi"), "hi");
737        assert_eq!(
738            <DisplayCodec as OutputCodec<u8>>::metadata(&DisplayCodec).extension,
739            Some("txt")
740        );
741    }
742
743    #[test]
744    fn space_join_codec_joins_words() {
745        assert_eq!(
746            SpaceJoinCodec.encode(&vec!["the".to_owned(), "woman".to_owned()]),
747            "the woman"
748        );
749        assert_eq!(SpaceJoinCodec.encode(&vec![]), "");
750        assert_eq!(SpaceJoinCodec.encode(&vec!["the".to_owned()]), "the");
751    }
752
753    #[test]
754    fn text_visualization_wraps_text() {
755        let codec = TextVisualizationCodec::new(SpaceJoinCodec);
756        let value = vec!["hello".to_owned(), "world".to_owned()];
757        assert!(matches!(
758            codec.encode(&value),
759            VisualRepresentation::Text(text) if text == "hello world"
760        ));
761    }
762
763    #[test]
764    fn registry_uses_exact_types_and_does_not_encode_during_lookup() {
765        struct CountingCodec(Arc<AtomicUsize>);
766
767        impl OutputCodec<u32> for CountingCodec {
768            type Output = String;
769
770            fn metadata(&self) -> &'static CodecMetadata {
771                static METADATA: CodecMetadata = CodecMetadata {
772                    name: "counting",
773                    description: "Counting codec",
774                    extension: Some("count"),
775                };
776                &METADATA
777            }
778
779            fn encode(&self, value: &u32) -> Self::Output {
780                self.0.fetch_add(1, Ordering::Relaxed);
781                value.to_string()
782            }
783        }
784
785        let calls = Arc::new(AtomicUsize::new(0));
786        let mut registry = OutputCodecRegistry::new();
787        registry.register::<u32, _>(CountingCodec(calls.clone()));
788
789        let codecs = registry.codecs_for::<u32>();
790        assert_eq!(codecs.len(), 1);
791        assert_eq!(codecs[0].metadata().extension, Some("count"));
792        assert!(registry.codecs_for::<u64>().is_empty());
793        assert_eq!(calls.load(Ordering::Relaxed), 0);
794
795        assert_eq!(registry.codec_for_name::<u32>("COUNTING").unwrap().encode(&7), "7");
796        assert_eq!(calls.load(Ordering::Relaxed), 1);
797        assert!(matches!(
798            registry.codec_for_name::<u64>("counting"),
799            Err(OutputCodecError::UnknownCodec(_))
800        ));
801    }
802}