Skip to main content

ftml/includes/includer/
debug.rs

1/*
2 * includes/includer/debug.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use super::prelude::*;
22use crate::tree::VariableMap;
23#[cfg(test)]
24use std::collections::HashMap;
25use std::convert::Infallible;
26use std::fmt::{self, Display};
27
28/// An [`Includer`] that replaces included references with the page content followed by the
29/// include variables and their values.
30///
31/// Useful for testing includes.
32#[derive(Debug)]
33pub struct DebugIncluder;
34
35impl<'t> Includer<'t> for DebugIncluder {
36    type Error = Infallible;
37
38    #[inline]
39    fn include_pages(
40        &mut self,
41        includes: &[IncludeRef<'t>],
42    ) -> Result<Vec<FetchedPage<'t>>, Infallible> {
43        let mut first = true;
44        let mut pages = Vec::new();
45
46        for include in includes {
47            let content = if first && includes.len() > 1 {
48                // If the requested inclusions are greater than one,
49                // then have the list be a missing page.
50                //
51                // This lets us test the no_such_include() method,
52                // without it affecting typical single-include test cases.
53
54                first = false;
55                None
56            } else {
57                let content = format!(
58                    "<INCLUDED-PAGE {} {}>",
59                    include.page_ref(),
60                    MapWrap(include.variables()),
61                );
62
63                Some(Cow::Owned(content))
64            };
65
66            let page_ref = include.page_ref().clone();
67            pages.push(FetchedPage { page_ref, content });
68        }
69
70        Ok(pages)
71    }
72
73    #[inline]
74    fn no_such_include(
75        &mut self,
76        page_ref: &PageRef,
77    ) -> Result<Cow<'t, str>, Infallible> {
78        Ok(Cow::Owned(format!("<MISSING-PAGE {page_ref}>")))
79    }
80}
81
82/// Rendering a `HashMap` as a string, sorted alphabetically.
83///
84/// Avoids the uncertain key-value pair ordering inherent in the `Debug`
85/// implementation, which could cause tests to be flakey or system-dependent.
86#[derive(Debug)]
87struct MapWrap<'m, 't>(&'m VariableMap<'t>);
88
89impl<'t> Display for MapWrap<'_, 't> {
90    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91        // Get all entries and sort by key
92        let mut entries: Vec<(&Cow<'t, str>, &Cow<'t, str>)> = self.0.iter().collect();
93        entries.sort_by(|(key1, _), (key2, _)| key1.cmp(key2));
94
95        // Write all entries
96        write!(f, "{{")?;
97
98        for (i, (key, value)) in entries.iter().enumerate() {
99            write!(f, "{key:?} => {value:?}")?;
100
101            if i < entries.len() - 1 {
102                write!(f, ", ")?;
103            }
104        }
105
106        write!(f, "}}")?;
107
108        // Return
109        Ok(())
110    }
111}
112
113#[test]
114fn map_wrap() {
115    macro_rules! test {
116        ($input:expr, $expected:expr $(,)?) => {{
117            // Get what was actually specified as the input,
118            // stripping out the "hashmap!".
119            let raw_input = &stringify!($input)[9..];
120
121            // Convert string literals into Cows
122            let input = {
123                let original = $input;
124                let mut map = HashMap::new();
125
126                for (key, value) in original {
127                    let key = Cow::Borrowed(key);
128                    let value = Cow::Borrowed(value);
129
130                    map.insert(key, value);
131                }
132
133                map
134            };
135
136            let actual = MapWrap(&input).to_string();
137            let expected = $expected;
138
139            println!("Input:    {raw_input}");
140            println!("Actual:   {actual}");
141            println!("Expected: {expected}");
142            println!();
143
144            assert_eq!(
145                &actual, $expected,
146                "Actual format string didn't match expected"
147            );
148        }};
149    }
150
151    test!(hashmap! {}, "{}");
152    test!(hashmap! { "apple" => "1" }, r#"{"apple" => "1"}"#);
153    test!(
154        hashmap! { "apple" => "1", "banana" => "2" },
155        r#"{"apple" => "1", "banana" => "2"}"#,
156    );
157    test!(
158        hashmap! { "banana" => "2", "apple" => "1" },
159        r#"{"apple" => "1", "banana" => "2"}"#,
160    );
161    test!(
162        hashmap! { "apple" => "1", "banana" => "2", "cherry" => "3" },
163        r#"{"apple" => "1", "banana" => "2", "cherry" => "3"}"#,
164    );
165    test!(
166        hashmap! { "banana" => "2", "apple" => "1", "cherry" => "3" },
167        r#"{"apple" => "1", "banana" => "2", "cherry" => "3"}"#,
168    );
169    test!(
170        hashmap! { "cherry" => "3", "banana" => "2", "apple" => "1" },
171        r#"{"apple" => "1", "banana" => "2", "cherry" => "3"}"#,
172    );
173    test!(
174        hashmap! { "apple" => "1", "cherry" => "3", "banana" => "2" },
175        r#"{"apple" => "1", "banana" => "2", "cherry" => "3"}"#,
176    );
177}