<commit_before>use super::Collector;
use super::SegmentCollector;
use collector::Fruit;
use downcast::Downcast;
use std::marker::PhantomData;
use DocId;
use Result;
use Score;
use SegmentLocalId;
use SegmentReader;
use TantivyError;

pub struct MultiFruit {
    sub_fruits: Vec<Option<Box<Fruit>>>,
}

pub struct CollectorWrapper<TCollector: Collector>(TCollector);

impl<TCollector: Collector> Collector for CollectorWrapper<TCollector> {
    type Fruit = Box<Fruit>;
    type Child = Box<BoxableSegmentCollector>;

    fn for_segment(
        &self,
        segment_local_id: u32,
        reader: &SegmentReader,
    ) -> Result<Box<BoxableSegmentCollector>> {
        let child = self.0.for_segment(segment_local_id, reader)?;
        Ok(Box::new(SegmentCollectorWrapper(child)))
    }

    fn requires_scoring(&self) -> bool {
        self.0.requires_scoring()
    }

    fn merge_fruits(&self, children: Vec<<Self as Collector>::Fruit>) -> Result<Box<Fruit>> {
        let typed_fruit: Vec<TCollector::Fruit> = children
            .into_iter()
            .map(|untyped_fruit| {
                Downcast::<TCollector::Fruit>::downcast(untyped_fruit)
                    .map(|boxed_but_typed| *boxed_but_typed)
                    .map_err(|e| {
                        let err_msg = format!("Failed to cast child collector fruit. {:?}", e);
                        TantivyError::InvalidArgument(err_msg)
                    })
            })
            .collect::<Result<_>>()?;
        let merged_fruit = self.0.merge_fruits(typed_fruit)?;
        Ok(Box::new(merged_fruit))
    }
}

impl SegmentCollector for Box<BoxableSegmentCollector> {
    type Fruit = Box<Fruit>;

    fn collect(&mut self, doc: u32, score: f32) {
        self.as_mut().collect(doc, score);
    }

    fn harvest(self) -> Box<Fruit> {
        BoxableSegmentCollector::harvest_from_box(self)
    }
}

pub trait BoxableSegmentCollector {
    fn collect(&mut self, doc: u32, score: f32);
    fn harvest_from_box(self: Box<Self>) -> Box<Fruit>;
}

pub struct SegmentCollectorWrapper<TSegmentCollector: SegmentCollector>(TSegmentCollector);

impl<TSegmentCollector: SegmentCollector> BoxableSegmentCollector
    for SegmentCollectorWrapper<TSegmentCollector>
{
    fn collect(&mut self, doc: u32, score: f32) {
        self.0.collect(doc, score);
    }

    fn harvest_from_box(self: Box<Self>) -> Box<Fruit> {
        Box::new(self.0.harvest())
    }
}

pub struct FruitHandle<TFruit: Fruit> {
    pos: usize,
    _phantom: PhantomData<TFruit>,
}

impl<TFruit: Fruit> FruitHandle<TFruit> {
    pub fn extract(self, fruits: &mut MultiFruit) -> TFruit {
        let boxed_fruit = fruits.sub_fruits[self.pos].take().expect("");
        *Downcast::<TFruit>::downcast(boxed_fruit).expect("Failed")
    }
}

/// Multicollector makes it possible to collect on more than one collector.
/// It should only be used for use cases where the Collector types is unknown
/// at compile time.
/// If the type of the collectors is known, you should prefer to use `ChainedCollector`.
///
/// ```rust
/// #[macro_use]
/// extern crate tantivy;
/// use tantivy::schema::{Schema, TEXT};
/// use tantivy::{Index, Result};
/// use tantivy::collector::{Count, TopDocs, MultiCollector};
/// use tantivy::query::QueryParser;
///
/// # fn main() { example().unwrap(); }
/// fn example() -> Result<()> {
///     let mut schema_builder = Schema::builder();
///     let title = schema_builder.add_text_field("title", TEXT);
///     let schema = schema_builder.build();
///     let index = Index::create_in_ram(schema);
///     {
///         let mut index_writer = index.writer(3_000_000)?;
///         index_writer.add_document(doc!(
///             title => "The Name of the Wind",
///         ));
///         index_writer.add_document(doc!(
///             title => "The Diary of Muadib",
///         ));
///         index_writer.add_document(doc!(
///             title => "A Dairy Cow",
///         ));
///         index_writer.add_document(doc!(
///             title => "The Diary of a Young Girl",
///         ));
///         index_writer.commit().unwrap();
///     }
///
///     index.load_searchers()?;
///     let searcher = index.searcher();
///
///     let mut collectors = MultiCollector::new();
///     let top_docs_handle = collectors.add_collector(TopDocs::with_limit(2));
///     let count_handle = collectors.add_collector(Count);
///     let query_parser = QueryParser::for_index(&index, vec![title]);
///     let query = query_parser.parse_query("diary")?;
///     let mut multi_fruit = searcher.search(&query, &collectors)?;
///
///     let count = count_handle.extract(&mut multi_fruit);
///     let top_docs = top_docs_handle.extract(&mut multi_fruit);
///
///     # assert_eq!(count, 2);
///     # assert_eq!(top_docs.len(), 2);
///
///     Ok(())
/// }
/// ```
#[allow(clippy::type_complexity)]
#[derive(Default)]
pub struct MultiCollector<'a> {
    collector_wrappers:
        Vec<Box<Collector<Child = Box<BoxableSegmentCollector>, Fruit = Box<Fruit>> + 'a>>,
}

impl<'a> MultiCollector<'a> {
    /// Create a new `MultiCollector`
    pub fn new() -> Self {
        Default::default()
    }

    /// Add a new collector to our `MultiCollector`.
    pub fn add_collector<'b: 'a, TCollector: Collector + 'b>(
        &mut self,
        collector: TCollector,
    ) -> FruitHandle<TCollector::Fruit> {
        let pos = self.collector_wrappers.len();
        self.collector_wrappers
            .push(Box::new(CollectorWrapper(collector)));
        FruitHandle {
            pos,
            _phantom: PhantomData,
        }
    }
}

impl<'a> Collector for MultiCollector<'a> {
    type Fruit = MultiFruit;
    type Child = MultiCollectorChild;

    fn for_segment(
        &self,
        segment_local_id: SegmentLocalId,
        segment: &SegmentReader,
    ) -> Result<MultiCollectorChild> {
        let children = self
            .collector_wrappers
            .iter()
            .map(|collector_wrapper| collector_wrapper.for_segment(segment_local_id, segment))
            .collect::<Result<Vec<_>>>()?;
        Ok(MultiCollectorChild { children })
    }

    fn requires_scoring(&self) -> bool {
        self.collector_wrappers.iter().any(|c| c.requires_scoring())
    }

    fn merge_fruits(&self, segments_multifruits: Vec<MultiFruit>) -> Result<MultiFruit> {
        let mut segment_fruits_list: Vec<Vec<Box<Fruit>>> = (0..self.collector_wrappers.len())
            .map(|_| Vec::with_capacity(segments_multifruits.len()))
            .collect::<Vec<_>>();
        for segment_multifruit in segments_multifruits {
            for (idx, segment_fruit_opt) in segment_multifruit.sub_fruits.into_iter().enumerate() {
                if let Some(segment_fruit) = segment_fruit_opt {
                    segment_fruits_list[idx].push(segment_fruit);
                }
            }
        }
        let sub_fruits = self
            .collector_wrappers
            .iter()
            .zip(segment_fruits_list)
            .map(|(child_collector, segment_fruits)| {
                Ok(Some(child_collector.merge_fruits(segment_fruits)?))
            })
            .collect::<Result<_>>()?;
        Ok(MultiFruit { sub_fruits })
    }
}

pub struct MultiCollectorChild {
    children: Vec<Box<BoxableSegmentCollector>>,
}

impl SegmentCollector for MultiCollectorChild {
    type Fruit = MultiFruit;

    fn collect(&mut self, doc: DocId, score: Score) {
        for child in &mut self.children {
            child.collect(doc, score);
        }
    }

    fn harvest(self) -> MultiFruit {
        MultiFruit {
            sub_fruits: self
                .children
                .into_iter()
                .map(|child| Some(child.harvest()))
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use collector::{Count, TopDocs};
    use query::TermQuery;
    use schema::IndexRecordOption;
    use schema::{Schema, TEXT};
    use Index;
    use Term;

    #[test]
    fn test_multi_collector() {
        let mut schema_builder = Schema::builder();
        let text = schema_builder.add_text_field("text", TEXT);
        let schema = schema_builder.build();

        let index = Index::create_in_ram(schema);
        {
            let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
            index_writer.add_document(doc!(text=>"abc"));
            index_writer.add_document(doc!(text=>"abc abc abc"));
            index_writer.add_document(doc!(text=>"abc abc"));
            index_writer.commit().unwrap();
            index_writer.add_document(doc!(text=>""));
            index_writer.add_document(doc!(text=>"abc abc abc abc"));
            index_writer.add_document(doc!(text=>"abc"));
            index_writer.commit().unwrap();
        }
        index.load_searchers().unwrap();
        let searcher = index.searcher();
        let term = Term::from_field_text(text, "abc");
        let query = TermQuery::new(term, IndexRecordOption::Basic);

        let mut collectors = MultiCollector::new();
        let topdocs_handler = collectors.add_collector(TopDocs::with_limit(2));
        let count_handler = collectors.add_collector(Count);
        let mut multifruits = searcher.search(&query, &mut collectors).unwrap();

        assert_eq!(count_handler.extract(&mut multifruits), 5);
        assert_eq!(topdocs_handler.extract(&mut multifruits).len(), 2);
    }
}
<commit_msg>Closes 466. Removing mentions of the chain collector. (#467)<commit_after>use super::Collector;
use super::SegmentCollector;
use collector::Fruit;
use downcast::Downcast;
use std::marker::PhantomData;
use DocId;
use Result;
use Score;
use SegmentLocalId;
use SegmentReader;
use TantivyError;

pub struct MultiFruit {
    sub_fruits: Vec<Option<Box<Fruit>>>,
}

pub struct CollectorWrapper<TCollector: Collector>(TCollector);

impl<TCollector: Collector> Collector for CollectorWrapper<TCollector> {
    type Fruit = Box<Fruit>;
    type Child = Box<BoxableSegmentCollector>;

    fn for_segment(
        &self,
        segment_local_id: u32,
        reader: &SegmentReader,
    ) -> Result<Box<BoxableSegmentCollector>> {
        let child = self.0.for_segment(segment_local_id, reader)?;
        Ok(Box::new(SegmentCollectorWrapper(child)))
    }

    fn requires_scoring(&self) -> bool {
        self.0.requires_scoring()
    }

    fn merge_fruits(&self, children: Vec<<Self as Collector>::Fruit>) -> Result<Box<Fruit>> {
        let typed_fruit: Vec<TCollector::Fruit> = children
            .into_iter()
            .map(|untyped_fruit| {
                Downcast::<TCollector::Fruit>::downcast(untyped_fruit)
                    .map(|boxed_but_typed| *boxed_but_typed)
                    .map_err(|e| {
                        let err_msg = format!("Failed to cast child collector fruit. {:?}", e);
                        TantivyError::InvalidArgument(err_msg)
                    })
            })
            .collect::<Result<_>>()?;
        let merged_fruit = self.0.merge_fruits(typed_fruit)?;
        Ok(Box::new(merged_fruit))
    }
}

impl SegmentCollector for Box<BoxableSegmentCollector> {
    type Fruit = Box<Fruit>;

    fn collect(&mut self, doc: u32, score: f32) {
        self.as_mut().collect(doc, score);
    }

    fn harvest(self) -> Box<Fruit> {
        BoxableSegmentCollector::harvest_from_box(self)
    }
}

pub trait BoxableSegmentCollector {
    fn collect(&mut self, doc: u32, score: f32);
    fn harvest_from_box(self: Box<Self>) -> Box<Fruit>;
}

pub struct SegmentCollectorWrapper<TSegmentCollector: SegmentCollector>(TSegmentCollector);

impl<TSegmentCollector: SegmentCollector> BoxableSegmentCollector
    for SegmentCollectorWrapper<TSegmentCollector>
{
    fn collect(&mut self, doc: u32, score: f32) {
        self.0.collect(doc, score);
    }

    fn harvest_from_box(self: Box<Self>) -> Box<Fruit> {
        Box::new(self.0.harvest())
    }
}

pub struct FruitHandle<TFruit: Fruit> {
    pos: usize,
    _phantom: PhantomData<TFruit>,
}

impl<TFruit: Fruit> FruitHandle<TFruit> {
    pub fn extract(self, fruits: &mut MultiFruit) -> TFruit {
        let boxed_fruit = fruits.sub_fruits[self.pos].take().expect("");
        *Downcast::<TFruit>::downcast(boxed_fruit).expect("Failed")
    }
}

/// Multicollector makes it possible to collect on more than one collector.
/// It should only be used for use cases where the Collector types is unknown
/// at compile time.
///
/// If the type of the collectors is known, you can just group yours collectors
/// in a tuple. See the
/// [Combining several collectors section of the collector documentation](./index.html#combining-several-collectors).
///
/// ```rust
/// #[macro_use]
/// extern crate tantivy;
/// use tantivy::schema::{Schema, TEXT};
/// use tantivy::{Index, Result};
/// use tantivy::collector::{Count, TopDocs, MultiCollector};
/// use tantivy::query::QueryParser;
///
/// # fn main() { example().unwrap(); }
/// fn example() -> Result<()> {
///     let mut schema_builder = Schema::builder();
///     let title = schema_builder.add_text_field("title", TEXT);
///     let schema = schema_builder.build();
///     let index = Index::create_in_ram(schema);
///     {
///         let mut index_writer = index.writer(3_000_000)?;
///         index_writer.add_document(doc!(
///             title => "The Name of the Wind",
///         ));
///         index_writer.add_document(doc!(
///             title => "The Diary of Muadib",
///         ));
///         index_writer.add_document(doc!(
///             title => "A Dairy Cow",
///         ));
///         index_writer.add_document(doc!(
///             title => "The Diary of a Young Girl",
///         ));
///         index_writer.commit().unwrap();
///     }
///
///     index.load_searchers()?;
///     let searcher = index.searcher();
///
///     let mut collectors = MultiCollector::new();
///     let top_docs_handle = collectors.add_collector(TopDocs::with_limit(2));
///     let count_handle = collectors.add_collector(Count);
///     let query_parser = QueryParser::for_index(&index, vec![title]);
///     let query = query_parser.parse_query("diary")?;
///     let mut multi_fruit = searcher.search(&query, &collectors)?;
///
///     let count = count_handle.extract(&mut multi_fruit);
///     let top_docs = top_docs_handle.extract(&mut multi_fruit);
///
///     # assert_eq!(count, 2);
///     # assert_eq!(top_docs.len(), 2);
///
///     Ok(())
/// }
/// ```
#[allow(clippy::type_complexity)]
#[derive(Default)]
pub struct MultiCollector<'a> {
    collector_wrappers:
        Vec<Box<Collector<Child = Box<BoxableSegmentCollector>, Fruit = Box<Fruit>> + 'a>>,
}

impl<'a> MultiCollector<'a> {
    /// Create a new `MultiCollector`
    pub fn new() -> Self {
        Default::default()
    }

    /// Add a new collector to our `MultiCollector`.
    pub fn add_collector<'b: 'a, TCollector: Collector + 'b>(
        &mut self,
        collector: TCollector,
    ) -> FruitHandle<TCollector::Fruit> {
        let pos = self.collector_wrappers.len();
        self.collector_wrappers
            .push(Box::new(CollectorWrapper(collector)));
        FruitHandle {
            pos,
            _phantom: PhantomData,
        }
    }
}

impl<'a> Collector for MultiCollector<'a> {
    type Fruit = MultiFruit;
    type Child = MultiCollectorChild;

    fn for_segment(
        &self,
        segment_local_id: SegmentLocalId,
        segment: &SegmentReader,
    ) -> Result<MultiCollectorChild> {
        let children = self
            .collector_wrappers
            .iter()
            .map(|collector_wrapper| collector_wrapper.for_segment(segment_local_id, segment))
            .collect::<Result<Vec<_>>>()?;
        Ok(MultiCollectorChild { children })
    }

    fn requires_scoring(&self) -> bool {
        self.collector_wrappers.iter().any(|c| c.requires_scoring())
    }

    fn merge_fruits(&self, segments_multifruits: Vec<MultiFruit>) -> Result<MultiFruit> {
        let mut segment_fruits_list: Vec<Vec<Box<Fruit>>> = (0..self.collector_wrappers.len())
            .map(|_| Vec::with_capacity(segments_multifruits.len()))
            .collect::<Vec<_>>();
        for segment_multifruit in segments_multifruits {
            for (idx, segment_fruit_opt) in segment_multifruit.sub_fruits.into_iter().enumerate() {
                if let Some(segment_fruit) = segment_fruit_opt {
                    segment_fruits_list[idx].push(segment_fruit);
                }
            }
        }
        let sub_fruits = self
            .collector_wrappers
            .iter()
            .zip(segment_fruits_list)
            .map(|(child_collector, segment_fruits)| {
                Ok(Some(child_collector.merge_fruits(segment_fruits)?))
            })
            .collect::<Result<_>>()?;
        Ok(MultiFruit { sub_fruits })
    }
}

pub struct MultiCollectorChild {
    children: Vec<Box<BoxableSegmentCollector>>,
}

impl SegmentCollector for MultiCollectorChild {
    type Fruit = MultiFruit;

    fn collect(&mut self, doc: DocId, score: Score) {
        for child in &mut self.children {
            child.collect(doc, score);
        }
    }

    fn harvest(self) -> MultiFruit {
        MultiFruit {
            sub_fruits: self
                .children
                .into_iter()
                .map(|child| Some(child.harvest()))
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use collector::{Count, TopDocs};
    use query::TermQuery;
    use schema::IndexRecordOption;
    use schema::{Schema, TEXT};
    use Index;
    use Term;

    #[test]
    fn test_multi_collector() {
        let mut schema_builder = Schema::builder();
        let text = schema_builder.add_text_field("text", TEXT);
        let schema = schema_builder.build();

        let index = Index::create_in_ram(schema);
        {
            let mut index_writer = index.writer_with_num_threads(1, 3_000_000).unwrap();
            index_writer.add_document(doc!(text=>"abc"));
            index_writer.add_document(doc!(text=>"abc abc abc"));
            index_writer.add_document(doc!(text=>"abc abc"));
            index_writer.commit().unwrap();
            index_writer.add_document(doc!(text=>""));
            index_writer.add_document(doc!(text=>"abc abc abc abc"));
            index_writer.add_document(doc!(text=>"abc"));
            index_writer.commit().unwrap();
        }
        index.load_searchers().unwrap();
        let searcher = index.searcher();
        let term = Term::from_field_text(text, "abc");
        let query = TermQuery::new(term, IndexRecordOption::Basic);

        let mut collectors = MultiCollector::new();
        let topdocs_handler = collectors.add_collector(TopDocs::with_limit(2));
        let count_handler = collectors.add_collector(Count);
        let mut multifruits = searcher.search(&query, &mut collectors).unwrap();

        assert_eq!(count_handler.extract(&mut multifruits), 5);
        assert_eq!(topdocs_handler.extract(&mut multifruits).len(), 2);
    }
}
<|endoftext|>


<commit_before>use std::collections::HashSet;
use std::io::Cursor;
use std::ops::Deref;
use std::rc::Rc;

use digest::digest_reader;
use serde_json;
use sha2::Sha256;
use url::Url;
use url_serde;

use resman::{AudioBuffer, ResMan};
use super::routegraph::RouteGraph;
use super::adjlist::AdjList;

#[derive(Debug)]
pub enum Error {
    /// No effect matches the metadata requested.
    NoMatchingEffect(EffectId),
}

/// Alias for a `Result` with our error type.
pub type ResultE<T> = Result<T, Error>;

/// Serializable info needed to look up an effect.
#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectId {
    /// Canonical name of the effect
    name: String,
    /// Hash of the effect's definition file, or None if the effect is primitive
    sha256: Option<[u8; 32]>,
    // TODO: consider a smallset.
    // TODO: use #[serde(with = "url_serde")]
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
}

/// All information that will be loaded from disk/network to describe how to
/// synthesize the Effect.
#[derive(Serialize, Deserialize)]
pub struct EffectDesc {
    meta: EffectMeta,
    adjlist: AdjList,
}

/// Validated version of `EffectDesc`. Guaranteed to be synthesizable.
pub struct Effect {
    id: EffectId,
    meta: EffectMeta,
    // TODO: Effects are immutable, so we can make this an AdjList and store
    // the connectivity information separately.
    // option, because effect MAY be primitive.
    data: EffectData,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectMeta {
    /// Canonical name of the effect
    name: String,
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
    inputs: Vec<EffectInput>,
    outputs: Vec<EffectOutput>,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectIO {
    name: String,
    channel: u8,
}
pub type EffectInput = EffectIO;
pub type EffectOutput = EffectIO;

/// Implementation details of an Effect.
/// Either its route graph, a primitive effect, etc.
pub enum EffectData {
    RouteGraph(RouteGraph),
    Primitive(PrimitiveEffect),
    Buffer(AudioBuffer),
}

/// Effects that cannot be decomposed; they have no implementation details and
/// must be implemented directly by the renderer.
#[derive(Debug, Copy, Clone)]
pub enum PrimitiveEffect {
    /// Primitive Delay effect
    Delay,
    /// Primitive Constant effect.
    /// Also serves as a unit step;
    /// Returns the float value for t >= 0, else 0.
    F32Constant,
    /// Primitive effect to multiply TWO input streams sample-wise.
    Multiply,
    /// Primitive effect to calculate A/B.
    /// Note: because not all floating point numbers have inverses,
    /// A * (1/B) != A/B. Hence, we need division (not inversion) for proper
    /// precision.
    Divide,
    /// Primitive effect to calculate A%B (true modulo; not remainder.
    /// Result, y,  is always positive: y is bounded by [0, B).
    Modulo,
    /// Primitive effect to return the sample-wise minimum of two input streams.
    /// Max(A, B) can be implemented as -Min(-A, -B).
    /// The choice to define Min instead of Max was mostly arbitrary,
    /// and chosen because Min is more common in linear programming to avoid dealing
    /// with Inf.
    Minimum,
}

impl Effect {
    pub fn are_slots_connected(&self, from_slot: u32, to_slot: u32) -> bool {
        match self.data {
            EffectData::RouteGraph(ref g) => g.are_slots_connected(from_slot, to_slot),
            // For primitive effects, we assume ALL slots are connected.
            _ => true,
        }
    }
    pub fn id(&self) -> &EffectId {
        &self.id
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Given the effect's information, and an interface by which to load
    /// resources, return an actual Effect.
    pub fn from_id(id: EffectId, resman: &ResMan) -> ResultE<Rc<Self>> {
        // For primitive effects, don't attempt to locate their descriptions (they don't exist)
        let prim_effect = id.get_primitive_url().map(|url| match url.path() {
            "/F32Constant" => PrimitiveEffect::F32Constant,
            "/Delay" => PrimitiveEffect::Delay,
            "/Multiply" => PrimitiveEffect::Multiply,
            "/Divide" => PrimitiveEffect::Divide,
            "/Modulo" => PrimitiveEffect::Modulo,
            "/Minimum" => PrimitiveEffect::Minimum,
            _ => panic!("Unrecognized primitive effect: {} (full url: {})", url.path(), url),
        });
        if let Some(prim_effect) = prim_effect {
            let me = Self {
                meta: EffectMeta {
                    name: id.name.clone(),
                    urls: id.urls.clone(),
                    // Primitive effects have undocumented I/O;
                    inputs: Default::default(),
                    outputs: Default::default(),
                },
                id: id,
                data: EffectData::Primitive(prim_effect),
            };
            return Ok(Rc::new(me));
        }

        // Locate descriptions for non-primitive effects
        for (path, reader) in resman.find_effect(&id) {
            // Try to deserialize to an effect description
            let desc: Result<EffectDesc, serde_json::Error> = serde_json::from_reader(reader);
            match desc {
                Ok(desc) => {
                    // TODO: since we've matched the effect, we only need to recalculate
                    // the id if the original search had missing hashes.
                    let id = desc.id();
                    match RouteGraph::from_adjlist(desc.adjlist, resman) {
                        Ok(graph) => {
                            let me = Self {
                                id,
                                meta: desc.meta,
                                data: EffectData::RouteGraph(graph),
                            };
                            // TODO: implement some form of caching
                            return Ok(Rc::new(me));
                        },
                        Err(error) => warn!("RouteGraph::from_adjlist failed: {:?}", error)
                    }
                },
                Err(error) => {
                    // Try parsing the file as an audio stream.
                    if let Ok(buffer) = AudioBuffer::from_path(path) {
                        let me = Self {
                            meta: EffectMeta {
                                name: id.name.clone(),
                                urls: id.urls.clone(),
                                // TODO: should be able to extract the number of outputs from the
                                // audio buffer
                                inputs: Default::default(),
                                outputs: Default::default(),
                            },
                            // TODO: refactor to avoid this clone.
                            id: id.clone(),
                            data: EffectData::Buffer(buffer),
                        };
                        // TODO: implement some form of caching
                        return Ok(Rc::new(me));
                    } else {
                        warn!("Unable to deserialize EffectDesc: {:?}", error)
                    }
                }
            }
        }
        // No matching effects
        Err(Error::NoMatchingEffect(id))
    }
    pub fn data(&self) -> &EffectData {
        &self.data
    }
}

impl EffectId {
    pub fn new<U>(name: String, sha256: Option<[u8; 32]>, urls: U) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            sha256,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
        }
    }
    pub fn sha256(&self) -> &Option<[u8; 32]> {
        &self.sha256
    }
    /// Returns true if the effect cannot be decomposed.
    /// This is determined by the effect providing a SINGLE url, with the primitive:// Schema
    pub fn is_primitive(&self) -> bool {
        self.urls.len() == 1 && self.urls.iter().all(|url| {
            url.scheme() == "primitive"
        })
    }
    pub fn get_primitive_url(&self) -> Option<&Url> {
        if self.is_primitive() {
            self.urls.iter().next().map(|url| url.deref())
        } else {
            None
        }
    }
}

impl EffectDesc {
    pub fn new(meta: EffectMeta, adjlist: AdjList) -> Self {
        Self{ meta, adjlist }
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    pub fn id(&self) -> EffectId {
        // TODO: calculate sha using a smaller buffer
        let as_vec = serde_json::to_vec(self).unwrap();
        let result = digest_reader::<Sha256>(&mut Cursor::new(as_vec)).unwrap();
        let mut hash: [u8; 32] = Default::default();
        hash.copy_from_slice(result.as_slice());
        EffectId {
            name: self.meta.name.clone(),
            sha256: Some(hash),
            urls: self.meta.urls.clone(),
        }
    }
}

impl EffectMeta {
    pub fn new<U>(name: String, urls: U, inputs: Vec<EffectInput>, outputs: Vec<EffectOutput>) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
            inputs,
            outputs,
        }
    }
    pub fn name(&self) -> &str {
        self.name.as_str()
    }
    pub fn inputs(&self) -> &Vec<EffectInput> {
        &self.inputs
    }
    pub fn outputs(&self) -> &Vec<EffectOutput> {
        &self.outputs
    }
    pub fn inputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectInput> + 'a {
        self.inputs.iter().filter(move |item| item.name() == name)
    }
    pub fn outputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectOutput> + 'a {
        self.outputs.iter().filter(move |item| item.name() == name)
    }
}

impl EffectIO {
    pub fn new(name: String, channel: u8) -> Self {
        Self{ name, channel }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn channel(&self) -> u8 {
        self.channel
    }
    pub fn is_source(&self) -> bool {
        self.name == "source"
    }
    pub fn is_result(&self) -> bool {
        self.name == "result"
    }
}

impl PartialEq for EffectMeta {
    // Equality implemented in a way where we can easily check things like
    //   "Is this the same primitive Delay effect this renderer knows how to implement?"
    fn eq(&self, other: &EffectMeta) -> bool {
        self.name == other.name &&
            self.urls == other.urls
    }
}
impl Eq for EffectMeta {}

impl PartialEq for Effect {
    fn eq(&self, other: &Effect) -> bool {
        self.meta == other.meta
    }
}
impl Eq for Effect {}

<commit_msg>Verify hash for primitive effects.<commit_after>use std::collections::HashSet;
use std::io::Cursor;
use std::ops::Deref;
use std::rc::Rc;

use digest::digest_reader;
use serde_json;
use sha2::Sha256;
use url::Url;
use url_serde;

use resman::{AudioBuffer, ResMan};
use super::routegraph::RouteGraph;
use super::adjlist::AdjList;

#[derive(Debug)]
pub enum Error {
    /// No effect matches the metadata requested.
    NoMatchingEffect(EffectId),
}

/// Alias for a `Result` with our error type.
pub type ResultE<T> = Result<T, Error>;

/// Serializable info needed to look up an effect.
#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectId {
    /// Canonical name of the effect
    name: String,
    /// Hash of the effect's definition file, or None if the effect is primitive
    sha256: Option<[u8; 32]>,
    // TODO: consider a smallset.
    // TODO: use #[serde(with = "url_serde")]
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
}

/// All information that will be loaded from disk/network to describe how to
/// synthesize the Effect.
#[derive(Serialize, Deserialize)]
pub struct EffectDesc {
    meta: EffectMeta,
    adjlist: AdjList,
}

/// Validated version of `EffectDesc`. Guaranteed to be synthesizable.
pub struct Effect {
    id: EffectId,
    meta: EffectMeta,
    // TODO: Effects are immutable, so we can make this an AdjList and store
    // the connectivity information separately.
    // option, because effect MAY be primitive.
    data: EffectData,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectMeta {
    /// Canonical name of the effect
    name: String,
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
    inputs: Vec<EffectInput>,
    outputs: Vec<EffectOutput>,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectIO {
    name: String,
    channel: u8,
}
pub type EffectInput = EffectIO;
pub type EffectOutput = EffectIO;

/// Implementation details of an Effect.
/// Either its route graph, a primitive effect, etc.
pub enum EffectData {
    RouteGraph(RouteGraph),
    Primitive(PrimitiveEffect),
    Buffer(AudioBuffer),
}

/// Effects that cannot be decomposed; they have no implementation details and
/// must be implemented directly by the renderer.
#[derive(Debug, Copy, Clone)]
pub enum PrimitiveEffect {
    /// Primitive Delay effect
    Delay,
    /// Primitive Constant effect.
    /// Also serves as a unit step;
    /// Returns the float value for t >= 0, else 0.
    F32Constant,
    /// Primitive effect to multiply TWO input streams sample-wise.
    Multiply,
    /// Primitive effect to calculate A/B.
    /// Note: because not all floating point numbers have inverses,
    /// A * (1/B) != A/B. Hence, we need division (not inversion) for proper
    /// precision.
    Divide,
    /// Primitive effect to calculate A%B (true modulo; not remainder.
    /// Result, y,  is always positive: y is bounded by [0, B).
    Modulo,
    /// Primitive effect to return the sample-wise minimum of two input streams.
    /// Max(A, B) can be implemented as -Min(-A, -B).
    /// The choice to define Min instead of Max was mostly arbitrary,
    /// and chosen because Min is more common in linear programming to avoid dealing
    /// with Inf.
    Minimum,
}

impl Effect {
    pub fn are_slots_connected(&self, from_slot: u32, to_slot: u32) -> bool {
        match self.data {
            EffectData::RouteGraph(ref g) => g.are_slots_connected(from_slot, to_slot),
            // For primitive effects, we assume ALL slots are connected.
            _ => true,
        }
    }
    pub fn id(&self) -> &EffectId {
        &self.id
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Given the effect's information, and an interface by which to load
    /// resources, return an actual Effect.
    pub fn from_id(id: EffectId, resman: &ResMan) -> ResultE<Rc<Self>> {
        // For primitive effects, don't attempt to locate their descriptions (they don't exist)
        let prim_effect = id.get_primitive_url().map(|url| match url.path() {
            "/F32Constant" => PrimitiveEffect::F32Constant,
            "/Delay" => PrimitiveEffect::Delay,
            "/Multiply" => PrimitiveEffect::Multiply,
            "/Divide" => PrimitiveEffect::Divide,
            "/Modulo" => PrimitiveEffect::Modulo,
            "/Minimum" => PrimitiveEffect::Minimum,
            // TODO: return an error type
            _ => panic!("Unrecognized primitive effect: {} (full url: {})", url.path(), url),
        });
        if let Some(prim_effect) = prim_effect {
            if id.sha256 == None {
                let me = Self {
                    meta: EffectMeta {
                        name: id.name.clone(),
                        urls: id.urls.clone(),
                        // Primitive effects have undocumented I/O;
                        inputs: Default::default(),
                        outputs: Default::default(),
                    },
                    // sha256 was already verified; no need to update it
                    id: id,
                    data: EffectData::Primitive(prim_effect),
                };
                return Ok(Rc::new(me));
            } else {
                warn!("Attempted to load a primitive Effect, but cannot because of mismatched sha256");
            }
        }

        // Locate descriptions for non-primitive effects
        for (path, reader) in resman.find_effect(&id) {
            // Try to deserialize to an effect description
            let desc: Result<EffectDesc, serde_json::Error> = serde_json::from_reader(reader);
            match desc {
                Ok(desc) => {
                    // TODO: since we've matched the effect, we only need to recalculate
                    // the id if the original search had missing hashes.
                    let id = desc.id();
                    match RouteGraph::from_adjlist(desc.adjlist, resman) {
                        Ok(graph) => {
                            let me = Self {
                                id,
                                meta: desc.meta,
                                data: EffectData::RouteGraph(graph),
                            };
                            // TODO: implement some form of caching
                            return Ok(Rc::new(me));
                        },
                        Err(error) => warn!("RouteGraph::from_adjlist failed: {:?}", error)
                    }
                },
                Err(error) => {
                    // Try parsing the file as an audio stream.
                    if let Ok(buffer) = AudioBuffer::from_path(path) {
                        let me = Self {
                            meta: EffectMeta {
                                name: id.name.clone(),
                                urls: id.urls.clone(),
                                // TODO: should be able to extract the number of outputs from the
                                // audio buffer
                                inputs: Default::default(),
                                outputs: Default::default(),
                            },
                            // TODO: refactor to avoid this clone.
                            // TODO: sha256 may need to be updated.
                            id: id.clone(),
                            data: EffectData::Buffer(buffer),
                        };
                        // TODO: implement some form of caching
                        return Ok(Rc::new(me));
                    } else {
                        warn!("Unable to deserialize EffectDesc: {:?}", error)
                    }
                }
            }
        }
        // No matching effects
        Err(Error::NoMatchingEffect(id))
    }
    pub fn data(&self) -> &EffectData {
        &self.data
    }
}

impl EffectId {
    pub fn new<U>(name: String, sha256: Option<[u8; 32]>, urls: U) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            sha256,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
        }
    }
    pub fn sha256(&self) -> &Option<[u8; 32]> {
        &self.sha256
    }
    /// Returns true if the effect cannot be decomposed.
    /// This is determined by the effect providing a SINGLE url, with the primitive:// Schema
    pub fn is_primitive(&self) -> bool {
        self.urls.len() == 1 && self.urls.iter().all(|url| {
            url.scheme() == "primitive"
        })
    }
    pub fn get_primitive_url(&self) -> Option<&Url> {
        if self.is_primitive() {
            self.urls.iter().next().map(|url| url.deref())
        } else {
            None
        }
    }
}

impl EffectDesc {
    pub fn new(meta: EffectMeta, adjlist: AdjList) -> Self {
        Self{ meta, adjlist }
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    pub fn id(&self) -> EffectId {
        // TODO: calculate sha using a smaller buffer
        let as_vec = serde_json::to_vec(self).unwrap();
        let result = digest_reader::<Sha256>(&mut Cursor::new(as_vec)).unwrap();
        let mut hash: [u8; 32] = Default::default();
        hash.copy_from_slice(result.as_slice());
        EffectId {
            name: self.meta.name.clone(),
            sha256: Some(hash),
            urls: self.meta.urls.clone(),
        }
    }
}

impl EffectMeta {
    pub fn new<U>(name: String, urls: U, inputs: Vec<EffectInput>, outputs: Vec<EffectOutput>) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
            inputs,
            outputs,
        }
    }
    pub fn name(&self) -> &str {
        self.name.as_str()
    }
    pub fn inputs(&self) -> &Vec<EffectInput> {
        &self.inputs
    }
    pub fn outputs(&self) -> &Vec<EffectOutput> {
        &self.outputs
    }
    pub fn inputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectInput> + 'a {
        self.inputs.iter().filter(move |item| item.name() == name)
    }
    pub fn outputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectOutput> + 'a {
        self.outputs.iter().filter(move |item| item.name() == name)
    }
}

impl EffectIO {
    pub fn new(name: String, channel: u8) -> Self {
        Self{ name, channel }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn channel(&self) -> u8 {
        self.channel
    }
    pub fn is_source(&self) -> bool {
        self.name == "source"
    }
    pub fn is_result(&self) -> bool {
        self.name == "result"
    }
}

impl PartialEq for EffectMeta {
    // Equality implemented in a way where we can easily check things like
    //   "Is this the same primitive Delay effect this renderer knows how to implement?"
    fn eq(&self, other: &EffectMeta) -> bool {
        self.name == other.name &&
            self.urls == other.urls
    }
}
impl Eq for EffectMeta {}

impl PartialEq for Effect {
    fn eq(&self, other: &Effect) -> bool {
        self.meta == other.meta
    }
}
impl Eq for Effect {}

<|endoftext|>


<commit_before>use std::collections::HashSet;
use std::io::Cursor;
use std::ops::Deref;
use std::rc::Rc;

use digest::Digest;
use serde_json;
use sha2::Sha256;
use url::Url;
use url_serde;

use resman::ResMan;
use super::routegraph::RouteGraph;
use super::adjlist::AdjList;

#[derive(Debug)]
pub enum Error {
    /// No effect matches the metadata requested.
    NoMatchingEffect(EffectId),
}

/// Alias for a `Result` with our error type.
pub type ResultE<T> = Result<T, Error>;

/// Serializable info needed to look up an effect.
#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectId {
    /// Canonical name of the effect
    name: String,
    /// Hash of the effect's definition file, or None if the effect is primitive
    sha256: Option<[u8; 32]>,
    // TODO: consider a smallset.
    // TODO: use #[serde(with = "url_serde")]
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
}

/// All information that will be loaded from disk/network to describe how to
/// synthesize the Effect.
#[derive(Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectDesc {
    meta: EffectMeta,
    adjlist: AdjList,
}

/// Validated version of `EffectDesc`. Guaranteed to be synthesizable.
#[derive(Debug)]
pub struct Effect {
    meta: EffectMeta,
    // TODO: Effects are immutable, so we can make this an AdjList and store
    // the connectivity information separately.
    data: EffectData,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectMeta {
    id: EffectId,
    inputs: Vec<EffectInput>,
    outputs: Vec<EffectOutput>,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectIO {
    name: String,
    channel: u8,
}
pub type EffectInput = EffectIO;
pub type EffectOutput = EffectIO;

/// Implementation details of an Effect.
/// Either its route graph, a primitive effect, etc.
#[derive(Debug)]
pub enum EffectData {
    RouteGraph(RouteGraph),
    Primitive(PrimitiveEffect),
}

/// Effects that cannot be decomposed; they have no implementation details and
/// must be implemented directly by the renderer.
#[derive(Debug, Copy, Clone)]
pub enum PrimitiveEffect {
    /// Primitive Delay effect
    Delay,
    /// Primitive Constant effect.
    /// Also serves as a unit step;
    /// Returns the float value for t >= 0, else 0.
    F32Constant,
    /// Sum 2 floating point streams.
    Sum2,
    /// Primitive effect to multiply TWO input streams sample-wise.
    Multiply,
    /// Primitive effect to calculate A/B.
    /// Note: because not all floating point numbers have inverses,
    /// A * (1/B) != A/B. Hence, we need division (not inversion) for proper
    /// precision.
    Divide,
    /// Primitive effect to calculate A%B (true modulo; not remainder.
    /// Result, y,  is always positive: y is bounded by [0, B).
    Modulo,
    /// Primitive effect to return the sample-wise minimum of two input streams.
    /// Max(A, B) can be implemented as -Min(-A, -B).
    /// The choice to define Min instead of Max was mostly arbitrary,
    /// and chosen because Min is more common in linear programming to avoid dealing
    /// with Inf.
    Minimum,
}

impl Effect {
    pub fn are_slots_connected(&self, from_slot: u32, to_slot: u32) -> bool {
        match self.data {
            EffectData::RouteGraph(ref g) => g.are_slots_connected(from_slot, to_slot),
            // For primitive effects, we assume ALL slots are connected.
            _ => true,
        }
    }
    pub fn id(&self) -> &EffectId {
        &self.meta.id
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Given the effect's information, and an interface by which to load
    /// resources, return an actual Effect.
    pub fn from_id(id: EffectId, resman: &ResMan) -> ResultE<Rc<Self>> {
        // For primitive effects, don't attempt to locate their descriptions (they don't exist)
        let prim_effect = id.get_primitive_url().and_then(PrimitiveEffect::from_url);
        // Attempt to instantiate a primitive effect, if the URL matched.
        if let Some(prim_effect) = prim_effect {
            if id.sha256 == None {
                let me = Self {
                    meta: EffectMeta {
                        // sha256 was already verified; no need to update it
                        id: id,
                        // Primitive effects have undocumented I/O;
                        inputs: Default::default(),
                        outputs: Default::default(),
                    },
                    data: EffectData::Primitive(prim_effect),
                };
                return Ok(Rc::new(me));
            } else {
                warn!("Attempted to load a primitive Effect, but cannot because of mismatched sha256: {:?}", id.sha256);
            }
        }

        // Locate descriptions for non-primitive effects
        for (path, reader) in resman.find_effect(&id) {
            // Try to deserialize to an effect description
            let desc: Result<EffectDesc, serde_json::Error> = serde_json::from_reader(reader);
            match desc {
                Ok(mut desc) => {
                    if desc.meta.id.name() == id.name() {
                        desc.update_id();
                        match RouteGraph::from_adjlist(desc.adjlist, resman) {
                            Ok(graph) => {
                                let me = Self {
                                    meta: desc.meta,
                                    data: EffectData::RouteGraph(graph),
                                };
                                // TODO: implement some form of caching
                                return Ok(Rc::new(me));
                            },
                            Err(error) => warn!("[{:?}] RouteGraph::from_adjlist failed: {:?}", path, error)
                        }
                    } else {
                        trace!("[{:?}] Effect names differ: wanted {:?} got {:?}", path, id.name(), desc.meta.id.name());
                    }
                },
                Err(error) => {
                    warn!("[{:?}] Unable to deserialize EffectDesc: {:?}", path, error)
                }
            }
        }
        // No matching effects
        Err(Error::NoMatchingEffect(id))
    }
    pub fn data(&self) -> &EffectData {
        &self.data
    }
}

impl EffectId {
    pub fn new<U>(name: String, sha256: Option<[u8; 32]>, urls: U) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            sha256,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
        }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn sha256(&self) -> &Option<[u8; 32]> {
        &self.sha256
    }
    /// Returns true if the effect cannot be decomposed.
    /// This is determined by the effect providing a SINGLE url, with the primitive:// Schema
    pub fn is_primitive(&self) -> bool {
        self.urls.len() == 1 && self.urls.iter().all(|url| {
            url.scheme() == "primitive"
        })
    }
    pub fn get_primitive_url(&self) -> Option<&Url> {
        if self.is_primitive() {
            self.urls.iter().next().map(|url| url.deref())
        } else {
            None
        }
    }
}

impl EffectDesc {
    pub fn new(meta: EffectMeta, adjlist: AdjList) -> Self {
        Self{ meta, adjlist }
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Make sure the id is fully populated with hashes, etc.
    fn update_id(&mut self) {
        if self.meta.id.sha256.is_none() {
            // TODO: calculate sha using a smaller buffer
            let as_vec = serde_json::to_vec(self).unwrap();
            let result = Sha256::digest_reader(&mut Cursor::new(as_vec)).unwrap();
            let mut hash: [u8; 32] = Default::default();
            hash.copy_from_slice(result.as_slice());
            self.meta.id.sha256 = Some(hash);
        }
    }
}

impl EffectMeta {
    pub fn new<U>(name: String, urls: U, inputs: Vec<EffectInput>, outputs: Vec<EffectOutput>) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            id: EffectId::new(name, None, urls),
            inputs,
            outputs,
        }
    }
    pub fn name(&self) -> &str {
        self.id.name()
    }
    /// Deprecated: will be replaced by an iterator which also works for
    /// primitive effects.
    /// TODO: remove.
    pub fn inputs(&self) -> &Vec<EffectInput> {
        &self.inputs
    }
    pub fn outputs(&self) -> &Vec<EffectOutput> {
        &self.outputs
    }
    pub fn inputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectInput> + 'a {
        self.inputs.iter().filter(move |item| item.name() == name)
    }
    pub fn outputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=&EffectOutput> + 'a {
        self.outputs.iter().filter(move |item| item.name() == name)
    }
    pub fn is_valid_input(&self, slotno: u32) -> bool {
        (slotno as usize) < self.inputs.len() || {
            match self.id.get_primitive_url().and_then(PrimitiveEffect::from_url) {
                Some(PrimitiveEffect::F32Constant) => false,
                Some(_) => slotno < 2,
                None => false
            }
        }
    }
    pub fn is_valid_output(&self, slotno: u32) -> bool {
        (slotno as usize) < self.outputs.len() || {
            match self.id.get_primitive_url().and_then(PrimitiveEffect::from_url) {
                Some(PrimitiveEffect::F32Constant) => true,
                Some(_) => slotno < 1,
                None => false
            }
        }
    }
}

impl EffectIO {
    pub fn new(name: String, channel: u8) -> Self {
        Self{ name, channel }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn channel(&self) -> u8 {
        self.channel
    }
    pub fn is_source(&self) -> bool {
        self.name == "source"
    }
    pub fn is_result(&self) -> bool {
        self.name == "result"
    }
}

impl PrimitiveEffect {
    fn from_url(url: &Url) -> Option<Self> {
        if url.scheme() == "primitive" {
            match url.path() {
                "/Delay"       => Some(PrimitiveEffect::Delay),
                "/F32Constant" => Some(PrimitiveEffect::F32Constant),
                "/Sum2"        => Some(PrimitiveEffect::Sum2),
                "/Multiply"    => Some(PrimitiveEffect::Multiply),
                "/Divide"      => Some(PrimitiveEffect::Divide),
                "/Modulo"      => Some(PrimitiveEffect::Modulo),
                "/Minimum"     => Some(PrimitiveEffect::Minimum),
                _ => {
                    warn!("Unrecognized primitive effect: {} (full url: {})", url.path(), url);
                    None
                }
            }
        } else {
            None
        }
    }
}

impl PartialEq for EffectMeta {
    // Equality implemented in a way where we can easily check things like
    //   "Is this the same primitive Delay effect this renderer knows how to implement?"
    fn eq(&self, other: &EffectMeta) -> bool {
        self.id.name == other.id.name &&
            self.id.urls == other.id.urls
    }
}
impl Eq for EffectMeta {}

<commit_msg>Consolidate Effect I/O metadata into only iterators<commit_after>use std;
use std::collections::HashSet;
use std::io::Cursor;
use std::mem;
use std::ops::{Deref, Range};
use std::rc::Rc;

use digest::Digest;
use serde_json;
use sha2::Sha256;
use url::Url;
use url_serde;

use resman::ResMan;
use super::routegraph::RouteGraph;
use super::adjlist::AdjList;

#[derive(Debug)]
pub enum Error {
    /// No effect matches the metadata requested.
    NoMatchingEffect(EffectId),
}

/// Alias for a `Result` with our error type.
pub type ResultE<T> = Result<T, Error>;

/// Serializable info needed to look up an effect.
#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectId {
    /// Canonical name of the effect
    name: String,
    /// Hash of the effect's definition file, or None if the effect is primitive
    sha256: Option<[u8; 32]>,
    // TODO: consider a smallset.
    // TODO: use #[serde(with = "url_serde")]
    /// List of URLs where the Effect can be obtained
    urls: HashSet<url_serde::Serde<Url>>,
}

/// All information that will be loaded from disk/network to describe how to
/// synthesize the Effect.
#[derive(Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectDesc {
    meta: EffectMeta,
    adjlist: AdjList,
}

/// Validated version of `EffectDesc`. Guaranteed to be synthesizable.
#[derive(Debug)]
pub struct Effect {
    meta: EffectMeta,
    // TODO: Effects are immutable, so we can make this an AdjList and store
    // the connectivity information separately.
    data: EffectData,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectMeta {
    id: EffectId,
    inputs: Vec<EffectInput>,
    outputs: Vec<EffectOutput>,
}

#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize)]
pub struct EffectIO {
    name: String,
    channel: u8,
}
pub type EffectInput = EffectIO;
pub type EffectOutput = EffectIO;

/// Implementation details of an Effect.
/// Either its route graph, a primitive effect, etc.
#[derive(Debug)]
pub enum EffectData {
    RouteGraph(RouteGraph),
    Primitive(PrimitiveEffect),
}

/// Effects that cannot be decomposed; they have no implementation details and
/// must be implemented directly by the renderer.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PrimitiveEffect {
    /// Primitive Delay effect
    Delay,
    /// Primitive Constant effect.
    /// Also serves as a unit step;
    /// Returns the float value for t >= 0, else 0.
    F32Constant,
    /// Sum 2 floating point streams.
    Sum2,
    /// Primitive effect to multiply TWO input streams sample-wise.
    Multiply,
    /// Primitive effect to calculate A/B.
    /// Note: because not all floating point numbers have inverses,
    /// A * (1/B) != A/B. Hence, we need division (not inversion) for proper
    /// precision.
    Divide,
    /// Primitive effect to calculate A%B (true modulo; not remainder.
    /// Result, y,  is always positive: y is bounded by [0, B).
    Modulo,
    /// Primitive effect to return the sample-wise minimum of two input streams.
    /// Max(A, B) can be implemented as -Min(-A, -B).
    /// The choice to define Min instead of Max was mostly arbitrary,
    /// and chosen because Min is more common in linear programming to avoid dealing
    /// with Inf.
    Minimum,
}

/// Iterator over the outputs of a F32Constant primitive effect
pub struct F32ConstIterator {
    loc: Range<u32>,
}

impl Effect {
    pub fn are_slots_connected(&self, from_slot: u32, to_slot: u32) -> bool {
        match self.data {
            EffectData::RouteGraph(ref g) => g.are_slots_connected(from_slot, to_slot),
            // For primitive effects, we assume ALL slots are connected.
            _ => true,
        }
    }
    pub fn id(&self) -> &EffectId {
        &self.meta.id
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Given the effect's information, and an interface by which to load
    /// resources, return an actual Effect.
    pub fn from_id(id: EffectId, resman: &ResMan) -> ResultE<Rc<Self>> {
        // For primitive effects, don't attempt to locate their descriptions (they don't exist)
        let prim_effect = id.get_primitive_url().and_then(PrimitiveEffect::from_url);
        // Attempt to instantiate a primitive effect, if the URL matched.
        if let Some(prim_effect) = prim_effect {
            if id.sha256 == None {
                let me = Self {
                    meta: EffectMeta {
                        // sha256 was already verified; no need to update it
                        id: id,
                        // Primitive effects have undocumented I/O;
                        inputs: Default::default(),
                        outputs: Default::default(),
                    },
                    data: EffectData::Primitive(prim_effect),
                };
                return Ok(Rc::new(me));
            } else {
                warn!("Attempted to load a primitive Effect, but cannot because of mismatched sha256: {:?}", id.sha256);
            }
        }

        // Locate descriptions for non-primitive effects
        for (path, reader) in resman.find_effect(&id) {
            // Try to deserialize to an effect description
            let desc: Result<EffectDesc, serde_json::Error> = serde_json::from_reader(reader);
            match desc {
                Ok(mut desc) => {
                    if desc.meta.id.name() == id.name() {
                        desc.update_id();
                        match RouteGraph::from_adjlist(desc.adjlist, resman) {
                            Ok(graph) => {
                                let me = Self {
                                    meta: desc.meta,
                                    data: EffectData::RouteGraph(graph),
                                };
                                // TODO: implement some form of caching
                                return Ok(Rc::new(me));
                            },
                            Err(error) => warn!("[{:?}] RouteGraph::from_adjlist failed: {:?}", path, error)
                        }
                    } else {
                        trace!("[{:?}] Effect names differ: wanted {:?} got {:?}", path, id.name(), desc.meta.id.name());
                    }
                },
                Err(error) => {
                    warn!("[{:?}] Unable to deserialize EffectDesc: {:?}", path, error)
                }
            }
        }
        // No matching effects
        Err(Error::NoMatchingEffect(id))
    }
    pub fn data(&self) -> &EffectData {
        &self.data
    }
}

impl EffectId {
    pub fn new<U>(name: String, sha256: Option<[u8; 32]>, urls: U) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            name,
            sha256,
            urls: urls.into_iter().map(url_serde::Serde).collect(),
        }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn sha256(&self) -> &Option<[u8; 32]> {
        &self.sha256
    }
    /// Returns true if the effect cannot be decomposed.
    /// This is determined by the effect providing a SINGLE url, with the primitive:// Schema
    pub fn is_primitive(&self) -> bool {
        self.urls.len() == 1 && self.urls.iter().all(|url| {
            url.scheme() == "primitive"
        })
    }
    pub fn get_primitive_url(&self) -> Option<&Url> {
        if self.is_primitive() {
            self.urls.iter().next().map(|url| url.deref())
        } else {
            None
        }
    }
}

impl EffectDesc {
    pub fn new(meta: EffectMeta, adjlist: AdjList) -> Self {
        Self{ meta, adjlist }
    }
    pub fn meta(&self) -> &EffectMeta {
        &self.meta
    }
    /// Make sure the id is fully populated with hashes, etc.
    fn update_id(&mut self) {
        if self.meta.id.sha256.is_none() {
            // TODO: calculate sha using a smaller buffer
            let as_vec = serde_json::to_vec(self).unwrap();
            let result = Sha256::digest_reader(&mut Cursor::new(as_vec)).unwrap();
            let mut hash: [u8; 32] = Default::default();
            hash.copy_from_slice(result.as_slice());
            self.meta.id.sha256 = Some(hash);
        }
    }
}

impl EffectMeta {
    pub fn new<U>(name: String, urls: U, inputs: Vec<EffectInput>, outputs: Vec<EffectOutput>) -> Self
        where U: IntoIterator<Item=Url>
    {
        Self {
            id: EffectId::new(name, None, urls),
            inputs,
            outputs,
        }
    }
    pub fn name(&self) -> &str {
        self.id.name()
    }
    fn inputs<'a>(&'a self) -> Box<Iterator<Item=EffectInput> + 'a> {
        match self.prim_effect() {
            Some(PrimitiveEffect::Delay) => Box::new(vec![
                    EffectInput::new("source".into(), 0),
                    EffectInput::new("frames".into(), 0)
                ].into_iter()),
            Some(PrimitiveEffect::F32Constant) => Box::new([].iter().cloned()),
            Some(PrimitiveEffect::Sum2) | Some(PrimitiveEffect::Multiply) | Some(PrimitiveEffect::Minimum) => Box::new(vec![
                    EffectInput::new("source".into(), 0),
                    EffectInput::new("source2".into(), 0),
                ].into_iter()),
            Some(PrimitiveEffect::Divide) | Some(PrimitiveEffect::Modulo) => Box::new(vec![
                    EffectInput::new("source".into(), 0),
                    EffectInput::new("divisor".into(), 0),
                ].into_iter()),
            _ => Box::new(self.inputs.iter().cloned())
        }
    }
    fn outputs<'a>(&'a self) -> Box<Iterator<Item=EffectOutput> + 'a> {
        match self.prim_effect() {
            Some(PrimitiveEffect::F32Constant) => Box::new(F32ConstIterator::new()),
            Some(_) => Box::new(Some(EffectOutput::new("result".into(), 0)).into_iter()),
            None => Box::new(self.outputs.iter().cloned())
        }
    }
    pub fn inputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=EffectInput> + 'a {
        self.inputs().filter(move |item| item.name() == name)
    }
    pub fn outputs_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item=EffectOutput> + 'a {
        self.outputs().filter(move |item| item.name() == name)
    }
    pub fn is_valid_input(&self, slotno: u32) -> bool {
        self.inputs().nth(slotno as usize).is_some()
    }
    pub fn is_valid_output(&self, slotno: u32) -> bool {
        self.outputs().nth(slotno as usize).is_some()
    }
    fn prim_effect(&self) -> Option<PrimitiveEffect> {
        self.id.get_primitive_url().and_then(PrimitiveEffect::from_url)
    }
}

impl EffectIO {
    pub fn new(name: String, channel: u8) -> Self {
        Self{ name, channel }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn channel(&self) -> u8 {
        self.channel
    }
    pub fn is_source(&self) -> bool {
        self.name == "source"
    }
    pub fn is_result(&self) -> bool {
        self.name == "result"
    }
}

impl PrimitiveEffect {
    fn from_url(url: &Url) -> Option<Self> {
        if url.scheme() == "primitive" {
            match url.path() {
                "/Delay"       => Some(PrimitiveEffect::Delay),
                "/F32Constant" => Some(PrimitiveEffect::F32Constant),
                "/Sum2"        => Some(PrimitiveEffect::Sum2),
                "/Multiply"    => Some(PrimitiveEffect::Multiply),
                "/Divide"      => Some(PrimitiveEffect::Divide),
                "/Modulo"      => Some(PrimitiveEffect::Modulo),
                "/Minimum"     => Some(PrimitiveEffect::Minimum),
                _ => {
                    warn!("Unrecognized primitive effect: {} (full url: {})", url.path(), url);
                    None
                }
            }
        } else {
            None
        }
    }
}

impl PartialEq for EffectMeta {
    // Equality implemented in a way where we can easily check things like
    //   "Is this the same primitive Delay effect this renderer knows how to implement?"
    fn eq(&self, other: &EffectMeta) -> bool {
        self.id.name == other.id.name &&
            self.id.urls == other.id.urls
    }
}
impl Eq for EffectMeta {}

impl F32ConstIterator {
    fn new() -> Self {
        Self{ loc: (0..std::u32::MAX) }
    }
    fn to_f32(v: u32) -> f32 {
        unsafe { mem::transmute(v) }
    }
    fn to_output(v: u32) -> EffectOutput {
        let name = format!("const{}", Self::to_f32(v));
        EffectOutput::new(name, 0)
    }
}

impl Iterator for F32ConstIterator {
    type Item = EffectOutput;
    fn next(&mut self) -> Option<Self::Item> {
        self.loc.next().map(Self::to_output)
    }
    fn nth(&mut self, n: usize) -> Option<Self::Item> {
        self.loc.nth(n).map(Self::to_output)
    }
}
<|endoftext|>


<commit_before>import result::result;
import std::getopts;

export output_format;
export output_style;
export config;
export default_config;
export parse_config;
export usage;
export markdown, pandoc_html;
export doc_per_crate, doc_per_mod;

/// The type of document to output
enum output_format {
    /// Markdown
    markdown,
    /// HTML, via markdown and pandoc
    pandoc_html
}

/// How to organize the output
enum output_style {
    /// All in a single document
    doc_per_crate,
    /// Each module in its own document
    doc_per_mod
}

/// The configuration for a rustdoc session
type config = {
    input_crate: ~str,
    output_dir: ~str,
    output_format: output_format,
    output_style: output_style,
    pandoc_cmd: option<~str>
};

fn opt_output_dir() -> ~str { ~"output-dir" }
fn opt_output_format() -> ~str { ~"output-format" }
fn opt_output_style() -> ~str { ~"output-style" }
fn opt_pandoc_cmd() -> ~str { ~"pandoc-cmd" }
fn opt_help() -> ~str { ~"h" }

fn opts() -> ~[(getopts::opt, ~str)] {
    ~[
        (getopts::optopt(opt_output_dir()),
         ~"--output-dir <val>     put documents here"),
        (getopts::optopt(opt_output_format()),
         ~"--output-format <val>  either 'markdown' or 'html'"),
        (getopts::optopt(opt_output_style()),
         ~"--output-style <val>   either 'doc-per-crate' or 'doc-per-mod'"),
        (getopts::optopt(opt_pandoc_cmd()),
         ~"--pandoc-cmd <val>     the command for running pandoc"),
        (getopts::optflag(opt_help()),
         ~"-h                     print help")
    ]
}

fn usage() {
    import io::println;

    println(~"Usage: rustdoc ~[options] <cratefile>\n");
    println(~"Options:\n");
    for opts().each |opt| {
        println(fmt!{"    %s", opt.second()});
    }
    println(~"");
}

fn default_config(input_crate: ~str) -> config {
    {
        input_crate: input_crate,
        output_dir: ~".",
        output_format: pandoc_html,
        output_style: doc_per_mod,
        pandoc_cmd: none
    }
}

type program_output = fn~((&str), (&[~str])) ->
    {status: int, out: ~str, err: ~str};

fn mock_program_output(_prog: ~str, _args: ~[~str]) -> {
    status: int, out: ~str, err: ~str
} {
    {
        status: 0,
        out: ~"",
        err: ~""
    }
}

fn parse_config(args: ~[~str]) -> result<config, ~str> {
    parse_config_(args, run::program_output)
}

fn parse_config_(
    args: ~[~str],
    program_output: program_output
) -> result<config, ~str> {
    let args = vec::tail(args);
    let opts = vec::unzip(opts()).first();
    match getopts::getopts(args, opts) {
        result::ok(matches) => {
            if vec::len(matches.free) == 1u {
                let input_crate = vec::head(matches.free);
                config_from_opts(input_crate, matches, program_output)
            } else if vec::is_empty(matches.free) {
                result::err(~"no crates specified")
            } else {
                result::err(~"multiple crates specified")
            }
        }
        result::err(f) => {
            result::err(getopts::fail_str(f))
        }
    }
}

fn config_from_opts(
    input_crate: ~str,
    matches: getopts::matches,
    program_output: program_output
) -> result<config, ~str> {

    let config = default_config(input_crate);
    let result = result::ok(config);
    let result = do result::chain(result) |config| {
        let output_dir = getopts::opt_maybe_str(matches, opt_output_dir());
        result::ok({
            output_dir: option::get_default(output_dir, config.output_dir)
            with config
        })
    };
    let result = do result::chain(result) |config| {
        let output_format = getopts::opt_maybe_str(
            matches, opt_output_format());
        do option::map_default(output_format, result::ok(config))
            |output_format| {
            do result::chain(parse_output_format(output_format))
                |output_format| {

                result::ok({
                    output_format: output_format
                    with config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let output_style =
            getopts::opt_maybe_str(matches, opt_output_style());
        do option::map_default(output_style, result::ok(config))
            |output_style| {
            do result::chain(parse_output_style(output_style))
                |output_style| {
                result::ok({
                    output_style: output_style
                    with config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let pandoc_cmd = getopts::opt_maybe_str(matches, opt_pandoc_cmd());
        let pandoc_cmd = maybe_find_pandoc(
            config, pandoc_cmd, program_output);
        do result::chain(pandoc_cmd) |pandoc_cmd| {
            result::ok({
                pandoc_cmd: pandoc_cmd
                with config
            })
        }
    };
    return result;
}

fn parse_output_format(output_format: ~str) -> result<output_format, ~str> {
    match output_format {
      ~"markdown" => result::ok(markdown),
      ~"html" => result::ok(pandoc_html),
      _ => result::err(fmt!{"unknown output format '%s'", output_format})
    }
}

fn parse_output_style(output_style: ~str) -> result<output_style, ~str> {
    match output_style {
      ~"doc-per-crate" => result::ok(doc_per_crate),
      ~"doc-per-mod" => result::ok(doc_per_mod),
      _ => result::err(fmt!{"unknown output style '%s'", output_style})
    }
}

fn maybe_find_pandoc(
    config: config,
    maybe_pandoc_cmd: option<~str>,
    program_output: program_output
) -> result<option<~str>, ~str> {
    if config.output_format != pandoc_html {
        return result::ok(maybe_pandoc_cmd);
    }

    let possible_pandocs = match maybe_pandoc_cmd {
      some(pandoc_cmd) => ~[pandoc_cmd],
      none => {
        ~[~"pandoc"] + match os::homedir() {
          some(dir) => {
            ~[path::connect(dir, ~".cabal/bin/pandoc")]
          }
          none => ~[]
        }
      }
    };

    let pandoc = do vec::find(possible_pandocs) |pandoc| {
        let output = program_output(pandoc, ~[~"--version"]);
        debug!{"testing pandoc cmd %s: %?", pandoc, output};
        output.status == 0
    };

    if option::is_some(pandoc) {
        result::ok(pandoc)
    } else {
        result::err(~"couldn't find pandoc")
    }
}

#[test]
fn should_find_pandoc() {
    let config = {
        output_format: pandoc_html
        with default_config(~"test")
    };
    let mock_program_output = fn~(_prog: ~str, _args: ~[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 0, out: ~"pandoc 1.8.2.1", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, none, mock_program_output);
    assert result == result::ok(some(~"pandoc"));
}

#[test]
fn should_error_with_no_pandoc() {
    let config = {
        output_format: pandoc_html
        with default_config(~"test")
    };
    let mock_program_output = fn~(_prog: ~str, _args: ~[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 1, out: ~"", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, none, mock_program_output);
    assert result == result::err(~"couldn't find pandoc");
}

#[cfg(test)]
mod test {
    fn parse_config(args: ~[~str]) -> result<config, ~str> {
        parse_config_(args, mock_program_output)
    }
}

#[test]
fn should_error_with_no_crates() {
    let config = test::parse_config(~[~"rustdoc"]);
    assert result::get_err(config) == ~"no crates specified";
}

#[test]
fn should_error_with_multiple_crates() {
    let config =
        test::parse_config(~[~"rustdoc", ~"crate1.rc", ~"crate2.rc"]);
    assert result::get_err(config) == ~"multiple crates specified";
}

#[test]
fn should_set_output_dir_to_cwd_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_dir == ~".";
}

#[test]
fn should_set_output_dir_if_provided() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-dir", ~"snuggles"
    ]);
    assert result::get(config).output_dir == ~"snuggles";
}

#[test]
fn should_set_output_format_to_pandoc_html_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_format == pandoc_html;
}

#[test]
fn should_set_output_format_to_markdown_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"markdown"
    ]);
    assert result::get(config).output_format == markdown;
}

#[test]
fn should_set_output_format_to_pandoc_html_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"html"
    ]);
    assert result::get(config).output_format == pandoc_html;
}

#[test]
fn should_error_on_bogus_format() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"bogus"
    ]);
    assert result::get_err(config) == ~"unknown output format 'bogus'";
}

#[test]
fn should_set_output_style_to_doc_per_mod_by_default() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_style == doc_per_mod;
}

#[test]
fn should_set_output_style_to_one_doc_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-crate"
    ]);
    assert result::get(config).output_style == doc_per_crate;
}

#[test]
fn should_set_output_style_to_doc_per_mod_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-mod"
    ]);
    assert result::get(config).output_style == doc_per_mod;
}

#[test]
fn should_error_on_bogus_output_style() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"bogus"
    ]);
    assert result::get_err(config) == ~"unknown output style 'bogus'";
}

#[test]
fn should_set_pandoc_command_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--pandoc-cmd", ~"panda-bear-doc"
    ]);
    assert result::get(config).pandoc_cmd == some(~"panda-bear-doc");
}

#[test]
fn should_set_pandoc_command_when_using_pandoc() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).pandoc_cmd == some(~"pandoc");
}
<commit_msg>rustdoc: Fix breakage<commit_after>import result::result;
import std::getopts;

export output_format;
export output_style;
export config;
export default_config;
export parse_config;
export usage;
export markdown, pandoc_html;
export doc_per_crate, doc_per_mod;

/// The type of document to output
enum output_format {
    /// Markdown
    markdown,
    /// HTML, via markdown and pandoc
    pandoc_html
}

/// How to organize the output
enum output_style {
    /// All in a single document
    doc_per_crate,
    /// Each module in its own document
    doc_per_mod
}

/// The configuration for a rustdoc session
type config = {
    input_crate: ~str,
    output_dir: ~str,
    output_format: output_format,
    output_style: output_style,
    pandoc_cmd: option<~str>
};

fn opt_output_dir() -> ~str { ~"output-dir" }
fn opt_output_format() -> ~str { ~"output-format" }
fn opt_output_style() -> ~str { ~"output-style" }
fn opt_pandoc_cmd() -> ~str { ~"pandoc-cmd" }
fn opt_help() -> ~str { ~"h" }

fn opts() -> ~[(getopts::opt, ~str)] {
    ~[
        (getopts::optopt(opt_output_dir()),
         ~"--output-dir <val>     put documents here"),
        (getopts::optopt(opt_output_format()),
         ~"--output-format <val>  either 'markdown' or 'html'"),
        (getopts::optopt(opt_output_style()),
         ~"--output-style <val>   either 'doc-per-crate' or 'doc-per-mod'"),
        (getopts::optopt(opt_pandoc_cmd()),
         ~"--pandoc-cmd <val>     the command for running pandoc"),
        (getopts::optflag(opt_help()),
         ~"-h                     print help")
    ]
}

fn usage() {
    import io::println;

    println(~"Usage: rustdoc ~[options] <cratefile>\n");
    println(~"Options:\n");
    for opts().each |opt| {
        println(fmt!{"    %s", opt.second()});
    }
    println(~"");
}

fn default_config(input_crate: ~str) -> config {
    {
        input_crate: input_crate,
        output_dir: ~".",
        output_format: pandoc_html,
        output_style: doc_per_mod,
        pandoc_cmd: none
    }
}

type program_output = fn~((&str), (&[~str])) ->
    {status: int, out: ~str, err: ~str};

fn mock_program_output(_prog: &str, _args: &[~str]) -> {
    status: int, out: ~str, err: ~str
} {
    {
        status: 0,
        out: ~"",
        err: ~""
    }
}

fn parse_config(args: ~[~str]) -> result<config, ~str> {
    parse_config_(args, run::program_output)
}

fn parse_config_(
    args: ~[~str],
    program_output: program_output
) -> result<config, ~str> {
    let args = vec::tail(args);
    let opts = vec::unzip(opts()).first();
    match getopts::getopts(args, opts) {
        result::ok(matches) => {
            if vec::len(matches.free) == 1u {
                let input_crate = vec::head(matches.free);
                config_from_opts(input_crate, matches, program_output)
            } else if vec::is_empty(matches.free) {
                result::err(~"no crates specified")
            } else {
                result::err(~"multiple crates specified")
            }
        }
        result::err(f) => {
            result::err(getopts::fail_str(f))
        }
    }
}

fn config_from_opts(
    input_crate: ~str,
    matches: getopts::matches,
    program_output: program_output
) -> result<config, ~str> {

    let config = default_config(input_crate);
    let result = result::ok(config);
    let result = do result::chain(result) |config| {
        let output_dir = getopts::opt_maybe_str(matches, opt_output_dir());
        result::ok({
            output_dir: option::get_default(output_dir, config.output_dir)
            with config
        })
    };
    let result = do result::chain(result) |config| {
        let output_format = getopts::opt_maybe_str(
            matches, opt_output_format());
        do option::map_default(output_format, result::ok(config))
            |output_format| {
            do result::chain(parse_output_format(output_format))
                |output_format| {

                result::ok({
                    output_format: output_format
                    with config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let output_style =
            getopts::opt_maybe_str(matches, opt_output_style());
        do option::map_default(output_style, result::ok(config))
            |output_style| {
            do result::chain(parse_output_style(output_style))
                |output_style| {
                result::ok({
                    output_style: output_style
                    with config
                })
            }
        }
    };
    let result = do result::chain(result) |config| {
        let pandoc_cmd = getopts::opt_maybe_str(matches, opt_pandoc_cmd());
        let pandoc_cmd = maybe_find_pandoc(
            config, pandoc_cmd, program_output);
        do result::chain(pandoc_cmd) |pandoc_cmd| {
            result::ok({
                pandoc_cmd: pandoc_cmd
                with config
            })
        }
    };
    return result;
}

fn parse_output_format(output_format: ~str) -> result<output_format, ~str> {
    match output_format {
      ~"markdown" => result::ok(markdown),
      ~"html" => result::ok(pandoc_html),
      _ => result::err(fmt!{"unknown output format '%s'", output_format})
    }
}

fn parse_output_style(output_style: ~str) -> result<output_style, ~str> {
    match output_style {
      ~"doc-per-crate" => result::ok(doc_per_crate),
      ~"doc-per-mod" => result::ok(doc_per_mod),
      _ => result::err(fmt!{"unknown output style '%s'", output_style})
    }
}

fn maybe_find_pandoc(
    config: config,
    maybe_pandoc_cmd: option<~str>,
    program_output: program_output
) -> result<option<~str>, ~str> {
    if config.output_format != pandoc_html {
        return result::ok(maybe_pandoc_cmd);
    }

    let possible_pandocs = match maybe_pandoc_cmd {
      some(pandoc_cmd) => ~[pandoc_cmd],
      none => {
        ~[~"pandoc"] + match os::homedir() {
          some(dir) => {
            ~[path::connect(dir, ~".cabal/bin/pandoc")]
          }
          none => ~[]
        }
      }
    };

    let pandoc = do vec::find(possible_pandocs) |pandoc| {
        let output = program_output(pandoc, ~[~"--version"]);
        debug!{"testing pandoc cmd %s: %?", pandoc, output};
        output.status == 0
    };

    if option::is_some(pandoc) {
        result::ok(pandoc)
    } else {
        result::err(~"couldn't find pandoc")
    }
}

#[test]
fn should_find_pandoc() {
    let config = {
        output_format: pandoc_html
        with default_config(~"test")
    };
    let mock_program_output = fn~(_prog: &str, _args: &[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 0, out: ~"pandoc 1.8.2.1", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, none, mock_program_output);
    assert result == result::ok(some(~"pandoc"));
}

#[test]
fn should_error_with_no_pandoc() {
    let config = {
        output_format: pandoc_html
        with default_config(~"test")
    };
    let mock_program_output = fn~(_prog: &str, _args: &[~str]) -> {
        status: int, out: ~str, err: ~str
    } {
        {
            status: 1, out: ~"", err: ~""
        }
    };
    let result = maybe_find_pandoc(config, none, mock_program_output);
    assert result == result::err(~"couldn't find pandoc");
}

#[cfg(test)]
mod test {
    fn parse_config(args: ~[~str]) -> result<config, ~str> {
        parse_config_(args, mock_program_output)
    }
}

#[test]
fn should_error_with_no_crates() {
    let config = test::parse_config(~[~"rustdoc"]);
    assert result::get_err(config) == ~"no crates specified";
}

#[test]
fn should_error_with_multiple_crates() {
    let config =
        test::parse_config(~[~"rustdoc", ~"crate1.rc", ~"crate2.rc"]);
    assert result::get_err(config) == ~"multiple crates specified";
}

#[test]
fn should_set_output_dir_to_cwd_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_dir == ~".";
}

#[test]
fn should_set_output_dir_if_provided() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-dir", ~"snuggles"
    ]);
    assert result::get(config).output_dir == ~"snuggles";
}

#[test]
fn should_set_output_format_to_pandoc_html_if_not_provided() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_format == pandoc_html;
}

#[test]
fn should_set_output_format_to_markdown_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"markdown"
    ]);
    assert result::get(config).output_format == markdown;
}

#[test]
fn should_set_output_format_to_pandoc_html_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"html"
    ]);
    assert result::get(config).output_format == pandoc_html;
}

#[test]
fn should_error_on_bogus_format() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-format", ~"bogus"
    ]);
    assert result::get_err(config) == ~"unknown output format 'bogus'";
}

#[test]
fn should_set_output_style_to_doc_per_mod_by_default() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).output_style == doc_per_mod;
}

#[test]
fn should_set_output_style_to_one_doc_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-crate"
    ]);
    assert result::get(config).output_style == doc_per_crate;
}

#[test]
fn should_set_output_style_to_doc_per_mod_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"doc-per-mod"
    ]);
    assert result::get(config).output_style == doc_per_mod;
}

#[test]
fn should_error_on_bogus_output_style() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--output-style", ~"bogus"
    ]);
    assert result::get_err(config) == ~"unknown output style 'bogus'";
}

#[test]
fn should_set_pandoc_command_if_requested() {
    let config = test::parse_config(~[
        ~"rustdoc", ~"crate.rc", ~"--pandoc-cmd", ~"panda-bear-doc"
    ]);
    assert result::get(config).pandoc_cmd == some(~"panda-bear-doc");
}

#[test]
fn should_set_pandoc_command_when_using_pandoc() {
    let config = test::parse_config(~[~"rustdoc", ~"crate.rc"]);
    assert result::get(config).pandoc_cmd == some(~"pandoc");
}
<|endoftext|>