1use std::sync::Arc;
32
33use minijinja::value::{Object, ObjectRepr, Value};
34use minijinja::{Error, ErrorKind};
35use sherpack_core::files::{FileProvider, Files};
36
37#[derive(Debug)]
42pub struct FilesObject {
43 files: Files,
44}
45
46impl FilesObject {
47 pub fn new(files: Files) -> Self {
49 Self { files }
50 }
51
52 pub fn from_provider(provider: impl FileProvider + 'static) -> Self {
54 Self {
55 files: Files::new(provider),
56 }
57 }
58
59 pub fn from_arc_provider(provider: Arc<dyn FileProvider>) -> Self {
61 Self {
62 files: Files::from_arc(provider),
63 }
64 }
65}
66
67impl Object for FilesObject {
68 fn repr(self: &Arc<Self>) -> ObjectRepr {
69 ObjectRepr::Plain
70 }
71
72 fn call_method(
73 self: &Arc<Self>,
74 _state: &minijinja::State,
75 method: &str,
76 args: &[Value],
77 ) -> Result<Value, Error> {
78 match method {
79 "get" => {
80 let path = get_path_arg(args, "get")?;
81 match self.files.get(&path) {
82 Ok(content) => Ok(Value::from(content)),
83 Err(e) => Err(Error::new(ErrorKind::InvalidOperation, e.to_string())),
84 }
85 }
86
87 "get_bytes" => {
88 let path = get_path_arg(args, "get_bytes")?;
89 match self.files.get_bytes(&path) {
90 Ok(bytes) => {
91 Ok(Value::from(bytes))
93 }
94 Err(e) => Err(Error::new(ErrorKind::InvalidOperation, e.to_string())),
95 }
96 }
97
98 "exists" => {
99 let path = get_path_arg(args, "exists")?;
100 Ok(Value::from(self.files.exists(&path)))
101 }
102
103 "glob" => {
104 let pattern = get_path_arg(args, "glob")?;
105 match self.files.glob(&pattern) {
106 Ok(entries) => {
107 let values: Vec<Value> = entries
109 .into_iter()
110 .map(|entry| {
111 Value::from_object(FileEntryObject {
112 path: entry.path,
113 name: entry.name,
114 content: entry.content,
115 size: entry.size,
116 })
117 })
118 .collect();
119 Ok(Value::from(values))
120 }
121 Err(e) => Err(Error::new(ErrorKind::InvalidOperation, e.to_string())),
122 }
123 }
124
125 "lines" => {
126 let path = get_path_arg(args, "lines")?;
127 match self.files.lines(&path) {
128 Ok(lines) => Ok(Value::from(lines)),
129 Err(e) => Err(Error::new(ErrorKind::InvalidOperation, e.to_string())),
130 }
131 }
132
133 _ => Err(Error::new(
134 ErrorKind::UnknownMethod,
135 format!(
136 "files object has no method '{}'. Available methods: get, get_bytes, exists, glob, lines",
137 method
138 ),
139 )),
140 }
141 }
142}
143
144fn get_path_arg(args: &[Value], method_name: &str) -> Result<String, Error> {
146 args.first()
147 .and_then(|v| v.as_str())
148 .map(|s| s.to_string())
149 .ok_or_else(|| {
150 Error::new(
151 ErrorKind::InvalidOperation,
152 format!("files.{}() requires a path string argument", method_name),
153 )
154 })
155}
156
157#[derive(Debug)]
159struct FileEntryObject {
160 path: String,
161 name: String,
162 content: String,
163 size: usize,
164}
165
166impl Object for FileEntryObject {
167 fn repr(self: &Arc<Self>) -> ObjectRepr {
168 ObjectRepr::Map
169 }
170
171 fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
172 let key_str = key.as_str()?;
173 match key_str {
174 "path" => Some(Value::from(self.path.clone())),
175 "name" => Some(Value::from(self.name.clone())),
176 "content" => Some(Value::from(self.content.clone())),
177 "size" => Some(Value::from(self.size)),
178 _ => None,
179 }
180 }
181
182 fn enumerate(self: &Arc<Self>) -> minijinja::value::Enumerator {
183 minijinja::value::Enumerator::Str(&["path", "name", "content", "size"])
184 }
185}
186
187pub fn create_files_value(files: Files) -> Value {
191 Value::from_object(FilesObject::new(files))
192}
193
194pub fn create_files_value_from_provider(provider: impl FileProvider + 'static) -> Value {
196 Value::from_object(FilesObject::from_provider(provider))
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use minijinja::Environment;
203 use sherpack_core::files::MockFileProvider;
204
205 fn create_test_env() -> (Environment<'static>, Value) {
206 let provider = MockFileProvider::new()
207 .with_text_file("config/app.yaml", "key: value\nother: data")
208 .with_text_file("config/db.yaml", "host: localhost")
209 .with_text_file("scripts/init.sh", "#!/bin/bash\necho hello");
210
211 let files_value = create_files_value_from_provider(provider);
212
213 let mut env = Environment::new();
214 env.add_global("files", files_value.clone());
215
216 (env, files_value)
217 }
218
219 #[test]
220 fn test_files_get() {
221 let (env, _) = create_test_env();
222
223 let result = env
224 .render_str(r#"{{ files.get("config/app.yaml") }}"#, ())
225 .unwrap();
226 assert_eq!(result, "key: value\nother: data");
227 }
228
229 #[test]
230 fn test_files_exists_true() {
231 let (env, _) = create_test_env();
232
233 let result = env
234 .render_str(r#"{{ files.exists("config/app.yaml") }}"#, ())
235 .unwrap();
236 assert_eq!(result, "true");
237 }
238
239 #[test]
240 fn test_files_exists_false() {
241 let (env, _) = create_test_env();
242
243 let result = env
244 .render_str(r#"{{ files.exists("nonexistent.txt") }}"#, ())
245 .unwrap();
246 assert_eq!(result, "false");
247 }
248
249 #[test]
250 fn test_files_glob() {
251 let (env, _) = create_test_env();
252
253 let template = r#"{% for f in files.glob("config/*.yaml") %}{{ f.name }}:{{ f.content | length }},{% endfor %}"#;
254 let result = env.render_str(template, ()).unwrap();
255
256 assert!(result.contains("app.yaml:"));
258 assert!(result.contains("db.yaml:"));
259 }
260
261 #[test]
262 fn test_files_glob_attributes() {
263 let (env, _) = create_test_env();
264
265 let template = r#"{% for f in files.glob("config/app.yaml") %}path={{ f.path }},name={{ f.name }},size={{ f.size }}{% endfor %}"#;
266 let result = env.render_str(template, ()).unwrap();
267
268 assert!(result.contains("path=config/app.yaml"));
269 assert!(result.contains("name=app.yaml"));
270 assert!(result.contains("size="));
271 }
272
273 #[test]
274 fn test_files_lines() {
275 let (env, _) = create_test_env();
276
277 let template =
278 r#"{% for line in files.lines("scripts/init.sh") %}[{{ line }}]{% endfor %}"#;
279 let result = env.render_str(template, ()).unwrap();
280
281 assert_eq!(result, "[#!/bin/bash][echo hello]");
282 }
283
284 #[test]
285 fn test_files_conditional() {
286 let (env, _) = create_test_env();
287
288 let template =
289 r#"{% if files.exists("config/app.yaml") %}found{% else %}not found{% endif %}"#;
290 let result = env.render_str(template, ()).unwrap();
291 assert_eq!(result, "found");
292
293 let template2 =
294 r#"{% if files.exists("missing.yaml") %}found{% else %}not found{% endif %}"#;
295 let result2 = env.render_str(template2, ()).unwrap();
296 assert_eq!(result2, "not found");
297 }
298
299 #[test]
300 fn test_files_get_not_found() {
301 let (env, _) = create_test_env();
302
303 let result = env.render_str(r#"{{ files.get("nonexistent.txt") }}"#, ());
304 assert!(result.is_err());
305 assert!(result.unwrap_err().to_string().contains("not found"));
306 }
307
308 #[test]
309 fn test_files_unknown_method() {
310 let (env, _) = create_test_env();
311
312 let result = env.render_str(r#"{{ files.unknown() }}"#, ());
313 assert!(result.is_err());
314 assert!(result.unwrap_err().to_string().contains("unknown"));
315 }
316
317 #[test]
318 fn test_files_with_filters() {
319 let (env, _) = create_test_env();
320
321 let template = r#"{{ files.get("config/app.yaml") | trim }}"#;
323 let result = env.render_str(template, ()).unwrap();
324 assert_eq!(result, "key: value\nother: data");
325 }
326}