From 25f2ce46efba54cd270e0bcd7b3f98bbe6fc90ca Mon Sep 17 00:00:00 2001 From: silver-ymz Date: Thu, 15 Jun 2023 15:21:15 +0800 Subject: [PATCH] feat(bindings/haskell): init haskell binding (#2463) * feat(bindings/haskell): init haskell binding Signed-off-by: silver-ymz * add license Signed-off-by: silver-ymz * add safety explanation Signed-off-by: silver-ymz * change Cargo.toml Signed-off-by: silver-ymz * change Cargo.toml Signed-off-by: silver-ymz --------- Signed-off-by: silver-ymz --- Cargo.lock | 7 + Cargo.toml | 1 + bindings/haskell/.gitignore | 2 + bindings/haskell/Cargo.toml | 34 +++ bindings/haskell/README.md | 34 +++ bindings/haskell/cabal.project.local | 18 ++ bindings/haskell/haskell-src/OpenDAL.hs | 87 ++++++++ bindings/haskell/haskell-src/OpenDAL/FFI.hs | 83 ++++++++ bindings/haskell/opendal-hs.cabal | 61 ++++++ bindings/haskell/src/lib.rs | 222 ++++++++++++++++++++ bindings/haskell/src/result.rs | 48 +++++ bindings/haskell/test/BasicTest.hs | 41 ++++ bindings/haskell/test/Spec.hs | 31 +++ 13 files changed, 669 insertions(+) create mode 100644 bindings/haskell/.gitignore create mode 100644 bindings/haskell/Cargo.toml create mode 100644 bindings/haskell/README.md create mode 100644 bindings/haskell/cabal.project.local create mode 100644 bindings/haskell/haskell-src/OpenDAL.hs create mode 100644 bindings/haskell/haskell-src/OpenDAL/FFI.hs create mode 100644 bindings/haskell/opendal-hs.cabal create mode 100644 bindings/haskell/src/lib.rs create mode 100644 bindings/haskell/src/result.rs create mode 100644 bindings/haskell/test/BasicTest.hs create mode 100644 bindings/haskell/test/Spec.hs diff --git a/Cargo.lock b/Cargo.lock index 443f0603b724..05de026f36c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2719,6 +2719,13 @@ dependencies = [ "opendal", ] +[[package]] +name = "opendal-hs" +version = "0.1.0" +dependencies = [ + "opendal", +] + [[package]] name = "opendal-java" version = "0.37.0" diff --git a/Cargo.toml b/Cargo.toml index 7f84c21526d9..a6b91ea6d082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "bindings/python", "bindings/ruby", "bindings/java", + "bindings/haskell", "bin/oli", "bin/oay", diff --git a/bindings/haskell/.gitignore b/bindings/haskell/.gitignore new file mode 100644 index 000000000000..f7a18e9f3bf4 --- /dev/null +++ b/bindings/haskell/.gitignore @@ -0,0 +1,2 @@ +dist-newstyle +.envrc \ No newline at end of file diff --git a/bindings/haskell/Cargo.toml b/bindings/haskell/Cargo.toml new file mode 100644 index 000000000000..12f320a12b52 --- /dev/null +++ b/bindings/haskell/Cargo.toml @@ -0,0 +1,34 @@ +# 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. + +[package] +name = "opendal-hs" +version = "0.1.0" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib"] +doc = false + +[dependencies] +opendal.workspace = true diff --git a/bindings/haskell/README.md b/bindings/haskell/README.md new file mode 100644 index 000000000000..16ac3a2116f8 --- /dev/null +++ b/bindings/haskell/README.md @@ -0,0 +1,34 @@ +# OpenDAL Haskell Binding (WIP) + +## Example + +```haskell +import OpenDAL +import qualified Data.HashMap.Strict as HashMap + +main :: IO () +main = do + Right op <- operator "memory" HashMap.empty + _ <- write op "key1" "value1" + _ <- write op "key2" "value2" + value1 <- read op "key1" + value2 <- read op "key2" + value1 @?= "value1" + value2 @?= "value2" +``` + +## Build + +1. Build OpenDAL Haskell Interface + +```bash +cargo build --package opendal-hs +``` + +2. Build Haskell binding + +If you don't want to install `libopendal_hs`, you need to specify library path manually by `LIBRARY_PATH=${OPENDAL_ROOT}/target/debug`. + +```bash +LIBRARY_PATH=... cabal build +``` \ No newline at end of file diff --git a/bindings/haskell/cabal.project.local b/bindings/haskell/cabal.project.local new file mode 100644 index 000000000000..b72cb7b111dc --- /dev/null +++ b/bindings/haskell/cabal.project.local @@ -0,0 +1,18 @@ +-- 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. + +tests: True \ No newline at end of file diff --git a/bindings/haskell/haskell-src/OpenDAL.hs b/bindings/haskell/haskell-src/OpenDAL.hs new file mode 100644 index 000000000000..8791d3127a46 --- /dev/null +++ b/bindings/haskell/haskell-src/OpenDAL.hs @@ -0,0 +1,87 @@ +-- 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. + +module OpenDAL ( + Operator, + createOp, + readOp, + writeOp, +) where + +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import Foreign +import Foreign.C.String +import OpenDAL.FFI + +newtype Operator = Operator (Ptr RawOperator) + +byteSliceToByteString :: ByteSlice -> IO ByteString +byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len) + +-- | Create a new Operator. +createOp :: String -> HashMap String String -> IO (Either String Operator) +createOp scheme hashMap = do + let keysAndValues = HashMap.toList hashMap + withCString scheme $ \cScheme -> + withMany withCString (map fst keysAndValues) $ \cKeys -> + withMany withCString (map snd keysAndValues) $ \cValues -> + allocaArray (length keysAndValues) $ \cKeysPtr -> + allocaArray (length keysAndValues) $ \cValuesPtr -> + alloca $ \ffiResultPtr -> do + pokeArray cKeysPtr cKeys + pokeArray cValuesPtr cValues + c_via_map_ffi cScheme cKeysPtr cValuesPtr (fromIntegral $ length keysAndValues) ffiResultPtr + ffiResult <- peek ffiResultPtr + if success ffiResult + then do + let op = Operator (castPtr $ dataPtr ffiResult) + return $ Right op + else do + errMsg <- peekCString (errorMessage ffiResult) + return $ Left errMsg + +readOp :: Operator -> String -> IO (Either String ByteString) +readOp (Operator op) path = (flip ($)) op $ \opptr -> + withCString path $ \cPath -> + alloca $ \ffiResultPtr -> do + c_blocking_read opptr cPath ffiResultPtr + ffiResult <- peek ffiResultPtr + if success ffiResult + then do + byteslice <- peek (castPtr $ dataPtr ffiResult) + byte <- byteSliceToByteString byteslice + c_free_byteslice (bsData byteslice) (bsLen byteslice) + return $ Right byte + else do + errMsg <- peekCString (errorMessage ffiResult) + return $ Left errMsg + +writeOp :: Operator -> String -> ByteString -> IO (Either String ()) +writeOp (Operator op) path byte = (flip ($)) 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 + then return $ Right () + else do + errMsg <- peekCString (errorMessage ffiResult) + return $ Left 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 new file mode 100644 index 000000000000..2f5c9ce8c78b --- /dev/null +++ b/bindings/haskell/haskell-src/OpenDAL/FFI.hs @@ -0,0 +1,83 @@ +-- 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. +{-# LANGUAGE ForeignFunctionInterface #-} + +module OpenDAL.FFI where + +import Foreign +import Foreign.C.String +import Foreign.C.Types + +data RawOperator + +data FFIResult = FFIResult + { success :: Bool + , dataPtr :: Ptr () + , errorMessage :: CString + } + deriving (Show) + +instance Storable FFIResult where + sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: CString) + alignment _ = alignment (undefined :: CIntPtr) + peek ptr = do + s <- ((/= (0 :: CSize)) <$> peekByteOff ptr successOffset) + d <- peekByteOff ptr dataPtrOffset + errMsg <- peekByteOff ptr errorMessageOffset + return $ FFIResult s d errMsg + where + successOffset = 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 dataPtrOffset d + pokeByteOff ptr errorMessageOffset errMsg + where + successOffset = 0 + dataPtrOffset = sizeOf (undefined :: CSize) + errorMessageOffset = dataPtrOffset + sizeOf (undefined :: Ptr ()) + +data ByteSlice = ByteSlice + { bsData :: Ptr CChar + , bsLen :: CSize + } + +instance Storable ByteSlice where + sizeOf _ = sizeOf (undefined :: Ptr CChar) + sizeOf (undefined :: CSize) + alignment _ = alignment (undefined :: Ptr CChar) + peek ptr = do + bsDataPtr <- peekByteOff ptr dataOffset + len <- peekByteOff ptr lenOffset + return $ ByteSlice bsDataPtr len + where + dataOffset = 0 + lenOffset = sizeOf (undefined :: Ptr ()) + poke ptr (ByteSlice bsDataPtr len) = do + pokeByteOff ptr dataOffset bsDataPtr + pokeByteOff ptr lenOffset len + where + dataOffset = 0 + lenOffset = sizeOf (undefined :: Ptr ()) + +foreign import ccall "via_map_ffi" + c_via_map_ffi :: + CString -> Ptr CString -> Ptr CString -> CSize -> Ptr FFIResult -> 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 () \ No newline at end of file diff --git a/bindings/haskell/opendal-hs.cabal b/bindings/haskell/opendal-hs.cabal new file mode 100644 index 000000000000..8b17d7b3eacd --- /dev/null +++ b/bindings/haskell/opendal-hs.cabal @@ -0,0 +1,61 @@ +cabal-version: 2.0 + +-- 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. + +name: opendal-hs +version: 0.1.0.0 +license: Apache-2.0 +synopsis: OpenDAL Haskell Binding +description: + OpenDAL Haskell Binding. Open Data Access Layer: Access data freely, painlessly, and efficiently + +category: Storage, Binding +build-type: Simple + +source-repository head + type: git + location: https://github.com/apache/incubator-opendal + +library + exposed-modules: + OpenDAL + other-modules: + OpenDAL.FFI + hs-source-dirs: haskell-src + default-language: Haskell2010 + extra-libraries: opendal_hs + ghc-options: -Wall + build-depends: + base >=4.10.0.0 && <5, + unordered-containers >=0.2.0.0, + bytestring >=0.11.0.0 + +test-suite opendal-hs-test + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: BasicTest + hs-source-dirs: test + default-language: Haskell2010 + other-extensions: OverloadedStrings + ghc-options: -Wall + build-depends: + base, + unordered-containers, + opendal-hs, + tasty, + tasty-hunit \ No newline at end of file diff --git a/bindings/haskell/src/lib.rs b/bindings/haskell/src/lib.rs new file mode 100644 index 000000000000..f32d80c67e1a --- /dev/null +++ b/bindings/haskell/src/lib.rs @@ -0,0 +1,222 @@ +// 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. + +mod result; + +use ::opendal as od; +use result::FFIResult; +use std::collections::HashMap; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::ptr; +use std::str::FromStr; + +/// # Safety +/// +/// * The `keys`, `values`, `len` are valid from `HashMap`. +/// * The memory pointed to by `scheme` contain a valid nul terminator at the end of +/// the string. +/// * The `result` is a valid pointer, and has available memory to write to. +/// +/// # Panics +/// +/// * If `keys` or `values` are not valid pointers. +/// * If `len` is not the same for `keys` and `values`. +/// * If `result` is not a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn via_map_ffi( + scheme: *const c_char, + keys: *const *const c_char, + values: *const *const c_char, + len: usize, + result: *mut FFIResult, +) { + let scheme_str = match CStr::from_ptr(scheme).to_str() { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to convert scheme to string"); + return; + } + }; + + let scheme = match od::Scheme::from_str(scheme_str) { + Ok(s) => s, + Err(_) => { + *result = FFIResult::err("Failed to parse scheme"); + return; + } + }; + + let keys_vec = std::slice::from_raw_parts(keys, len); + let values_vec = std::slice::from_raw_parts(values, len); + + let map = keys_vec + .iter() + .zip(values_vec.iter()) + .map(|(&k, &v)| { + ( + CStr::from_ptr(k).to_string_lossy().into_owned(), + CStr::from_ptr(v).to_string_lossy().into_owned(), + ) + }) + .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"), + }; + + *result = res; +} + +/// # Safety +/// +/// * `operator` is a valid pointer to a `BlockingOperator`. +/// +/// # Panics +/// +/// * If `operator` is not a valid pointer. +#[no_mangle] +pub unsafe extern "C" fn free_operator(operator: *mut od::BlockingOperator) { + if !operator.is_null() { + drop(Box::from_raw(operator)); + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct ByteSlice { + data: *mut c_char, + len: usize, +} + +impl ByteSlice { + fn from_vec(vec: Vec) -> 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 { + 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`. +/// * `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_read( + 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.read(path_str) { + Ok(bytes) => FFIResult::ok(Box::into_raw(Box::new(ByteSlice::from_vec(bytes)))), + Err(_) => FFIResult::err("Failed to read from Operator"), + }; + + *result = res; +} + +/// # Safety +/// +/// * `op` is a valid pointer to a `BlockingOperator`. +/// * `path` is a valid pointer to a nul terminated string. +/// * `bytes` is a valid pointer to a byte array. +/// * `len` is the length of `bytes`. +/// * `result` is a valid pointer, and has available memory to write to +/// +/// # Panics +/// +/// * If `op` is not a valid pointer. +/// * If `bytes` is not a valid pointer, or `len` is more than the length of `bytes`. +/// * If `result` is not a valid pointer, or does not have available memory to write to. +#[no_mangle] +pub unsafe extern "C" fn blocking_write( + op: *mut od::BlockingOperator, + path: *const c_char, + bytes: *const c_char, + len: usize, + 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 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"), + }; + + *result = res; +} diff --git a/bindings/haskell/src/result.rs b/bindings/haskell/src/result.rs new file mode 100644 index 000000000000..3ecaee00d3dc --- /dev/null +++ b/bindings/haskell/src/result.rs @@ -0,0 +1,48 @@ +// 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 std::{ + ffi::{c_char, CString}, + ptr, +}; + +#[repr(C)] +#[derive(Debug)] +pub struct FFIResult { + success: bool, + data_ptr: *mut T, + error_message: *mut c_char, +} + +impl FFIResult { + pub fn ok(data_ptr: *mut T) -> Self { + FFIResult { + success: true, + data_ptr, + error_message: ptr::null_mut(), + } + } + + pub fn err(error_message: &str) -> Self { + let c_string = CString::new(error_message).unwrap(); + FFIResult { + success: false, + data_ptr: ptr::null_mut(), + error_message: c_string.into_raw(), + } + } +} diff --git a/bindings/haskell/test/BasicTest.hs b/bindings/haskell/test/BasicTest.hs new file mode 100644 index 000000000000..ed8fe5b5cb36 --- /dev/null +++ b/bindings/haskell/test/BasicTest.hs @@ -0,0 +1,41 @@ +-- 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. +{-# LANGUAGE OverloadedStrings #-} + +module BasicTest (basicTests) where + +import qualified Data.HashMap.Strict as HashMap +import OpenDAL +import Test.Tasty +import Test.Tasty.HUnit + +basicTests :: TestTree +basicTests = + testGroup + "Basic Tests" + [ testCase "read and write to memory" testReadAndWriteToMemory + ] + +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 diff --git a/bindings/haskell/test/Spec.hs b/bindings/haskell/test/Spec.hs new file mode 100644 index 000000000000..35c5a7d140d2 --- /dev/null +++ b/bindings/haskell/test/Spec.hs @@ -0,0 +1,31 @@ +-- 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. + +import Test.Tasty +import Test.Tasty.Ingredients.Basic (consoleTestReporter) + +import BasicTest + +main :: IO () +main = do + tests <- + testGroup "All Tests" + <$> sequence + [ return basicTests + -- Add other test groups here as needed + ] + defaultMainWithIngredients [consoleTestReporter] tests \ No newline at end of file