iota_grpc_types/
lib.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! gRPC-specific versioned types for forward compatibility.
5//!
6//! These types provide versioning for gRPC streaming while positioning
7//! for future core type evolution. When core types themselves
8//! need versioning, these wrappers will evolve naturally.
9
10pub mod field;
11pub mod headers;
12pub mod proto;
13pub mod read_masks;
14
15/// Joins field names with commas to build a read mask string constant.
16///
17/// Accepts string **literals** only (for compile-time concatenation via
18/// `concat!`). To combine pre-defined constants at runtime, use
19/// [`field_masks_merge!`] instead.
20///
21/// # Example
22/// ```
23/// use iota_grpc_types::field_mask;
24///
25/// const MASK: &str = field_mask!("transaction.digest", "effects.bcs");
26/// assert_eq!(MASK, "transaction.digest,effects.bcs");
27/// ```
28#[macro_export]
29macro_rules! field_mask {
30    ($field:literal) => {
31        $field
32    };
33    ($first:literal, $($rest:literal),+ $(,)?) => {
34        concat!($first, ",", $crate::field_mask!($($rest),+))
35    };
36}
37
38/// Normalizes a comma-separated field mask by removing paths that are
39/// subsumed by broader (ancestor) paths.
40///
41/// A path `"a.b"` is subsumed by `"a"` because requesting `"a"` already
42/// includes all of its sub-fields. Exact duplicates are also removed.
43///
44/// # Examples
45/// ```
46/// use iota_grpc_types::field_mask_normalize;
47///
48/// assert_eq!(field_mask_normalize("effects,effects.bcs"), "effects");
49/// assert_eq!(field_mask_normalize("effects.bcs,effects"), "effects");
50/// assert_eq!(field_mask_normalize("a,b.c,b"), "a,b");
51/// ```
52pub fn field_mask_normalize(mask: &str) -> String {
53    let mut paths: Vec<&str> = mask.split(',').filter(|s| !s.is_empty()).collect();
54    // Sort by length so broader (shorter) paths are processed first.
55    paths.sort_by_key(|p| p.len());
56
57    let mut result: Vec<&str> = Vec::new();
58    for path in paths {
59        let subsumed = result.iter().any(|&kept| {
60            path == kept
61                || (path.starts_with(kept) && path.as_bytes().get(kept.len()) == Some(&b'.'))
62        });
63        if !subsumed {
64            result.push(path);
65        }
66    }
67    result.join(",")
68}
69
70/// Merges multiple read mask expressions into a single comma-separated
71/// [`String`], normalizing overlapping paths.
72///
73/// Unlike [`field_mask!`], this macro works with any expression that
74/// evaluates to `&str`, including `const` values from
75/// [`read_masks`](crate::read_masks). The result is a heap-allocated
76/// `String` suitable for passing to the client's `read_mask` parameter
77/// (e.g. `Some(&mask)`).
78///
79/// Overlapping paths are normalized: a broader path subsumes all of its
80/// sub-paths. For example, `"effects"` and `"effects.bcs"` are merged into
81/// just `"effects"`.
82///
83/// # Examples
84/// ```
85/// use iota_grpc_types::{field_masks_merge, read_masks::*};
86///
87/// let mask = field_masks_merge!(CHECKPOINT_RESPONSE_SUMMARY, CHECKPOINT_RESPONSE_CONTENTS,);
88/// assert_eq!(mask, "checkpoint.summary,checkpoint.contents");
89/// ```
90///
91/// Broader paths subsume narrower ones:
92/// ```
93/// use iota_grpc_types::field_masks_merge;
94///
95/// let mask = field_masks_merge!("effects", "effects.bcs");
96/// assert_eq!(mask, "effects");
97/// ```
98#[macro_export]
99macro_rules! field_masks_merge {
100    ($mask:expr $(,)?) => {
101        $crate::field_mask_normalize($mask)
102    };
103    ($first:expr, $($rest:expr),+ $(,)?) => {{
104        let parts: &[&str] = &[$first, $($rest),+];
105        $crate::field_mask_normalize(&parts.join(","))
106    }};
107}
108
109// Re-export google namespace
110pub mod google {
111    pub use super::proto::google::*;
112}
113
114// Re-export under v1 namespace
115pub mod v1 {
116    pub use super::proto::iota::grpc::v1::*;
117}