arnalisa 0.6.8

Pipeline system for calculating values
Documentation
//! A bin outputs the only input value which is not `Nothing`.
//!
//! If exactly one input has a value that is not `Nothing`, this value
//! gets propagated to the output. If either zero or more than one inputs
//! have a value that is not `Nothing`, the output will be driven by
//! a `Nothing` value.
//!
//! ```text
//!   ┌──[single_not_null]──┐
//!  ⇒│<input 0>      output│⇒
//!  ⇒│<input 1>            │
//!  ⇒│<input 2>            │
//!  ⇒┊…                    ┊
//!  ⇒│<input n>            │
//!   └─────────────────────┘
//! ```

use super::{
    source_names_output, BinBuildEnvironment, BinDescription, Calculator,
    FetchItem, GetCalibration, Item, Iteration, Result, Scope, SinkBin,
    SinkNames, SourceBin, SourceId, SourceNames, SourceSinkBinDescription,
    WriteDotSimple, SOURCE_OUTPUT,
};
use crate::error;
use indexmap::{IndexMap, IndexSet};

static BIN_TYPE: &str = "single_not_null";

/// A bin outputs the only input value which is not `Nothing`.
#[derive(Debug)]
pub struct Bin {
    scope: Scope,
    inputs: IndexMap<String, Box<dyn FetchItem>>,
    result_output: Item,
}

impl SinkBin for Bin {}

impl SourceBin for Bin {
    fn get_source_data(&self, source: &SourceId) -> Result<Item> {
        if source.id == SOURCE_OUTPUT {
            Ok(self.result_output.clone())
        } else {
            error::MissingSourceName {
                scope: self.scope.clone(),
                name: source.id.to_string(),
                bin_type: BIN_TYPE.to_string(),
            }
            .fail()
        }
    }
}

impl Calculator for Bin {
    fn calculate(&mut self, _iteration: &Iteration) -> Result<()> {
        let items = self
            .inputs
            .iter()
            .map(|(s, ds)| {
                ds.fetch_item(&self.scope).map(|d| (s.to_string(), d))
            })
            .collect::<Result<IndexMap<String, Item>>>()?;

        let values = items
            .values()
            .filter_map(|v| {
                if v == &Item::Nothing {
                    None
                } else {
                    Some(v.clone())
                }
            })
            .collect::<Vec<_>>();
        self.result_output = if values.len() == 1 {
            values.into_iter().next().unwrap()
        } else {
            Item::Nothing
        };
        Ok(())
    }
}

/// Description for the single_not_null bin.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Description {
    /// The names of the inputs.
    pub inputs: IndexSet<String>,
}

impl BinDescription for Description {
    type Bin = Bin;

    fn check_validity(
        &self,
        _scope: &Scope,
        _get_calibration: &mut dyn GetCalibration,
    ) -> Result<()> {
        Ok(())
    }

    fn bin_type(&self) -> &'static str {
        BIN_TYPE
    }
}

impl SinkNames for Description {
    fn sink_names(&self) -> IndexSet<String> {
        self.inputs.clone()
    }
}

impl SourceNames for Description {
    fn source_names(&self) -> Result<IndexSet<String>> {
        Ok(source_names_output())
    }
}

impl SourceSinkBinDescription for Description {
    fn build_bin(
        &self,
        scope: &Scope,
        env: &mut dyn BinBuildEnvironment,
    ) -> Result<Self::Bin> {
        let inputs = self
            .inputs
            .iter()
            .map(|s| env.resolve(s).map(|v| (s.to_string(), v)))
            .collect::<Result<IndexMap<_, _>>>()?;

        Ok(Bin {
            scope: scope.clone(),
            inputs,
            result_output: Item::Nothing,
        })
    }
}

impl WriteDotSimple for Description {}

#[cfg(test)]
mod tests {
    use super::Description;
    use crate::bins::{directsource, verificationsink};
    use crate::{run_bin, Result};
    use indexmap::indexset;

    #[test]
    fn simulate() -> Result<()> {
        use crate::Item::*;

        let input = directsource::Description {
            columns: indexset![
                "a".to_string(),
                "b".to_string(),
                "c".to_string(),
                "d".to_string(),
            ],
            rows: vec![
                vec![Nothing, Nothing, Nothing, Nothing],
                vec![U8(0), Nothing, Nothing, Nothing],
                vec![Nothing, U8(1), Nothing, Nothing],
                vec![Nothing, Nothing, U8(2), Nothing],
                vec![Nothing, Nothing, Nothing, U8(3)],
                vec![U8(0), U8(1), Nothing, Nothing],
                vec![U8(0), Nothing, U8(2), Nothing],
                vec![U8(0), Nothing, Nothing, U8(3)],
                vec![Nothing, U8(1), U8(2), Nothing],
                vec![Nothing, U8(1), Nothing, U8(3)],
                vec![Nothing, Nothing, U8(2), U8(3)],
            ]
            .into(),
        };
        let verification = verificationsink::Description {
            columns: indexset!["output".to_string()],
            expected: vec![
                vec![Nothing],
                vec![U8(0)],
                vec![U8(1)],
                vec![U8(2)],
                vec![U8(3)],
                vec![Nothing],
                vec![Nothing],
                vec![Nothing],
                vec![Nothing],
                vec![Nothing],
                vec![Nothing],
            ]
            .into(),
        };

        let description = Description {
            inputs: vec!["a", "b", "c", "d"]
                .into_iter()
                .map(str::to_string)
                .collect(),
        };

        run_bin(&input, &description, &verification)
    }
}