thymeleaf 0.1.0-beta.0

A framework-neutral Thymeleaf-compatible dynamic template engine for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};

use crate::util::Utf16String;

const TEXT_PARSE_EXCEPTION_CLASS: &str = "org.thymeleaf.templateparser.text.TextParseException";

/// `TextParseException` 的 Java Throwable 原因适配。
///
/// 对应 Java: `java.lang.Throwable`,由
/// `org.thymeleaf.templateparser.text.TextParseException` 构造器接收。
///
/// Rust `Error` 不暴露可空 UTF-16 消息或 Java 类名,本对象显式保存这些可观察
/// 元数据,并保留底层错误对象供 [`Error::source`] 返回。由同类型解析异常创建时,
/// 还保存其可空行列,以复现上游 `instanceof TextParseException` 继承逻辑。
pub struct TextParseCause {
    error: Box<dyn Error + Send + Sync>,
    class_name: String,
    message: Option<Utf16String>,
    text_parse_location: Option<(i32, i32, Utf16String)>,
}

impl TextParseCause {
    /// 使用显式 Java 元数据包装普通 Rust 错误。
    ///
    /// # 参数
    /// - `error`:原因对象,所有权转移但分配身份保持不变。
    /// - `class_name`:Java `getClass().getName()`。
    /// - `message`:Java `Throwable#getMessage()`,允许 null 和孤立代理项。
    ///
    /// # 返回
    /// 不携带 TextParseException 行列继承标记的原因。
    /// 对应 Java 语义:`TextParseException` 的 `with_java_metadata` 行为(Rust 侧辅助/私有路径)。
    #[must_use]
    pub fn with_java_metadata(
        error: Box<dyn Error + Send + Sync>,
        class_name: impl Into<String>,
        message: Option<Utf16String>,
    ) -> Self {
        Self {
            error,
            class_name: class_name.into(),
            message,
            text_parse_location: None,
        }
    }

    /// 将另一个 `TextParseException` 作为原因并启用行列继承。
    ///
    /// # 参数
    /// - `exception`:被包装的同类型异常。
    ///
    /// # 返回
    /// 保存原消息、行列和 Java 类名的原因;底层 Box 供 source 链使用。
    /// 对应 Java 语义:`TextParseException` 的 `from_text_parse` 行为(Rust 侧辅助/私有路径)。
    #[must_use]
    pub fn from_text_parse(exception: TextParseException) -> Self {
        let message = exception.message.clone();
        // 上游所有设置 line/col 的构造器也必定设置位置前缀消息,把这一不变量编码
        // 到适配状态中,避免产生 Java 不可构造的“有位置但消息为 null”组合。
        let text_parse_location = exception.line.zip(exception.col).map(|(line, col)| {
            (
                line,
                col,
                message
                    .clone()
                    .expect("located TextParseException always has a message"),
            )
        });
        Self {
            error: Box::new(exception),
            class_name: TEXT_PARSE_EXCEPTION_CLASS.to_owned(),
            message,
            text_parse_location,
        }
    }

    /// 返回原因的 Java 类全限定名。
    ///
    /// # 返回
    /// 构造适配器时保存的 `Throwable#getClass().getName()`。
    /// 对应 Java 语义:`TextParseException` 的 `class_name` 行为(Rust 侧辅助/私有路径)。
    #[must_use]
    pub fn class_name(&self) -> &str {
        &self.class_name
    }

    fn source_error(&self) -> &(dyn Error + 'static) {
        self.error.as_ref()
    }
}

impl Debug for TextParseCause {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("TextParseCause")
            .field("class_name", &self.class_name)
            .field("message", &self.message)
            .field("text_parse_location", &self.text_parse_location)
            .finish_non_exhaustive()
    }
}

/// 文本模板解析期间产生的 checked exception。
///
/// 对应 Java: `org.thymeleaf.templateparser.text.TextParseException`。
///
/// 本对象完整映射八个公开构造器、可空 UTF-16 消息、原因链和可空行列。用另一个
/// 带行列的 `TextParseException` 作为原因时,外层异常继承行列并按上游规则重新
/// 拼接消息;显式 location 构造器则始终使用调用方行列,不读取原因消息。
#[derive(Debug)]
pub struct TextParseException {
    message: Option<Utf16String>,
    line: Option<i32>,
    col: Option<i32>,
    cause: Option<TextParseCause>,
}

impl TextParseException {
    /// 创建消息、原因和行列均为 null 的异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException()`。
    #[must_use]
    pub const fn new() -> Self {
        Self {
            message: None,
            line: None,
            col: None,
            cause: None,
        }
    }

    /// 仅使用可空消息创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(String)`。
    ///
    /// # 参数
    /// - `message`:原始 Java UTF-16 消息;`None` 对应 null。
    #[must_use]
    pub fn with_message(message: Option<Utf16String>) -> Self {
        Self {
            message,
            line: None,
            col: None,
            cause: None,
        }
    }

    /// 使用可空消息和可空原因创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(String,Throwable)`。
    ///
    /// # 参数
    /// - `message`:调用方消息。
    /// - `cause`:原因;`None` 对应 Java null。
    ///
    /// # 返回
    /// 同类型且带行列的原因会把行列传播到新异常。
    #[must_use]
    pub fn with_message_and_cause(
        message: Option<Utf16String>,
        cause: Option<TextParseCause>,
    ) -> Self {
        let (line, col) = inherited_location(cause.as_ref());
        let message = compose_inherited_message(message.as_ref(), cause.as_ref());
        Self {
            message,
            line,
            col,
            cause,
        }
    }

    /// 仅使用可空原因创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(Throwable)`。
    ///
    /// # 参数
    /// - `cause`:原因;`None` 对应 Java null。
    #[must_use]
    pub fn with_cause(cause: Option<TextParseCause>) -> Self {
        Self::with_message_and_cause(None, cause)
    }

    /// 使用显式行列创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(int,int)`。
    ///
    /// # 参数
    /// - `line`:原样保存的行号,包括负数。
    /// - `col`:原样保存的列号,包括负数。
    #[must_use]
    pub fn with_location(line: i32, col: i32) -> Self {
        Self {
            message: Some(message_prefix(line, col)),
            line: Some(line),
            col: Some(col),
            cause: None,
        }
    }

    /// 使用可空消息、可空原因和显式行列创建异常。
    ///
    /// 对应 Java:
    /// `TextParseException#TextParseException(String,Throwable,int,int)`。
    ///
    /// # 参数
    /// - `message`:追加到位置前缀后的消息;null 按 Java 拼接为 `"null"`。
    /// - `cause`:原因;不参与消息或位置推导。
    /// - `line`:显式行号。
    /// - `col`:显式列号。
    #[must_use]
    pub fn with_message_and_cause_at(
        message: Option<&Utf16String>,
        cause: Option<TextParseCause>,
        line: i32,
        col: i32,
    ) -> Self {
        Self {
            message: Some(prefix_with_message(line, col, message)),
            line: Some(line),
            col: Some(col),
            cause,
        }
    }

    /// 使用可空消息和显式行列创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(String,int,int)`。
    ///
    /// # 参数
    /// - `message`:追加消息;`None` 拼接为 `"null"`。
    /// - `line`:显式行号。
    /// - `col`:显式列号。
    #[must_use]
    pub fn with_message_at(message: Option<&Utf16String>, line: i32, col: i32) -> Self {
        Self::with_message_and_cause_at(message, None, line, col)
    }

    /// 使用可空原因和显式行列创建异常。
    ///
    /// 对应 Java: `TextParseException#TextParseException(Throwable,int,int)`。
    ///
    /// # 参数
    /// - `cause`:原因;不参与前缀消息计算。
    /// - `line`:显式行号。
    /// - `col`:显式列号。
    #[must_use]
    pub fn with_cause_at(cause: Option<TextParseCause>, line: i32, col: i32) -> Self {
        Self {
            message: Some(message_prefix(line, col)),
            line: Some(line),
            col: Some(col),
            cause,
        }
    }

    /// 返回可空 Java UTF-16 消息。
    ///
    /// 对应 Java: 继承的 `Throwable#getMessage()`。
    ///
    /// # 返回
    /// 构造器最终保存的消息;`None` 对应 Java null。
    #[must_use]
    pub fn get_message(&self) -> Option<&Utf16String> {
        self.message.as_ref()
    }

    /// 返回可空行号。
    ///
    /// 对应 Java: `TextParseException#getLine()`。
    ///
    /// # 返回
    /// 显式位置或从同类型原因继承的位置;缺失为 `None`。
    #[must_use]
    pub const fn get_line(&self) -> Option<i32> {
        self.line
    }

    /// 返回可空列号。
    ///
    /// 对应 Java: `TextParseException#getCol()`。
    ///
    /// # 返回
    /// 显式位置或从同类型原因继承的位置;缺失为 `None`。
    #[must_use]
    pub const fn get_col(&self) -> Option<i32> {
        self.col
    }

    /// 返回原因适配对象。
    ///
    /// # 返回
    /// 原因存在时返回共享借用,用于检查 Java 类名和原因身份。
    #[must_use]
    pub const fn get_cause(&self) -> Option<&TextParseCause> {
        self.cause.as_ref()
    }
}

impl Default for TextParseException {
    fn default() -> Self {
        Self::new()
    }
}

impl Display for TextParseException {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(
            &self
                .message
                .as_ref()
                .map_or_else(|| "null".to_owned(), Utf16String::to_string_lossy),
        )
    }
}

impl Error for TextParseException {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.cause.as_ref().map(TextParseCause::source_error)
    }
}

fn inherited_location(cause: Option<&TextParseCause>) -> (Option<i32>, Option<i32>) {
    cause
        .and_then(|cause| cause.text_parse_location.as_ref())
        .map_or((None, None), |(line, col, _)| (Some(*line), Some(*col)))
}

fn compose_inherited_message(
    message: Option<&Utf16String>,
    cause: Option<&TextParseCause>,
) -> Option<Utf16String> {
    if let Some(cause) = cause
        && let Some((line, col, cause_message)) = cause.text_parse_location.as_ref()
    {
        let mut result = message_prefix(*line, *col).as_utf16().to_vec();
        match message {
            Some(message) => {
                result.push(u16::from(b' '));
                result.extend_from_slice(message.as_utf16());
            }
            None => result.extend_from_slice(cause_message.as_utf16()),
        }
        return Some(Utf16String::from_utf16(result));
    }
    if let Some(message) = message {
        return Some(message.clone());
    }
    cause.and_then(|cause| cause.message.clone())
}

fn message_prefix(line: i32, col: i32) -> Utf16String {
    Utf16String::from_rust_str(&format!("(Line = {line}, Column = {col})"))
}

fn prefix_with_message(line: i32, col: i32, message: Option<&Utf16String>) -> Utf16String {
    let mut result = message_prefix(line, col).as_utf16().to_vec();
    result.push(u16::from(b' '));
    match message {
        Some(message) => result.extend_from_slice(message.as_utf16()),
        None => result.extend("null".encode_utf16()),
    }
    Utf16String::from_utf16(result)
}

#[cfg(test)]
mod tests {
    use std::error::Error;
    use std::fmt::{Display, Formatter};

    use super::{TextParseCause, TextParseException};
    use crate::util::Utf16String;

    #[derive(Debug)]
    struct PlainError;

    impl Display for PlainError {
        fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
            formatter.write_str("cause")
        }
    }

    impl Error for PlainError {}

    #[test]
    fn default_display_and_source_identity_are_preserved() {
        assert!(TextParseException::default().get_message().is_none());
        assert_eq!(TextParseException::new().to_string(), "null");

        let error: Box<dyn Error + Send + Sync> = Box::new(PlainError);
        let identity = error.as_ref() as *const dyn Error as *const ();
        let cause = TextParseCause::with_java_metadata(
            error,
            "example.PlainError",
            Some(Utf16String::from_rust_str("cause")),
        );
        assert_eq!(PlainError.to_string(), "cause");
        assert!(format!("{cause:?}").contains("example.PlainError"));
        let exception = TextParseException::with_cause(Some(cause));
        let source_identity = exception.source().expect("source") as *const dyn Error as *const ();
        assert_eq!(source_identity, identity);
        assert_eq!(
            exception.get_cause().expect("cause").class_name(),
            "example.PlainError"
        );
    }
}