You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2023/06/21 07:54:16 UTC
[incubator-opendal] branch main updated: feat(bindings/haskell): support stat (#2504)
This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new b50d7f070 feat(bindings/haskell): support stat (#2504)
b50d7f070 is described below
commit b50d7f070d0c4a8c6729cde6dd68df972dc442f3
Author: silver-ymz <yi...@gmail.com>
AuthorDate: Wed Jun 21 15:54:11 2023 +0800
feat(bindings/haskell): support stat (#2504)
* feat(bindings/haskell): support stat
Signed-off-by: silver-ymz <yi...@gmail.com>
* enhance tests for stat
Signed-off-by: silver-ymz <yi...@gmail.com>
* remove content range
Signed-off-by: silver-ymz <yi...@gmail.com>
* support nanoseconds for metadata
Signed-off-by: silver-ymz <yi...@gmail.com>
---------
Signed-off-by: silver-ymz <yi...@gmail.com>
---
Cargo.lock | 1 +
bindings/haskell/Cargo.toml | 1 +
bindings/haskell/haskell-src/OpenDAL.hs | 90 ++++++++++++++++--
bindings/haskell/haskell-src/OpenDAL/FFI.hs | 77 ++++++++++++---
bindings/haskell/opendal-hs.cabal | 3 +-
bindings/haskell/src/lib.rs | 80 ++++++++--------
bindings/haskell/src/types.rs | 141 ++++++++++++++++++++++++++++
bindings/haskell/test/BasicTest.hs | 29 +++++-
8 files changed, 364 insertions(+), 58 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index cfc298619..0d22a7b72 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2785,6 +2785,7 @@ dependencies = [
name = "opendal-hs"
version = "0.1.0"
dependencies = [
+ "chrono",
"opendal",
]
diff --git a/bindings/haskell/Cargo.toml b/bindings/haskell/Cargo.toml
index 12f320a12..d204eafb2 100644
--- a/bindings/haskell/Cargo.toml
+++ b/bindings/haskell/Cargo.toml
@@ -32,3 +32,4 @@ doc = false
[dependencies]
opendal.workspace = true
+chrono = "0.4"
\ No newline at end of file
diff --git a/bindings/haskell/haskell-src/OpenDAL.hs b/bindings/haskell/haskell-src/OpenDAL.hs
index 293215a36..121c2f1bb 100644
--- a/bindings/haskell/haskell-src/OpenDAL.hs
+++ b/bindings/haskell/haskell-src/OpenDAL.hs
@@ -20,6 +20,8 @@ module OpenDAL (
Operator,
OpenDALError (..),
ErrorCode (..),
+ EntryMode (..),
+ Metadata (..),
OpMonad,
MonadOperation (..),
runOp,
@@ -31,6 +33,7 @@ module OpenDAL (
copyOpRaw,
renameOpRaw,
deleteOpRaw,
+ statOpRaw,
) where
import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)
@@ -39,9 +42,10 @@ import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
+import Data.Time (UTCTime, parseTimeM, zonedTimeToUTC)
+import Data.Time.Format (defaultTimeLocale)
import Foreign
import Foreign.C.String
-import Foreign.C.Types (CChar)
import OpenDAL.FFI
newtype Operator = Operator (ForeignPtr RawOperator)
@@ -63,6 +67,20 @@ data ErrorCode
data OpenDALError = OpenDALError {errorCode :: ErrorCode, message :: String}
deriving (Eq, Show)
+data EntryMode = File | Dir | Unknown deriving (Eq, Show)
+
+data Metadata = Metadata
+ { mMode :: EntryMode
+ , mCacheControl :: Maybe String
+ , mContentDisposition :: Maybe String
+ , mContentLength :: Integer
+ , mContentMD5 :: Maybe String
+ , mContentType :: Maybe String
+ , mETag :: Maybe String
+ , mLastModified :: Maybe UTCTime
+ }
+ deriving (Eq, Show)
+
newtype OpMonad a = OpMonad (ReaderT Operator (ExceptT OpenDALError IO) a)
deriving
( Functor
@@ -81,6 +99,7 @@ class (Monad m) => MonadOperation m where
copyOp :: String -> String -> m ()
renameOp :: String -> String -> m ()
deleteOp :: String -> m ()
+ statOp :: String -> m Metadata
instance MonadOperation OpMonad where
readOp path = do
@@ -111,6 +130,12 @@ instance MonadOperation OpMonad where
op <- ask
result <- liftIO $ deleteOpRaw op path
either throwError return result
+ statOp path = do
+ op <- ask
+ result <- liftIO $ statOpRaw op path
+ either throwError return result
+
+-- helper functions
byteSliceToByteString :: ByteSlice -> IO ByteString
byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len)
@@ -129,10 +154,48 @@ parseErrorCode 10 = RateLimited
parseErrorCode 11 = IsSameFile
parseErrorCode _ = FFIError
+parseEntryMode :: Int -> EntryMode
+parseEntryMode 0 = File
+parseEntryMode 1 = Dir
+parseEntryMode _ = Unknown
+
+parseCString :: CString -> IO (Maybe String)
+parseCString value | value == nullPtr = return Nothing
+parseCString value = do
+ value' <- peekCString value
+ free value
+ return $ Just value'
+
+parseTime :: String -> Maybe UTCTime
+parseTime time = zonedTimeToUTC <$> parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" time
+
+parseFFIMetadata :: FFIMetadata -> IO Metadata
+parseFFIMetadata (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
+ let mode' = parseEntryMode $ fromIntegral mode
+ cacheControl' <- parseCString cacheControl
+ contentDisposition' <- parseCString contentDisposition
+ let contentLength' = toInteger contentLength
+ contentMD5' <- parseCString contentMD5
+ contentType' <- parseCString contentType
+ eTag' <- parseCString eTag
+ lastModified' <- (>>= parseTime) <$> parseCString lastModified
+ return $
+ Metadata
+ { mMode = mode'
+ , mCacheControl = cacheControl'
+ , mContentDisposition = contentDisposition'
+ , mContentLength = contentLength'
+ , mContentMD5 = contentMD5'
+ , mContentType = contentType'
+ , mETag = eTag'
+ , mLastModified = lastModified'
+ }
+
+-- Exported functions
+
runOp :: Operator -> OpMonad a -> IO (Either OpenDALError a)
runOp operator (OpMonad op) = runExceptT $ runReaderT op operator
--- | Create a new Operator.
newOp :: String -> HashMap String String -> IO (Either OpenDALError Operator)
newOp scheme hashMap = do
let keysAndValues = HashMap.toList hashMap
@@ -148,7 +211,7 @@ newOp scheme hashMap = do
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
- op <- Operator <$> (newForeignPtr c_free_operator $ castPtr $ dataPtr ffiResult)
+ op <- Operator <$> (newForeignPtr c_free_operator $ dataPtr ffiResult)
return $ Right op
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
@@ -163,7 +226,7 @@ readOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
- byteslice <- peek (castPtr $ dataPtr ffiResult)
+ byteslice <- peek $ dataPtr ffiResult
byte <- byteSliceToByteString byteslice
c_free_byteslice (bsData byteslice) (bsLen byteslice)
return $ Right byte
@@ -194,8 +257,7 @@ isExistOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
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)
+ val <- peek $ dataPtr ffiResult
let isExist = val /= 0
return $ Right isExist
else do
@@ -252,6 +314,22 @@ deleteOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
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
+
+statOpRaw :: Operator -> String -> IO (Either OpenDALError Metadata)
+statOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
+ withCString path $ \cPath ->
+ alloca $ \ffiResultPtr -> do
+ c_blocking_stat opptr cPath ffiResultPtr
+ ffiResult <- peek ffiResultPtr
+ if ffiCode ffiResult == 0
+ then do
+ ffimatadata <- peek $ dataPtr ffiResult
+ metadata <- parseFFIMetadata ffimatadata
+ return $ Right metadata
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
diff --git a/bindings/haskell/haskell-src/OpenDAL/FFI.hs b/bindings/haskell/haskell-src/OpenDAL/FFI.hs
index ac4d8df55..cc682e979 100644
--- a/bindings/haskell/haskell-src/OpenDAL/FFI.hs
+++ b/bindings/haskell/haskell-src/OpenDAL/FFI.hs
@@ -15,6 +15,7 @@
-- specific language governing permissions and limitations
-- under the License.
{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE ScopedTypeVariables #-}
module OpenDAL.FFI where
@@ -24,14 +25,14 @@ import Foreign.C.Types
data RawOperator
-data FFIResult = FFIResult
+data FFIResult a = FFIResult
{ ffiCode :: CUInt
- , dataPtr :: Ptr ()
+ , dataPtr :: Ptr a
, errorMessage :: CString
}
deriving (Show)
-instance Storable FFIResult where
+instance Storable (FFIResult a) where
sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: CString)
alignment _ = alignment (undefined :: CSize)
peek ptr = do
@@ -74,15 +75,69 @@ instance Storable ByteSlice where
dataOffset = 0
lenOffset = sizeOf (undefined :: Ptr ())
+data FFIMetadata = FFIMetadata
+ { ffiMode :: CUInt
+ , ffiCacheControl :: CString
+ , ffiContentDisposition :: CString
+ , ffiContentLength :: CULong
+ , ffiContentMD5 :: CString
+ , ffiContentType :: CString
+ , ffiETag :: CString
+ , ffiLastModified :: CString
+ }
+ deriving (Show)
+
+instance Storable FFIMetadata where
+ sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: CString) * 6 + sizeOf (undefined :: CULong)
+ alignment _ = alignment (undefined :: CSize)
+ peek ptr = do
+ mode <- peekByteOff ptr modeOffset
+ cacheControl <- peekByteOff ptr cacheControlOffset
+ contentDisposition <- peekByteOff ptr contentDispositionOffset
+ contentLength <- peekByteOff ptr contentLengthOffset
+ contentMD5 <- peekByteOff ptr contentMD5Offset
+ contentType <- peekByteOff ptr contentTypeOffset
+ eTag <- peekByteOff ptr eTagOffset
+ lastModified <- peekByteOff ptr lastModifiedOffset
+ return $ FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified
+ where
+ modeOffset = 0
+ cacheControlOffset = modeOffset + sizeOf (undefined :: CSize)
+ contentDispositionOffset = cacheControlOffset + sizeOf (undefined :: CString)
+ contentLengthOffset = contentDispositionOffset + sizeOf (undefined :: CString)
+ contentMD5Offset = contentLengthOffset + sizeOf (undefined :: CULong)
+ contentTypeOffset = contentMD5Offset + sizeOf (undefined :: CString)
+ eTagOffset = contentTypeOffset + sizeOf (undefined :: CString)
+ lastModifiedOffset = eTagOffset + sizeOf (undefined :: CString)
+ poke ptr (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
+ pokeByteOff ptr modeOffset mode
+ pokeByteOff ptr cacheControlOffset cacheControl
+ pokeByteOff ptr contentDispositionOffset contentDisposition
+ pokeByteOff ptr contentLengthOffset contentLength
+ pokeByteOff ptr contentMD5Offset contentMD5
+ pokeByteOff ptr contentTypeOffset contentType
+ pokeByteOff ptr eTagOffset eTag
+ pokeByteOff ptr lastModifiedOffset lastModified
+ where
+ modeOffset = 0
+ cacheControlOffset = modeOffset + sizeOf (undefined :: CSize)
+ contentDispositionOffset = cacheControlOffset + sizeOf (undefined :: CString)
+ contentLengthOffset = contentDispositionOffset + sizeOf (undefined :: CString)
+ contentMD5Offset = contentLengthOffset + sizeOf (undefined :: CULong)
+ contentTypeOffset = contentMD5Offset + sizeOf (undefined :: CString)
+ eTagOffset = contentTypeOffset + sizeOf (undefined :: CString)
+ lastModifiedOffset = eTagOffset + sizeOf (undefined :: CString)
+
foreign import ccall "via_map_ffi"
c_via_map_ffi ::
- CString -> Ptr CString -> Ptr CString -> CSize -> Ptr FFIResult -> IO ()
+ CString -> Ptr CString -> Ptr CString -> CSize -> Ptr (FFIResult RawOperator) -> IO ()
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 ()
-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
+foreign import ccall "blocking_read" c_blocking_read :: Ptr RawOperator -> CString -> Ptr (FFIResult ByteSlice) -> IO ()
+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 CBool) -> 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 ()
+foreign import ccall "blocking_stat" c_blocking_stat :: Ptr RawOperator -> CString -> Ptr (FFIResult FFIMetadata) -> IO ()
\ No newline at end of file
diff --git a/bindings/haskell/opendal-hs.cabal b/bindings/haskell/opendal-hs.cabal
index 25351697b..02049b4b2 100644
--- a/bindings/haskell/opendal-hs.cabal
+++ b/bindings/haskell/opendal-hs.cabal
@@ -44,7 +44,8 @@ library
base >=4.10.0.0 && <5,
unordered-containers >=0.2.0.0,
bytestring >=0.11.0.0,
- mtl >=2.2.0.0
+ mtl >=2.2.0.0,
+ time >=1.10.0.0
test-suite opendal-hs-test
type: exitcode-stdio-1.0
diff --git a/bindings/haskell/src/lib.rs b/bindings/haskell/src/lib.rs
index 2d73e01ea..30c5c2df8 100644
--- a/bindings/haskell/src/lib.rs
+++ b/bindings/haskell/src/lib.rs
@@ -16,6 +16,7 @@
// under the License.
mod result;
+mod types;
use ::opendal as od;
use result::FFIResult;
@@ -24,6 +25,7 @@ use std::ffi::CStr;
use std::mem;
use std::os::raw::c_char;
use std::str::FromStr;
+use types::{ByteSlice, Metadata};
/// # Safety
///
@@ -97,45 +99,6 @@ pub unsafe extern "C" fn free_operator(operator: *mut od::BlockingOperator) {
}
}
-#[repr(C)]
-#[derive(Debug)]
-pub struct ByteSlice {
- data: *mut c_char,
- len: usize,
-}
-
-impl ByteSlice {
- fn from_vec(vec: Vec<u8>) -> Self {
- let data = vec.as_ptr() as *mut c_char;
- let len = vec.len();
-
- // Leak the memory to pass the ownership to Haskell
- std::mem::forget(vec);
-
- ByteSlice { data, len }
- }
-
- /// # Safety
- ///
- /// * `ptr` is a valid pointer to a `ByteSlice`.
- ///
- /// # Panics
- ///
- /// * If `ptr` is not a valid pointer.
- #[no_mangle]
- pub unsafe extern "C" fn free_byteslice(ptr: *mut c_char, len: usize) {
- if !ptr.is_null() {
- drop(Vec::from_raw_parts(ptr, len, len));
- }
- }
-}
-
-impl From<&mut ByteSlice> for Vec<u8> {
- fn from(val: &mut ByteSlice) -> Self {
- unsafe { Vec::from_raw_parts(val.data as *mut u8, val.len, val.len) }
- }
-}
-
/// # Safety
///
/// * `op` is a valid pointer to a `BlockingOperator`.
@@ -438,3 +401,42 @@ pub unsafe extern "C" fn blocking_delete(
*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_stat(
+ op: *mut od::BlockingOperator,
+ path: *const c_char,
+ result: *mut FFIResult<Metadata>,
+) {
+ 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.stat(path_str) {
+ Ok(meta) => FFIResult::ok(meta.into()),
+ Err(e) => FFIResult::err_with_source("Failed to delete", e),
+ };
+
+ *result = res;
+}
diff --git a/bindings/haskell/src/types.rs b/bindings/haskell/src/types.rs
new file mode 100644
index 000000000..877abbd47
--- /dev/null
+++ b/bindings/haskell/src/types.rs
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use ::opendal as od;
+use chrono::SecondsFormat;
+use std::ffi::c_char;
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct ByteSlice {
+ data: *mut c_char,
+ len: usize,
+}
+
+impl ByteSlice {
+ pub fn from_vec(vec: Vec<u8>) -> Self {
+ let data = vec.as_ptr() as *mut c_char;
+ let len = vec.len();
+
+ // Leak the memory to pass the ownership to Haskell
+ std::mem::forget(vec);
+
+ ByteSlice { data, len }
+ }
+
+ /// # Safety
+ ///
+ /// * `ptr` is a valid pointer to a `ByteSlice`.
+ ///
+ /// # Panics
+ ///
+ /// * If `ptr` is not a valid pointer.
+ #[no_mangle]
+ pub unsafe extern "C" fn free_byteslice(ptr: *mut c_char, len: usize) {
+ if !ptr.is_null() {
+ drop(Vec::from_raw_parts(ptr, len, len));
+ }
+ }
+}
+
+impl From<&mut ByteSlice> for Vec<u8> {
+ fn from(val: &mut ByteSlice) -> Self {
+ unsafe { Vec::from_raw_parts(val.data as *mut u8, val.len, val.len) }
+ }
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub enum EntryMode {
+ File,
+ Dir,
+ Unknown,
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct Metadata {
+ mode: EntryMode,
+ cache_control: *const c_char,
+ content_disposition: *const c_char,
+ content_length: u64,
+ content_md5: *const c_char,
+ content_type: *const c_char,
+ etag: *const c_char,
+ last_modified: *const c_char,
+}
+
+impl From<od::Metadata> for Metadata {
+ fn from(val: od::Metadata) -> Self {
+ let mode = match val.mode() {
+ od::EntryMode::FILE => EntryMode::File,
+ od::EntryMode::DIR => EntryMode::Dir,
+ od::EntryMode::Unknown => EntryMode::Unknown,
+ };
+
+ let cache_control = match val.cache_control() {
+ Some(s) => unsafe { leak_str(s.to_string()) },
+ None => std::ptr::null(),
+ };
+
+ let content_disposition = match val.content_disposition() {
+ Some(s) => unsafe { leak_str(s.to_string()) },
+ None => std::ptr::null(),
+ };
+
+ let content_length = val.content_length();
+
+ let content_md5 = match val.content_md5() {
+ Some(s) => unsafe { leak_str(s.to_string()) },
+ None => std::ptr::null(),
+ };
+
+ let content_type = match val.content_type() {
+ Some(s) => unsafe { leak_str(s.to_string()) },
+ None => std::ptr::null(),
+ };
+
+ let etag = match val.etag() {
+ Some(s) => unsafe { leak_str(s.to_string()) },
+ None => std::ptr::null(),
+ };
+
+ let last_modified = match val.last_modified() {
+ Some(s) => unsafe { leak_str(s.to_rfc3339_opts(SecondsFormat::Nanos, false)) },
+ None => std::ptr::null(),
+ };
+
+ Metadata {
+ mode,
+ cache_control,
+ content_disposition,
+ content_length,
+ content_md5,
+ content_type,
+ etag,
+ last_modified,
+ }
+ }
+}
+
+// Leak the memory to pass the ownership to Haskell
+// Please note that haskell should free the memory after using it
+unsafe fn leak_str(s: String) -> *const c_char {
+ let ptr = s.as_ptr() as *const c_char;
+ std::mem::forget(s);
+ ptr
+}
diff --git a/bindings/haskell/test/BasicTest.hs b/bindings/haskell/test/BasicTest.hs
index 8c1197ff4..e48117076 100644
--- a/bindings/haskell/test/BasicTest.hs
+++ b/bindings/haskell/test/BasicTest.hs
@@ -44,8 +44,23 @@ testRawOperation = do
isExistOpRaw op "key2" >>= (@?= Right True)
createDirOpRaw op "dir1/" >>= (@?= Right ())
isExistOpRaw op "dir1/" >>= (@?= Right True)
+ statOpRaw op "key1" >>= \v -> case v of
+ Right meta -> meta @?= except_meta
+ Left _ -> assertFailure "should not reach here"
deleteOpRaw op "key1" >>= (@?= Right ())
isExistOpRaw op "key1" >>= (@?= Right False)
+ where
+ except_meta =
+ Metadata
+ { mMode = File
+ , mCacheControl = Nothing
+ , mContentDisposition = Nothing
+ , mContentLength = 6
+ , mContentMD5 = Nothing
+ , mContentType = Nothing
+ , mETag = Nothing
+ , mLastModified = Nothing
+ }
testMonad :: Assertion
testMonad = do
@@ -61,8 +76,20 @@ testMonad = do
isExistOp "key2" >>= liftIO . (@?= True)
createDirOp "dir1/"
isExistOp "dir1/" >>= liftIO . (@?= True)
+ statOp "key1" >>= liftIO . (@?= except_meta)
deleteOp "key1"
isExistOp "key1" >>= liftIO . (@?= False)
+ except_meta =
+ Metadata
+ { mMode = File
+ , mCacheControl = Nothing
+ , mContentDisposition = Nothing
+ , mContentLength = 6
+ , mContentMD5 = Nothing
+ , mContentType = Nothing
+ , mETag = Nothing
+ , mLastModified = Nothing
+ }
testError :: Assertion
testError = do
@@ -71,4 +98,4 @@ testError = do
Left err -> errorCode err @?= NotFound
Right _ -> assertFailure "should not reach here"
where
- operation = readOp "non-exist-path"
+ operation = readOp "non-exist-path"
\ No newline at end of file