hugr_core/envelope/
writer.rs1use 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
13pub(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
43fn 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
67fn 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 if format == EnvelopeFormat::ModelWithExtensions {
85 serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
86 }
87
88 Ok(())
89}
90
91fn 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 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#[derive(Error, Debug)]
118#[non_exhaustive]
119#[error(transparent)]
120pub struct WriteError(pub(crate) WriteErrorInner);
121
122impl WriteError {
123 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)]
132pub(crate) enum WriteErrorInner {
134 #[deprecated(since = "0.27.0")]
136 JsonWrite(#[from] PackageEncodingError),
137 ModelBinary(#[from] ModelBinaryWriteError),
139 SExpression(#[from] SExpressionWriteError),
141 Header(#[from] HeaderError),
143 FormatUnsupported(#[from] FormatUnsupportedError),
145 #[error("Envelope format {format} cannot be represented as ASCII.")]
149 NonASCIIFormat {
150 format: EnvelopeFormat,
152 },
153 #[error(transparent)]
155 IO(#[from] std::io::Error),
156 #[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 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}