apollo_federation/utils/
human_readable.rs

1pub(crate) struct JoinStringsOptions<'a> {
2    pub(crate) separator: &'a str,
3    pub(crate) first_separator: Option<&'a str>,
4    pub(crate) last_separator: Option<&'a str>,
5    /// When displaying a list of something in a human-readable form, after what size (in number of
6    /// characters) we start displaying only a subset of the list. Note this only counts characters
7    /// in list elements, and ignores separators.
8    pub(crate) output_length_limit: Option<usize>,
9}
10
11impl Default for JoinStringsOptions<'_> {
12    fn default() -> Self {
13        Self {
14            separator: ", ",
15            first_separator: None,
16            last_separator: Some(" and "),
17            output_length_limit: None,
18        }
19    }
20}
21
22/// Joins an iterator of strings, but with the ability to use a specific different separator for the
23/// first and/or last occurrence (if both are given and the list is size two, the first separator is
24/// used). Optionally, if the resulting list to print is "too long", it can display a subset of the
25/// elements and uses an ellipsis (...) for the rest.
26///
27/// The goal is to make the reading flow slightly better. For instance, if you have a vector of
28/// subgraphs `s = ["A", "B", "C"]`, then `join_strings(s.iter(), Default::default())` will yield
29/// "A, B and C".
30pub(crate) fn join_strings(
31    mut iter: impl Iterator<Item = impl AsRef<str>>,
32    options: JoinStringsOptions,
33) -> String {
34    let mut output = String::new();
35    let Some(first) = iter.next() else {
36        return output;
37    };
38    output.push_str(first.as_ref());
39    let Some(second) = iter.next() else {
40        return output;
41    };
42    // PORT_NOTE: The analogous JS code in `printHumanReadableList()` was only tracking the length
43    // of elements getting added to the list and ignored separators, so we do the same here.
44    let mut element_length = first.as_ref().chars().count();
45    // Returns true if push would exceed limit, and instead pushes default separator and "...".
46    let mut push_sep_and_element = |sep: &str, element: &str| {
47        if let Some(output_length_limit) = options.output_length_limit {
48            // PORT_NOTE: The analogous JS code in `printHumanReadableList()` has a bug where it
49            // doesn't early exit when the length would be too long, and later small elements in the
50            // list may erroneously extend the printed subset. That bug is fixed here.
51            let new_element_length = element_length + element.chars().count();
52            return if new_element_length <= output_length_limit {
53                element_length = new_element_length;
54                output.push_str(sep);
55                output.push_str(element);
56                false
57            } else {
58                output.push_str(options.separator);
59                output.push_str("...");
60                true
61            };
62        }
63        output.push_str(sep);
64        output.push_str(element);
65        false
66    };
67    let last_sep = options.last_separator.unwrap_or(options.separator);
68    let Some(mut current) = iter.next() else {
69        push_sep_and_element(options.first_separator.unwrap_or(last_sep), second.as_ref());
70        return output;
71    };
72    if push_sep_and_element(
73        options.first_separator.unwrap_or(options.separator),
74        second.as_ref(),
75    ) {
76        return output;
77    }
78    for next in iter {
79        if push_sep_and_element(options.separator, current.as_ref()) {
80            return output;
81        }
82        current = next;
83    }
84    push_sep_and_element(last_sep, current.as_ref());
85    output
86}
87
88pub(crate) struct HumanReadableListOptions<'a> {
89    pub(crate) prefix: Option<HumanReadableListPrefix<'a>>,
90    pub(crate) last_separator: Option<&'a str>,
91    /// When displaying a list of something in a human-readable form, after what size (in number of
92    /// characters) we start displaying only a subset of the list.
93    pub(crate) output_length_limit: usize,
94    /// If there are no elements, this string will be used instead.
95    pub(crate) empty_output: &'a str,
96}
97
98pub(crate) struct HumanReadableListPrefix<'a> {
99    pub(crate) singular: &'a str,
100    pub(crate) plural: &'a str,
101}
102
103impl Default for HumanReadableListOptions<'_> {
104    fn default() -> Self {
105        Self {
106            prefix: None,
107            last_separator: Some(" and "),
108            output_length_limit: 100,
109            empty_output: "",
110        }
111    }
112}
113
114// PORT_NOTE: Named `printHumanReadableList` in the JS codebase, but "print" in Rust has the
115// implication it prints to stdout/stderr, so we remove it here. Also, the "emptyValue" option is
116// never used, so it's not ported.
117/// Like [join_strings], joins an iterator of strings, but with a few differences, namely:
118/// - It allows prefixing the whole list, and to use a different prefix if there's only a single
119///   element in the list.
120/// - It forces the use of ", " as separator, but allows a different last separator.
121/// - It forces an output length limit to be specified. In other words, this function assumes it's
122///   more useful to avoid flooding the output than printing everything when the list is too long.
123pub(crate) fn human_readable_list(
124    mut iter: impl Iterator<Item = impl AsRef<str>>,
125    options: HumanReadableListOptions,
126) -> String {
127    let Some(first) = iter.next() else {
128        return options.empty_output.to_owned();
129    };
130    let Some(second) = iter.next() else {
131        return if let Some(prefix) = options.prefix {
132            format!("{} {}", prefix.singular, first.as_ref())
133        } else {
134            first.as_ref().to_owned()
135        };
136    };
137    let joined_strings = join_strings(
138        [first, second].into_iter().chain(iter),
139        JoinStringsOptions {
140            last_separator: options.last_separator,
141            output_length_limit: Some(options.output_length_limit),
142            ..Default::default()
143        },
144    );
145    if let Some(prefix) = options.prefix {
146        format!("{} {}", prefix.plural, joined_strings)
147    } else {
148        joined_strings
149    }
150}
151
152// PORT_NOTE: Named `printSubgraphNames` in the JS codebase, but "print" in Rust has the implication
153// it prints to stdout/stderr, so we've renamed it here to `human_readable_subgraph_names`
154pub(crate) fn human_readable_subgraph_names(
155    subgraph_names: impl Iterator<Item = impl AsRef<str>>,
156) -> String {
157    human_readable_list(
158        subgraph_names.map(|name| format!("\"{}\"", name.as_ref())),
159        HumanReadableListOptions {
160            prefix: Some(HumanReadableListPrefix {
161                singular: "subgraph",
162                plural: "subgraphs",
163            }),
164            ..Default::default()
165        },
166    )
167}
168
169// PORT_NOTE: Named `printTypes` in the JS codebase, but "print" in Rust has the implication
170// it prints to stdout/stderr, so we've renamed it here to `human_readable_types`
171pub(crate) fn human_readable_types(types: impl Iterator<Item = impl AsRef<str>>) -> String {
172    human_readable_list(
173        types.map(|t| format!("\"{}\"", t.as_ref())),
174        HumanReadableListOptions {
175            prefix: Some(HumanReadableListPrefix {
176                singular: "type",
177                plural: "types",
178            }),
179            ..Default::default()
180        },
181    )
182}