use crate::error::{SageError, SageResult};
use crate::mock::{try_get_mock, MockResponse};
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct FsClient {
root: PathBuf,
}
impl FsClient {
pub fn new() -> Self {
Self {
root: PathBuf::from("."),
}
}
pub fn from_env() -> Self {
let root = std::env::var("SAGE_FS_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
Self { root }
}
pub fn with_root(root: PathBuf) -> Self {
Self { root }
}
fn resolve_path(&self, path: &str) -> PathBuf {
self.root.join(path)
}
pub async fn read(&self, path: String) -> SageResult<String> {
if let Some(mock_response) = try_get_mock("Fs", "read") {
return Self::apply_mock_string(mock_response);
}
let full_path = self.resolve_path(&path);
let content = tokio::fs::read_to_string(&full_path).await?;
Ok(content)
}
pub async fn write(&self, path: String, content: String) -> SageResult<()> {
if let Some(mock_response) = try_get_mock("Fs", "write") {
return Self::apply_mock_unit(mock_response);
}
let full_path = self.resolve_path(&path);
if let Some(parent) = full_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&full_path, content).await?;
Ok(())
}
pub async fn exists(&self, path: String) -> SageResult<bool> {
if let Some(mock_response) = try_get_mock("Fs", "exists") {
return Self::apply_mock_bool(mock_response);
}
let full_path = self.resolve_path(&path);
Ok(full_path.exists())
}
pub async fn list(&self, path: String) -> SageResult<Vec<String>> {
if let Some(mock_response) = try_get_mock("Fs", "list") {
return Self::apply_mock_vec_string(mock_response);
}
let full_path = self.resolve_path(&path);
let mut entries = tokio::fs::read_dir(&full_path).await?;
let mut names = Vec::new();
while let Some(entry) = entries.next_entry().await? {
if let Some(name) = entry.file_name().to_str() {
names.push(name.to_string());
}
}
Ok(names)
}
pub async fn delete(&self, path: String) -> SageResult<()> {
if let Some(mock_response) = try_get_mock("Fs", "delete") {
return Self::apply_mock_unit(mock_response);
}
let full_path = self.resolve_path(&path);
tokio::fs::remove_file(&full_path).await?;
Ok(())
}
fn apply_mock_string(mock_response: MockResponse) -> SageResult<String> {
match mock_response {
MockResponse::Value(v) => serde_json::from_value(v)
.map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
}
}
fn apply_mock_unit(mock_response: MockResponse) -> SageResult<()> {
match mock_response {
MockResponse::Value(_) => Ok(()),
MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
}
}
fn apply_mock_bool(mock_response: MockResponse) -> SageResult<bool> {
match mock_response {
MockResponse::Value(v) => serde_json::from_value(v)
.map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
}
}
fn apply_mock_vec_string(mock_response: MockResponse) -> SageResult<Vec<String>> {
match mock_response {
MockResponse::Value(v) => serde_json::from_value(v)
.map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
}
}
}
impl Default for FsClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filesystem_client_creates() {
let client = FsClient::new();
assert_eq!(client.root, PathBuf::from("."));
}
#[test]
fn filesystem_client_with_root() {
let client = FsClient::with_root(PathBuf::from("/tmp"));
assert_eq!(client.root, PathBuf::from("/tmp"));
}
#[tokio::test]
async fn filesystem_read_write() {
let temp_dir = tempfile::tempdir().unwrap();
let client = FsClient::with_root(temp_dir.path().to_path_buf());
client
.write("test.txt".to_string(), "Hello, World!".to_string())
.await
.unwrap();
let content = client.read("test.txt".to_string()).await.unwrap();
assert_eq!(content, "Hello, World!");
assert!(client.exists("test.txt".to_string()).await.unwrap());
client.delete("test.txt".to_string()).await.unwrap();
assert!(!client.exists("test.txt".to_string()).await.unwrap());
}
#[tokio::test]
async fn filesystem_list() {
let temp_dir = tempfile::tempdir().unwrap();
let client = FsClient::with_root(temp_dir.path().to_path_buf());
client
.write("a.txt".to_string(), "a".to_string())
.await
.unwrap();
client
.write("b.txt".to_string(), "b".to_string())
.await
.unwrap();
let mut files = client.list(".".to_string()).await.unwrap();
files.sort();
assert_eq!(files, vec!["a.txt", "b.txt"]);
}
#[tokio::test]
async fn filesystem_write_creates_parents() {
let temp_dir = tempfile::tempdir().unwrap();
let client = FsClient::with_root(temp_dir.path().to_path_buf());
client
.write("nested/dir/file.txt".to_string(), "content".to_string())
.await
.unwrap();
assert!(client
.exists("nested/dir/file.txt".to_string())
.await
.unwrap());
}
}