annatto/
lib.rs

1#![cfg_attr(not(test), warn(clippy::unwrap_used))]
2
3pub(crate) mod core;
4pub mod error;
5pub mod estarde;
6pub mod exporter;
7pub mod importer;
8pub mod manipulator;
9pub mod models;
10pub mod progress;
11#[cfg(test)]
12pub(crate) mod test_util;
13pub(crate) mod util;
14pub mod workflow;
15
16use std::{
17    fmt::Display,
18    path::{Path, PathBuf},
19};
20
21use documented::{Documented, DocumentedFields};
22use error::Result;
23use exporter::{
24    Exporter, conllu::ExportCoNLLU, exmaralda::ExportExmaralda, graphml::GraphMLExporter,
25    meta::ExportMeta, saltxml::ExportSaltXml, sequence::ExportSequence, table::ExportTable,
26    textgrid::ExportTextGrid, xlsx::ExportXlsx,
27};
28use graphannis::AnnotationGraph;
29use importer::{
30    Importer, conllu::ImportCoNLLU, exmaralda::ImportEXMARaLDA, file_nodes::CreateFileNodes,
31    graphml::GraphMLImporter, meta::AnnotateCorpus, none::CreateEmptyCorpus, opus::ImportOpusLinks,
32    ptb::ImportPTB, relannis::ImportRelAnnis, saltxml::ImportSaltXml, table::ImportTable,
33    textgrid::ImportTextgrid, toolbox::ImportToolBox, treetagger::ImportTreeTagger,
34    webanno::ImportWebAnnoTSV, whisper::ImportWhisper, xlsx::ImportSpreadsheet, xml::ImportXML,
35};
36use manipulator::{
37    Manipulator, align::AlignNodes, check::Check, chunker::Chunk, collapse::Collapse,
38    enumerate::EnumerateMatches, filter::FilterNodes, link::LinkNodes, map::MapAnnos, no_op::NoOp,
39    re::Revise, sleep::Sleep, split::SplitValues, time::Filltime, visualize::Visualize,
40};
41use serde::Serialize;
42use serde_derive::Deserialize;
43use struct_field_names_as_array::FieldNamesAsSlice;
44use strum::{AsRefStr, EnumDiscriminants, EnumIter};
45use tabled::Tabled;
46use workflow::StatusSender;
47
48use crate::importer::git::ImportGitMetadata;
49
50#[derive(Tabled)]
51pub struct ModuleConfiguration {
52    pub name: String,
53    pub description: String,
54}
55
56#[derive(Deserialize, EnumDiscriminants, AsRefStr, Serialize)]
57#[strum(serialize_all = "lowercase")]
58#[strum_discriminants(derive(EnumIter, AsRefStr), strum(serialize_all = "lowercase"))]
59#[serde(tag = "format", rename_all = "lowercase", content = "config")]
60pub enum WriteAs {
61    CoNLLU(#[serde(default)] Box<ExportCoNLLU>),
62    EXMARaLDA(#[serde(default)] ExportExmaralda),
63    GraphML(#[serde(default)] GraphMLExporter), // the purpose of serde(default) here is, that an empty `[export.config]` table can be omited in the future
64    Meta(#[serde(default)] ExportMeta),
65    SaltXml(#[serde(default)] ExportSaltXml),
66    Sequence(#[serde(default)] ExportSequence),
67    Table(#[serde(default)] ExportTable),
68    TextGrid(ExportTextGrid), // do not use default, as all attributes have their individual defaults
69    Xlsx(#[serde(default)] ExportXlsx),
70}
71
72impl Default for WriteAs {
73    // the purpose of this default is to allow to omit `format` in an `[[export]]` table
74    fn default() -> Self {
75        WriteAs::GraphML(GraphMLExporter::default())
76    }
77}
78
79impl WriteAs {
80    fn writer(&self) -> &dyn Exporter {
81        match self {
82            WriteAs::EXMARaLDA(m) => m,
83            WriteAs::GraphML(m) => m,
84            WriteAs::SaltXml(m) => m,
85            WriteAs::Sequence(m) => m,
86            WriteAs::Table(m) => m,
87            WriteAs::TextGrid(m) => m,
88            WriteAs::Xlsx(m) => m,
89            WriteAs::CoNLLU(m) => &**m,
90            WriteAs::Meta(m) => m,
91        }
92    }
93}
94
95impl WriteAsDiscriminants {
96    pub fn module_doc(&self) -> &str {
97        match self {
98            WriteAsDiscriminants::EXMARaLDA => ExportExmaralda::DOCS,
99            WriteAsDiscriminants::GraphML => GraphMLExporter::DOCS,
100            WriteAsDiscriminants::SaltXml => ExportSaltXml::DOCS,
101            WriteAsDiscriminants::Sequence => ExportSequence::DOCS,
102            WriteAsDiscriminants::Table => ExportTable::DOCS,
103            WriteAsDiscriminants::TextGrid => ExportTextGrid::DOCS,
104            WriteAsDiscriminants::Xlsx => ExportXlsx::DOCS,
105            WriteAsDiscriminants::CoNLLU => ExportCoNLLU::DOCS,
106            WriteAsDiscriminants::Meta => ExportMeta::DOCS,
107        }
108    }
109
110    pub fn module_configs(&self) -> Vec<ModuleConfiguration> {
111        let mut result = Vec::new();
112        let (field_names, field_docs) = match self {
113            WriteAsDiscriminants::EXMARaLDA => (
114                ExportExmaralda::FIELD_NAMES_AS_SLICE,
115                ExportExmaralda::FIELD_DOCS,
116            ),
117            WriteAsDiscriminants::GraphML => (
118                GraphMLExporter::FIELD_NAMES_AS_SLICE,
119                GraphMLExporter::FIELD_DOCS,
120            ),
121            WriteAsDiscriminants::SaltXml => (
122                ExportSaltXml::FIELD_NAMES_AS_SLICE,
123                ExportSaltXml::FIELD_DOCS,
124            ),
125            WriteAsDiscriminants::Sequence => (
126                ExportSequence::FIELD_NAMES_AS_SLICE,
127                ExportSequence::FIELD_DOCS,
128            ),
129            WriteAsDiscriminants::Table => {
130                (ExportTable::FIELD_NAMES_AS_SLICE, ExportTable::FIELD_DOCS)
131            }
132            WriteAsDiscriminants::TextGrid => (
133                ExportTextGrid::FIELD_NAMES_AS_SLICE,
134                ExportTextGrid::FIELD_DOCS,
135            ),
136            WriteAsDiscriminants::Xlsx => {
137                (ExportXlsx::FIELD_NAMES_AS_SLICE, ExportXlsx::FIELD_DOCS)
138            }
139            WriteAsDiscriminants::CoNLLU => {
140                (ExportCoNLLU::FIELD_NAMES_AS_SLICE, ExportCoNLLU::FIELD_DOCS)
141            }
142            WriteAsDiscriminants::Meta => {
143                (ExportMeta::FIELD_NAMES_AS_SLICE, ExportMeta::FIELD_DOCS)
144            }
145        };
146        for (idx, n) in field_names.iter().enumerate() {
147            if idx < field_docs.len() {
148                result.push(ModuleConfiguration {
149                    name: n.to_string(),
150                    description: field_docs[idx].to_string(),
151                });
152            } else {
153                result.push(ModuleConfiguration {
154                    name: n.to_string(),
155                    description: String::default(),
156                });
157            }
158        }
159        result
160    }
161}
162
163#[derive(Deserialize, EnumDiscriminants, AsRefStr, Serialize)]
164#[strum(serialize_all = "lowercase")]
165#[strum_discriminants(derive(EnumIter, AsRefStr), strum(serialize_all = "lowercase"))]
166#[serde(tag = "format", rename_all = "lowercase", content = "config")]
167pub enum ReadFrom {
168    CoNLLU(#[serde(default)] ImportCoNLLU),
169    EXMARaLDA(#[serde(default)] ImportEXMARaLDA),
170    Git(ImportGitMetadata),
171    GraphML(#[serde(default)] GraphMLImporter),
172    Meta(#[serde(default)] AnnotateCorpus),
173    None(#[serde(default)] CreateEmptyCorpus),
174    Opus(#[serde(default)] ImportOpusLinks),
175    Path(#[serde(default)] CreateFileNodes),
176    PTB(#[serde(default)] ImportPTB),
177    RelAnnis(#[serde(default)] ImportRelAnnis),
178    SaltXml(#[serde(default)] ImportSaltXml),
179    Table(#[serde(default)] ImportTable),
180    TextGrid(#[serde(default)] ImportTextgrid),
181    Toolbox(#[serde(default)] ImportToolBox),
182    TreeTagger(#[serde(default)] ImportTreeTagger),
183    Webanno(#[serde(default)] ImportWebAnnoTSV),
184    Whisper(#[serde(default)] ImportWhisper),
185    Xlsx(#[serde(default)] ImportSpreadsheet),
186    Xml(ImportXML),
187}
188
189impl Default for ReadFrom {
190    // the purpose of this default is to allow to omit `format` in an `[[import]]` table
191    fn default() -> Self {
192        ReadFrom::None(CreateEmptyCorpus::default())
193    }
194}
195
196impl ReadFrom {
197    fn reader(&self) -> &dyn Importer {
198        match self {
199            ReadFrom::CoNLLU(m) => m,
200            ReadFrom::EXMARaLDA(m) => m,
201            ReadFrom::GraphML(m) => m,
202            ReadFrom::Meta(m) => m,
203            ReadFrom::None(m) => m,
204            ReadFrom::Opus(m) => m,
205            ReadFrom::Path(m) => m,
206            ReadFrom::PTB(m) => m,
207            ReadFrom::RelAnnis(m) => m,
208            ReadFrom::SaltXml(m) => m,
209            ReadFrom::Table(m) => m,
210            ReadFrom::TextGrid(m) => m,
211            ReadFrom::Toolbox(m) => m,
212            ReadFrom::TreeTagger(m) => m,
213            ReadFrom::Whisper(m) => m,
214            ReadFrom::Xlsx(m) => m,
215            ReadFrom::Xml(m) => m,
216            ReadFrom::Webanno(m) => m,
217            ReadFrom::Git(m) => m,
218        }
219    }
220}
221
222impl ReadFromDiscriminants {
223    pub fn module_doc(&self) -> &str {
224        match self {
225            ReadFromDiscriminants::CoNLLU => ImportCoNLLU::DOCS,
226            ReadFromDiscriminants::EXMARaLDA => ImportEXMARaLDA::DOCS,
227            ReadFromDiscriminants::GraphML => GraphMLImporter::DOCS,
228            ReadFromDiscriminants::Meta => AnnotateCorpus::DOCS,
229            ReadFromDiscriminants::None => CreateEmptyCorpus::DOCS,
230            ReadFromDiscriminants::Opus => ImportOpusLinks::DOCS,
231            ReadFromDiscriminants::Path => CreateFileNodes::DOCS,
232            ReadFromDiscriminants::PTB => ImportPTB::DOCS,
233            ReadFromDiscriminants::RelAnnis => ImportRelAnnis::DOCS,
234            ReadFromDiscriminants::SaltXml => ImportSaltXml::DOCS,
235            ReadFromDiscriminants::Table => ImportTable::DOCS,
236            ReadFromDiscriminants::TextGrid => ImportTextgrid::DOCS,
237            ReadFromDiscriminants::Toolbox => ImportToolBox::DOCS,
238            ReadFromDiscriminants::TreeTagger => ImportTreeTagger::DOCS,
239            ReadFromDiscriminants::Whisper => ImportWhisper::DOCS,
240            ReadFromDiscriminants::Xlsx => ImportSpreadsheet::DOCS,
241            ReadFromDiscriminants::Xml => ImportXML::DOCS,
242            ReadFromDiscriminants::Webanno => ImportWebAnnoTSV::DOCS,
243            ReadFromDiscriminants::Git => ImportGitMetadata::DOCS,
244        }
245    }
246
247    pub fn module_configs(&self) -> Vec<ModuleConfiguration> {
248        let mut result = Vec::new();
249        let (field_names, field_docs) = match self {
250            ReadFromDiscriminants::CoNLLU => {
251                (ImportCoNLLU::FIELD_NAMES_AS_SLICE, ImportCoNLLU::FIELD_DOCS)
252            }
253            ReadFromDiscriminants::EXMARaLDA => (
254                ImportEXMARaLDA::FIELD_NAMES_AS_SLICE,
255                ImportEXMARaLDA::FIELD_DOCS,
256            ),
257            ReadFromDiscriminants::GraphML => (
258                GraphMLImporter::FIELD_NAMES_AS_SLICE,
259                GraphMLImporter::FIELD_DOCS,
260            ),
261            ReadFromDiscriminants::Meta => (
262                AnnotateCorpus::FIELD_NAMES_AS_SLICE,
263                AnnotateCorpus::FIELD_DOCS,
264            ),
265            ReadFromDiscriminants::None => (
266                CreateEmptyCorpus::FIELD_NAMES_AS_SLICE,
267                CreateEmptyCorpus::FIELD_DOCS,
268            ),
269            ReadFromDiscriminants::Opus => (
270                ImportOpusLinks::FIELD_NAMES_AS_SLICE,
271                ImportOpusLinks::FIELD_DOCS,
272            ),
273            ReadFromDiscriminants::Path => (
274                CreateFileNodes::FIELD_NAMES_AS_SLICE,
275                CreateFileNodes::FIELD_DOCS,
276            ),
277            ReadFromDiscriminants::PTB => (ImportPTB::FIELD_NAMES_AS_SLICE, ImportPTB::FIELD_DOCS),
278            ReadFromDiscriminants::TextGrid => (
279                ImportTextgrid::FIELD_NAMES_AS_SLICE,
280                ImportTextgrid::FIELD_DOCS,
281            ),
282            ReadFromDiscriminants::Table => {
283                (ImportTable::FIELD_NAMES_AS_SLICE, ImportTable::FIELD_DOCS)
284            }
285            ReadFromDiscriminants::TreeTagger => (
286                ImportTreeTagger::FIELD_NAMES_AS_SLICE,
287                ImportTreeTagger::FIELD_DOCS,
288            ),
289            ReadFromDiscriminants::Xlsx => (
290                ImportSpreadsheet::FIELD_NAMES_AS_SLICE,
291                ImportSpreadsheet::FIELD_DOCS,
292            ),
293            ReadFromDiscriminants::Xml => (ImportXML::FIELD_NAMES_AS_SLICE, ImportXML::FIELD_DOCS),
294            ReadFromDiscriminants::Toolbox => (
295                ImportToolBox::FIELD_NAMES_AS_SLICE,
296                ImportToolBox::FIELD_DOCS,
297            ),
298            ReadFromDiscriminants::RelAnnis => (
299                ImportRelAnnis::FIELD_NAMES_AS_SLICE,
300                ImportRelAnnis::FIELD_DOCS,
301            ),
302            ReadFromDiscriminants::SaltXml => (
303                ImportSaltXml::FIELD_NAMES_AS_SLICE,
304                ImportSaltXml::FIELD_DOCS,
305            ),
306            ReadFromDiscriminants::Whisper => (
307                ImportWhisper::FIELD_NAMES_AS_SLICE,
308                ImportWhisper::FIELD_DOCS,
309            ),
310            ReadFromDiscriminants::Webanno => (
311                ImportWebAnnoTSV::FIELD_NAMES_AS_SLICE,
312                ImportWebAnnoTSV::FIELD_DOCS,
313            ),
314            ReadFromDiscriminants::Git => (
315                ImportGitMetadata::FIELD_NAMES_AS_SLICE,
316                ImportGitMetadata::FIELD_DOCS,
317            ),
318        };
319        for (idx, n) in field_names.iter().enumerate() {
320            if idx < field_docs.len() {
321                result.push(ModuleConfiguration {
322                    name: n.to_string(),
323                    description: field_docs[idx].to_string(),
324                });
325            } else {
326                result.push(ModuleConfiguration {
327                    name: n.to_string(),
328                    description: String::default(),
329                });
330            }
331        }
332        result
333    }
334}
335
336#[derive(Deserialize, EnumDiscriminants, AsRefStr, Serialize)]
337#[strum(serialize_all = "lowercase")]
338#[strum_discriminants(derive(EnumIter, AsRefStr), strum(serialize_all = "lowercase"))]
339#[serde(tag = "action", rename_all = "lowercase", content = "config")]
340pub enum GraphOp {
341    Align(AlignNodes),  // no default
342    Check(Check),       // no default, has a (required) path attribute
343    Collapse(Collapse), // no default, there is no such thing as a default component
344    Filter(FilterNodes),
345    Visualize(#[serde(default)] Visualize),
346    Enumerate(#[serde(default)] EnumerateMatches),
347    Link(LinkNodes),                  // no default, has required attributes
348    Map(MapAnnos),                    // no default, has a (required) path attribute
349    Revise(#[serde(default)] Revise), // does nothing on default
350    Time(#[serde(default)] Filltime),
351    Chunk(#[serde(default)] Chunk),
352    Split(#[serde(default)] SplitValues), // default does nothing
353    Sleep(#[serde(default)] Sleep),
354    None(#[serde(default)] NoOp), // has no attributes
355}
356
357impl Default for GraphOp {
358    // the purpose of this default is to allow to omit `format` in an `[[graph_op]]` table
359    fn default() -> Self {
360        GraphOp::None(NoOp::default())
361    }
362}
363
364impl GraphOp {
365    fn processor(&self) -> &dyn Manipulator {
366        match self {
367            GraphOp::Check(m) => m,
368            GraphOp::Collapse(m) => m,
369            GraphOp::Visualize(m) => m,
370            GraphOp::Link(m) => m,
371            GraphOp::Map(m) => m,
372            GraphOp::Revise(m) => m,
373            GraphOp::None(m) => m,
374            GraphOp::Enumerate(m) => m,
375            GraphOp::Chunk(m) => m,
376            GraphOp::Split(m) => m,
377            GraphOp::Filter(m) => m,
378            GraphOp::Time(m) => m,
379            GraphOp::Sleep(m) => m,
380            GraphOp::Align(m) => m,
381        }
382    }
383}
384
385impl GraphOpDiscriminants {
386    pub fn module_doc(&self) -> &str {
387        match self {
388            GraphOpDiscriminants::Check => Check::DOCS,
389            GraphOpDiscriminants::Collapse => Collapse::DOCS,
390            GraphOpDiscriminants::Visualize => Visualize::DOCS,
391            GraphOpDiscriminants::Enumerate => EnumerateMatches::DOCS,
392            GraphOpDiscriminants::Link => LinkNodes::DOCS,
393            GraphOpDiscriminants::Map => MapAnnos::DOCS,
394            GraphOpDiscriminants::Revise => Revise::DOCS,
395            GraphOpDiscriminants::Chunk => Chunk::DOCS,
396            GraphOpDiscriminants::None => NoOp::DOCS,
397            GraphOpDiscriminants::Split => SplitValues::DOCS,
398            GraphOpDiscriminants::Filter => FilterNodes::DOCS,
399            GraphOpDiscriminants::Time => Filltime::DOCS,
400            GraphOpDiscriminants::Sleep => Sleep::DOCS,
401            GraphOpDiscriminants::Align => AlignNodes::DOCS,
402        }
403    }
404
405    pub fn module_configs(&self) -> Vec<ModuleConfiguration> {
406        let mut result = Vec::new();
407        let (field_names, field_docs) = match self {
408            GraphOpDiscriminants::Check => (Check::FIELD_NAMES_AS_SLICE, Check::FIELD_DOCS),
409            GraphOpDiscriminants::Collapse => {
410                (Collapse::FIELD_NAMES_AS_SLICE, Collapse::FIELD_DOCS)
411            }
412            GraphOpDiscriminants::Visualize => {
413                (Visualize::FIELD_NAMES_AS_SLICE, Visualize::FIELD_DOCS)
414            }
415            GraphOpDiscriminants::Enumerate => (
416                EnumerateMatches::FIELD_NAMES_AS_SLICE,
417                EnumerateMatches::FIELD_DOCS,
418            ),
419            GraphOpDiscriminants::Link => (LinkNodes::FIELD_NAMES_AS_SLICE, LinkNodes::FIELD_DOCS),
420            GraphOpDiscriminants::Map => (MapAnnos::FIELD_NAMES_AS_SLICE, MapAnnos::FIELD_DOCS),
421            GraphOpDiscriminants::Revise => (Revise::FIELD_NAMES_AS_SLICE, Revise::FIELD_DOCS),
422            GraphOpDiscriminants::Chunk => (Chunk::FIELD_NAMES_AS_SLICE, Chunk::FIELD_DOCS),
423            GraphOpDiscriminants::None => (NoOp::FIELD_NAMES_AS_SLICE, NoOp::FIELD_DOCS),
424            GraphOpDiscriminants::Split => {
425                (SplitValues::FIELD_NAMES_AS_SLICE, SplitValues::FIELD_DOCS)
426            }
427            GraphOpDiscriminants::Filter => {
428                (FilterNodes::FIELD_NAMES_AS_SLICE, FilterNodes::FIELD_DOCS)
429            }
430            GraphOpDiscriminants::Time => (Filltime::FIELD_NAMES_AS_SLICE, Filltime::FIELD_DOCS),
431            GraphOpDiscriminants::Sleep => (Sleep::FIELD_NAMES_AS_SLICE, Sleep::FIELD_DOCS),
432            GraphOpDiscriminants::Align => {
433                (AlignNodes::FIELD_NAMES_AS_SLICE, AlignNodes::FIELD_DOCS)
434            }
435        };
436        for (idx, n) in field_names.iter().enumerate() {
437            if idx < field_docs.len() {
438                result.push(ModuleConfiguration {
439                    name: n.to_string(),
440                    description: field_docs[idx].to_string(),
441                });
442            } else {
443                result.push(ModuleConfiguration {
444                    name: n.to_string(),
445                    description: String::default(),
446                });
447            }
448        }
449        result
450    }
451}
452
453/// Unique ID of a single step in the conversion pipeline.
454#[derive(Eq, PartialEq, Hash, Debug, Clone)]
455pub struct StepID {
456    /// The name of the module used in this step.
457    pub module_name: String,
458    /// The path (input or output) used in this step.
459    pub path: Option<PathBuf>,
460}
461
462impl StepID {
463    pub fn from_importer_step(step: &ImporterStep) -> StepID {
464        StepID {
465            module_name: format!("import_{}", step.module.as_ref().to_lowercase()),
466            path: Some(step.path.clone()),
467        }
468    }
469
470    pub fn from_graphop_step(step: &ManipulatorStep, position_in_workflow: usize) -> StepID {
471        StepID {
472            module_name: format!(
473                "{position_in_workflow}_{}",
474                step.module.as_ref().to_lowercase()
475            ),
476            path: None,
477        }
478    }
479
480    pub fn from_exporter_step(step: &ExporterStep) -> StepID {
481        StepID {
482            module_name: format!("export_{}", step.module.as_ref().to_lowercase()),
483            path: Some(step.path.clone()),
484        }
485    }
486}
487
488impl Display for StepID {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        if let Some(path) = &self.path {
491            write!(f, "{} ({})", self.module_name, path.to_string_lossy())
492        } else {
493            write!(f, "{}", self.module_name)
494        }
495    }
496}
497
498/// Represents a single step in a conversion pipeline.
499pub trait Step {}
500
501#[derive(Deserialize, Serialize)]
502#[serde(deny_unknown_fields)]
503pub struct ImporterStep {
504    #[serde(flatten)]
505    module: ReadFrom,
506    path: PathBuf,
507}
508
509impl ImporterStep {
510    #[cfg(test)]
511    fn execute(
512        &self,
513        tx: Option<StatusSender>,
514    ) -> std::result::Result<graphannis::update::GraphUpdate, Box<dyn std::error::Error>> {
515        self.module
516            .reader()
517            .import_corpus(&self.path, StepID::from_importer_step(&self), tx)
518    }
519}
520
521impl Step for ImporterStep {}
522
523#[derive(Deserialize, Serialize)]
524#[serde(deny_unknown_fields)]
525pub struct ExporterStep {
526    #[serde(flatten)]
527    module: WriteAs,
528    path: PathBuf,
529}
530
531impl ExporterStep {
532    #[cfg(test)]
533    fn execute(
534        &self,
535        graph: &AnnotationGraph,
536        tx: Option<StatusSender>,
537    ) -> std::result::Result<(), Box<dyn std::error::Error>> {
538        self.module
539            .writer()
540            .export_corpus(graph, &self.path, StepID::from_exporter_step(&self), tx)
541    }
542}
543
544impl Step for ExporterStep {}
545
546#[derive(Deserialize, Serialize)]
547#[serde(deny_unknown_fields)]
548pub struct ManipulatorStep {
549    #[serde(flatten)]
550    module: GraphOp,
551    workflow_directory: Option<PathBuf>,
552}
553
554impl ManipulatorStep {
555    fn execute(
556        &self,
557        graph: &mut AnnotationGraph,
558        workflow_directory: &Path,
559        position_in_workflow: usize,
560        tx: Option<StatusSender>,
561    ) -> std::result::Result<(), Box<dyn std::error::Error>> {
562        let step_id = StepID::from_graphop_step(self, position_in_workflow);
563        self.module
564            .processor()
565            .validate_graph(graph, step_id.clone(), tx.clone())?;
566        self.module
567            .processor()
568            .manipulate_corpus(graph, workflow_directory, step_id, tx)
569    }
570}
571
572impl Step for ManipulatorStep {}
573
574#[cfg(test)]
575mod tests {
576    use std::fs;
577
578    use serde::de::DeserializeOwned;
579
580    use crate::{GraphOp, ReadFrom, WriteAs};
581
582    #[test]
583    fn deser_read_from_pass() {
584        assert!(deserialize_toml::<ReadFrom>("tests/deser/deser_read_from.toml").is_ok());
585    }
586
587    #[test]
588    fn deser_read_from_fail_unknown() {
589        assert!(deserialize_toml::<ReadFrom>("tests/deser/deser_read_from_fail.toml").is_err());
590    }
591
592    #[test]
593    fn deser_graph_op_pass() {
594        assert!(deserialize_toml::<GraphOp>("tests/deser/deser_graph_op.toml").is_ok());
595    }
596
597    #[test]
598    fn deser_graph_op_fail_unknown() {
599        assert!(deserialize_toml::<GraphOp>("tests/deser/deser_graph_op_fail.toml").is_err());
600    }
601
602    #[test]
603    fn deser_write_as_pass() {
604        assert!(deserialize_toml::<WriteAs>("tests/deser/deser_write_as.toml").is_ok());
605    }
606
607    #[test]
608    fn deser_write_as_fail_unknown() {
609        assert!(deserialize_toml::<WriteAs>("tests/deser/deser_write_as_fail.toml").is_err());
610    }
611
612    fn deserialize_toml<E: DeserializeOwned>(path: &str) -> Result<E, toml::de::Error> {
613        let toml_string = fs::read_to_string(path);
614        assert!(toml_string.is_ok());
615        toml::from_str(&toml_string.unwrap())
616    }
617}