ralph_workflow/prompts/
io.rs1use std::io;
7use std::path::{Path, PathBuf};
8
9pub use crate::prompts::template_registry::TemplateError;
10pub use crate::prompts::template_validator::TemplateMetadata;
11pub use crate::prompts::template_validator::ValidationError;
12pub use crate::prompts::template_validator::VariableInfo;
13
14pub fn get_xdg_config_home() -> Option<PathBuf> {
15 std::env::var("XDG_CONFIG_HOME")
16 .ok()
17 .map(PathBuf::from)
18 .or_else(|| {
19 std::env::var("HOME")
20 .ok()
21 .map(|h| PathBuf::from(h).join(".config"))
22 })
23}
24
25pub fn template_exists(path: &Path) -> bool {
26 path.exists()
27}
28
29#[derive(Debug, thiserror::Error)]
30pub enum LoadTemplateError {
31 #[error("failed to read template from {path:?}")]
32 Io {
33 path: PathBuf,
34 #[source]
35 source: io::Error,
36 },
37}
38
39pub fn load_template(path: &Path) -> Result<String, LoadTemplateError> {
40 std::fs::read_to_string(path).map_err(|source| LoadTemplateError::Io {
41 path: path.to_path_buf(),
42 source,
43 })
44}
45
46pub fn validate_syntax(content: &str) -> Vec<ValidationError> {
47 let bytes = content.as_bytes();
48 let state = crate::prompts::template_parsing::validate_template_bytes(content, bytes);
49 state
50 .errors
51 .into_iter()
52 .map(|e| match e {
53 crate::prompts::template_parsing::ValidationError::UnclosedComment { line } => {
54 ValidationError::UnclosedComment { line }
55 }
56 crate::prompts::template_parsing::ValidationError::UnclosedConditional { line } => {
57 ValidationError::UnclosedConditional { line }
58 }
59 crate::prompts::template_parsing::ValidationError::UnclosedLoop { line } => {
60 ValidationError::UnclosedLoop { line }
61 }
62 crate::prompts::template_parsing::ValidationError::InvalidConditional {
63 line,
64 syntax,
65 } => ValidationError::InvalidConditional { line, syntax },
66 crate::prompts::template_parsing::ValidationError::InvalidLoop { line, syntax } => {
67 ValidationError::InvalidLoop { line, syntax }
68 }
69 })
70 .collect()
71}
72
73use crate::prompts::prompt_history_entry::PromptHistoryEntry;
74use serde::{Serialize, Serializer};
75
76impl Serialize for PromptHistoryEntry {
77 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
78 use serde::ser::SerializeStruct;
79 let mut s = serializer.serialize_struct(
80 "PromptHistoryEntry",
81 if self.content_id.is_some() { 2 } else { 1 },
82 )?;
83 s.serialize_field("content", &self.content)?;
84 if let Some(content_id) = &self.content_id {
85 s.serialize_field("content_id", content_id)?;
86 }
87 s.end()
88 }
89}
90
91fn parse_metadata_line(line: &str) -> Option<(Option<String>, Option<String>)> {
92 crate::prompts::template_parsing::parse_metadata_line_impl(line)
93}
94
95pub fn extract_variables(content: &str) -> Vec<VariableInfo> {
96 crate::prompts::template_parsing::extract_variables_impl(content)
97}
98
99pub fn extract_partials(content: &str) -> Vec<String> {
100 crate::prompts::template_parsing::extract_partials_impl(content)
101}
102
103fn update_metadata_from_line(
104 line: &str,
105 version: &mut Option<String>,
106 purpose: &mut Option<String>,
107) {
108 if !line.starts_with("{#") || !line.ends_with("#}") {
109 return;
110 }
111 if let Some((v, p)) = parse_metadata_line(line) {
112 *version = version.take().or(v);
113 *purpose = purpose.take().or(p);
114 }
115}
116
117pub fn extract_metadata(content: &str) -> TemplateMetadata {
118 let mut version = None;
119 let mut purpose = None;
120 for line in content.lines().take(50) {
121 update_metadata_from_line(line.trim(), &mut version, &mut purpose);
122 }
123 TemplateMetadata { version, purpose }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::io;
130 use std::time::{SystemTime, UNIX_EPOCH};
131
132 fn missing_template_path() -> PathBuf {
133 let now = SystemTime::now()
134 .duration_since(UNIX_EPOCH)
135 .expect("system time is after UNIX_EPOCH")
136 .as_nanos();
137 std::env::temp_dir().join(format!("load_template_missing_{now}"))
138 }
139
140 #[test]
141 fn load_template_missing_file_returns_not_found_error() {
142 let path = missing_template_path();
143 assert!(!path.exists(), "generated path should not already exist");
144
145 let err = load_template(&path).expect_err("expected missing file to return an error");
146 match err {
147 LoadTemplateError::Io {
148 path: err_path,
149 source,
150 } => {
151 assert_eq!(err_path, path);
152 assert_eq!(source.kind(), io::ErrorKind::NotFound);
153 }
154 }
155 }
156}