hugr_core/envelope/serde_with.rs
1//! Derivation to serialize and deserialize Hugrs and Packages as envelopes in a
2//! serde compatible way.
3//!
4//! This module provides a default wrapper, [`AsStringEnvelope`], that decodes
5//! hugrs and packages using the [`STD_REG`] extension registry.
6//!
7//! When a different extension registry is needed, use the
8//! [`impl_serde_as_string_envelope!`] macro to create a custom wrapper.
9//!
10//! These are meant to be used with `serde_with`'s `#[serde_as]` decorator, see
11//! <https://docs.rs/serde_with/latest/serde_with>.
12
13use crate::std_extensions::STD_REG;
14
15/// De/Serialize a package or hugr by encoding it into a textual Envelope and
16/// storing it as a string.
17///
18/// This is similar to [`AsBinaryEnvelope`], but uses a textual envelope instead
19/// of a binary one.
20///
21/// Note that only PRELUDE extensions are used to decode the package's content.
22/// When serializing a HUGR, any additional extensions required to load it are
23/// embedded in the envelope. Packages should manually add any required
24/// extensions before serializing.
25///
26/// # Examples
27///
28/// ```rust
29/// # use serde::{Deserialize, Serialize};
30/// # use serde_json::json;
31/// # use serde_with::{serde_as};
32/// # use hugr_core::Hugr;
33/// # use hugr_core::package::Package;
34/// # use hugr_core::envelope::serde_with::AsStringEnvelope;
35/// #
36/// #[serde_as]
37/// #[derive(Deserialize, Serialize)]
38/// struct A {
39/// #[serde_as(as = "AsStringEnvelope")]
40/// package: Package,
41/// #[serde_as(as = "Vec<AsStringEnvelope>")]
42/// hugrs: Vec<Hugr>,
43/// }
44/// ```
45///
46/// # Backwards compatibility
47///
48/// When reading an encoded HUGR, the `AsStringEnvelope` deserializer will first
49/// try to decode the value as an string-encoded envelope. If that fails, it
50/// will fallback to decoding the legacy HUGR serde definition. This temporary
51/// compatibility is required to support `hugr <= 0.19` and will be removed in
52/// a future version.
53pub struct AsStringEnvelope;
54
55/// De/Serialize a package or hugr by encoding it into a binary envelope and
56/// storing it as a base64-encoded string.
57///
58/// This is similar to [`AsStringEnvelope`], but uses a binary envelope instead
59/// of a string.
60/// When deserializing, if the string starts with the envelope magic 'HUGRiHJv'
61/// it will be loaded as a string envelope without base64 decoding.
62///
63/// Note that only PRELUDE extensions are used to decode the package's content.
64/// When serializing a HUGR, any additional extensions required to load it are
65/// embedded in the envelope. Packages should manually add any required
66/// extensions before serializing.
67///
68/// # Examples
69///
70/// ```rust
71/// # use serde::{Deserialize, Serialize};
72/// # use serde_json::json;
73/// # use serde_with::{serde_as};
74/// # use hugr_core::Hugr;
75/// # use hugr_core::package::Package;
76/// # use hugr_core::envelope::serde_with::AsBinaryEnvelope;
77/// #
78/// #[serde_as]
79/// #[derive(Deserialize, Serialize)]
80/// struct A {
81/// #[serde_as(as = "AsBinaryEnvelope")]
82/// package: Package,
83/// #[serde_as(as = "Vec<AsBinaryEnvelope>")]
84/// hugrs: Vec<Hugr>,
85/// }
86/// ```
87///
88/// # Backwards compatibility
89///
90/// When reading an encoded HUGR, the `AsBinaryEnvelope` deserializer will first
91/// try to decode the value as an binary-encoded envelope. If that fails, it
92/// will fallback to decoding a string envelope instead, and then finally to
93/// decoding the legacy HUGR serde definition. This temporary compatibility
94/// layer is required to support `hugr <= 0.19` and will be removed in a future
95/// version.
96pub struct AsBinaryEnvelope;
97
98/// Implements [`serde_with::DeserializeAs`] and [`serde_with::SerializeAs`] for
99/// the helper to deserialize `Hugr` and `Package` types, using the given
100/// extension registry.
101///
102/// This macro is used to implement the default [`AsStringEnvelope`] wrapper.
103///
104/// # Parameters
105///
106/// - `$adaptor`: The name of the adaptor type to implement.
107/// - `$extension_reg`: A reference to the extension registry to use for deserialization.
108///
109/// # Examples
110///
111/// ```rust
112/// # use serde::{Deserialize, Serialize};
113/// # use serde_json::json;
114/// # use serde_with::{serde_as};
115/// # use hugr_core::Hugr;
116/// # use hugr_core::package::Package;
117/// # use hugr_core::envelope::serde_with::AsStringEnvelope;
118/// # use hugr_core::envelope::serde_with::impl_serde_as_string_envelope;
119/// # use hugr_core::extension::ExtensionRegistry;
120/// #
121/// struct CustomAsEnvelope;
122///
123/// impl_serde_as_string_envelope!(CustomAsEnvelope, &hugr_core::extension::EMPTY_REG);
124///
125/// #[serde_as]
126/// #[derive(Deserialize, Serialize)]
127/// struct A {
128/// #[serde_as(as = "CustomAsEnvelope")]
129/// package: Package,
130/// }
131/// ```
132///
133#[macro_export]
134macro_rules! impl_serde_as_string_envelope {
135 ($adaptor:ident, $extension_reg:expr) => {
136 impl<'de> serde_with::DeserializeAs<'de, $crate::package::Package> for $adaptor {
137 fn deserialize_as<D>(deserializer: D) -> Result<$crate::package::Package, D::Error>
138 where
139 D: serde::Deserializer<'de>,
140 {
141 struct Helper;
142 impl serde::de::Visitor<'_> for Helper {
143 type Value = $crate::package::Package;
144
145 fn expecting(
146 &self,
147 formatter: &mut std::fmt::Formatter<'_>,
148 ) -> std::fmt::Result {
149 formatter.write_str("a string-encoded envelope")
150 }
151
152 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
153 where
154 E: serde::de::Error,
155 {
156 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
157 $crate::package::Package::load_str(value, Some(extensions))
158 .map_err(serde::de::Error::custom)
159 }
160 }
161
162 deserializer.deserialize_str(Helper)
163 }
164 }
165
166 impl<'de> serde_with::DeserializeAs<'de, $crate::Hugr> for $adaptor {
167 fn deserialize_as<D>(deserializer: D) -> Result<$crate::Hugr, D::Error>
168 where
169 D: serde::Deserializer<'de>,
170 {
171 struct Helper;
172 impl<'vis> serde::de::Visitor<'vis> for Helper {
173 type Value = $crate::Hugr;
174
175 fn expecting(
176 &self,
177 formatter: &mut std::fmt::Formatter<'_>,
178 ) -> std::fmt::Result {
179 formatter.write_str("a string-encoded envelope")
180 }
181
182 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
183 where
184 E: serde::de::Error,
185 {
186 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
187 $crate::Hugr::load_str(value, Some(extensions))
188 .map_err(serde::de::Error::custom)
189 }
190
191 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
192 where
193 A: serde::de::MapAccess<'vis>,
194 {
195 // Backwards compatibility: If the encoded value is not a
196 // string, we may have a legacy HUGR serde structure instead. In that
197 // case, we can add an envelope header and try again.
198 //
199 // TODO: Remove this fallback in 0.21.0
200 let deserializer = serde::de::value::MapAccessDeserializer::new(map);
201 #[expect(deprecated)]
202 let mut hugr =
203 $crate::hugr::serialize::serde_deserialize_hugr(deserializer)
204 .map_err(serde::de::Error::custom)?;
205
206 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
207 hugr.resolve_extension_defs(extensions)
208 .map_err(serde::de::Error::custom)?;
209 Ok(hugr)
210 }
211 }
212
213 // TODO: Go back to `deserialize_str` once the fallback is removed.
214 deserializer.deserialize_any(Helper)
215 }
216 }
217
218 impl serde_with::SerializeAs<$crate::package::Package> for $adaptor {
219 fn serialize_as<S>(
220 source: &$crate::package::Package,
221 serializer: S,
222 ) -> Result<S::Ok, S::Error>
223 where
224 S: serde::Serializer,
225 {
226 let str = source
227 .store_str($crate::envelope::EnvelopeConfig::text())
228 .map_err(serde::ser::Error::custom)?;
229 serializer.collect_str(&str)
230 }
231 }
232
233 impl serde_with::SerializeAs<$crate::Hugr> for $adaptor {
234 fn serialize_as<S>(source: &$crate::Hugr, serializer: S) -> Result<S::Ok, S::Error>
235 where
236 S: serde::Serializer,
237 {
238 // Include any additional extension required to load the HUGR in the envelope.
239 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
240 let mut extra_extensions = $crate::extension::ExtensionRegistry::default();
241 for ext in $crate::hugr::views::HugrView::extensions(source).iter() {
242 if !extensions.contains(ext.name()) {
243 extra_extensions.register_updated(ext.clone());
244 }
245 }
246
247 let str = source
248 .store_str_with_exts(
249 $crate::envelope::EnvelopeConfig::text(),
250 &extra_extensions,
251 )
252 .map_err(serde::ser::Error::custom)?;
253 serializer.collect_str(&str)
254 }
255 }
256 };
257}
258pub use impl_serde_as_string_envelope;
259
260impl_serde_as_string_envelope!(AsStringEnvelope, &STD_REG);
261
262/// Implements [`serde_with::DeserializeAs`] and [`serde_with::SerializeAs`] for
263/// the helper to deserialize `Hugr` and `Package` types, using the given
264/// extension registry.
265///
266/// This macro is used to implement the default [`AsBinaryEnvelope`] wrapper.
267///
268/// # Parameters
269///
270/// - `$adaptor`: The name of the adaptor type to implement.
271/// - `$extension_reg`: A reference to the extension registry to use for deserialization.
272///
273/// # Examples
274///
275/// ```rust
276/// # use serde::{Deserialize, Serialize};
277/// # use serde_json::json;
278/// # use serde_with::{serde_as};
279/// # use hugr_core::Hugr;
280/// # use hugr_core::package::Package;
281/// # use hugr_core::envelope::serde_with::AsBinaryEnvelope;
282/// # use hugr_core::envelope::serde_with::impl_serde_as_binary_envelope;
283/// # use hugr_core::extension::ExtensionRegistry;
284/// #
285/// struct CustomAsEnvelope;
286///
287/// impl_serde_as_binary_envelope!(CustomAsEnvelope, &hugr_core::extension::EMPTY_REG);
288///
289/// #[serde_as]
290/// #[derive(Deserialize, Serialize)]
291/// struct A {
292/// #[serde_as(as = "CustomAsEnvelope")]
293/// package: Package,
294/// }
295/// ```
296///
297#[macro_export]
298macro_rules! impl_serde_as_binary_envelope {
299 ($adaptor:ident, $extension_reg:expr) => {
300 impl<'de> serde_with::DeserializeAs<'de, $crate::package::Package> for $adaptor {
301 fn deserialize_as<D>(deserializer: D) -> Result<$crate::package::Package, D::Error>
302 where
303 D: serde::Deserializer<'de>,
304 {
305 struct Helper;
306 impl serde::de::Visitor<'_> for Helper {
307 type Value = $crate::package::Package;
308
309 fn expecting(
310 &self,
311 formatter: &mut std::fmt::Formatter<'_>,
312 ) -> std::fmt::Result {
313 formatter.write_str("a base64-encoded envelope")
314 }
315
316 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
317 where
318 E: serde::de::Error,
319 {
320 use $crate::envelope::serde_with::base64::{DecoderReader, STANDARD};
321
322 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
323
324 if value
325 .as_bytes()
326 .starts_with($crate::envelope::MAGIC_NUMBERS)
327 {
328 // If the string starts with the envelope magic 'HUGRiHJv',
329 // skip the base64 decoding.
330 let reader = std::io::Cursor::new(value.as_bytes());
331 $crate::package::Package::load(reader, Some(extensions))
332 .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
333 } else {
334 let reader = DecoderReader::new(value.as_bytes(), &STANDARD);
335 let buf_reader = std::io::BufReader::new(reader);
336 $crate::package::Package::load(buf_reader, Some(extensions))
337 .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
338 }
339 }
340 }
341
342 deserializer.deserialize_str(Helper)
343 }
344 }
345
346 impl<'de> serde_with::DeserializeAs<'de, $crate::Hugr> for $adaptor {
347 fn deserialize_as<D>(deserializer: D) -> Result<$crate::Hugr, D::Error>
348 where
349 D: serde::Deserializer<'de>,
350 {
351 struct Helper;
352 impl<'vis> serde::de::Visitor<'vis> for Helper {
353 type Value = $crate::Hugr;
354
355 fn expecting(
356 &self,
357 formatter: &mut std::fmt::Formatter<'_>,
358 ) -> std::fmt::Result {
359 formatter.write_str("a base64-encoded envelope")
360 }
361
362 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
363 where
364 E: serde::de::Error,
365 {
366 use $crate::envelope::serde_with::base64::{DecoderReader, STANDARD};
367
368 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
369
370 if value
371 .as_bytes()
372 .starts_with($crate::envelope::MAGIC_NUMBERS)
373 {
374 // If the string starts with the envelope magic 'HUGRiHJv',
375 // skip the base64 decoding.
376 let reader = std::io::Cursor::new(value.as_bytes());
377 $crate::Hugr::load(reader, Some(extensions))
378 .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
379 } else {
380 let reader = DecoderReader::new(value.as_bytes(), &STANDARD);
381 let buf_reader = std::io::BufReader::new(reader);
382 $crate::Hugr::load(buf_reader, Some(extensions))
383 .map_err(|e| serde::de::Error::custom(format!("{e:?}")))
384 }
385 }
386
387 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
388 where
389 A: serde::de::MapAccess<'vis>,
390 {
391 // Backwards compatibility: If the encoded value is not a
392 // string, we may have a legacy HUGR serde structure instead. In that
393 // case, we can add an envelope header and try again.
394 //
395 // TODO: Remove this fallback in a breaking change
396 let deserializer = serde::de::value::MapAccessDeserializer::new(map);
397 #[expect(deprecated)]
398 let mut hugr =
399 $crate::hugr::serialize::serde_deserialize_hugr(deserializer)
400 .map_err(serde::de::Error::custom)?;
401
402 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
403 hugr.resolve_extension_defs(extensions)
404 .map_err(serde::de::Error::custom)?;
405 Ok(hugr)
406 }
407 }
408
409 // TODO: Go back to `deserialize_str` once the fallback is removed.
410 deserializer.deserialize_any(Helper)
411 }
412 }
413
414 impl serde_with::SerializeAs<$crate::package::Package> for $adaptor {
415 fn serialize_as<S>(
416 source: &$crate::package::Package,
417 serializer: S,
418 ) -> Result<S::Ok, S::Error>
419 where
420 S: serde::Serializer,
421 {
422 use $crate::envelope::serde_with::base64::{EncoderStringWriter, STANDARD};
423
424 let mut writer = EncoderStringWriter::new(&STANDARD);
425 source
426 .store(&mut writer, $crate::envelope::EnvelopeConfig::binary())
427 .map_err(serde::ser::Error::custom)?;
428 let str = writer.into_inner();
429 serializer.collect_str(&str)
430 }
431 }
432
433 impl serde_with::SerializeAs<$crate::Hugr> for $adaptor {
434 fn serialize_as<S>(source: &$crate::Hugr, serializer: S) -> Result<S::Ok, S::Error>
435 where
436 S: serde::Serializer,
437 {
438 // Include any additional extension required to load the HUGR in the envelope.
439 let extensions: &$crate::extension::ExtensionRegistry = $extension_reg;
440 let mut extra_extensions = $crate::extension::ExtensionRegistry::default();
441 for ext in $crate::hugr::views::HugrView::extensions(source).iter() {
442 if !extensions.contains(ext.name()) {
443 extra_extensions.register_updated(ext.clone());
444 }
445 }
446 use $crate::envelope::serde_with::base64::{EncoderStringWriter, STANDARD};
447
448 let mut writer = EncoderStringWriter::new(&STANDARD);
449 source
450 .store_with_exts(
451 &mut writer,
452 $crate::envelope::EnvelopeConfig::binary(),
453 &extra_extensions,
454 )
455 .map_err(serde::ser::Error::custom)?;
456 let str = writer.into_inner();
457 serializer.collect_str(&str)
458 }
459 }
460 };
461}
462pub use impl_serde_as_binary_envelope;
463
464impl_serde_as_binary_envelope!(AsBinaryEnvelope, &STD_REG);
465
466// Hidden re-export required to expand the binary envelope macros on external
467// crates.
468#[doc(hidden)]
469pub mod base64 {
470 pub use base64::Engine;
471 pub use base64::engine::general_purpose::STANDARD;
472 pub use base64::read::DecoderReader;
473 pub use base64::write::EncoderStringWriter;
474}
475
476#[cfg(test)]
477mod test {
478 use rstest::rstest;
479 use serde::{Deserialize, Serialize};
480 use serde_with::serde_as;
481
482 use crate::Hugr;
483 use crate::package::Package;
484
485 use super::*;
486
487 #[serde_as]
488 #[derive(Deserialize, Serialize)]
489 struct TextPkg {
490 #[serde_as(as = "AsStringEnvelope")]
491 data: Package,
492 }
493
494 #[serde_as]
495 #[derive(Default, Deserialize, Serialize)]
496 struct TextHugr {
497 #[serde_as(as = "AsStringEnvelope")]
498 data: Hugr,
499 }
500
501 #[serde_as]
502 #[derive(Deserialize, Serialize)]
503 struct BinaryPkg {
504 #[serde_as(as = "AsBinaryEnvelope")]
505 data: Package,
506 }
507
508 #[serde_as]
509 #[derive(Default, Deserialize, Serialize)]
510 struct BinaryHugr {
511 #[serde_as(as = "AsBinaryEnvelope")]
512 data: Hugr,
513 }
514
515 #[derive(Default, Deserialize, Serialize)]
516 struct LegacyHugr {
517 #[serde(deserialize_with = "Hugr::serde_deserialize")]
518 #[serde(serialize_with = "Hugr::serde_serialize")]
519 data: Hugr,
520 }
521
522 impl Default for TextPkg {
523 fn default() -> Self {
524 // Default package with a single hugr (so it can be decoded as a hugr too).
525 Self {
526 data: Package::from_hugr(Hugr::default()),
527 }
528 }
529 }
530
531 impl Default for BinaryPkg {
532 fn default() -> Self {
533 // Default package with a single hugr (so it can be decoded as a hugr too).
534 Self {
535 data: Package::from_hugr(Hugr::default()),
536 }
537 }
538 }
539
540 fn decode<T: for<'a> serde::Deserialize<'a>>(encoded: String) -> Result<(), serde_json::Error> {
541 let _: T = serde_json::de::from_str(&encoded)?;
542 Ok(())
543 }
544
545 #[rstest]
546 // Text formats are swappable
547 #[case::text_pkg_text_pkg(TextPkg::default(), decode::<TextPkg>, false)]
548 #[case::text_pkg_text_hugr(TextPkg::default(), decode::<TextHugr>, false)]
549 #[case::text_hugr_text_pkg(TextHugr::default(), decode::<TextPkg>, false)]
550 #[case::text_hugr_text_hugr(TextHugr::default(), decode::<TextHugr>, false)]
551 // Binary formats can read each other
552 #[case::bin_pkg_bin_pkg(BinaryPkg::default(), decode::<BinaryPkg>, false)]
553 #[case::bin_pkg_bin_hugr(BinaryPkg::default(), decode::<BinaryHugr>, false)]
554 #[case::bin_hugr_bin_pkg(BinaryHugr::default(), decode::<BinaryPkg>, false)]
555 #[case::bin_hugr_bin_hugr(BinaryHugr::default(), decode::<BinaryHugr>, false)]
556 // Binary formats can read text ones
557 #[case::text_pkg_bin_pkg(TextPkg::default(), decode::<BinaryPkg>, false)]
558 #[case::text_pkg_bin_hugr(TextPkg::default(), decode::<BinaryHugr>, false)]
559 #[case::text_hugr_bin_pkg(TextHugr::default(), decode::<BinaryPkg>, false)]
560 #[case::text_hugr_bin_hugr(TextHugr::default(), decode::<BinaryHugr>, false)]
561 // But text formats can't read binary
562 #[case::bin_pkg_text_pkg(BinaryPkg::default(), decode::<TextPkg>, true)]
563 #[case::bin_pkg_text_hugr(BinaryPkg::default(), decode::<TextHugr>, true)]
564 #[case::bin_hugr_text_pkg(BinaryHugr::default(), decode::<TextPkg>, true)]
565 #[case::bin_hugr_text_hugr(BinaryHugr::default(), decode::<TextHugr>, true)]
566 // We can read old hugrs into hugrs, but not packages
567 #[case::legacy_hugr_text_pkg(LegacyHugr::default(), decode::<TextPkg>, true)]
568 #[case::legacy_hugr_text_hugr(LegacyHugr::default(), decode::<TextHugr>, false)]
569 #[case::legacy_hugr_bin_pkg(LegacyHugr::default(), decode::<BinaryPkg>, true)]
570 #[case::legacy_hugr_bin_hugr(LegacyHugr::default(), decode::<BinaryHugr>, false)]
571 // Decoding any new format as legacy hugr always fails
572 #[case::text_pkg_legacy_hugr(TextPkg::default(), decode::<LegacyHugr>, true)]
573 #[case::text_hugr_legacy_hugr(TextHugr::default(), decode::<LegacyHugr>, true)]
574 #[case::bin_pkg_legacy_hugr(BinaryPkg::default(), decode::<LegacyHugr>, true)]
575 #[case::bin_hugr_legacy_hugr(BinaryHugr::default(), decode::<LegacyHugr>, true)]
576 #[cfg_attr(all(miri, feature = "zstd"), ignore)] // FFI calls (required to compress with zstd) are not supported in miri
577 fn check_format_compatibility(
578 #[case] encoder: impl serde::Serialize,
579 #[case] decoder: fn(String) -> Result<(), serde_json::Error>,
580 #[case] errors: bool,
581 ) {
582 let encoded = serde_json::to_string(&encoder).unwrap();
583 let decoded = decoder(encoded);
584 match (errors, decoded) {
585 (false, Err(e)) => {
586 panic!("Decoding error: {e}");
587 }
588 (true, Ok(_)) => {
589 panic!("Roundtrip should have failed");
590 }
591 _ => {}
592 }
593 }
594}