quillmark_core/quill/
load.rs1use std::path::{Component, Path};
3
4use crate::error::{Diagnostic, Severity};
5use crate::value::QuillValue;
6
7use super::{FileTreeNode, QuillConfig, QuillSource};
8
9fn diag(message: impl Into<String>, code: &str) -> Diagnostic {
10 Diagnostic::new(Severity::Error, message.into()).with_code(code.to_string())
11}
12
13impl QuillSource {
14 pub fn from_tree(root: FileTreeNode) -> Result<Self, Vec<Diagnostic>> {
31 let quill_yaml_bytes = root.get_file("Quill.yaml").ok_or_else(|| {
32 vec![diag(
33 "Quill.yaml not found in file tree",
34 "quill::missing_file",
35 )]
36 })?;
37
38 let quill_yaml_content = String::from_utf8(quill_yaml_bytes.to_vec()).map_err(|e| {
39 vec![diag(
40 format!("Quill.yaml is not valid UTF-8: {}", e),
41 "quill::invalid_utf8",
42 )]
43 })?;
44
45 let (config, _warnings) = QuillConfig::from_yaml_with_warnings(&quill_yaml_content)?;
48
49 Self::from_config(config, root)
50 }
51
52 fn from_config(mut config: QuillConfig, root: FileTreeNode) -> Result<Self, Vec<Diagnostic>> {
54 let mut metadata: std::collections::HashMap<String, QuillValue> =
55 std::collections::HashMap::new();
56
57 metadata.insert(
58 "backend".to_string(),
59 QuillValue::from_json(serde_json::Value::String(config.backend.clone())),
60 );
61
62 metadata.insert(
63 "description".to_string(),
64 QuillValue::from_json(serde_json::Value::String(config.description.clone())),
65 );
66
67 metadata.insert(
68 "author".to_string(),
69 QuillValue::from_json(serde_json::Value::String(config.author.clone())),
70 );
71
72 metadata.insert(
73 "version".to_string(),
74 QuillValue::from_json(serde_json::Value::String(config.version.clone())),
75 );
76
77 for (key, value) in &config.backend_config {
79 metadata.insert(format!("{}_{}", config.backend, key), value.clone());
80 }
81
82 let plate_content: Option<String> = if let Some(ref plate_file_name) = config.plate_file {
84 let plate_bytes = root.get_file(plate_file_name).ok_or_else(|| {
85 vec![diag(
86 format!("Plate file '{}' not found in file tree", plate_file_name),
87 "quill::plate_missing",
88 )]
89 })?;
90
91 let content = String::from_utf8(plate_bytes.to_vec()).map_err(|e| {
92 vec![diag(
93 format!("Plate file '{}' is not valid UTF-8: {}", plate_file_name, e),
94 "quill::invalid_utf8",
95 )]
96 })?;
97 Some(content)
98 } else {
99 None
100 };
101
102 let example_content = if let Some(ref example_file_name) = config.example_file {
104 let example_path = Path::new(example_file_name);
105 if example_path.is_absolute()
106 || example_path
107 .components()
108 .any(|c| matches!(c, Component::ParentDir | Component::Prefix(_)))
109 {
110 return Err(vec![diag(
111 format!(
112 "Example file '{}' is outside the quill directory",
113 example_file_name
114 ),
115 "quill::example_path_traversal",
116 )]);
117 }
118
119 let bytes = root.get_file(example_file_name).ok_or_else(|| {
120 vec![diag(
121 format!(
122 "Example file '{}' referenced in Quill.yaml not found",
123 example_file_name
124 ),
125 "quill::example_missing",
126 )]
127 })?;
128 Some(String::from_utf8(bytes.to_vec()).map_err(|e| {
129 vec![diag(
130 format!(
131 "Example file '{}' is not valid UTF-8: {}",
132 example_file_name, e
133 ),
134 "quill::invalid_utf8",
135 )]
136 })?)
137 } else if root.file_exists("example.md") {
138 let bytes = root
139 .get_file("example.md")
140 .expect("invariant violation: file_exists(example.md) but get_file returned None");
141 Some(String::from_utf8(bytes.to_vec()).map_err(|e| {
142 vec![diag(
143 format!(
144 "Default example file 'example.md' is not valid UTF-8: {}",
145 e
146 ),
147 "quill::invalid_utf8",
148 )]
149 })?)
150 } else {
151 None
152 };
153
154 config.example_markdown = example_content.clone();
155
156 let source = QuillSource {
157 metadata,
158 name: config.name.clone(),
159 backend_id: config.backend.clone(),
160 plate: plate_content,
161 example: example_content,
162 config,
163 files: root,
164 };
165
166 Ok(source)
167 }
168}