Skip to main content

hugr_core/envelope/
writer.rs

1use std::io::Write;
2
3use itertools::Itertools as _;
4use thiserror::Error;
5
6use crate::Hugr;
7use crate::extension::ExtensionRegistry;
8
9use super::header::{EnvelopeConfig, EnvelopeFormat, HeaderError};
10use super::package_json::PackageEncodingError;
11use super::{FormatUnsupportedError, check_model_version};
12
13/// Write a package to an envelope with the specified configuration.
14///
15/// # Errors
16///
17/// - If the header cannot be written.
18/// - If the payload cannot be encoded.
19/// - If zstd compression is requested but the `zstd` feature is not enabled.
20pub(super) fn write_envelope<'h>(
21    mut writer: impl Write,
22    hugrs: impl IntoIterator<Item = &'h Hugr>,
23    extensions: &ExtensionRegistry,
24    config: EnvelopeConfig,
25) -> Result<(), WriteError> {
26    let header = config.make_header();
27    header.write(&mut writer)?;
28
29    match config.zstd {
30        #[cfg(feature = "zstd")]
31        Some(zstd) => {
32            let writer = zstd::Encoder::new(writer, zstd.level())?.auto_finish();
33            write_impl(writer, hugrs, extensions, config)?;
34        }
35        #[cfg(not(feature = "zstd"))]
36        Some(_) => return Err(WriteErrorInner::ZstdUnsupported.into()),
37        None => write_impl(writer, hugrs, extensions, config)?,
38    }
39
40    Ok(())
41}
42
43/// Internal implementation of write to call with/without the zstd compression wrapper.
44fn write_impl<'h>(
45    writer: impl Write,
46    hugrs: impl IntoIterator<Item = &'h Hugr>,
47    extensions: &ExtensionRegistry,
48    config: EnvelopeConfig,
49) -> Result<(), WriteError> {
50    match config.format {
51        #[expect(deprecated)]
52        EnvelopeFormat::PackageJson => {
53            super::package_json::to_json_writer(hugrs, extensions, writer)?
54        }
55        EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
56            check_model_version(config.format)?;
57            encode_model_binary(writer, hugrs, extensions, config.format)?;
58        }
59        EnvelopeFormat::SExpression | EnvelopeFormat::SExpressionWithExtensions => {
60            check_model_version(config.format)?;
61            encode_model_text(writer, hugrs, extensions, config.format)?;
62        }
63    }
64    Ok(())
65}
66
67/// Encode the package as a binary HUGR model.
68fn encode_model_binary<'h>(
69    mut writer: impl Write,
70    hugrs: impl IntoIterator<Item = &'h Hugr>,
71    extensions: &ExtensionRegistry,
72    format: EnvelopeFormat,
73) -> Result<(), ModelBinaryWriteError> {
74    use hugr_model::v0::{binary::write_to_writer, bumpalo::Bump};
75
76    use crate::export::export_package;
77
78    let bump = Bump::default();
79    let model_package = export_package(hugrs, extensions, &bump);
80
81    write_to_writer(&model_package, &mut writer)?;
82
83    // Append extensions for binary model.
84    if format == EnvelopeFormat::ModelWithExtensions {
85        serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
86    }
87
88    Ok(())
89}
90
91/// Encode the package as a text HUGR model.
92fn encode_model_text<'h>(
93    mut writer: impl Write,
94    hugrs: impl IntoIterator<Item = &'h Hugr>,
95    extensions: &ExtensionRegistry,
96    format: EnvelopeFormat,
97) -> Result<(), SExpressionWriteError> {
98    use hugr_model::v0::bumpalo::Bump;
99
100    use crate::export::export_package;
101
102    // Prepend extensions for text model.
103    if format == EnvelopeFormat::SExpressionWithExtensions {
104        serde_json::to_writer(&mut writer, &extensions.iter().collect_vec())?;
105    }
106
107    let bump = Bump::default();
108    let model_package = export_package(hugrs, extensions, &bump);
109
110    let model_package = model_package.as_ast().unwrap();
111    writeln!(writer, "{model_package}")?;
112
113    Ok(())
114}
115
116/// Error encoding an envelope payload.
117#[derive(Error, Debug)]
118#[non_exhaustive]
119#[error(transparent)]
120pub struct WriteError(pub(crate) WriteErrorInner);
121
122impl WriteError {
123    /// Create a new error for a non-ASCII format.
124    pub(crate) fn non_ascii_format(format: EnvelopeFormat) -> Self {
125        WriteErrorInner::NonASCIIFormat { format }.into()
126    }
127}
128
129#[derive(Error, Debug)]
130#[non_exhaustive]
131#[error(transparent)]
132/// Error encoding an envelope payload with enumerated variants.
133pub(crate) enum WriteErrorInner {
134    /// Error encoding a JSON format package.
135    #[deprecated(since = "0.27.0")]
136    JsonWrite(#[from] PackageEncodingError),
137    /// Error encoding a binary model format package.
138    ModelBinary(#[from] ModelBinaryWriteError),
139    /// Error encoding a text model format package.
140    SExpression(#[from] SExpressionWriteError),
141    /// Error writing the envelope header.
142    Header(#[from] HeaderError),
143    /// The specified payload format is not supported.
144    FormatUnsupported(#[from] FormatUnsupportedError),
145    /// Not all envelope formats can be represented as ASCII.
146    ///
147    /// This error is used when trying to store the envelope into a string.
148    #[error("Envelope format {format} cannot be represented as ASCII.")]
149    NonASCIIFormat {
150        /// The unsupported format.
151        format: EnvelopeFormat,
152    },
153    /// IO read/write error.
154    #[error(transparent)]
155    IO(#[from] std::io::Error),
156    /// Envelope encoding required zstd compression, but the feature is not enabled.
157    #[error("Zstd compression is not supported. This requires the 'zstd' feature for `hugr`.")]
158    #[cfg_attr(feature = "zstd", allow(dead_code))]
159    ZstdUnsupported,
160}
161
162impl<T: Into<WriteErrorInner>> From<T> for WriteError {
163    fn from(value: T) -> Self {
164        Self(value.into())
165    }
166}
167
168#[derive(Debug, Error)]
169#[error(transparent)]
170pub(crate) enum SExpressionWriteError {
171    JsonSerialize(#[from] serde_json::Error),
172    StringWrite(#[from] std::io::Error),
173}
174
175#[derive(Debug, Error)]
176#[error(transparent)]
177pub(crate) enum ModelBinaryWriteError {
178    WriteBinary(#[from] hugr_model::v0::binary::WriteError),
179    JsonSerialize(#[from] serde_json::Error),
180}
181
182#[cfg(test)]
183mod test {
184    use super::*;
185    use crate::extension::ExtensionRegistry;
186    use std::io::Cursor;
187
188    #[test]
189    #[expect(deprecated)]
190    fn test_write_empty_package() {
191        let config = EnvelopeConfig {
192            format: EnvelopeFormat::PackageJson,
193            zstd: None,
194        };
195        let cursor = Cursor::new(Vec::new());
196        let hugrs: Vec<&Hugr> = vec![];
197        let extensions = ExtensionRegistry::new([]);
198
199        let result = write_envelope(cursor, hugrs, &extensions, config);
200        // Empty JSON package should succeed
201        assert!(result.is_ok());
202    }
203
204    #[test]
205    fn test_non_ascii_format_error() {
206        let format = EnvelopeFormat::Model;
207        let error = WriteError::non_ascii_format(format);
208        let error_msg = error.to_string();
209        assert!(error_msg.contains("cannot be represented as ASCII"));
210    }
211}