docspec 1.2.0

Streaming document conversion: convenience facade re-exporting readers, writers, and event types
Documentation
//! Writer factory for creating writers from output formats.

use std::io::Write;

use docspec_core::{AssetProvider, Event, EventSink, Result, StackTrackingSink};

#[cfg(feature = "blocknote")]
use docspec_blocknote_writer::BlockNoteWriter;
#[cfg(feature = "oxa")]
use docspec_oxa_writer::OxaWriter;

use crate::format::OutputFormat;

/// Enum-dispatch writer for any registered output format.
///
/// Internally wraps the chosen writer in [`StackTrackingSink`] so callers
/// never need to compose normalization manually. Constructed via
/// [`AnyWriter::new`] or [`AnyWriter::with_assets`].
pub struct AnyWriter<'a, W: Write> {
    inner: StackTrackingSink<AnyWriterInner<'a, W>>,
}

enum AnyWriterInner<'a, W: Write> {
    #[cfg(feature = "blocknote")]
    BlockNote(BlockNoteWriter<'a, W>),
    #[cfg(feature = "oxa")]
    Oxa(OxaWriter<W>),
    #[cfg(not(feature = "blocknote"))]
    _Phantom(std::marker::PhantomData<&'a W>),
}

impl<'a, W: Write> AnyWriter<'a, W> {
    /// Construct a writer for the given format.
    #[inline]
    #[must_use]
    pub fn new(format: OutputFormat, writer: W) -> Self {
        #[cfg(not(any(feature = "blocknote", feature = "oxa")))]
        {
            let _ = writer;
            match format {}
        }
        #[cfg(any(feature = "blocknote", feature = "oxa"))]
        {
            let inner = match format {
                #[cfg(feature = "blocknote")]
                OutputFormat::Blocknote => AnyWriterInner::BlockNote(BlockNoteWriter::new(writer)),
                #[cfg(feature = "oxa")]
                OutputFormat::Oxa => AnyWriterInner::Oxa(OxaWriter::new(writer)),
            };
            Self {
                inner: StackTrackingSink::new(inner),
            }
        }
    }

    /// Construct a writer with an asset provider, where supported.
    ///
    /// Writers that do not consume an [`AssetProvider`] (currently
    /// `OxaWriter`) silently ignore the `assets` argument.
    #[inline]
    #[must_use]
    pub fn with_assets(format: OutputFormat, writer: W, assets: &'a dyn AssetProvider) -> Self {
        #[cfg(not(any(feature = "blocknote", feature = "oxa")))]
        {
            let _ = (writer, assets);
            match format {}
        }
        #[cfg(any(feature = "blocknote", feature = "oxa"))]
        {
            let inner = match format {
                #[cfg(feature = "blocknote")]
                OutputFormat::Blocknote => {
                    AnyWriterInner::BlockNote(BlockNoteWriter::with_assets(writer, assets))
                }
                #[cfg(feature = "oxa")]
                OutputFormat::Oxa => {
                    let _ = assets;
                    AnyWriterInner::Oxa(OxaWriter::new(writer))
                }
            };
            Self {
                inner: StackTrackingSink::new(inner),
            }
        }
    }
}

impl<W: Write> EventSink for AnyWriterInner<'_, W> {
    fn finish(self) -> Result<()> {
        match self {
            #[cfg(feature = "blocknote")]
            Self::BlockNote(w) => w.finish(),
            #[cfg(feature = "oxa")]
            Self::Oxa(w) => w.finish(),
            #[cfg(not(feature = "blocknote"))]
            Self::_Phantom(_) => Ok(()),
        }
    }

    fn handle_event(&mut self, event: Event) -> Result<()> {
        match self {
            #[cfg(feature = "blocknote")]
            Self::BlockNote(w) => w.handle_event(event),
            #[cfg(feature = "oxa")]
            Self::Oxa(w) => w.handle_event(event),
            #[cfg(not(feature = "blocknote"))]
            Self::_Phantom(_) => {
                let _ = event;
                Ok(())
            }
        }
    }
}

impl<W: Write> EventSink for AnyWriter<'_, W> {
    #[inline]
    fn finish(self) -> Result<()> {
        self.inner.finish()
    }

    #[inline]
    fn handle_event(&mut self, event: Event) -> Result<()> {
        self.inner.handle_event(event)
    }
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;
    use std::io::Write;

    use docspec_core::{AssetProvider, Event, EventSink as _};

    use super::{AnyWriter, OutputFormat};

    struct NullAssets;

    impl AssetProvider for NullAssets {
        fn content_type(&self, _asset_id: &str) -> Option<Cow<'_, str>> {
            None
        }

        fn stream_to(
            &self,
            _asset_id: &str,
            _writer: &mut dyn Write,
        ) -> Option<std::io::Result<u64>> {
            None
        }
    }

    #[cfg(feature = "blocknote")]
    #[test]
    fn with_assets_constructs_writer_for_blocknote() {
        let assets = NullAssets;
        assert!(assets.content_type("any-id").is_none());
        assert!(assets.stream_to("any-id", &mut std::io::sink()).is_none());
        let mut buf = Vec::new();
        let mut writer = AnyWriter::with_assets(OutputFormat::Blocknote, &mut buf, &assets);
        assert!(writer
            .handle_event(Event::StartDocument {
                id: None,
                language: None,
                metadata: None,
            })
            .is_ok());
        assert!(writer.handle_event(Event::EndDocument).is_ok());
        assert!(writer.finish().is_ok());
        assert!(!buf.is_empty());
    }

    #[cfg(feature = "oxa")]
    #[test]
    fn with_assets_constructs_writer_for_oxa() {
        let assets = NullAssets;
        let mut buf = Vec::new();
        let mut writer = AnyWriter::with_assets(OutputFormat::Oxa, &mut buf, &assets);
        assert!(writer
            .handle_event(Event::StartDocument {
                id: None,
                language: None,
                metadata: None,
            })
            .is_ok());
        assert!(writer.handle_event(Event::EndDocument).is_ok());
        assert!(writer.finish().is_ok());
        assert_eq!(buf, br#"{"type":"Document","children":[]}"#);
    }
}