pax_manifest/code_serialization/
mod.rs

1use 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
66/// Serialize a component to a string
67pub 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    // Add macros template
74    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    // Add manifest code serialization template
86    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    // Serialize context
114    let context = tera::Context::from_serialize(args)
115        .map_err(|err| format!("Failed to serialize context: {}", err))?;
116
117    // Render template
118    let template = tera
119        .render(MANIFEST_CODE_SERIALIZATION_TEMPLATE, &context)
120        .map_err(|err| format!("Failed to render template: {}", err))?;
121
122    // Format template
123    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        //handle edge-cases where the change is empty
156        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
184/// Serialize a component to a file
185/// Replaces entire .pax file and replaces inlined attribute directly for .rs files
186pub 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    // adds rust file if we're serializing a new component
192    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            // Print the diff
202            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
277/// Adds mod and use for newly created componen
278fn 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        // Lines already present, no need to add them again.
290        return Ok(());
291    }
292
293    // Initialize with the full content; this might be replaced based on finding the last use statement.
294    let mut new_content = file_content.clone();
295
296    // Track the last position where a `use` statement ended.
297    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    // Insert the mod and use statements after the last use statement if found, or prepend if no use statements.
311    match last_use_end_pos {
312        Some(pos) => {
313            insert_at_line(&mut new_content, pos.line, &insertion_content);
314        }
315        None => {
316            // If no use statements are found, simply prepend the mod and use lines.
317            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
331/// Serialize a new component rust file
332pub 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    // Serialize rust
348    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    // Split the string into lines
354    let mut lines: Vec<&str> = s.lines().collect();
355
356    // Check if the specified line number is within the bounds of the lines vector
357    if line_number <= lines.len() {
358        // Insert the content at the specified line number
359        lines.insert(line_number, content_to_insert);
360    } else {
361        // If the line number is beyond the existing lines, append the content instead
362        lines.push(content_to_insert);
363    }
364    // Rejoin the lines and update the original string
365    *s = lines.join("\n");
366}