use crate::capability::{Capability, Context, Output};
use crate::validation::path::{validate_path, PathContext};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileReadArgs {
pub path: String,
}
pub struct FileRead;
impl Capability for FileRead {
fn name(&self) -> &'static str {
"FileRead"
}
fn schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
})
}
fn validate(&self, args: &Value) -> Result<()> {
let args: FileReadArgs = serde_json::from_value(args.clone())
.map_err(|e| Error::SchemaValidationFailed(e.to_string()))?;
let ctx = PathContext {
require_exists: true,
require_file: true,
..Default::default()
};
validate_path(&args.path, &ctx)
.map_err(Error::SchemaValidationFailed)?;
Ok(())
}
fn execute(&self, args: &Value, _ctx: &Context) -> Result<Output> {
let args: FileReadArgs = serde_json::from_value(args.clone())
.map_err(|e| Error::ExecutionFailed(e.to_string()))?;
let path = Path::new(&args.path);
let metadata = path
.metadata()
.map_err(|e| Error::ExecutionFailed(format!("stat {}: {}", args.path, e)))?;
if metadata.len() > MAX_FILE_SIZE {
return Err(Error::ExecutionFailed(format!(
"File too large: {} bytes (limit: {} bytes)",
metadata.len(),
MAX_FILE_SIZE
)));
}
let content = std::fs::read_to_string(&args.path)
.map_err(|e| Error::ExecutionFailed(format!("read {}: {}", args.path, e)))?;
Ok(Output {
success: true,
data: serde_json::json!({ "content": content, "path": args.path }),
message: Some(format!("Read {} bytes from {}", content.len(), args.path)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn reads_existing_file() {
let mut tmp = std::env::temp_dir();
tmp.push("runtimo_test_read.txt");
{
let mut f = std::fs::File::create(&tmp).unwrap();
writeln!(f, "hello world").unwrap();
}
let result = FileRead
.execute(
&serde_json::json!({ "path": tmp.to_str().unwrap() }),
&Context {
dry_run: false,
job_id: "test".into(),
working_dir: std::env::temp_dir(),
},
)
.unwrap();
assert!(result.success);
assert!(result.data["content"]
.as_str()
.unwrap()
.contains("hello world"));
std::fs::remove_file(&tmp).ok();
}
#[test]
fn rejects_missing_file() {
let err = FileRead
.validate(&serde_json::json!({
"path": "/tmp/nonexistent_runtimo_test.txt"
}))
.unwrap_err();
assert!(err.to_string().contains("does not exist"));
}
#[test]
fn rejects_empty_path() {
assert!(FileRead
.validate(&serde_json::json!({ "path": "" }))
.is_err());
}
}