oxirs_ttl/toolkit/
serializer.rs1use crate::error::TurtleResult;
7use std::io::Write;
9
10pub trait Serializer<Input> {
12 fn serialize<W: Write>(&self, input: &[Input], writer: W) -> TurtleResult<()>;
14
15 fn serialize_item<W: Write>(&self, input: &Input, writer: W) -> TurtleResult<()>;
17}
18
19#[cfg(feature = "async-tokio")]
21pub trait AsyncSerializer<Input> {
22 fn serialize_async<W: tokio::io::AsyncWrite + Unpin>(
24 &self,
25 input: &[Input],
26 writer: W,
27 ) -> impl std::future::Future<Output = TurtleResult<()>> + Send;
28
29 fn serialize_item_async<W: tokio::io::AsyncWrite + Unpin>(
31 &self,
32 input: &Input,
33 writer: W,
34 ) -> impl std::future::Future<Output = TurtleResult<()>> + Send;
35}
36
37#[derive(Debug, Clone)]
39pub struct SerializationConfig {
40 pub pretty: bool,
42 pub base_iri: Option<String>,
44 pub prefixes: std::collections::HashMap<String, String>,
46 pub use_prefixes: bool,
48 pub max_line_length: Option<usize>,
50 pub indent: String,
52 pub normalize_iris: bool,
62}
63
64impl Default for SerializationConfig {
65 fn default() -> Self {
66 Self {
67 pretty: true,
68 base_iri: None,
69 prefixes: std::collections::HashMap::new(),
70 use_prefixes: true,
71 max_line_length: Some(80),
72 indent: " ".to_string(),
73 normalize_iris: false, }
75 }
76}
77
78impl SerializationConfig {
79 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn with_pretty(mut self, pretty: bool) -> Self {
86 self.pretty = pretty;
87 self
88 }
89
90 pub fn with_base_iri(mut self, base_iri: String) -> Self {
92 self.base_iri = Some(base_iri);
93 self
94 }
95
96 pub fn with_prefix(mut self, prefix: String, iri: String) -> Self {
98 self.prefixes.insert(prefix, iri);
99 self
100 }
101
102 pub fn with_use_prefixes(mut self, use_prefixes: bool) -> Self {
104 self.use_prefixes = use_prefixes;
105 self
106 }
107
108 pub fn with_max_line_length(mut self, max_length: Option<usize>) -> Self {
110 self.max_line_length = max_length;
111 self
112 }
113
114 pub fn with_indent(mut self, indent: String) -> Self {
116 self.indent = indent;
117 self
118 }
119
120 pub fn with_normalize_iris(mut self, normalize: bool) -> Self {
136 self.normalize_iris = normalize;
137 self
138 }
139}
140
141fn is_pn_chars_base(c: char) -> bool {
160 matches!(c,
161 'A'..='Z' | 'a'..='z' |
162 '\u{00C0}'..='\u{00D6}' | '\u{00D8}'..='\u{00F6}' | '\u{00F8}'..='\u{02FF}' |
163 '\u{0370}'..='\u{037D}' | '\u{037F}'..='\u{1FFF}' |
164 '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' |
165 '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' |
166 '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' |
167 '\u{10000}'..='\u{EFFFF}'
168 )
169}
170
171fn is_pn_chars_u(c: char) -> bool {
172 is_pn_chars_base(c) || c == '_'
173}
174
175fn is_pn_chars(c: char) -> bool {
176 is_pn_chars_u(c)
177 || c == '-'
178 || c.is_ascii_digit()
179 || c == '\u{00B7}'
180 || ('\u{0300}'..='\u{036F}').contains(&c)
181 || ('\u{203F}'..='\u{2040}').contains(&c)
182}
183
184fn is_pn_local_esc_char(c: char) -> bool {
186 matches!(
187 c,
188 '_' | '~'
189 | '.'
190 | '-'
191 | '!'
192 | '$'
193 | '&'
194 | '\''
195 | '('
196 | ')'
197 | '*'
198 | '+'
199 | ','
200 | ';'
201 | '='
202 | '/'
203 | '?'
204 | '#'
205 | '@'
206 | '%'
207 )
208}
209
210fn escape_pn_local(local: &str) -> Option<String> {
218 if local.is_empty() {
219 return Some(String::new());
221 }
222
223 let chars: Vec<char> = local.chars().collect();
224 let n = chars.len();
225 let mut result = String::with_capacity(local.len());
226 let mut i = 0;
227
228 while i < n {
229 let c = chars[i];
230 let is_first = i == 0;
231
232 if c == '%' {
233 if i + 2 < n && chars[i + 1].is_ascii_hexdigit() && chars[i + 2].is_ascii_hexdigit() {
236 result.push('%');
237 result.push(chars[i + 1]);
238 result.push(chars[i + 2]);
239 i += 3;
240 continue;
241 }
242 return None;
243 }
244
245 let allowed_unescaped = if is_first {
246 is_pn_chars_u(c) || c.is_ascii_digit() || c == ':'
247 } else {
248 is_pn_chars(c) || c == ':' || c == '.'
249 };
250
251 if allowed_unescaped {
252 result.push(c);
253 i += 1;
254 continue;
255 }
256
257 if is_pn_local_esc_char(c) {
258 result.push('\\');
259 result.push(c);
260 i += 1;
261 continue;
262 }
263
264 return None;
267 }
268
269 if result.ends_with('.') && !result.ends_with("\\.") {
273 result.pop();
274 result.push('\\');
275 result.push('.');
276 }
277
278 Some(result)
279}
280
281pub struct FormattedWriter<W: Write> {
283 writer: W,
284 config: SerializationConfig,
285 current_line_length: usize,
286 indent_level: usize,
287}
288
289impl<W: Write> FormattedWriter<W> {
290 pub fn new(writer: W, config: SerializationConfig) -> Self {
292 Self {
293 writer,
294 config,
295 current_line_length: 0,
296 indent_level: 0,
297 }
298 }
299
300 pub fn write_str(&mut self, s: &str) -> std::io::Result<()> {
302 if self.config.pretty {
303 if let Some(max_len) = self.config.max_line_length {
305 if self.current_line_length + s.len() > max_len && self.current_line_length > 0 {
306 self.write_newline()?;
307 }
308 }
309 }
310
311 self.writer.write_all(s.as_bytes())?;
312 self.current_line_length += s.len();
313 Ok(())
314 }
315
316 pub fn write_newline(&mut self) -> std::io::Result<()> {
318 self.writer.write_all(b"\n")?;
319 self.current_line_length = 0;
320
321 if self.config.pretty {
322 for _ in 0..self.indent_level {
323 self.writer.write_all(self.config.indent.as_bytes())?;
324 self.current_line_length += self.config.indent.len();
325 }
326 }
327 Ok(())
328 }
329
330 pub fn increase_indent(&mut self) {
332 self.indent_level += 1;
333 }
334
335 pub fn decrease_indent(&mut self) {
337 if self.indent_level > 0 {
338 self.indent_level -= 1;
339 }
340 }
341
342 pub fn write_space(&mut self) -> std::io::Result<()> {
344 if self.config.pretty {
345 self.write_str(" ")
346 } else {
347 Ok(())
348 }
349 }
350
351 pub fn abbreviate_iri(&self, iri: &str) -> String {
363 if !self.config.use_prefixes {
364 return format!("<{iri}>");
365 }
366
367 let mut candidates: Vec<(&String, &String)> = self
371 .config
372 .prefixes
373 .iter()
374 .filter(|(_, prefix_iri)| {
375 !prefix_iri.is_empty() && iri.starts_with(prefix_iri.as_str())
376 })
377 .collect();
378 candidates.sort_by_key(|(_, prefix_iri)| std::cmp::Reverse(prefix_iri.len()));
379
380 for (prefix, prefix_iri) in candidates {
381 let local = &iri[prefix_iri.len()..];
382 if let Some(escaped_local) = escape_pn_local(local) {
383 return format!("{prefix}:{escaped_local}");
384 }
385 }
386
387 if let Some(ref base) = self.config.base_iri {
389 if iri.starts_with(base) {
390 let relative = &iri[base.len()..];
391 return format!("<{relative}>");
392 }
393 }
394
395 format!("<{iri}>")
396 }
397
398 pub fn escape_string(&self, s: &str) -> String {
400 let mut result = String::with_capacity(s.len() + 2);
401 result.push('"');
402
403 for ch in s.chars() {
404 match ch {
405 '"' => result.push_str("\\\""),
406 '\\' => result.push_str("\\\\"),
407 '\n' => result.push_str("\\n"),
408 '\r' => result.push_str("\\r"),
409 '\t' => result.push_str("\\t"),
410 c if c.is_control() => {
411 result.push_str(&format!("\\u{:04X}", c as u32));
412 }
413 c => result.push(c),
414 }
415 }
416
417 result.push('"');
418 result
419 }
420
421 pub fn into_inner(self) -> W {
423 self.writer
424 }
425}
426
427impl<W: Write> Write for FormattedWriter<W> {
428 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
429 let s = std::str::from_utf8(buf)
430 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
431 self.write_str(s)?;
432 Ok(buf.len())
433 }
434
435 fn flush(&mut self) -> std::io::Result<()> {
436 self.writer.flush()
437 }
438}
439
440#[cfg(test)]
441mod regression_tests {
442 use super::*;
443 use std::io::Cursor;
444
445 fn writer_with_prefix(prefix: &str, ns: &str) -> FormattedWriter<Cursor<Vec<u8>>> {
446 let config = SerializationConfig::new().with_prefix(prefix.to_string(), ns.to_string());
447 FormattedWriter::new(Cursor::new(Vec::new()), config)
448 }
449
450 #[test]
451 fn regression_abbreviate_iri_escapes_parentheses_in_local_part() {
452 let w = writer_with_prefix("ex", "http://example.org/");
453 let out = w.abbreviate_iri("http://example.org/page(disambiguation)");
454 assert_eq!(out, "ex:page\\(disambiguation\\)");
455 }
456
457 #[test]
458 fn regression_abbreviate_iri_falls_back_to_full_iri_for_illegal_local() {
459 let w = writer_with_prefix("ex", "http://example.org/");
461 let out = w.abbreviate_iri("http://example.org/a<b");
462 assert_eq!(out, "<http://example.org/a<b>");
463 }
464
465 #[test]
466 fn regression_abbreviate_iri_escapes_extra_slash_in_local_part() {
467 let w = writer_with_prefix("ex", "http://example.org/");
468 let out = w.abbreviate_iri("http://example.org/a/b");
469 assert_eq!(out, "ex:a\\/b");
470 }
471
472 #[test]
473 fn regression_abbreviate_iri_escapes_trailing_dot() {
474 let w = writer_with_prefix("ex", "http://example.org/");
475 let out = w.abbreviate_iri("http://example.org/v1.0.");
476 assert_eq!(out, "ex:v1.0\\.");
477 }
478
479 #[test]
480 fn regression_abbreviate_iri_plain_local_unchanged() {
481 let w = writer_with_prefix("ex", "http://example.org/");
482 let out = w.abbreviate_iri("http://example.org/alice");
483 assert_eq!(out, "ex:alice");
484 }
485
486 #[test]
487 fn regression_abbreviate_iri_escapes_every_reserved_char() {
488 let w = writer_with_prefix("ex", "http://example.org/");
489 let out = w.abbreviate_iri("http://example.org/page(disambiguation),v2");
490 assert_eq!(out, "ex:page\\(disambiguation\\)\\,v2");
491 }
492}