1use std::collections::HashMap;
2use std::path::Path;
3
4use handlebars::Handlebars;
5use serde::Serialize;
6
7use crate::element::SysmlElement;
8use crate::graph::SysmlGraph;
9use crate::relationship::SysmlRelationship;
10use nomograph_core::traits::KnowledgeGraph;
11use nomograph_core::types::CheckType;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum BuiltinTemplate {
15 TraceabilityMatrix,
16 RequirementsTable,
17 CompletenessReport,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RenderFormat {
22 Markdown,
23 Html,
24 Csv,
25}
26
27#[derive(Debug, thiserror::Error)]
28pub enum RenderError {
29 #[error("template error: {0}")]
30 Template(#[from] handlebars::RenderError),
31 #[error("template parse error: {0}")]
32 TemplateParse(#[from] Box<handlebars::TemplateError>),
33 #[error("io error: {0}")]
34 Io(#[from] std::io::Error),
35}
36
37#[derive(Serialize)]
38struct TraceabilityRow {
39 requirement: String,
40 kind: String,
41 file: String,
42 satisfied_by: Vec<String>,
43 verified_by: Vec<String>,
44 status: String,
45}
46
47#[derive(Serialize)]
48struct TraceabilityContext {
49 rows: Vec<TraceabilityRow>,
50 total_requirements: usize,
51 satisfied_count: usize,
52 verified_count: usize,
53 coverage_pct: String,
54}
55
56#[derive(Serialize)]
57struct RequirementRow {
58 name: String,
59 kind: String,
60 file: String,
61 line: u32,
62 doc: String,
63 member_count: usize,
64}
65
66#[derive(Serialize)]
67struct RequirementsContext {
68 rows: Vec<RequirementRow>,
69 total: usize,
70}
71
72#[derive(Serialize)]
73struct CheckSummary {
74 name: String,
75 count: usize,
76}
77
78#[derive(Serialize)]
79struct CompletenessContext {
80 files: usize,
81 elements: usize,
82 relationships: usize,
83 completeness_score: String,
84 checks: Vec<CheckSummary>,
85 total_findings: usize,
86 type_breakdown: Vec<TypeCount>,
87}
88
89#[derive(Serialize)]
90struct TypeCount {
91 kind: String,
92 count: usize,
93}
94
95const TRACEABILITY_MATRIX_MD: &str = r#"# Traceability Matrix
96
97| Requirement | Kind | Satisfied By | Verified By | Status |
98|-------------|------|-------------|-------------|--------|
99{{#each rows}}
100| {{requirement}} | {{kind}} | {{#each satisfied_by}}{{this}}{{#unless @last}}, {{/unless}}{{/each}} | {{#each verified_by}}{{this}}{{#unless @last}}, {{/unless}}{{/each}} | {{status}} |
101{{/each}}
102
103**Summary**: {{total_requirements}} requirements, {{satisfied_count}} satisfied, {{verified_count}} verified ({{coverage_pct}}% coverage)
104"#;
105
106const TRACEABILITY_MATRIX_HTML: &str = r#"<html><head><title>Traceability Matrix</title>
107<style>table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f4f4f4}.gap{background:#fee}.ok{background:#efe}</style>
108</head><body>
109<h1>Traceability Matrix</h1>
110<table><thead><tr><th>Requirement</th><th>Kind</th><th>Satisfied By</th><th>Verified By</th><th>Status</th></tr></thead><tbody>
111{{#each rows}}
112<tr class="{{#if (eq status "gap")}}gap{{else}}ok{{/if}}"><td>{{requirement}}</td><td>{{kind}}</td><td>{{#each satisfied_by}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</td><td>{{#each verified_by}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</td><td>{{status}}</td></tr>
113{{/each}}
114</tbody></table>
115<p><strong>Summary</strong>: {{total_requirements}} requirements, {{satisfied_count}} satisfied, {{verified_count}} verified ({{coverage_pct}}% coverage)</p>
116</body></html>"#;
117
118const TRACEABILITY_MATRIX_CSV: &str = r#"Requirement,Kind,Satisfied By,Verified By,Status
119{{#each rows}}
120{{requirement}},{{kind}},"{{#each satisfied_by}}{{this}}{{#unless @last}}; {{/unless}}{{/each}}","{{#each verified_by}}{{this}}{{#unless @last}}; {{/unless}}{{/each}}",{{status}}
121{{/each}}"#;
122
123const REQUIREMENTS_TABLE_MD: &str = r#"# Requirements Table
124
125| # | Requirement | Kind | File | Line | Doc | Members |
126|---|-------------|------|------|------|-----|---------|
127{{#each rows}}
128| {{@index}} | {{name}} | {{kind}} | {{file}} | {{line}} | {{doc}} | {{member_count}} |
129{{/each}}
130
131**Total**: {{total}} requirements
132"#;
133
134const REQUIREMENTS_TABLE_HTML: &str = r#"<html><head><title>Requirements Table</title>
135<style>table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f4f4f4}</style>
136</head><body>
137<h1>Requirements Table</h1>
138<table><thead><tr><th>#</th><th>Requirement</th><th>Kind</th><th>File</th><th>Line</th><th>Doc</th><th>Members</th></tr></thead><tbody>
139{{#each rows}}
140<tr><td>{{@index}}</td><td>{{name}}</td><td>{{kind}}</td><td>{{file}}</td><td>{{line}}</td><td>{{doc}}</td><td>{{member_count}}</td></tr>
141{{/each}}
142</tbody></table>
143<p><strong>Total</strong>: {{total}} requirements</p>
144</body></html>"#;
145
146const REQUIREMENTS_TABLE_CSV: &str = r#"#,Requirement,Kind,File,Line,Doc,Members
147{{#each rows}}
148{{@index}},{{name}},{{kind}},{{file}},{{line}},"{{doc}}",{{member_count}}
149{{/each}}"#;
150
151const COMPLETENESS_REPORT_MD: &str = r#"# Model Completeness Report
152
153## Overview
154
155| Metric | Value |
156|--------|-------|
157| Files | {{files}} |
158| Elements | {{elements}} |
159| Relationships | {{relationships}} |
160| Completeness Score | {{completeness_score}} |
161| Total Findings | {{total_findings}} |
162
163## Check Results
164
165| Check | Findings |
166|-------|----------|
167{{#each checks}}
168| {{name}} | {{count}} |
169{{/each}}
170
171## Type Breakdown
172
173| Kind | Count |
174|------|-------|
175{{#each type_breakdown}}
176| {{kind}} | {{count}} |
177{{/each}}
178"#;
179
180const COMPLETENESS_REPORT_HTML: &str = r#"<html><head><title>Model Completeness Report</title>
181<style>table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f4f4f4}h1{color:#333}</style>
182</head><body>
183<h1>Model Completeness Report</h1>
184<h2>Overview</h2>
185<table><tbody>
186<tr><td>Files</td><td>{{files}}</td></tr>
187<tr><td>Elements</td><td>{{elements}}</td></tr>
188<tr><td>Relationships</td><td>{{relationships}}</td></tr>
189<tr><td>Completeness Score</td><td>{{completeness_score}}</td></tr>
190<tr><td>Total Findings</td><td>{{total_findings}}</td></tr>
191</tbody></table>
192<h2>Check Results</h2>
193<table><thead><tr><th>Check</th><th>Findings</th></tr></thead><tbody>
194{{#each checks}}
195<tr><td>{{name}}</td><td>{{count}}</td></tr>
196{{/each}}
197</tbody></table>
198<h2>Type Breakdown</h2>
199<table><thead><tr><th>Kind</th><th>Count</th></tr></thead><tbody>
200{{#each type_breakdown}}
201<tr><td>{{kind}}</td><td>{{count}}</td></tr>
202{{/each}}
203</tbody></table>
204</body></html>"#;
205
206const COMPLETENESS_REPORT_CSV: &str = r#"Metric,Value
207Files,{{files}}
208Elements,{{elements}}
209Relationships,{{relationships}}
210Completeness Score,{{completeness_score}}
211Total Findings,{{total_findings}}
212
213Check,Findings
214{{#each checks}}
215{{name}},{{count}}
216{{/each}}
217
218Kind,Count
219{{#each type_breakdown}}
220{{kind}},{{count}}
221{{/each}}"#;
222
223fn get_template(builtin: BuiltinTemplate, format: RenderFormat) -> &'static str {
224 match (builtin, format) {
225 (BuiltinTemplate::TraceabilityMatrix, RenderFormat::Markdown) => TRACEABILITY_MATRIX_MD,
226 (BuiltinTemplate::TraceabilityMatrix, RenderFormat::Html) => TRACEABILITY_MATRIX_HTML,
227 (BuiltinTemplate::TraceabilityMatrix, RenderFormat::Csv) => TRACEABILITY_MATRIX_CSV,
228 (BuiltinTemplate::RequirementsTable, RenderFormat::Markdown) => REQUIREMENTS_TABLE_MD,
229 (BuiltinTemplate::RequirementsTable, RenderFormat::Html) => REQUIREMENTS_TABLE_HTML,
230 (BuiltinTemplate::RequirementsTable, RenderFormat::Csv) => REQUIREMENTS_TABLE_CSV,
231 (BuiltinTemplate::CompletenessReport, RenderFormat::Markdown) => COMPLETENESS_REPORT_MD,
232 (BuiltinTemplate::CompletenessReport, RenderFormat::Html) => COMPLETENESS_REPORT_HTML,
233 (BuiltinTemplate::CompletenessReport, RenderFormat::Csv) => COMPLETENESS_REPORT_CSV,
234 }
235}
236
237fn short_name(qualified: &str) -> &str {
238 qualified.rsplit("::").next().unwrap_or(qualified)
239}
240
241fn short_path(p: &Path) -> String {
242 p.file_name()
243 .and_then(|n| n.to_str())
244 .unwrap_or_default()
245 .to_string()
246}
247
248fn is_requirement(elem: &SysmlElement) -> bool {
249 elem.kind.to_lowercase().contains("requirement")
250}
251
252fn build_satisfy_map(rels: &[SysmlRelationship]) -> HashMap<String, Vec<String>> {
253 let mut map: HashMap<String, Vec<String>> = HashMap::new();
254 for rel in rels {
255 if rel.kind.eq_ignore_ascii_case("satisfy") {
256 map.entry(rel.target.to_lowercase())
257 .or_default()
258 .push(short_name(&rel.source).to_string());
259 let short = short_name(&rel.target).to_lowercase();
260 if short != rel.target.to_lowercase() {
261 map.entry(short)
262 .or_default()
263 .push(short_name(&rel.source).to_string());
264 }
265 }
266 }
267 map
268}
269
270fn build_verify_map(rels: &[SysmlRelationship]) -> HashMap<String, Vec<String>> {
271 let mut map: HashMap<String, Vec<String>> = HashMap::new();
272 for rel in rels {
273 if rel.kind.eq_ignore_ascii_case("verify") {
274 map.entry(rel.target.to_lowercase())
275 .or_default()
276 .push(short_name(&rel.source).to_string());
277 let short = short_name(&rel.target).to_lowercase();
278 if short != rel.target.to_lowercase() {
279 map.entry(short)
280 .or_default()
281 .push(short_name(&rel.source).to_string());
282 }
283 }
284 }
285 map
286}
287
288fn build_traceability_context(graph: &SysmlGraph) -> TraceabilityContext {
289 let satisfy_map = build_satisfy_map(graph.relationships());
290 let verify_map = build_verify_map(graph.relationships());
291
292 let mut rows = Vec::new();
293 let mut satisfied_count = 0;
294 let mut verified_count = 0;
295
296 for elem in graph.elements() {
297 if !is_requirement(elem) {
298 continue;
299 }
300
301 let qname_lower = elem.qualified_name.to_lowercase();
302 let short_lower = short_name(&elem.qualified_name).to_lowercase();
303
304 let mut satisfied_by: Vec<String> = Vec::new();
305 if let Some(v) = satisfy_map.get(&qname_lower) {
306 satisfied_by.extend(v.iter().cloned());
307 }
308 if qname_lower != short_lower {
309 if let Some(v) = satisfy_map.get(&short_lower) {
310 for s in v {
311 if !satisfied_by.contains(s) {
312 satisfied_by.push(s.clone());
313 }
314 }
315 }
316 }
317
318 let mut verified_by: Vec<String> = Vec::new();
319 if let Some(v) = verify_map.get(&qname_lower) {
320 verified_by.extend(v.iter().cloned());
321 }
322 if qname_lower != short_lower {
323 if let Some(v) = verify_map.get(&short_lower) {
324 for s in v {
325 if !verified_by.contains(s) {
326 verified_by.push(s.clone());
327 }
328 }
329 }
330 }
331
332 let has_satisfy = !satisfied_by.is_empty();
333 let has_verify = !verified_by.is_empty();
334
335 if has_satisfy {
336 satisfied_count += 1;
337 }
338 if has_verify {
339 verified_count += 1;
340 }
341
342 let status = if has_satisfy && has_verify {
343 "complete"
344 } else if has_satisfy || has_verify {
345 "partial"
346 } else {
347 "gap"
348 };
349
350 rows.push(TraceabilityRow {
351 requirement: short_name(&elem.qualified_name).to_string(),
352 kind: elem.kind.clone(),
353 file: short_path(&elem.file_path),
354 satisfied_by,
355 verified_by,
356 status: status.to_string(),
357 });
358 }
359
360 let total = rows.len();
361 let coverage_pct = if total > 0 {
362 format!(
363 "{:.0}",
364 (satisfied_count.min(total) + verified_count.min(total)) as f64 / (2 * total) as f64
365 * 100.0
366 )
367 } else {
368 "100".to_string()
369 };
370
371 TraceabilityContext {
372 rows,
373 total_requirements: total,
374 satisfied_count,
375 verified_count,
376 coverage_pct,
377 }
378}
379
380fn build_requirements_context(graph: &SysmlGraph) -> RequirementsContext {
381 let mut rows: Vec<RequirementRow> = graph
382 .elements()
383 .iter()
384 .filter(|e| is_requirement(e))
385 .map(|e| RequirementRow {
386 name: short_name(&e.qualified_name).to_string(),
387 kind: e.kind.clone(),
388 file: short_path(&e.file_path),
389 line: e.span.start_line,
390 doc: e.doc.as_deref().unwrap_or("").to_string(),
391 member_count: e.members.len(),
392 })
393 .collect();
394
395 rows.sort_by(|a, b| a.name.cmp(&b.name));
396 let total = rows.len();
397 RequirementsContext { rows, total }
398}
399
400fn build_completeness_context(graph: &SysmlGraph) -> CompletenessContext {
401 let all_checks = [
402 (CheckType::OrphanRequirements, "Orphan Requirements"),
403 (CheckType::UnverifiedRequirements, "Unverified Requirements"),
404 (CheckType::MissingVerification, "Missing Verification"),
405 (CheckType::UnconnectedPorts, "Unconnected Ports"),
406 (CheckType::DanglingReferences, "Dangling References"),
407 ];
408
409 let mut checks = Vec::new();
410 let mut total_findings = 0;
411 let mut orphan_count = 0;
412 let mut unverified_count = 0;
413
414 for (ct, name) in &all_checks {
415 let findings = graph.check(ct.clone());
416 let count = findings.len();
417 total_findings += count;
418 match ct {
419 CheckType::OrphanRequirements => orphan_count = count,
420 CheckType::UnverifiedRequirements => unverified_count = count,
421 _ => {}
422 }
423 checks.push(CheckSummary {
424 name: name.to_string(),
425 count,
426 });
427 }
428
429 let total_requirements = graph
430 .elements()
431 .iter()
432 .filter(|e| is_requirement(e))
433 .count();
434
435 let completeness_score = if total_requirements > 0 {
436 let gap = (orphan_count + unverified_count).min(total_requirements);
437 1.0 - (gap as f64 / total_requirements as f64)
438 } else {
439 1.0
440 };
441
442 let mut type_map: HashMap<&str, usize> = HashMap::new();
443 for elem in graph.elements() {
444 *type_map.entry(&elem.kind).or_insert(0) += 1;
445 }
446 let mut type_breakdown: Vec<TypeCount> = type_map
447 .into_iter()
448 .map(|(kind, count)| TypeCount {
449 kind: kind.to_string(),
450 count,
451 })
452 .collect();
453 type_breakdown.sort_by(|a, b| b.count.cmp(&a.count));
454
455 CompletenessContext {
456 files: graph.file_count(),
457 elements: graph.element_count(),
458 relationships: graph.relationship_count(),
459 completeness_score: format!("{:.3}", completeness_score),
460 checks,
461 total_findings,
462 type_breakdown,
463 }
464}
465
466pub fn render_builtin(
467 graph: &SysmlGraph,
468 template: BuiltinTemplate,
469 format: RenderFormat,
470) -> Result<String, RenderError> {
471 let tmpl_str = get_template(template, format);
472 let mut hbs = Handlebars::new();
473 hbs.set_strict_mode(false);
474 hbs.register_template_string("t", tmpl_str)
475 .map_err(|e| RenderError::TemplateParse(Box::new(e)))?;
476
477 match template {
478 BuiltinTemplate::TraceabilityMatrix => {
479 let ctx = build_traceability_context(graph);
480 Ok(hbs.render("t", &ctx)?)
481 }
482 BuiltinTemplate::RequirementsTable => {
483 let ctx = build_requirements_context(graph);
484 Ok(hbs.render("t", &ctx)?)
485 }
486 BuiltinTemplate::CompletenessReport => {
487 let ctx = build_completeness_context(graph);
488 Ok(hbs.render("t", &ctx)?)
489 }
490 }
491}
492
493pub fn render_custom(graph: &SysmlGraph, template_path: &Path) -> Result<String, RenderError> {
494 let tmpl_str = std::fs::read_to_string(template_path)?;
495 let mut hbs = Handlebars::new();
496 hbs.set_strict_mode(false);
497 hbs.register_template_string("t", &tmpl_str)
498 .map_err(|e| RenderError::TemplateParse(Box::new(e)))?;
499
500 let ctx = build_full_context(graph);
501 Ok(hbs.render("t", &ctx)?)
502}
503
504#[derive(Serialize)]
505struct FullContext {
506 files: usize,
507 elements: usize,
508 relationships: usize,
509 traceability: TraceabilityContext,
510 requirements: RequirementsContext,
511 completeness: CompletenessContext,
512}
513
514fn build_full_context(graph: &SysmlGraph) -> FullContext {
515 FullContext {
516 files: graph.file_count(),
517 elements: graph.element_count(),
518 relationships: graph.relationship_count(),
519 traceability: build_traceability_context(graph),
520 requirements: build_requirements_context(graph),
521 completeness: build_completeness_context(graph),
522 }
523}
524
525pub fn parse_builtin_template(name: &str) -> Option<BuiltinTemplate> {
526 match name.to_lowercase().replace('-', "_").as_str() {
527 "traceability_matrix" | "traceability" => Some(BuiltinTemplate::TraceabilityMatrix),
528 "requirements_table" | "requirements" => Some(BuiltinTemplate::RequirementsTable),
529 "completeness_report" | "completeness" => Some(BuiltinTemplate::CompletenessReport),
530 _ => None,
531 }
532}
533
534pub fn parse_render_format(name: &str) -> Option<RenderFormat> {
535 match name.to_lowercase().as_str() {
536 "markdown" | "md" => Some(RenderFormat::Markdown),
537 "html" => Some(RenderFormat::Html),
538 "csv" => Some(RenderFormat::Csv),
539 _ => None,
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::parser::SysmlParser;
547 use nomograph_core::traits::Parser as NomographParser;
548 use std::path::PathBuf;
549
550 fn fixture_dir() -> PathBuf {
551 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/eve")
552 }
553
554 fn walkdir(dir: PathBuf) -> Vec<PathBuf> {
555 let mut files = Vec::new();
556 if let Ok(entries) = std::fs::read_dir(&dir) {
557 for entry in entries.flatten() {
558 let path = entry.path();
559 if path.is_dir() {
560 files.extend(walkdir(path));
561 } else {
562 files.push(path);
563 }
564 }
565 }
566 files
567 }
568
569 fn build_eve_graph() -> SysmlGraph {
570 let parser = SysmlParser::new();
571 let mut results = Vec::new();
572 for entry in walkdir(fixture_dir()) {
573 if entry.extension().and_then(|e| e.to_str()) == Some("sysml") {
574 let source = std::fs::read_to_string(&entry).expect("read fixture");
575 let result = parser.parse(&source, &entry).expect("parse fixture");
576 results.push(result);
577 }
578 }
579 let mut graph = SysmlGraph::new();
580 graph.index(results).expect("index");
581 graph
582 }
583
584 #[test]
585 fn test_render_traceability_matrix_md() {
586 let graph = build_eve_graph();
587 let output = render_builtin(
588 &graph,
589 BuiltinTemplate::TraceabilityMatrix,
590 RenderFormat::Markdown,
591 )
592 .expect("render should succeed");
593 assert!(output.contains("Traceability Matrix"));
594 assert!(output.contains("Requirement"));
595 assert!(output.contains("Summary"));
596 }
597
598 #[test]
599 fn test_render_traceability_matrix_html() {
600 let graph = build_eve_graph();
601 let output = render_builtin(
602 &graph,
603 BuiltinTemplate::TraceabilityMatrix,
604 RenderFormat::Html,
605 )
606 .expect("render should succeed");
607 assert!(output.contains("<html>"));
608 assert!(output.contains("Traceability Matrix"));
609 assert!(output.contains("<table>"));
610 }
611
612 #[test]
613 fn test_render_traceability_matrix_csv() {
614 let graph = build_eve_graph();
615 let output = render_builtin(
616 &graph,
617 BuiltinTemplate::TraceabilityMatrix,
618 RenderFormat::Csv,
619 )
620 .expect("render should succeed");
621 assert!(output.contains("Requirement,Kind,Satisfied By,Verified By,Status"));
622 }
623
624 #[test]
625 fn test_render_requirements_table_md() {
626 let graph = build_eve_graph();
627 let output = render_builtin(
628 &graph,
629 BuiltinTemplate::RequirementsTable,
630 RenderFormat::Markdown,
631 )
632 .expect("render should succeed");
633 assert!(output.contains("Requirements Table"));
634 assert!(output.contains("Total"));
635 }
636
637 #[test]
638 fn test_render_completeness_report_md() {
639 let graph = build_eve_graph();
640 let output = render_builtin(
641 &graph,
642 BuiltinTemplate::CompletenessReport,
643 RenderFormat::Markdown,
644 )
645 .expect("render should succeed");
646 assert!(output.contains("Model Completeness Report"));
647 assert!(output.contains("Files"));
648 assert!(output.contains("Completeness Score"));
649 assert!(output.contains("Orphan Requirements"));
650 }
651
652 #[test]
653 fn test_render_completeness_report_html() {
654 let graph = build_eve_graph();
655 let output = render_builtin(
656 &graph,
657 BuiltinTemplate::CompletenessReport,
658 RenderFormat::Html,
659 )
660 .expect("render should succeed");
661 assert!(output.contains("<html>"));
662 assert!(output.contains("Model Completeness Report"));
663 }
664
665 #[test]
666 fn test_parse_builtin_template() {
667 assert_eq!(
668 parse_builtin_template("traceability-matrix"),
669 Some(BuiltinTemplate::TraceabilityMatrix)
670 );
671 assert_eq!(
672 parse_builtin_template("requirements-table"),
673 Some(BuiltinTemplate::RequirementsTable)
674 );
675 assert_eq!(
676 parse_builtin_template("completeness-report"),
677 Some(BuiltinTemplate::CompletenessReport)
678 );
679 assert_eq!(
680 parse_builtin_template("traceability"),
681 Some(BuiltinTemplate::TraceabilityMatrix)
682 );
683 assert!(parse_builtin_template("unknown").is_none());
684 }
685
686 #[test]
687 fn test_parse_render_format() {
688 assert_eq!(
689 parse_render_format("markdown"),
690 Some(RenderFormat::Markdown)
691 );
692 assert_eq!(parse_render_format("md"), Some(RenderFormat::Markdown));
693 assert_eq!(parse_render_format("html"), Some(RenderFormat::Html));
694 assert_eq!(parse_render_format("csv"), Some(RenderFormat::Csv));
695 assert!(parse_render_format("xml").is_none());
696 }
697}