pax_manifest/code_serialization/
mod.rs1use colored::Colorize;
2use core::panic;
3use serde_derive::{Deserialize, Serialize};
4use std::{
5 collections::HashMap,
6 fs::{self, File},
7 io::{self, Write},
8 path::{Path, PathBuf},
9};
10
11use similar::{ChangeTag, TextDiff};
12use syn::{parse_file, spanned::Spanned, visit::Visit, Item};
13use tera::{Context, Tera};
14
15use include_dir::{include_dir, Dir};
16
17use crate::{pax_runtime_api::PaxValue, ComponentDefinition, ExpressionInfo, PaxManifest, PaxType};
18use pax_lang::{
19 formatting::{format_file, format_pax_template},
20 helpers::{replace_by_line_column, InlinedTemplateFinder},
21};
22
23#[allow(unused)]
24static TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/code_serialization");
25#[allow(unused)]
26static MANIFEST_CODE_SERIALIZATION_TEMPLATE: &str = "manifest-code-serialization.tera";
27#[allow(unused)]
28static MACROS_TEMPLATE: &str = "macros.tera";
29#[allow(unused)]
30static RUST_FILE_SERIALIZATION_TEMPLATE: &str = "rust-file-serialization.tera";
31
32fn to_pax_value(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
33 match args.get("value") {
34 Some(val) => {
35 let value: Result<PaxValue, serde_json::Error> = serde_json::from_value(val.clone());
36 if let Ok(value) = value {
37 return Ok(tera::Value::String(value.to_string()));
38 }
39 Err(tera::Error::msg("Failed to deserialize value to PaxValue"))
40 }
41 None => Err(tera::Error::msg(
42 "No value provided to to_pax_value function",
43 )),
44 }
45}
46
47fn to_pax_expression(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
48 match args.get("value") {
49 Some(val) => {
50 let value: Result<ExpressionInfo, serde_json::Error> =
51 serde_json::from_value(val.clone());
52 if let Ok(value) = value {
53 return Ok(tera::Value::String(value.expression.to_string()));
54 }
55 Err(tera::Error::msg(format!(
56 "Failed to deserialize value to PaxExpression: {:?}",
57 val
58 )))
59 }
60 None => Err(tera::Error::msg(
61 "No value provided to to_pax_expression function",
62 )),
63 }
64}
65
66pub fn press_code_serialization_template(args: ComponentDefinition) -> Result<String, String> {
68 let mut tera = Tera::default();
69
70 tera.register_function("to_pax_value", to_pax_value);
71 tera.register_function("to_pax_expression", to_pax_expression);
72
73 let macros_file = TEMPLATE_DIR
75 .get_file(MACROS_TEMPLATE)
76 .ok_or_else(|| format!("Failed to get template file: {}", MACROS_TEMPLATE))?;
77
78 let macros_template_contents = macros_file
79 .contents_utf8()
80 .ok_or_else(|| format!("Failed to read template contents: {}", MACROS_TEMPLATE))?;
81
82 tera.add_raw_template(MACROS_TEMPLATE, macros_template_contents)
83 .map_err(|err| format!("Failed to add template '{}': {}", MACROS_TEMPLATE, err))?;
84
85 let manifest_file = TEMPLATE_DIR
87 .get_file(MANIFEST_CODE_SERIALIZATION_TEMPLATE)
88 .ok_or_else(|| {
89 format!(
90 "Failed to get template file: {}",
91 MANIFEST_CODE_SERIALIZATION_TEMPLATE
92 )
93 })?;
94
95 let manifest_template_contents = manifest_file.contents_utf8().ok_or_else(|| {
96 format!(
97 "Failed to read template contents: {}",
98 MANIFEST_CODE_SERIALIZATION_TEMPLATE
99 )
100 })?;
101
102 tera.add_raw_template(
103 MANIFEST_CODE_SERIALIZATION_TEMPLATE,
104 manifest_template_contents,
105 )
106 .map_err(|err| {
107 format!(
108 "Failed to add template '{}': {}",
109 MANIFEST_CODE_SERIALIZATION_TEMPLATE, err
110 )
111 })?;
112
113 let context = tera::Context::from_serialize(args)
115 .map_err(|err| format!("Failed to serialize context: {}", err))?;
116
117 let template = tera
119 .render(MANIFEST_CODE_SERIALIZATION_TEMPLATE, &context)
120 .map_err(|err| format!("Failed to render template: {}", err))?;
121
122 let formatted_template = format_pax_template(template)
124 .map_err(|err| format!("Failed to format template: {}", err))?;
125
126 Ok(formatted_template)
127}
128
129pub fn diff(old_content: &str, new_content: &str) -> Option<String> {
130 let diff = TextDiff::from_lines(old_content, new_content);
131 let mut all_diffs = vec![];
132 for change in diff.iter_all_changes() {
133 let output = match change.tag() {
134 ChangeTag::Delete => Some(format!("-{}", change).red()),
135 ChangeTag::Insert => Some(format!("+{}", change).green()),
136 ChangeTag::Equal => None,
137 };
138
139 if let Some(o) = output {
140 all_diffs.push(format!("{}", o));
141 }
142 }
143 if all_diffs.len() > 0 {
144 Some(all_diffs.join(""))
145 } else {
146 None
147 }
148}
149
150pub fn diff_html(old_content: &str, new_content: &str) -> Option<String> {
151 let diff = TextDiff::from_lines(old_content, new_content);
152 let mut all_diffs = vec![];
153
154 for change in diff.iter_all_changes() {
155 if change.to_string().trim() == "" {
157 continue;
158 }
159
160 let output = match change.tag() {
161 ChangeTag::Delete => Some(format!(
162 "<br/><span style=\"color: red;\">---<code>{}</code></span>",
163 html_escape::encode_text(&change.to_string())
164 )),
165 ChangeTag::Insert => Some(format!(
166 "<span style=\"color: green;\">+++<code>{}</code></span>",
167 html_escape::encode_text(&change.to_string())
168 )),
169 ChangeTag::Equal => None,
170 };
171
172 if let Some(o) = output {
173 all_diffs.push(o);
174 }
175 }
176
177 if !all_diffs.is_empty() {
178 Some(all_diffs.join(""))
179 } else {
180 None
181 }
182}
183
184pub fn serialize_component_to_file(component: &ComponentDefinition, file_path: String) {
187 let path = Path::new(&file_path);
188 let pascal_identifier = component.type_id.get_pascal_identifier().unwrap();
189 let serialized_component = press_code_serialization_template(component.clone()).unwrap();
190
191 serialize_new_component_rust_file(component, file_path.clone());
193
194 match path.extension().and_then(|s| s.to_str()) {
195 Some("pax") => {
196 let old_content = fs::read_to_string(&file_path).unwrap_or_default();
197 let mut file = File::create(&file_path).expect("Failed to create file");
198 file.write_all(serialized_component.as_bytes())
199 .expect("Failed to write to file");
200
201 if let Some(diff) = diff(&old_content, &serialized_component) {
203 println!("{}", diff);
204 }
205 }
206 Some("rs") => write_inlined_pax(serialized_component, path, pascal_identifier),
207 _ => panic!("Unsupported file extension."),
208 }
209}
210
211pub fn serialize_main_component(manifest: &PaxManifest, repo_root: &str) {
212 let mc = manifest.components.get(&manifest.main_component_type_id);
213 if let Some(mc) = mc {
214 if let Some(template) = &mc.template {
215 if let Some(file_path) = &template.get_file_path() {
216 let suffix = file_path.split_once("www.pax.dev/").unwrap().1;
217 let file_path = format!("{}/{}", repo_root, suffix);
218 serialize_component_to_file(mc, file_path.clone());
219 }
220 }
221 }
222}
223
224pub fn serialize_main_component_to_string(manifest: &PaxManifest) -> String {
225 let mc = manifest.components.get(&manifest.main_component_type_id);
226 if let Some(mc) = mc {
227 if let Some(_) = &mc.template {
228 return press_code_serialization_template(mc.clone()).unwrap();
229 }
230 }
231 "".to_string()
232}
233
234fn write_inlined_pax(serialized_component: String, path: &Path, pascal_identifier: String) {
235 let content = fs::read_to_string(path).expect("Failed to read file");
236 let ast = parse_file(&content).expect("Failed to parse file");
237 let mut finder = InlinedTemplateFinder::new(content.clone());
238 finder.visit_file(&ast);
239
240 let template = finder
241 .templates
242 .iter()
243 .find(|t| t.struct_name == pascal_identifier);
244
245 if let Some(data) = template {
246 let new_template = format!("(\n{}\n)", serialized_component);
247 let modified_content =
248 replace_by_line_column(&content, data.start, data.end, new_template).unwrap();
249 fs::write(path, modified_content).expect("Failed to write to file");
250 }
251}
252
253pub fn serialize_new_component_rust_file(comp_def: &ComponentDefinition, pax_file_path: String) {
254 if let PaxType::BlankComponent { pascal_identifier } = comp_def.type_id.get_pax_type() {
255 let path = PathBuf::from(&pax_file_path);
256 let pax_file_name = path.file_name().unwrap().to_str().unwrap();
257 let src = path.parent().unwrap();
258 let entry_point = src.join("lib.rs");
259 let rust_file_path = pax_file_path.replace(".pax", ".rs");
260
261 let rust_file_serialization = RustFileSerialization {
262 pax_path: pax_file_name.to_string(),
263 pascal_identifier: pascal_identifier.clone(),
264 };
265 let rust_file_serialization =
266 press_rust_file_serialization_template(rust_file_serialization);
267 fs::write(rust_file_path, rust_file_serialization).expect("Failed to write to file");
268 add_mod_and_use_if_missing(
269 Path::new(&entry_point),
270 pascal_identifier,
271 &pax_file_name.replace(".pax", ""),
272 )
273 .expect("Failed to add mod and use");
274 }
275}
276
277fn add_mod_and_use_if_missing(
279 file_name: &Path,
280 pascal_identifier: &str,
281 rust_file_name: &str,
282) -> io::Result<()> {
283 let file_content = fs::read_to_string(file_name)?;
284 let syntax_tree = parse_file(&file_content).expect("Failed to parse file");
285
286 if file_content.contains(&format!("pub mod {};", rust_file_name))
287 || file_content.contains(&format!("use {}::{};", rust_file_name, pascal_identifier))
288 {
289 return Ok(());
291 }
292
293 let mut new_content = file_content.clone();
295
296 let mut last_use_end_pos = None;
298
299 for item in syntax_tree.items {
300 if let Item::Use(item_use) = item {
301 last_use_end_pos = Some(item_use.span().end());
302 }
303 }
304
305 let insertion_content = format!(
306 "pub mod {};\nuse {}::{};",
307 rust_file_name, rust_file_name, pascal_identifier
308 );
309
310 match last_use_end_pos {
312 Some(pos) => {
313 insert_at_line(&mut new_content, pos.line, &insertion_content);
314 }
315 None => {
316 new_content = format!("{}{}", insertion_content.trim_end(), new_content);
318 }
319 }
320
321 fs::write(file_name, new_content)?;
322 Ok(())
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct RustFileSerialization {
327 pub pax_path: String,
328 pub pascal_identifier: String,
329}
330
331pub fn press_rust_file_serialization_template(args: RustFileSerialization) -> String {
333 let mut tera = Tera::default();
334
335 tera.add_raw_template(
336 RUST_FILE_SERIALIZATION_TEMPLATE,
337 TEMPLATE_DIR
338 .get_file(RUST_FILE_SERIALIZATION_TEMPLATE)
339 .unwrap()
340 .contents_utf8()
341 .unwrap(),
342 )
343 .expect("Failed to add rust-file-serialization.tera");
344
345 let context = Context::from_serialize(args).unwrap();
346
347 tera.render(RUST_FILE_SERIALIZATION_TEMPLATE, &context)
349 .expect("Failed to render template")
350}
351
352fn insert_at_line(s: &mut String, line_number: usize, content_to_insert: &str) {
353 let mut lines: Vec<&str> = s.lines().collect();
355
356 if line_number <= lines.len() {
358 lines.insert(line_number, content_to_insert);
360 } else {
361 lines.push(content_to_insert);
363 }
364 *s = lines.join("\n");
366}