summaryrefslogtreecommitdiff
path: root/systems/asset/src
diff options
context:
space:
mode:
Diffstat (limited to 'systems/asset/src')
-rw-r--r--systems/asset/src/asset.rs552
-rw-r--r--systems/asset/src/error.rs106
-rw-r--r--systems/asset/src/lib.rs6
-rw-r--r--systems/asset/src/rw.rs85
4 files changed, 749 insertions, 0 deletions
diff --git a/systems/asset/src/asset.rs b/systems/asset/src/asset.rs
new file mode 100644
index 0000000..247a654
--- /dev/null
+++ b/systems/asset/src/asset.rs
@@ -0,0 +1,552 @@
+use std::{
+ borrow::Cow,
+ collections::HashSet,
+ ffi::OsStr,
+ marker::PhantomData,
+ path::{Path, PathBuf},
+};
+
+use constants::{LOCK_FILE_PREFIX, TEMP_FILE_PREFIX};
+use string_proc::format_path::format_path;
+
+use crate::{
+ error::{DataApplyError, DataReadError, DataWriteError, HandleLockError, PrecheckFailed},
+ rw::RWData,
+};
+
+pub struct ReadOnlyAsset<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ _data_type: PhantomData<RWDataType>,
+ path: PathBuf,
+}
+
+/// Nothing special, I'm just too lazy
+macro_rules! asset_from {
+ (|$v:ident| $src:ty => $expr:expr) => {
+ impl<RWDataType> From<$src> for ReadOnlyAsset<RWDataType>
+ where
+ RWDataType: RWData<RWDataType>,
+ {
+ fn from($v: $src) -> Self {
+ ReadOnlyAsset {
+ _data_type: PhantomData,
+ path: $expr,
+ }
+ }
+ }
+ };
+}
+
+asset_from!(|v| PathBuf => v);
+asset_from!(|v| &Path => v.to_path_buf());
+asset_from!(|v| String => PathBuf::from(v));
+asset_from!(|v| &str => PathBuf::from(v));
+
+impl<RWDataType> AsRef<Path> for ReadOnlyAsset<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ fn as_ref(&self) -> &Path {
+ self.path.as_ref()
+ }
+}
+
+impl<RWDataType> From<ReadOnlyAsset<RWDataType>> for PathBuf
+where
+ RWDataType: RWData<RWDataType>,
+{
+ fn from(value: ReadOnlyAsset<RWDataType>) -> Self {
+ value.path
+ }
+}
+
+impl<RWDataType> ReadOnlyAsset<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ /// Read asset content from `ReadOnlyAsset`
+ /// ```ignore
+ /// let sheet_asset: ReadOnlyAsset<Sheet> = "my_sheet.sheet".into();
+ /// let sheet = sheet_asset.read().await.unwrap();
+ /// ```
+ pub async fn read(&self) -> Result<RWDataType, DataReadError> {
+ RWDataType::read(&self.path).await
+ }
+
+ /// Create a `Handle` from `ReadOnlyAsset` to exclusively edit this `ReadOnlyAsset`
+ /// ```ignore
+ /// let sheet_asset: ReadOnlyAsset<Sheet> = "my_sheet.sheet".into();
+ /// let mut sheet_handle = sheet_asset.get_handle().await.unwrap();
+ /// ```
+ pub async fn get_handle(&self) -> Result<Handle<RWDataType>, HandleLockError> {
+ let asset_path = &self.path;
+
+ // If the asset file does not exist, cannot create an editable handle
+ if !asset_path.exists() {
+ return Err(HandleLockError::AssetFileNotFound(asset_path.clone()));
+ }
+
+ // Get the lock path and temp path
+ let lock_path = self.get_lock_path()?;
+ let temp_path = self.get_temp_path()?;
+
+ // Attempt to create the lock file; success means the lock does not exist and editing is allowed
+ match tokio::fs::OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(&lock_path)
+ .await
+ {
+ Ok(_) => {
+ return Ok(Handle {
+ _data_type: PhantomData,
+ writed: false,
+ asset_path: self.path.clone(),
+ lock_path,
+ temp_path,
+ });
+ }
+ Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
+ return Err(HandleLockError::AssetLocked);
+ }
+ Err(e) => {
+ return Err(HandleLockError::IoError(e));
+ }
+ }
+ }
+
+ /// Get the lock file name for the current `ReadOnlyAsset`
+ /// ```
+ /// # use asset::rw::FooData;
+ /// # use asset::asset::ReadOnlyAsset;
+ /// let foo_asset = ReadOnlyAsset::<FooData>::from("my/foo.txt");
+ /// let lock_path = foo_asset
+ /// .get_lock_path()
+ /// .unwrap();
+ /// assert_eq!(lock_path.to_string_lossy(), "my/~foo.txt");
+ /// ```
+ pub fn get_lock_path(&self) -> Result<PathBuf, HandleLockError> {
+ // Get the file name
+ let Some(file_name) = self.path.file_name() else {
+ return Err(HandleLockError::ReadFileNameFailed);
+ };
+
+ let file_name_str = check_path(file_name)?;
+
+ let mut edit_path = self.path.clone();
+ edit_path.set_file_name(format!("{}{}", LOCK_FILE_PREFIX, file_name_str));
+
+ let Ok(edit_path) = format_path(edit_path) else {
+ return Err(HandleLockError::ParsePathFailed);
+ };
+
+ Ok(edit_path)
+ }
+
+ /// Get the name of the temporary editing file for the current `ReadOnlyAsset`
+ /// ```
+ /// # use asset::rw::FooData;
+ /// # use asset::asset::ReadOnlyAsset;
+ /// let foo_asset = ReadOnlyAsset::<FooData>::from("my/foo.txt");
+ /// let temp_path = foo_asset
+ /// .get_temp_path()
+ /// .unwrap();
+ /// assert_eq!(temp_path.to_string_lossy(), "my/.tmp_foo.txt");
+ /// ```
+ pub fn get_temp_path(&self) -> Result<PathBuf, HandleLockError> {
+ // Get the file name
+ let Some(file_name) = self.path.file_name() else {
+ return Err(HandleLockError::ReadFileNameFailed);
+ };
+
+ let file_name_str = check_path(file_name)?;
+
+ let mut temp_path = self.path.clone();
+ temp_path.set_file_name(format!("{}{}", TEMP_FILE_PREFIX, file_name_str));
+
+ let Ok(edit_path) = format_path(temp_path) else {
+ return Err(HandleLockError::ParsePathFailed);
+ };
+
+ Ok(edit_path)
+ }
+}
+
+pub struct Handle<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ _data_type: PhantomData<RWDataType>,
+ writed: bool,
+ asset_path: PathBuf,
+ lock_path: PathBuf,
+ temp_path: PathBuf,
+}
+
+impl<RWDataType> Drop for Handle<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ fn drop(&mut self) {
+ if self.lock_path.exists() {
+ let _ = std::fs::remove_file(&self.lock_path);
+ }
+ }
+}
+
+impl<RWDataType> Handle<RWDataType>
+where
+ RWDataType: RWData<RWDataType>,
+{
+ /// Write content into the `Handle`, this will not affect the associated `ReadOnlyAsset`
+ pub async fn write(&mut self, data: RWDataType) -> Result<(), DataWriteError> {
+ self.writed = true;
+ RWDataType::write(data, &self.temp_path).await
+ }
+
+ /// Read the content of this `Handle`;
+ /// if the `Handle` has never been written to, read the content of the original `ReadOnlyAsset`
+ pub async fn read(&self) -> Result<RWDataType, DataReadError> {
+ let path = if self.writed {
+ &self.temp_path
+ } else {
+ &self.asset_path
+ };
+ RWDataType::read(path).await
+ }
+
+ /// Atomically write the content of the `Handle` to the `ReadOnlyAsset`
+ pub async fn apply(self) -> Result<(), DataApplyError> {
+ let from = &self.temp_path;
+ let to = &self.asset_path;
+
+ // Cannot perform Apply operation when the target file does not exist
+ // Reason: Cannot apply modifications to a non-existent file, even if editing was declared
+ if !to.exists() {
+ return Err(DataApplyError::AssetFileNotFound(to.clone()));
+ }
+
+ if self.writed {
+ tokio::fs::rename(&from, &to)
+ .await
+ .map_err(|e| DataApplyError::IoError(e))?;
+ }
+ Ok(())
+ }
+
+ pub fn get_rename_operation(&self) -> (PathBuf, PathBuf) {
+ (self.temp_path.clone(), self.asset_path.clone())
+ }
+}
+
+/// Strict Apply precheck
+///
+/// It checks:
+/// 1. Whether related files exist
+/// 2. If the Handle has been written to, whether the written content can be found
+/// 3. Whether the Handle and its ReadOnlyAsset are in the same directory
+/// 4. Whether the ReadOnlyAsset is locked (preventing writes/renames)
+/// 5. Whether the Handle is locked (preventing writes/renames)
+///
+/// > This is only necessary when you need to ensure `multiple Handles must either all succeed or all fail together`
+///
+/// If you only need to ensure a single Handle can Apply correctly,
+/// use:
+/// ```ignore
+/// // ...
+/// let mut handle = my_asset.get_handle().await?;
+/// handle.write(/* Your Data */).await?;
+/// handle.apply().await?;
+/// ```
+pub async fn apply_precheck<D>(handle: &Handle<D>) -> Result<(), PrecheckFailed>
+where
+ D: RWData<D>,
+{
+ let Ok(from) = format_path(&handle.temp_path) else {
+ return Err(PrecheckFailed::FormatPathFailed);
+ };
+ let Ok(to) = format_path(&handle.asset_path) else {
+ return Err(PrecheckFailed::FormatPathFailed);
+ };
+
+ tokio::try_join!(
+ check_lock_file_exist(handle),
+ check_asset_file_exist(handle),
+ check_temp_file_exist_when_writed(handle),
+ check_asset_path(handle),
+ check_handle_is_cross_directory(&from, &to),
+ )?;
+ Ok(())
+}
+
+/// Check if all rename operations to be performed are valid
+pub async fn apply_precheck_rename_operations(
+ rename_operations: &[(PathBuf, PathBuf)],
+) -> Result<(), PrecheckFailed> {
+ let mut seen = HashSet::new();
+
+ for (from, to) in rename_operations {
+ if !seen.insert(from) {
+ return Err(PrecheckFailed::HasSamePath);
+ }
+ if !seen.insert(to) {
+ return Err(PrecheckFailed::HasSamePath);
+ }
+ }
+
+ // This validation must be executed sequentially
+ // Because on Unix, we need to attempt writing files to directories
+ // If executed in parallel, conflicts may arise
+
+ for (from, to) in rename_operations {
+ #[cfg(windows)]
+ check_temp_file_moveable_windows(from, to).await?;
+
+ #[cfg(windows)]
+ check_asset_file_writeable_windows(to).await?;
+
+ #[cfg(unix)]
+ check_temp_file_moveable_unix(from, to).await?;
+
+ #[cfg(unix)]
+ check_asset_file_writeable_unix(to).await?;
+ }
+ Ok(())
+}
+
+async fn check_lock_file_exist<D>(handle: &Handle<D>) -> Result<(), PrecheckFailed>
+where
+ D: RWData<D>,
+{
+ if !handle.lock_path.exists() {
+ return Err(PrecheckFailed::LockNotFound);
+ }
+ Ok(())
+}
+
+async fn check_asset_file_exist<D>(handle: &Handle<D>) -> Result<(), PrecheckFailed>
+where
+ D: RWData<D>,
+{
+ if !handle.asset_path.exists() {
+ return Err(PrecheckFailed::AssetNotFound);
+ }
+ Ok(())
+}
+
+async fn check_temp_file_exist_when_writed<D>(handle: &Handle<D>) -> Result<(), PrecheckFailed>
+where
+ D: RWData<D>,
+{
+ if handle.writed && !handle.temp_path.exists() {
+ return Err(PrecheckFailed::WritedButTempNotFound);
+ }
+ Ok(())
+}
+
+async fn check_asset_path<D>(handle: &Handle<D>) -> Result<(), PrecheckFailed>
+where
+ D: RWData<D>,
+{
+ if let Some(file_name) = handle.asset_path.file_name() {
+ if check_path(file_name).is_ok() {
+ return Ok(());
+ }
+ }
+ Err(PrecheckFailed::AssetPathInvalid)
+}
+
+async fn check_handle_is_cross_directory(
+ from: &PathBuf,
+ to: &PathBuf,
+) -> Result<(), PrecheckFailed> {
+ let from_parent = from.parent();
+ let to_parent = to.parent();
+
+ // "Huh? You ask why we report an error when the parent directory doesn't exist?"
+ //
+ // Operations are not allowed in the root directory or when there is no parent directory.
+ // 1. This indicates they are not in the correct location
+ // (ideally there should be at least one parent directory).
+ // 2. This makes it impossible to compare the two directories.
+ if from_parent.is_none() || to_parent.is_none() {
+ return Err(PrecheckFailed::HandleFileIsNoParent);
+ }
+
+ if from_parent.unwrap() != to_parent.unwrap() {
+ return Err(PrecheckFailed::HandleIsCrossDirectory);
+ }
+
+ Ok(())
+}
+
+fn check_path(file_name: &OsStr) -> Result<Cow<'_, str>, PrecheckFailed> {
+ // When operating on a TEMP_FILE or LOCK_FILE,
+ // names like `~~foo.txt` or `.tmp_.tmp_foo.txt` would be generated
+ // This is not expected and should result in an error
+
+ // Check if the file name starts with ~
+ let file_name_str = file_name.to_string_lossy();
+ if file_name_str.starts_with(LOCK_FILE_PREFIX) {
+ return Err(PrecheckFailed::LockOnLockFile);
+ }
+
+ // Check if the file name starts with .tmp_
+ let file_name_str = file_name.to_string_lossy();
+ if file_name_str.starts_with(TEMP_FILE_PREFIX) {
+ return Err(PrecheckFailed::TempForTempFile);
+ }
+
+ Ok(file_name_str)
+}
+
+#[cfg(windows)]
+async fn check_asset_file_writeable_windows(path: &Path) -> Result<(), PrecheckFailed> {
+ use std::os::windows::fs::MetadataExt;
+
+ let metadata = tokio::fs::metadata(path)
+ .await
+ .map_err(|_| PrecheckFailed::AssetNotWritable)?;
+
+ if metadata.file_attributes() & 0x1 != 0 {
+ return Err(PrecheckFailed::AssetNotWritable);
+ }
+
+ if !check_file_can_be_deleted_windows(path) {
+ return Err(PrecheckFailed::AssetNotWritable);
+ }
+
+ Ok(())
+}
+
+#[cfg(windows)]
+async fn check_temp_file_moveable_windows(temp: &Path, asset: &Path) -> Result<(), PrecheckFailed> {
+ if !check_file_can_be_deleted_windows(temp) {
+ return Err(PrecheckFailed::TempNotMoveable);
+ }
+ if asset.exists() && !check_file_can_be_deleted_windows(asset) {
+ return Err(PrecheckFailed::TempNotMoveable);
+ }
+
+ Ok(())
+}
+
+#[cfg(windows)]
+fn check_file_can_be_deleted_windows(path: &Path) -> bool {
+ use std::os::windows::fs::OpenOptionsExt;
+
+ use winapi::um::winnt::{DELETE, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE};
+ std::fs::OpenOptions::new()
+ .access_mode(DELETE)
+ .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
+ .open(path)
+ .is_ok()
+}
+
+#[cfg(unix)]
+async fn check_asset_file_writeable_unix(path: &Path) -> Result<(), PrecheckFailed> {
+ let parent = path.parent().ok_or(PrecheckFailed::AssetPathInvalid)?;
+
+ if !check_dir_writable_unix(parent) {
+ return Err(PrecheckFailed::AssetNotWritable);
+ }
+
+ Ok(())
+}
+
+#[cfg(unix)]
+async fn check_temp_file_moveable_unix(temp: &Path, asset: &Path) -> Result<(), PrecheckFailed> {
+ if !temp.exists() {
+ return Err(PrecheckFailed::TempNotMoveable);
+ }
+
+ let temp_parent = temp.parent().ok_or(PrecheckFailed::TempNotMoveable)?;
+ if !check_dir_writable_unix(temp_parent) {
+ return Err(PrecheckFailed::TempNotMoveable);
+ }
+
+ let asset_parent = asset.parent().ok_or(PrecheckFailed::TempNotMoveable)?;
+ if !check_dir_writable_unix(asset_parent) {
+ return Err(PrecheckFailed::TempNotMoveable);
+ }
+
+ Ok(())
+}
+
+#[cfg(unix)]
+fn check_dir_writable_unix(dir: &Path) -> bool {
+ use std::fs::OpenOptions;
+
+ let test_path = dir.join(".write_test");
+
+ let result = OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open(&test_path);
+
+ match result {
+ Ok(_) => {
+ let _ = std::fs::remove_file(test_path);
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[macro_export]
+macro_rules! apply {
+ // Single handle
+ ($handle:expr $(,)?) => {{
+ async {
+ // Single-handle precheck
+ if let Err(e) = asset::asset::apply_precheck(&$handle).await {
+ return Err(asset::error::DataApplyError::PrecheckFailed(e));
+ }
+
+ // Direct apply
+ $handle.apply().await
+ }
+ }};
+
+ // Multiple handles
+ ($first:expr, $($rest:expr),+ $(,)?) => {{
+ async {
+ // Collect rename ops
+ let rename_ops: Vec<(std::path::PathBuf, std::path::PathBuf)> = vec![
+ $first.get_rename_operation(),
+ $(
+ $rest.get_rename_operation(),
+ )+
+ ];
+
+ // Batch precheck
+ if let Err(e) = asset::asset::apply_precheck_rename_operations(&rename_ops).await {
+ return Err(asset::error::DataApplyError::PrecheckFailed(e));
+ }
+
+ // Per-handle precheck
+ if let Err(e) = asset::asset::apply_precheck(&$first).await {
+ return Err(asset::error::DataApplyError::PrecheckFailed(e));
+ }
+ $(
+ if let Err(e) = asset::asset::apply_precheck(&$rest).await {
+ return Err(asset::error::DataApplyError::PrecheckFailed(e));
+ }
+ )+
+
+ // Apply
+ if let Err(e) = $first.apply().await {
+ return Err(e);
+ }
+ $(
+ if let Err(e) = $rest.apply().await {
+ return Err(e);
+ }
+ )+
+
+ Ok(())
+ }
+ }};
+}
diff --git a/systems/asset/src/error.rs b/systems/asset/src/error.rs
new file mode 100644
index 0000000..5dc529f
--- /dev/null
+++ b/systems/asset/src/error.rs
@@ -0,0 +1,106 @@
+use std::path::PathBuf;
+
+#[derive(Debug, thiserror::Error)]
+pub enum DataReadError {
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+
+ #[error("Parse error: {0}")]
+ ParseError(String),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum DataWriteError {
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum DataApplyError {
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+
+ #[error("Asset file not found: {0}")]
+ AssetFileNotFound(PathBuf),
+
+ #[error("Pre-check failed: {0}")]
+ PrecheckFailed(#[from] PrecheckFailed),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum PrecheckFailed {
+ /// Lock file does not exist
+ /// Means trying to apply a modification that cannot be applied
+ #[error("Lock not found")]
+ LockNotFound,
+
+ /// Asset file does not exist
+ /// Apply phase will fail due to this condition
+ #[error("Asset not found")]
+ AssetNotFound,
+
+ // Note: !writed is allowed,
+ // but writed without a TEMP_FILE is not allowed
+ /// Handle produced a write
+ /// but the temporary file does not exist, indicating an abnormal write operation
+ #[error("Temp not found")]
+ WritedButTempNotFound,
+
+ /// Asset path is invalid
+ /// This is not an issue that should arise from normal Handle creation flow
+ #[error("Asset path invalid")]
+ AssetPathInvalid,
+
+ /// Temporary file cannot be moved
+ #[error("Temp file not moveable")]
+ TempNotMoveable,
+
+ /// Asset file cannot be moved
+ #[error("Asset file not writable")]
+ AssetNotWritable,
+
+ /// Handle is processing a cross-directory operation
+ /// This is not atomic
+ #[error("Handle is cross-directory")]
+ HandleIsCrossDirectory,
+
+ /// Asset file has no parent directory
+ /// This is not a valid path for file operations
+ #[error("Asset file has no parent directory")]
+ HandleFileIsNoParent,
+
+ /// A handle with the same path already exists
+ /// This operation will cause a conflict
+ #[error("Handle with same path exists")]
+ HasSamePath,
+
+ #[error("Lock on lock file")]
+ LockOnLockFile,
+
+ #[error("Temp file for temp file")]
+ TempForTempFile,
+
+ #[error("Asset path cannot be formatted")]
+ FormatPathFailed,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum HandleLockError {
+ #[error("Parse path failed")]
+ ParsePathFailed,
+
+ #[error("Asset file not found")]
+ AssetFileNotFound(PathBuf),
+
+ #[error("Read file name failed")]
+ ReadFileNameFailed,
+
+ #[error("Asset locked")]
+ AssetLocked,
+
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+
+ #[error("Pre-check failed: {0}")]
+ PrecheckFailed(#[from] PrecheckFailed),
+}
diff --git a/systems/asset/src/lib.rs b/systems/asset/src/lib.rs
new file mode 100644
index 0000000..ac4317f
--- /dev/null
+++ b/systems/asset/src/lib.rs
@@ -0,0 +1,6 @@
+pub mod asset;
+pub mod error;
+pub mod rw;
+
+#[allow(unused)]
+pub use asset_macros::*;
diff --git a/systems/asset/src/rw.rs b/systems/asset/src/rw.rs
new file mode 100644
index 0000000..784d44d
--- /dev/null
+++ b/systems/asset/src/rw.rs
@@ -0,0 +1,85 @@
+use std::path::PathBuf;
+
+use crate::error::{DataReadError, DataWriteError};
+
+pub trait RWData<DataType> {
+ type DataType;
+
+ /// Implement read logic
+ /// Given a path, return the specific data
+ fn read(path: &PathBuf) -> impl Future<Output = Result<DataType, DataReadError>> + Send + Sync;
+
+ /// Implement write logic
+ /// Given data and a path, write to the filesystem
+ fn write(
+ data: DataType,
+ path: &PathBuf,
+ ) -> impl Future<Output = Result<(), DataWriteError>> + Send + Sync;
+
+ /// Provide test data
+ fn test_data() -> DataType;
+
+ /// Given two sets of data, determine if they are equal
+ ///
+ /// Add RWDataTest derive to your struct to automatically generate tests
+ /// ```ignore
+ /// #[derive(RWDataTest)]
+ /// struct FooData;
+ /// ```
+ fn verify_data(data_a: DataType, data_b: DataType) -> bool;
+}
+
+#[macro_export]
+macro_rules! ensure_eq {
+ ($a:expr, $b:expr) => {
+ if $a != $b {
+ return false;
+ }
+ };
+}
+
+// Test Data
+pub struct FooData {
+ pub age: i32,
+ pub name: String,
+}
+
+impl RWData<FooData> for FooData {
+ type DataType = FooData;
+
+ async fn read(path: &PathBuf) -> Result<FooData, DataReadError> {
+ let content = tokio::fs::read_to_string(path)
+ .await
+ .map_err(|e| DataReadError::IoError(e))?;
+ let parts: Vec<&str> = content.split('=').collect();
+ if parts.len() != 2 {
+ return Err(DataReadError::ParseError("Invalid format".to_string()));
+ }
+ let name = parts[0].to_string();
+ let age: i32 = parts[1]
+ .parse()
+ .map_err(|_| DataReadError::ParseError("Invalid age".to_string()))?;
+ Ok(FooData { age, name })
+ }
+
+ async fn write(data: FooData, path: &PathBuf) -> Result<(), DataWriteError> {
+ let content = format!("{}={}", data.name, data.age);
+ tokio::fs::write(path, content)
+ .await
+ .map_err(|e| DataWriteError::IoError(e))?;
+ Ok(())
+ }
+
+ fn test_data() -> FooData {
+ FooData {
+ age: 24,
+ name: "OneOneFourFiveOneFour".to_string(),
+ }
+ }
+
+ fn verify_data(data_a: FooData, data_b: FooData) -> bool {
+ crate::ensure_eq!(data_a.age, data_b.age);
+ crate::ensure_eq!(data_a.name, data_b.name);
+ return true;
+ }
+}