diff --git a/bindings/haskell/CONTRIBUTING.md b/bindings/haskell/CONTRIBUTING.md index 45ce942c40de..15facacf356f 100644 --- a/bindings/haskell/CONTRIBUTING.md +++ b/bindings/haskell/CONTRIBUTING.md @@ -80,14 +80,14 @@ Test suite logged to: If you don't want to specify `LIBRARY_PATH` and `LD_LIBRARY_PATH` every time, you can use [`direnv`](https://direnv.net/) to set the environment variable automatically. Add the following to your `.envrc`: ```shell -export LIBRARY_PATH=../../target/debug:LIBRARY_PATH -export LD_LIBRARY_PATH=../../target/debug:LD_LIBRARY_PATH +export LIBRARY_PATH=../../target/debug:$LIBRARY_PATH +export LD_LIBRARY_PATH=../../target/debug:$LD_LIBRARY_PATH ``` If you are using [`Haskell`](https://marketplace.visualstudio.com/items?itemName=haskell.haskell) in VSCode, you may need to add the following configuration to your `settings.json`: ```json "haskell.serverEnvironment": { - "LIBRARY_PATH": "../../target/debug:LIBRARY_PATH" + "LIBRARY_PATH": "../../target/debug:$LIBRARY_PATH" }, ``` \ No newline at end of file diff --git a/bindings/haskell/haskell-src/OpenDAL.hs b/bindings/haskell/haskell-src/OpenDAL.hs index 8791d3127a46..5697b2ba3a86 100644 --- a/bindings/haskell/haskell-src/OpenDAL.hs +++ b/bindings/haskell/haskell-src/OpenDAL.hs @@ -17,9 +17,16 @@ module OpenDAL ( Operator, - createOp, + OpenDALError, + ErrorCode (..), + newOp, readOp, writeOp, + isExistOp, + createDirOp, + copyOp, + renameOp, + deleteOp, ) where import Data.ByteString (ByteString) @@ -28,16 +35,48 @@ import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Foreign import Foreign.C.String +import Foreign.C.Types (CChar) import OpenDAL.FFI -newtype Operator = Operator (Ptr RawOperator) +newtype Operator = Operator (ForeignPtr RawOperator) + +data ErrorCode + = FFIError + | Unexpected + | Unsupported + | ConfigInvalid + | NotFound + | PermissionDenied + | IsADirectory + | NotADirectory + | AlreadyExists + | RateLimited + | IsSameFile + deriving (Eq, Show) + +data OpenDALError = OpenDALError {errorCode :: ErrorCode, message :: String} + deriving (Eq, Show) byteSliceToByteString :: ByteSlice -> IO ByteString byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len) +parseErrorCode :: Int -> ErrorCode +parseErrorCode 1 = FFIError +parseErrorCode 2 = Unexpected +parseErrorCode 3 = Unsupported +parseErrorCode 4 = ConfigInvalid +parseErrorCode 5 = NotFound +parseErrorCode 6 = PermissionDenied +parseErrorCode 7 = IsADirectory +parseErrorCode 8 = NotADirectory +parseErrorCode 9 = AlreadyExists +parseErrorCode 10 = RateLimited +parseErrorCode 11 = IsSameFile +parseErrorCode _ = FFIError + -- | Create a new Operator. -createOp :: String -> HashMap String String -> IO (Either String Operator) -createOp scheme hashMap = do +newOp :: String -> HashMap String String -> IO (Either OpenDALError Operator) +newOp scheme hashMap = do let keysAndValues = HashMap.toList hashMap withCString scheme $ \cScheme -> withMany withCString (map fst keysAndValues) $ \cKeys -> @@ -49,39 +88,113 @@ createOp scheme hashMap = do pokeArray cValuesPtr cValues c_via_map_ffi cScheme cKeysPtr cValuesPtr (fromIntegral $ length keysAndValues) ffiResultPtr ffiResult <- peek ffiResultPtr - if success ffiResult + if ffiCode ffiResult == 0 then do - let op = Operator (castPtr $ dataPtr ffiResult) + op <- Operator <$> (newForeignPtr c_free_operator $ castPtr $ dataPtr ffiResult) return $ Right op else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult errMsg <- peekCString (errorMessage ffiResult) - return $ Left errMsg + return $ Left $ OpenDALError code errMsg -readOp :: Operator -> String -> IO (Either String ByteString) -readOp (Operator op) path = (flip ($)) op $ \opptr -> +readOp :: Operator -> String -> IO (Either OpenDALError ByteString) +readOp (Operator op) path = withForeignPtr op $ \opptr -> withCString path $ \cPath -> alloca $ \ffiResultPtr -> do c_blocking_read opptr cPath ffiResultPtr ffiResult <- peek ffiResultPtr - if success ffiResult + if ffiCode ffiResult == 0 then do byteslice <- peek (castPtr $ dataPtr ffiResult) byte <- byteSliceToByteString byteslice c_free_byteslice (bsData byteslice) (bsLen byteslice) return $ Right byte else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult errMsg <- peekCString (errorMessage ffiResult) - return $ Left errMsg + return $ Left $ OpenDALError code errMsg -writeOp :: Operator -> String -> ByteString -> IO (Either String ()) -writeOp (Operator op) path byte = (flip ($)) op $ \opptr -> +writeOp :: Operator -> String -> ByteString -> IO (Either OpenDALError ()) +writeOp (Operator op) path byte = withForeignPtr op $ \opptr -> withCString path $ \cPath -> BS.useAsCStringLen byte $ \(cByte, len) -> alloca $ \ffiResultPtr -> do c_blocking_write opptr cPath cByte (fromIntegral len) ffiResultPtr ffiResult <- peek ffiResultPtr - if success ffiResult + if ffiCode ffiResult == 0 + then return $ Right () + else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult + errMsg <- peekCString (errorMessage ffiResult) + return $ Left $ OpenDALError code errMsg + +isExistOp :: Operator -> String -> IO (Either OpenDALError Bool) +isExistOp (Operator op) path = withForeignPtr op $ \opptr -> + withCString path $ \cPath -> + alloca $ \ffiResultPtr -> do + c_blocking_is_exist opptr cPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if ffiCode ffiResult == 0 + then do + -- For Bool type, the memory layout is different between C and Haskell. + val <- peek ((castPtr $ dataPtr ffiResult) :: Ptr CChar) + let isExist = val /= 0 + return $ Right isExist + else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult + errMsg <- peekCString (errorMessage ffiResult) + return $ Left $ OpenDALError code errMsg + +createDirOp :: Operator -> String -> IO (Either OpenDALError ()) +createDirOp (Operator op) path = withForeignPtr op $ \opptr -> + withCString path $ \cPath -> + alloca $ \ffiResultPtr -> do + c_blocking_create_dir opptr cPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if ffiCode ffiResult == 0 + then return $ Right () + else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult + errMsg <- peekCString (errorMessage ffiResult) + return $ Left $ OpenDALError code errMsg + +copyOp :: Operator -> String -> String -> IO (Either OpenDALError ()) +copyOp (Operator op) srcPath dstPath = withForeignPtr op $ \opptr -> + withCString srcPath $ \cSrcPath -> + withCString dstPath $ \cDstPath -> + alloca $ \ffiResultPtr -> do + c_blocking_copy opptr cSrcPath cDstPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if ffiCode ffiResult == 0 then return $ Right () else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult errMsg <- peekCString (errorMessage ffiResult) - return $ Left errMsg \ No newline at end of file + return $ Left $ OpenDALError code errMsg + +renameOp :: Operator -> String -> String -> IO (Either OpenDALError ()) +renameOp (Operator op) srcPath dstPath = withForeignPtr op $ \opptr -> + withCString srcPath $ \cSrcPath -> + withCString dstPath $ \cDstPath -> + alloca $ \ffiResultPtr -> do + c_blocking_rename opptr cSrcPath cDstPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if ffiCode ffiResult == 0 + then return $ Right () + else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult + errMsg <- peekCString (errorMessage ffiResult) + return $ Left $ OpenDALError code errMsg + +deleteOp :: Operator -> String -> IO (Either OpenDALError ()) +deleteOp (Operator op) path = withForeignPtr op $ \opptr -> + withCString path $ \cPath -> + alloca $ \ffiResultPtr -> do + c_blocking_delete opptr cPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if ffiCode ffiResult == 0 + then return $ Right () + else do + let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult + errMsg <- peekCString (errorMessage ffiResult) + return $ Left $ OpenDALError code errMsg \ No newline at end of file diff --git a/bindings/haskell/haskell-src/OpenDAL/FFI.hs b/bindings/haskell/haskell-src/OpenDAL/FFI.hs index 2f5c9ce8c78b..ac4d8df55803 100644 --- a/bindings/haskell/haskell-src/OpenDAL/FFI.hs +++ b/bindings/haskell/haskell-src/OpenDAL/FFI.hs @@ -25,7 +25,7 @@ import Foreign.C.Types data RawOperator data FFIResult = FFIResult - { success :: Bool + { ffiCode :: CUInt , dataPtr :: Ptr () , errorMessage :: CString } @@ -33,22 +33,22 @@ data FFIResult = FFIResult instance Storable FFIResult where sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: CString) - alignment _ = alignment (undefined :: CIntPtr) + alignment _ = alignment (undefined :: CSize) peek ptr = do - s <- ((/= (0 :: CSize)) <$> peekByteOff ptr successOffset) + s <- peekByteOff ptr codeOffset d <- peekByteOff ptr dataPtrOffset errMsg <- peekByteOff ptr errorMessageOffset return $ FFIResult s d errMsg where - successOffset = 0 + codeOffset = 0 dataPtrOffset = sizeOf (undefined :: CSize) errorMessageOffset = dataPtrOffset + sizeOf (undefined :: Ptr ()) poke ptr (FFIResult s d errMsg) = do - pokeByteOff ptr successOffset (fromBool s :: CSize) + pokeByteOff ptr codeOffset s pokeByteOff ptr dataPtrOffset d pokeByteOff ptr errorMessageOffset errMsg where - successOffset = 0 + codeOffset = 0 dataPtrOffset = sizeOf (undefined :: CSize) errorMessageOffset = dataPtrOffset + sizeOf (undefined :: Ptr ()) @@ -59,7 +59,7 @@ data ByteSlice = ByteSlice instance Storable ByteSlice where sizeOf _ = sizeOf (undefined :: Ptr CChar) + sizeOf (undefined :: CSize) - alignment _ = alignment (undefined :: Ptr CChar) + alignment _ = alignment (undefined :: CSize) peek ptr = do bsDataPtr <- peekByteOff ptr dataOffset len <- peekByteOff ptr lenOffset @@ -80,4 +80,9 @@ foreign import ccall "via_map_ffi" foreign import ccall "&free_operator" c_free_operator :: FunPtr (Ptr RawOperator -> IO ()) foreign import ccall "free_byteslice" c_free_byteslice :: Ptr CChar -> CSize -> IO () foreign import ccall "blocking_read" c_blocking_read :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO () -foreign import ccall "blocking_write" c_blocking_write :: Ptr RawOperator -> CString -> Ptr CChar -> CSize -> Ptr FFIResult -> IO () \ No newline at end of file +foreign import ccall "blocking_write" c_blocking_write :: Ptr RawOperator -> CString -> Ptr CChar -> CSize -> Ptr FFIResult -> IO () +foreign import ccall "blocking_is_exist" c_blocking_is_exist :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO () +foreign import ccall "blocking_create_dir" c_blocking_create_dir :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO () +foreign import ccall "blocking_copy" c_blocking_copy :: Ptr RawOperator -> CString -> CString -> Ptr FFIResult -> IO () +foreign import ccall "blocking_rename" c_blocking_rename :: Ptr RawOperator -> CString -> CString -> Ptr FFIResult -> IO () +foreign import ccall "blocking_delete" c_blocking_delete :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO () \ No newline at end of file diff --git a/bindings/haskell/opendal-hs.cabal b/bindings/haskell/opendal-hs.cabal index 8b17d7b3eacd..dce0fb16234d 100644 --- a/bindings/haskell/opendal-hs.cabal +++ b/bindings/haskell/opendal-hs.cabal @@ -40,7 +40,7 @@ library default-language: Haskell2010 extra-libraries: opendal_hs ghc-options: -Wall - build-depends: + build-depends: base >=4.10.0.0 && <5, unordered-containers >=0.2.0.0, bytestring >=0.11.0.0 diff --git a/bindings/haskell/src/lib.rs b/bindings/haskell/src/lib.rs index f32d80c67e1a..2d73e01eaaf5 100644 --- a/bindings/haskell/src/lib.rs +++ b/bindings/haskell/src/lib.rs @@ -21,8 +21,8 @@ use ::opendal as od; use result::FFIResult; use std::collections::HashMap; use std::ffi::CStr; +use std::mem; use std::os::raw::c_char; -use std::ptr; use std::str::FromStr; /// # Safety @@ -76,8 +76,8 @@ pub unsafe extern "C" fn via_map_ffi( .collect::>(); let res = match od::Operator::via_map(scheme, map) { - Ok(operator) => FFIResult::ok(Box::into_raw(Box::new(operator.blocking()))), - Err(_) => FFIResult::err("Failed to create Operator via map"), + Ok(operator) => FFIResult::ok(operator.blocking()), + Err(e) => FFIResult::err_with_source("Failed to create Operator", e), }; *result = res; @@ -168,8 +168,8 @@ pub unsafe extern "C" fn blocking_read( }; let res = match op.read(path_str) { - Ok(bytes) => FFIResult::ok(Box::into_raw(Box::new(ByteSlice::from_vec(bytes)))), - Err(_) => FFIResult::err("Failed to read from Operator"), + Ok(bytes) => FFIResult::ok(ByteSlice::from_vec(bytes)), + Err(e) => FFIResult::err_with_source("Failed to read", e), }; *result = res; @@ -213,9 +213,227 @@ pub unsafe extern "C" fn blocking_write( let bytes = Vec::from_raw_parts(bytes as *mut u8, len, len); - let res = match op.write(path_str, bytes) { - Ok(()) => FFIResult::ok(ptr::null_mut()), - Err(_) => FFIResult::err("Failed to read from Operator"), + let res = match op.write(path_str, bytes.clone()) { + Ok(()) => FFIResult::ok(()), + Err(e) => FFIResult::err_with_source("Failed to write", e), + }; + + *result = res; + + // bytes memory is controlled by Haskell, we can't drop it here + mem::forget(bytes); +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path` is a valid pointer to a nul terminated string. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_is_exist( + op: *mut od::BlockingOperator, + path: *const c_char, + result: *mut FFIResult, +) { + let op = if op.is_null() { + *result = FFIResult::err("Operator is null"); + return; + } else { + &mut *op + }; + + let path_str = match CStr::from_ptr(path).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let res = match op.is_exist(path_str) { + Ok(exist) => FFIResult::ok(exist), + Err(e) => FFIResult::err_with_source("Failed to check if path exists", e), + }; + + *result = res; +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path` is a valid pointer to a nul terminated string. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_create_dir( + op: *mut od::BlockingOperator, + path: *const c_char, + result: *mut FFIResult<()>, +) { + let op = if op.is_null() { + *result = FFIResult::err("Operator is null"); + return; + } else { + &mut *op + }; + + let path_str = match CStr::from_ptr(path).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let res = match op.create_dir(path_str) { + Ok(()) => FFIResult::ok(()), + Err(e) => FFIResult::err_with_source("Failed to create directory", e), + }; + + *result = res; +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path_from` is a valid pointer to a nul terminated string. +/// * `path_to` is a valid pointer to a nul terminated string. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_copy( + op: *mut od::BlockingOperator, + path_from: *const c_char, + path_to: *const c_char, + result: *mut FFIResult<()>, +) { + let op = if op.is_null() { + *result = FFIResult::err("Operator is null"); + return; + } else { + &mut *op + }; + + let path_from_str = match CStr::from_ptr(path_from).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let path_to_str = match CStr::from_ptr(path_to).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let res = match op.copy(path_from_str, path_to_str) { + Ok(()) => FFIResult::ok(()), + Err(e) => FFIResult::err_with_source("Failed to copy", e), + }; + + *result = res; +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path_from` is a valid pointer to a nul terminated string. +/// * `path_to` is a valid pointer to a nul terminated string. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_rename( + op: *mut od::BlockingOperator, + path_from: *const c_char, + path_to: *const c_char, + result: *mut FFIResult<()>, +) { + let op = if op.is_null() { + *result = FFIResult::err("Operator is null"); + return; + } else { + &mut *op + }; + + let path_from_str = match CStr::from_ptr(path_from).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let path_to_str = match CStr::from_ptr(path_to).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let res = match op.rename(path_from_str, path_to_str) { + Ok(()) => FFIResult::ok(()), + Err(e) => FFIResult::err_with_source("Failed to rename", e), + }; + + *result = res; +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path` is a valid pointer to a nul terminated string. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_delete( + op: *mut od::BlockingOperator, + path: *const c_char, + result: *mut FFIResult<()>, +) { + let op = if op.is_null() { + *result = FFIResult::err("Operator is null"); + return; + } else { + &mut *op + }; + + let path_str = match CStr::from_ptr(path).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let res = match op.delete(path_str) { + Ok(()) => FFIResult::ok(()), + Err(e) => FFIResult::err_with_source("Failed to delete", e), }; *result = res; diff --git a/bindings/haskell/src/result.rs b/bindings/haskell/src/result.rs index 3ecaee00d3dc..5916c48e70a0 100644 --- a/bindings/haskell/src/result.rs +++ b/bindings/haskell/src/result.rs @@ -20,19 +20,38 @@ use std::{ ptr, }; +use ::opendal as od; + #[repr(C)] #[derive(Debug)] pub struct FFIResult { - success: bool, + code: FFIErrorCode, data_ptr: *mut T, error_message: *mut c_char, } +#[repr(C)] +#[derive(Debug)] +pub enum FFIErrorCode { + Ok, + FFIError, + Unexpected, + Unsupported, + ConfigInvalid, + NotFound, + PermissionDenied, + IsADirectory, + NotADirectory, + AlreadyExists, + RateLimited, + IsSameFile, +} + impl FFIResult { - pub fn ok(data_ptr: *mut T) -> Self { + pub fn ok(data: T) -> Self { FFIResult { - success: true, - data_ptr, + code: FFIErrorCode::Ok, + data_ptr: Box::into_raw(Box::new(data)), error_message: ptr::null_mut(), } } @@ -40,9 +59,37 @@ impl FFIResult { pub fn err(error_message: &str) -> Self { let c_string = CString::new(error_message).unwrap(); FFIResult { - success: false, + code: FFIErrorCode::FFIError, data_ptr: ptr::null_mut(), error_message: c_string.into_raw(), } } + + pub fn err_with_source(error_message: &str, source: od::Error) -> Self { + let msg = format!("{}, source error: {}", error_message, source); + let c_string = CString::new(msg).unwrap(); + FFIResult { + code: source.kind().into(), + data_ptr: ptr::null_mut(), + error_message: c_string.into_raw(), + } + } +} + +impl From for FFIErrorCode { + fn from(kind: od::ErrorKind) -> Self { + match kind { + od::ErrorKind::Unexpected => FFIErrorCode::Unexpected, + od::ErrorKind::Unsupported => FFIErrorCode::Unsupported, + od::ErrorKind::ConfigInvalid => FFIErrorCode::ConfigInvalid, + od::ErrorKind::NotFound => FFIErrorCode::NotFound, + od::ErrorKind::PermissionDenied => FFIErrorCode::PermissionDenied, + od::ErrorKind::IsADirectory => FFIErrorCode::IsADirectory, + od::ErrorKind::NotADirectory => FFIErrorCode::NotADirectory, + od::ErrorKind::AlreadyExists => FFIErrorCode::AlreadyExists, + od::ErrorKind::RateLimited => FFIErrorCode::RateLimited, + od::ErrorKind::IsSameFile => FFIErrorCode::IsSameFile, + _ => FFIErrorCode::Unexpected, + } + } } diff --git a/bindings/haskell/test/BasicTest.hs b/bindings/haskell/test/BasicTest.hs index ed8fe5b5cb36..135a5e6feb31 100644 --- a/bindings/haskell/test/BasicTest.hs +++ b/bindings/haskell/test/BasicTest.hs @@ -27,15 +27,25 @@ basicTests :: TestTree basicTests = testGroup "Basic Tests" - [ testCase "read and write to memory" testReadAndWriteToMemory + [ testCase "testBasicOperation" testBasicOperation ] -testReadAndWriteToMemory :: Assertion -testReadAndWriteToMemory = do - Right op <- createOp "memory" HashMap.empty - _ <- writeOp op "key1" "value1" - _ <- writeOp op "key2" "value2" - value1 <- readOp op "key1" - value2 <- readOp op "key2" - value1 @?= Right "value1" - value2 @?= Right "value2" \ No newline at end of file +testBasicOperation :: Assertion +testBasicOperation = do + Right op <- newOp "fs" $ HashMap.fromList [("root", "/tmp/opendal-test")] + writeOp op "key1" "value1" >>= (@?= Right ()) + writeOp op "key2" "value2" >>= (@?= Right ()) + readOp op "key1" >>= (@?= Right "value1") + readOp op "key2" >>= (@?= Right "value2") + isExistOp op "key1" >>= (@?= Right True) + isExistOp op "key2" >>= (@?= Right True) + createDirOp op "dir1/" >>= (@?= Right ()) + isExistOp op "dir1/" >>= (@?= Right True) + copyOp op "key1" "key3" >>= (@?= Right ()) + isExistOp op "key3" >>= (@?= Right True) + isExistOp op "key1" >>= (@?= Right True) + renameOp op "key2" "key4" >>= (@?= Right ()) + isExistOp op "key4" >>= (@?= Right True) + isExistOp op "key2" >>= (@?= Right False) + deleteOp op "key1" >>= (@?= Right ()) + isExistOp op "key1" >>= (@?= Right False) \ No newline at end of file