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}