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 2025/08/03 09:34:10 UTC
(opendal) branch main updated: feat(bindings/ocaml): add more blocking api for ocaml (#6238)
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/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new b04da7c8c feat(bindings/ocaml): add more blocking api for ocaml (#6238)
b04da7c8c is described below
commit b04da7c8cb8175237a54783c81a86d868a06ccc8
Author: Asuka Minato <i...@asukaminato.eu.org>
AuthorDate: Sun Aug 3 18:34:04 2025 +0900
feat(bindings/ocaml): add more blocking api for ocaml (#6238)
* add more api
* try fix
* try fix
* fix test
* fmt
* clippy
* prepare for pub
* seperate pr
* Apply suggestions from code review
---
.github/actions/setup-ocaml/action.yaml | 1 +
.github/workflows/ci_bindings_ocaml.yml | 9 -
bindings/ocaml/CHANGES.md | 8 +
bindings/ocaml/dune-project | 4 +-
bindings/ocaml/lib/opendal.ml | 8 +
bindings/ocaml/lib/operator.ml | 51 ++
bindings/ocaml/lib/operator.mli | 651 ++++++++++++++++++---
bindings/ocaml/opendal.opam | 2 +
bindings/ocaml/release.sh | 68 +++
bindings/ocaml/src/operator.ml | 44 ++
bindings/ocaml/src/operator.mli | 44 ++
bindings/ocaml/src/operator/_type.rs | 21 +
bindings/ocaml/src/operator/capability.rs | 114 ++++
.../ocaml/src/operator/{_type.rs => lister.rs} | 26 +-
bindings/ocaml/src/operator/mod.rs | 40 ++
.../src/operator/{_type.rs => operator_info.rs} | 30 +-
.../ocaml/src/operator/{_type.rs => writer.rs} | 26 +-
bindings/ocaml/test/test.ml | 284 ++++++++-
18 files changed, 1282 insertions(+), 149 deletions(-)
diff --git a/.github/actions/setup-ocaml/action.yaml b/.github/actions/setup-ocaml/action.yaml
index cd096d6d7..765be73b9 100644
--- a/.github/actions/setup-ocaml/action.yaml
+++ b/.github/actions/setup-ocaml/action.yaml
@@ -32,6 +32,7 @@ runs:
ocaml-compiler: 4.14
opam-depext: ${{inputs.need-depext == true}}
opam-pin: ${{inputs.need-pin == true}}
+ dune-cache: true
- name: Set Opam env
shell: bash
diff --git a/.github/workflows/ci_bindings_ocaml.yml b/.github/workflows/ci_bindings_ocaml.yml
index 44783ad0e..85e539e3e 100644
--- a/.github/workflows/ci_bindings_ocaml.yml
+++ b/.github/workflows/ci_bindings_ocaml.yml
@@ -50,15 +50,6 @@ jobs:
${{ runner.os }}-opam-
${{ runner.os }}-opam-doc-
- - name: Cache Dune build artifacts
- uses: actions/cache@v4
- with:
- path: bindings/ocaml/_build
- key: ${{ runner.os }}-dune-${{ hashFiles('bindings/ocaml/**/*.{ml,mli,opam}') }}
- restore-keys: |
- ${{ runner.os }}-dune-
- ${{ runner.os }}-dune-doc-
-
- name: Setup OCaml toolchain
uses: ./.github/actions/setup-ocaml
with:
diff --git a/bindings/ocaml/CHANGES.md b/bindings/ocaml/CHANGES.md
new file mode 100644
index 000000000..74bc51ccb
--- /dev/null
+++ b/bindings/ocaml/CHANGES.md
@@ -0,0 +1,8 @@
+# Changelog
+
+All notable changes to the OpenDAL OCaml bindings will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
diff --git a/bindings/ocaml/dune-project b/bindings/ocaml/dune-project
index b9cd925bf..b42102100 100644
--- a/bindings/ocaml/dune-project
+++ b/bindings/ocaml/dune-project
@@ -17,6 +17,7 @@
; under the License.
(name opendal)
+(version 0.1.0)
(generate_opam_files true)
(license Apache-2.0)
(maintainers "OpenDAL Contributors <de...@opendal.apache.org>")
@@ -32,4 +33,5 @@
(ounit2 (and (>= 2.2.6) :with-test))
(ocaml (and (>= "4.10.0") (< 5)))
(dune (>= "2.1"))
- conf-rust))
+ conf-rust)
+ (tags (storage "data access" s3 filesystem cloud)))
diff --git a/bindings/ocaml/lib/opendal.ml b/bindings/ocaml/lib/opendal.ml
index 2d549daf3..a48afa9d3 100644
--- a/bindings/ocaml/lib/opendal.ml
+++ b/bindings/ocaml/lib/opendal.ml
@@ -19,4 +19,12 @@
module Operator = Operator
+(* Re-export types from Opendal_core for convenience *)
type operator = Opendal_core.Operator.operator
+type reader = Opendal_core.Operator.reader
+type writer = Opendal_core.Operator.writer
+type lister = Opendal_core.Operator.lister
+type metadata = Opendal_core.Operator.metadata
+type entry = Opendal_core.Operator.entry
+type operator_info = Opendal_core.Operator.operator_info
+type capability = Opendal_core.Operator.capability
diff --git a/bindings/ocaml/lib/operator.ml b/bindings/ocaml/lib/operator.ml
index 5c6f6e502..b22fc0b66 100644
--- a/bindings/ocaml/lib/operator.ml
+++ b/bindings/ocaml/lib/operator.ml
@@ -17,24 +17,43 @@
under the License.
*)
+(** Core operator functions *)
let new_operator = Opendal_core.Operator.operator
+
let list = Opendal_core.Operator.blocking_list
+let lister = Opendal_core.Operator.blocking_lister
let stat = Opendal_core.Operator.blocking_stat
let is_exist = Opendal_core.Operator.blocking_is_exist
let create_dir = Opendal_core.Operator.blocking_create_dir
let read = Opendal_core.Operator.blocking_read
let reader = Opendal_core.Operator.blocking_reader
let write = Opendal_core.Operator.blocking_write
+let writer = Opendal_core.Operator.blocking_writer
let copy = Opendal_core.Operator.blocking_copy
let rename = Opendal_core.Operator.blocking_rename
let delete = Opendal_core.Operator.blocking_delete
let remove = Opendal_core.Operator.blocking_remove
let remove_all = Opendal_core.Operator.blocking_remove_all
+let check = Opendal_core.Operator.blocking_check
+let info = Opendal_core.Operator.operator_info
+(** Reader module for reading data *)
module Reader = struct
let pread = Opendal_core.Operator.reader_pread
end
+(** Writer module for writing data *)
+module Writer = struct
+ let write = Opendal_core.Operator.writer_write
+ let close = Opendal_core.Operator.writer_close
+end
+
+(** Lister module for streaming directory listings *)
+module Lister = struct
+ let next = Opendal_core.Operator.lister_next
+end
+
+(** Metadata module for file/directory information *)
module Metadata = struct
let is_file = Opendal_core.Operator.metadata_is_file
let is_dir = Opendal_core.Operator.metadata_is_dir
@@ -46,8 +65,40 @@ module Metadata = struct
let last_modified = Opendal_core.Operator.metadata_last_modified
end
+(** Entry module for directory entry information *)
module Entry = struct
let path = Opendal_core.Operator.entry_path
let name = Opendal_core.Operator.entry_name
let metadata = Opendal_core.Operator.entry_metadata
end
+
+(** OperatorInfo module for operator information *)
+module OperatorInfo = struct
+ let name = Opendal_core.Operator.operator_info_name
+ let scheme = Opendal_core.Operator.operator_info_scheme
+ let root = Opendal_core.Operator.operator_info_root
+ let capability = Opendal_core.Operator.operator_info_capability
+end
+
+(** Capability module for checking supported operations *)
+module Capability = struct
+ let stat = Opendal_core.Operator.capability_stat
+ let read = Opendal_core.Operator.capability_read
+ let write = Opendal_core.Operator.capability_write
+ let create_dir = Opendal_core.Operator.capability_create_dir
+ let delete = Opendal_core.Operator.capability_delete
+ let copy = Opendal_core.Operator.capability_copy
+ let rename = Opendal_core.Operator.capability_rename
+ let list = Opendal_core.Operator.capability_list
+ let list_with_limit = Opendal_core.Operator.capability_list_with_limit
+
+ let list_with_start_after =
+ Opendal_core.Operator.capability_list_with_start_after
+
+ let list_with_recursive = Opendal_core.Operator.capability_list_with_recursive
+ let presign = Opendal_core.Operator.capability_presign
+ let presign_read = Opendal_core.Operator.capability_presign_read
+ let presign_stat = Opendal_core.Operator.capability_presign_stat
+ let presign_write = Opendal_core.Operator.capability_presign_write
+ let shared = Opendal_core.Operator.capability_shared
+end
diff --git a/bindings/ocaml/lib/operator.mli b/bindings/ocaml/lib/operator.mli
index 7b0f0e0ba..0b156fbbe 100644
--- a/bindings/ocaml/lib/operator.mli
+++ b/bindings/ocaml/lib/operator.mli
@@ -17,170 +17,641 @@
under the License.
*)
+(** {1 OpenDAL OCaml Bindings}
+
+ Apache OpenDAL™ OCaml binding provides a unified data access layer that
+ allows users to easily and efficiently retrieve data from various storage
+ services.
+
+ {2 Basic Usage}
+
+ {[
+ (* Create an operator for local filesystem *)
+ let op =
+ Operator.new_operator "fs" [ ("root", "/tmp") ] |> Result.get_ok
+ in
+
+ (* Write data to a file *)
+ let _ = Operator.write op "hello.txt" (Bytes.of_string "Hello, World!") in
+
+ (* Read data back *)
+ let content = Operator.read op "hello.txt" |> Result.get_ok in
+ print_endline (String.of_bytes content)
+ ]} *)
+
+(** {2 Core Operations} *)
+
val new_operator :
string ->
(string * string) list ->
(Opendal_core.Operator.operator, string) result
-(** [new_operator scheme config_map] Create a new block operator from given
- scheme and config_map.
+(** [new_operator scheme config_map] creates a new blocking operator from given
+ scheme and configuration.
@param scheme
- Supported services, for details, refer to
- https://opendal.apache.org/docs/category/services/
- @param config_map Configuration information required by the target service
- @return The block operator *)
+ The storage service scheme. Supported services include:
+ - "fs" for local filesystem
+ - "s3" for Amazon S3
+ - "gcs" for Google Cloud Storage
+ - "azblob" for Azure Blob Storage
+ - And many more. See {{:https://opendal.apache.org/docs/category/services/} the full list}.
+
+ @param config_map
+ Configuration key-value pairs required by the target service.
+ For example, for S3: [("bucket", "my-bucket"); ("region", "us-east-1")]
+
+ @return
+ A blocking operator wrapped in Result.
+
+ @example
+ {[
+ (* Local filesystem *)
+ let fs_op = new_operator "fs" [("root", "/tmp")] in
+
+ (* S3 storage *)
+ let s3_op = new_operator "s3" [
+ ("bucket", "my-bucket");
+ ("region", "us-east-1");
+ ("access_key_id", "...");
+ ("secret_access_key", "...")
+ ] in
+ ]}
+*)
val list :
Opendal_core.Operator.operator ->
string ->
(Opendal_core.Operator.entry array, string) result
+(** [list operator path] lists all entries in the given directory path.
+
+ @param operator The blocking operator
+ @param path Directory path to list (should end with "/")
+ @return Array of directory entries
+
+ Note: This loads all entries into memory. For large directories,
+ consider using {!lister} for streaming access.
+
+ @example
+ {[
+ match list op "data/" with
+ | Ok entries ->
+ Array.iter (fun entry ->
+ printf "Found: %s\n" (Entry.name entry)
+ ) entries
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
+
+val lister :
+ Opendal_core.Operator.operator ->
+ string ->
+ (Opendal_core.Operator.lister, string) result
+(** [lister operator path] creates a streaming lister for the given directory.
+
+ @param operator The blocking operator
+ @param path Directory path to list (should end with "/")
+ @return A lister for streaming access to entries
+
+ Use {!Lister.next} to iterate through entries one by one.
+ This is memory-efficient for large directories.
+
+ @example
+ {[
+ match lister op "data/" with
+ | Ok lst ->
+ let rec iter () =
+ match Lister.next lst with
+ | Ok (Some entry) ->
+ printf "Found: %s\n" (Entry.name entry);
+ iter ()
+ | Ok None -> () (* End of listing *)
+ | Error err -> printf "Error: %s\n" err
+ in iter ()
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val stat :
Opendal_core.Operator.operator ->
string ->
(Opendal_core.Operator.metadata, string) result
-(** [is_exist operator path] Get current path's metadata **without cache**
- directly.
-
- @param operator The operator
- @param path want to stat
- @return metadata *)
+(** [stat operator path] gets metadata for the given path.
+
+ @param operator The blocking operator
+ @param path Path to get metadata for
+ @return Metadata for the path
+
+ @example
+ {[
+ match stat op "file.txt" with
+ | Ok meta ->
+ printf "Size: %Ld bytes\n" (Metadata.content_length meta);
+ printf "Is file: %b\n" (Metadata.is_file meta)
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val is_exist : Opendal_core.Operator.operator -> string -> (bool, string) result
-(** [is_exist operator path] Check if this path exists or not.
-
- @param operator The operator
- @param path want to check
- @return is exists *)
+(** [is_exist operator path] checks if the given path exists.
+
+ @param operator The blocking operator
+ @param path Path to check
+ @return true if path exists, false otherwise
+
+ @example
+ {[
+ match is_exist op "file.txt" with
+ | Ok true -> print_endline "File exists"
+ | Ok false -> print_endline "File does not exist"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val create_dir :
Opendal_core.Operator.operator -> string -> (bool, string) result
-(** [create_dir operator path] Create a dir at given path.
-
- # Notes
-
- To indicate that a path is a directory, it is compulsory to include a
- trailing / in the path. Failure to do so may result in `NotADirectory` error
- being returned by OpenDAL.
-
- # Behavior
-
- - Create on existing dir will succeed.
- - Create dir is always recursive, works like `mkdir -p`
-
- @param operator The operator
- @param path want to create dir *)
+(** [create_dir operator path] creates a directory at the given path.
+
+ @param operator The blocking operator
+ @param path Directory path to create (must end with "/")
+
+ Notes:
+ - Creating existing directories succeeds
+ - Creates parent directories recursively (like "mkdir -p")
+ - Path must end with "/" to indicate it's a directory
+
+ @example
+ {[
+ match create_dir op "data/subdir/" with
+ | Ok () -> print_endline "Directory created"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val read :
Opendal_core.Operator.operator -> string -> (char array, string) result
-(** [read operator path] Read the whole path into a bytes.
-
- @param operator The operator
- @param path want to read
- @return data of path *)
+(** [read operator path] reads the entire file content into memory.
+
+ @param operator The blocking operator
+ @param path File path to read
+ @return File content as a char array
+
+ For large files or streaming access, consider using {!reader}.
+
+ @example
+ {[
+ match read op "file.txt" with
+ | Ok content ->
+ let bytes = Array.to_seq content |> Bytes.of_seq in
+ print_endline (Bytes.to_string bytes)
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val reader :
Opendal_core.Operator.operator ->
string ->
(Opendal_core.Operator.reader, string) result
-(** [read operator path] Create a new reader which can read the whole path.
-
- @param operator The operator
- @param path want to read
- @return reader *)
+(** [reader operator path] creates a reader for streaming file access.
+
+ @param operator The blocking operator
+ @param path File path to read
+ @return A reader for streaming access
+
+ Use {!Reader.pread} to read data from specific positions.
+
+ @example
+ {[
+ match reader op "file.txt" with
+ | Ok r ->
+ let buf = Bytes.create 1024 in
+ (match Reader.pread r buf 0L with
+ | Ok bytes_read -> printf "Read %d bytes\n" bytes_read
+ | Error err -> printf "Error: %s\n" err)
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val write :
Opendal_core.Operator.operator -> string -> bytes -> (unit, string) result
-(** [write operator path data] Write bytes into given path.
- - Write will make sure all bytes has been written, or an error will be
- returned.
+(** [write operator path data] writes data to the given path.
+
+ @param operator The blocking operator
+ @param path File path to write to
+ @param data Data to write
+
+ Notes:
+ - Overwrites existing files
+ - Creates parent directories if needed
+ - Ensures all data is written atomically
+
+ @example
+ {[
+ let data = Bytes.of_string "Hello, World!" in
+ match write op "hello.txt" data with
+ | Ok () -> print_endline "File written"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
- @param operator The operator
- @param path want to write
- @param data want to write *)
+val writer :
+ Opendal_core.Operator.operator ->
+ string ->
+ (Opendal_core.Operator.writer, string) result
+(** [writer operator path] creates a writer for streaming file writes.
+
+ @param operator The blocking operator
+ @param path File path to write to
+ @return A writer for streaming writes
+
+ Use {!Writer.write} to write data chunks and {!Writer.close} to finalize.
+
+ @example
+ {[
+ match writer op "large_file.txt" with
+ | Ok w ->
+ let _ = Writer.write w (Bytes.of_string "chunk1") in
+ let _ = Writer.write w (Bytes.of_string "chunk2") in
+ Writer.close w
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val copy :
Opendal_core.Operator.operator -> string -> string -> (unit, string) result
-(** [copy operator from to] Copy a file from [from] to [to].
- - [from] and [to] must be a file.
- - [to] will be overwritten if it exists.
- - If [from] and [to] are the same, nothing will happen.
- - copy is idempotent. For same [from] and [to] input, the result will be the
- same.
-
- @param operator The operator
- @param from file path
- @param to file path *)
+(** [copy operator from to] copies a file from source to destination.
+
+ @param operator The blocking operator
+ @param from Source file path
+ @param to Destination file path
+
+ Notes:
+ - Overwrites destination if it exists
+ - Creates parent directories if needed
+ - Both paths must be files (not directories)
+
+ @example
+ {[
+ match copy op "source.txt" "backup.txt" with
+ | Ok () -> print_endline "File copied"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val rename :
Opendal_core.Operator.operator -> string -> string -> (unit, string) result
-(** [rename operator from to] Rename a file from [from] to [to].
- - [from] and [to] must be a file.
- - [to] will be overwritten if it exists.
- - If [from] and [to] are the same, a `IsSameFile` error will occur.
-
- @param operator The operator
- @param from file path
- @param to file path *)
+(** [rename operator from to] renames/moves a file from source to destination.
+
+ @param operator The blocking operator
+ @param from Source file path
+ @param to Destination file path
+
+ Notes:
+ - Overwrites destination if it exists
+ - Creates parent directories if needed
+ - Source file is removed after successful operation
+
+ @example
+ {[
+ match rename op "old_name.txt" "new_name.txt" with
+ | Ok () -> print_endline "File renamed"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val delete : Opendal_core.Operator.operator -> string -> (unit, string) result
-(** [delete operator path] Delete given path.
- - Delete not existing error won't return errors.
-
- @param operator The block operator
- @param path file path *)
+(** [delete operator path] deletes the file at the given path.
+
+ @param operator The blocking operator
+ @param path File path to delete
+
+ Notes:
+ - Succeeds even if file doesn't exist (idempotent)
+ - Cannot be used to delete directories (use {!remove_all} instead)
+
+ @example
+ {[
+ match delete op "unwanted.txt" with
+ | Ok () -> print_endline "File deleted"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val remove :
Opendal_core.Operator.operator -> string array -> (unit, string) result
-(** [remove operator paths] Remove path array.
- - We don't support batch delete now, will call delete on each object in turn
-
- @param operator The block operator
- @param paths file path array *)
+(** [remove operator paths] deletes multiple files in a batch operation.
+
+ @param operator The blocking operator
+ @param paths Array of file paths to delete
+
+ This is more efficient than calling {!delete} multiple times
+ for services that support batch deletion.
+
+ @example
+ {[
+ let files = [|"file1.txt"; "file2.txt"; "file3.txt"|] in
+ match remove op files with
+ | Ok () -> print_endline "Files deleted"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
val remove_all :
Opendal_core.Operator.operator -> string -> (unit, string) result
-(** [remove_all operator path] Remove the path and all nested dirs and files
- recursively.
- - We don't support batch delete now, will call delete on each object in turn
+(** [remove_all operator path] recursively deletes the directory and all its contents.
+
+ @param operator The blocking operator
+ @param path Directory path to delete (should end with "/")
+
+ ⚠️ WARNING: This operation permanently deletes all files and subdirectories.
+ Use with extreme caution!
+
+ @example
+ {[
+ match remove_all op "temp_data/" with
+ | Ok () -> print_endline "Directory deleted"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
- @param operator The block operator
- @param path file path *)
+val check : Opendal_core.Operator.operator -> (unit, string) result
+(** [check operator] performs a health check on the storage service.
+
+ @param operator The blocking operator
+
+ This verifies that the operator can connect to and access
+ the configured storage service.
+
+ @example
+ {[
+ match check op with
+ | Ok () -> print_endline "Storage service is accessible"
+ | Error err -> printf "Error: %s\n" err
+ ]}
+*)
+val info : Opendal_core.Operator.operator -> Opendal_core.Operator.operator_info
+(** [info operator] returns information about the operator.
+
+ @param operator The blocking operator
+ @return Operator information (name, scheme, root path, capabilities)
+
+ @example
+ {[
+ let info = info op in
+ printf "Service: %s\n" (OperatorInfo.name info);
+ printf "Scheme: %s\n" (OperatorInfo.scheme info);
+ printf "Root: %s\n" (OperatorInfo.root info)
+ ]}
+*)
+
+(** {2 Reader Operations}
+
+ Module for reading data from files with fine-grained control. *)
module Reader : sig
val pread :
Opendal_core.Operator.reader -> bytes -> int64 -> (int, string) result
- (** [read reader buf] Read data to [buf] and return data size.*)
+ (** [pread reader buf offset] reads data from the reader at the given offset.
+
+ @param reader The reader instance
+ @param buf Buffer to read data into
+ @param offset Byte offset in the file to start reading from
+ @return Number of bytes actually read
+
+ @example
+ {[
+ let buf = Bytes.create 1024 in
+ match Reader.pread reader buf 100L with
+ | Ok bytes_read ->
+ printf "Read %d bytes starting at offset 100\n" bytes_read
+ | Error err -> printf "Error: %s\n" err
+ ]}
+ *)
+end
+
+(** {2 Writer Operations}
+
+ Module for writing data to files in chunks. *)
+module Writer : sig
+ val write : Opendal_core.Operator.writer -> bytes -> (unit, string) result
+ (** [write writer data] writes a chunk of data.
+
+ @param writer The writer instance
+ @param data Data chunk to write
+
+ @example
+ {[
+ match Writer.write writer (Bytes.of_string "Hello ") with
+ | Ok () ->
+ (match Writer.write writer (Bytes.of_string "World!") with
+ | Ok () -> print_endline "Data written"
+ | Error err -> printf "Error: %s\n" err)
+ | Error err -> printf "Error: %s\n" err
+ ]}
+ *)
+
+ val close :
+ Opendal_core.Operator.writer ->
+ (Opendal_core.Operator.metadata, string) result
+ (** [close writer] finalizes the write operation and returns metadata.
+
+ Must be called to ensure all data is properly written and committed.
+
+ @param writer The writer instance
+ @return Metadata about the written file
+
+ @example
+ {[
+ (* After writing all chunks *)
+ match Writer.close writer with
+ | Ok metadata ->
+ printf "File closed successfully, size: %Ld bytes\n"
+ (Metadata.content_length metadata)
+ | Error err -> printf "Error closing file: %s\n" err
+ ]}
+ *)
+end
+
+(** {2 Lister Operations}
+
+ Module for streaming directory listings. *)
+module Lister : sig
+ val next :
+ Opendal_core.Operator.lister ->
+ (Opendal_core.Operator.entry option, string) result
+ (** [next lister] gets the next entry from the directory listing.
+
+ @param lister The lister instance
+ @return Some entry if available, None if no more entries
+
+ @example
+ {[
+ let rec process_all lister =
+ match Lister.next lister with
+ | Ok (Some entry) ->
+ printf "Processing: %s\n" (Entry.name entry);
+ process_all lister
+ | Ok None ->
+ print_endline "Finished processing all entries"
+ | Error err ->
+ printf "Error: %s\n" err
+ in
+ process_all my_lister
+ ]}
+ *)
end
+(** {2 Metadata Operations}
+
+ Module for accessing file and directory metadata. *)
module Metadata : sig
val is_file : Opendal_core.Operator.metadata -> bool
- (** [is_file metadata] Returns `true` if this metadata is for a file.*)
+ (** [is_file metadata] checks if the metadata represents a file.
+
+ @example {[if Metadata.is_file meta then print_endline "It's a file"]}
+ *)
val is_dir : Opendal_core.Operator.metadata -> bool
- (** [is_dir metadata] Returns `true` if this metadata is for a directory.*)
+ (** [is_dir metadata] checks if the metadata represents a directory.
+
+ @example {[if Metadata.is_dir meta then print_endline "It's a directory"]}
+ *)
val content_length : Opendal_core.Operator.metadata -> int64
- (** [content_length metadata] Content length of this entry.*)
+ (** [content_length metadata] gets the size of the content in bytes.
+
+ @example {[printf "File size: %Ld bytes\n" (Metadata.content_length meta)]}
+ *)
val content_md5 : Opendal_core.Operator.metadata -> string option
- (** [content_md5 metadata] Content MD5 of this entry.*)
+ (** [content_md5 metadata] gets the MD5 hash of the content, if available. *)
val content_type : Opendal_core.Operator.metadata -> string option
- (** [content_type metadata] Content Type of this entry.*)
+ (** [content_type metadata] gets the MIME type of the content, if available.
+ *)
val content_disposition : Opendal_core.Operator.metadata -> string option
- (** [content_disposition metadata] Content-Disposition of this entry*)
+ (** [content_disposition metadata] gets the Content-Disposition header, if
+ available. *)
val etag : Opendal_core.Operator.metadata -> string option
- (** [etag metadata] ETag of this entry.*)
+ (** [etag metadata] gets the ETag of the content, if available. *)
val last_modified : Opendal_core.Operator.metadata -> int64 option
- (** [last_modified metadata] Last modified of this entry.*)
+ (** [last_modified metadata] gets the last modification time as Unix
+ timestamp, if available. *)
end
+(** {2 Entry Operations}
+
+ Module for accessing directory entry information. *)
module Entry : sig
val path : Opendal_core.Operator.entry -> string
+ (** [path entry] gets the full path of the directory entry.
+
+ @example {[printf "Full path: %s\n" (Entry.path entry)]}
+ *)
+
val name : Opendal_core.Operator.entry -> string
+ (** [name entry] gets the name (filename) of the directory entry.
+
+ @example {[printf "Name: %s\n" (Entry.name entry)]}
+ *)
+
val metadata : Opendal_core.Operator.entry -> Opendal_core.Operator.metadata
+ (** [metadata entry] gets the metadata for the directory entry.
+
+ @example
+ {[
+ let meta = Entry.metadata entry in
+ printf "Size: %Ld\n" (Metadata.content_length meta)
+ ]}
+ *)
+end
+
+(** {2 Operator Information}
+
+ Module for accessing operator configuration and capabilities. *)
+module OperatorInfo : sig
+ val name : Opendal_core.Operator.operator_info -> string
+ (** [name info] gets the name of the operator instance. *)
+
+ val scheme : Opendal_core.Operator.operator_info -> string
+ (** [scheme info] gets the storage scheme (e.g., "fs", "s3", "gcs").
+
+ @example {[printf "Using scheme: %s\n" (OperatorInfo.scheme info)]}
+ *)
+
+ val root : Opendal_core.Operator.operator_info -> string
+ (** [root info] gets the root path configured for the operator.
+
+ @example {[printf "Root path: %s\n" (OperatorInfo.root info)]}
+ *)
+
+ val capability :
+ Opendal_core.Operator.operator_info -> Opendal_core.Operator.capability
+ (** [capability info] gets the capability information for the operator.
+
+ @example
+ {[
+ let cap = OperatorInfo.capability info in
+ if Capability.write cap then print_endline "Write operations supported"
+ ]}
+ *)
+end
+
+(** {2 Capability Checking}
+
+ Module for checking what operations are supported by the storage backend. *)
+module Capability : sig
+ val stat : Opendal_core.Operator.capability -> bool
+ (** [stat cap] checks if stat operations are supported. *)
+
+ val read : Opendal_core.Operator.capability -> bool
+ (** [read cap] checks if read operations are supported. *)
+
+ val write : Opendal_core.Operator.capability -> bool
+ (** [write cap] checks if write operations are supported. *)
+
+ val create_dir : Opendal_core.Operator.capability -> bool
+ (** [create_dir cap] checks if directory creation is supported. *)
+
+ val delete : Opendal_core.Operator.capability -> bool
+ (** [delete cap] checks if delete operations are supported. *)
+
+ val copy : Opendal_core.Operator.capability -> bool
+ (** [copy cap] checks if copy operations are supported. *)
+
+ val rename : Opendal_core.Operator.capability -> bool
+ (** [rename cap] checks if rename operations are supported. *)
+
+ val list : Opendal_core.Operator.capability -> bool
+ (** [list cap] checks if list operations are supported. *)
+
+ val list_with_limit : Opendal_core.Operator.capability -> bool
+ (** [list_with_limit cap] checks if list operations with limit are supported.
+ *)
+
+ val list_with_start_after : Opendal_core.Operator.capability -> bool
+ (** [list_with_start_after cap] checks if list operations with start_after are
+ supported. *)
+
+ val list_with_recursive : Opendal_core.Operator.capability -> bool
+ (** [list_with_recursive cap] checks if recursive list operations are
+ supported. *)
+
+ val presign : Opendal_core.Operator.capability -> bool
+ (** [presign cap] checks if presigned URL generation is supported. *)
+
+ val presign_read : Opendal_core.Operator.capability -> bool
+ (** [presign_read cap] checks if presigned read URLs are supported. *)
+
+ val presign_stat : Opendal_core.Operator.capability -> bool
+ (** [presign_stat cap] checks if presigned stat URLs are supported. *)
+
+ val presign_write : Opendal_core.Operator.capability -> bool
+ (** [presign_write cap] checks if presigned write URLs are supported. *)
+
+ val shared : Opendal_core.Operator.capability -> bool
+ (** [shared cap] checks if the operator can be safely shared between threads.
+ *)
end
diff --git a/bindings/ocaml/opendal.opam b/bindings/ocaml/opendal.opam
index 99b84a9ea..5b089ee63 100644
--- a/bindings/ocaml/opendal.opam
+++ b/bindings/ocaml/opendal.opam
@@ -1,11 +1,13 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
+version: "0.1.0"
synopsis: "Apache OpenDAL™ OCaml Binding"
description:
"Apache OpenDAL™ is a data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way."
maintainer: ["OpenDAL Contributors <de...@opendal.apache.org>"]
authors: ["OpenDAL Contributors <de...@opendal.apache.org>"]
license: "Apache-2.0"
+tags: ["storage" "data access" "s3" "filesystem" "cloud"]
homepage: "https://github.com/apache/opendal"
doc: "https://opendal.apache.org/"
bug-reports: "https://github.com/apache/opendal/issues"
diff --git a/bindings/ocaml/release.sh b/bindings/ocaml/release.sh
new file mode 100755
index 000000000..855f12efa
--- /dev/null
+++ b/bindings/ocaml/release.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+# 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.
+
+set -euo pipefail
+
+# Release script for OpenDAL OCaml bindings to opam
+
+VERSION=${1:-""}
+if [ -z "$VERSION" ]; then
+ echo "Usage: $0 <version>"
+ echo "Example: $0 0.48.0"
+ exit 1
+fi
+
+echo "🚀 Releasing OpenDAL OCaml bindings version $VERSION"
+
+# Step 1: Update version in dune-project
+echo "📝 Updating version in dune-project..."
+sed -i "s/(version [^)]*)/(version $VERSION)/" dune-project
+
+# Step 2: Regenerate opam file
+echo "🔄 Regenerating opam file..."
+dune build
+
+# Step 3: Run tests
+echo "🧪 Running tests..."
+dune runtest
+
+# Step 4: Build documentation
+echo "📚 Building documentation..."
+dune build @doc
+
+# Step 5: Create a source distribution
+echo "📦 Creating source distribution..."
+dune-release tag v$VERSION
+dune-release distrib
+
+# Step 6: Publish to opam-repository (requires manual PR)
+echo "🌐 Creating opam-repository PR..."
+echo "Run the following command to submit to opam-repository:"
+echo "dune-release publish distrib --verbose"
+echo ""
+echo "Or manually create a PR to opam-repository with the generated opam file:"
+echo "https://github.com/ocaml/opam-repository"
+
+echo "✅ Release preparation complete!"
+echo ""
+echo "Next steps:"
+echo "1. Review the generated tarball in _build/"
+echo "2. Test installation: opam install ./"
+echo "3. Submit PR to opam-repository"
+echo "4. Tag the release in git: git tag v$VERSION && git push origin v$VERSION"
\ No newline at end of file
diff --git a/bindings/ocaml/src/operator.ml b/bindings/ocaml/src/operator.ml
index 91d8a16e3..9ad8440a6 100644
--- a/bindings/ocaml/src/operator.ml
+++ b/bindings/ocaml/src/operator.ml
@@ -6,8 +6,32 @@ open! Bigarray
type operator
type reader
+type writer
+type lister
type metadata
type entry
+type operator_info
+type capability
+type presigned_request
+
+(* file: capability.rs *)
+
+external capability_stat: capability -> bool = "capability_stat"
+external capability_read: capability -> bool = "capability_read"
+external capability_write: capability -> bool = "capability_write"
+external capability_create_dir: capability -> bool = "capability_create_dir"
+external capability_delete: capability -> bool = "capability_delete"
+external capability_copy: capability -> bool = "capability_copy"
+external capability_rename: capability -> bool = "capability_rename"
+external capability_list: capability -> bool = "capability_list"
+external capability_list_with_limit: capability -> bool = "capability_list_with_limit"
+external capability_list_with_start_after: capability -> bool = "capability_list_with_start_after"
+external capability_list_with_recursive: capability -> bool = "capability_list_with_recursive"
+external capability_presign: capability -> bool = "capability_presign"
+external capability_presign_read: capability -> bool = "capability_presign_read"
+external capability_presign_stat: capability -> bool = "capability_presign_stat"
+external capability_presign_write: capability -> bool = "capability_presign_write"
+external capability_shared: capability -> bool = "capability_shared"
(* file: entry.rs *)
@@ -15,6 +39,10 @@ external entry_path: entry -> string = "entry_path"
external entry_name: entry -> string = "entry_name"
external entry_metadata: entry -> metadata = "entry_metadata"
+(* file: lister.rs *)
+
+external lister_next: lister -> (entry option, string) Result.t = "lister_next"
+
(* file: metadata.rs *)
external metadata_is_file: metadata -> bool = "metadata_is_file"
@@ -30,18 +58,34 @@ external metadata_last_modified: metadata -> int64 option = "metadata_last_modi
external operator: string -> (string * string) list -> (operator, string) Result.t = "operator"
external blocking_list: operator -> string -> (entry array, string) Result.t = "blocking_list"
+external blocking_lister: operator -> string -> (lister, string) Result.t = "blocking_lister"
external blocking_stat: operator -> string -> (metadata, string) Result.t = "blocking_stat"
external blocking_is_exist: operator -> string -> (bool, string) Result.t = "blocking_is_exist"
external blocking_create_dir: operator -> string -> (bool, string) Result.t = "blocking_create_dir"
external blocking_read: operator -> string -> (char array, string) Result.t = "blocking_read"
external blocking_reader: operator -> string -> (reader, string) Result.t = "blocking_reader"
external blocking_write: operator -> string -> bytes -> (unit, string) Result.t = "blocking_write"
+external blocking_writer: operator -> string -> (writer, string) Result.t = "blocking_writer"
external blocking_copy: operator -> string -> string -> (unit, string) Result.t = "blocking_copy"
external blocking_rename: operator -> string -> string -> (unit, string) Result.t = "blocking_rename"
external blocking_delete: operator -> string -> (unit, string) Result.t = "blocking_delete"
external blocking_remove: operator -> string array -> (unit, string) Result.t = "blocking_remove"
external blocking_remove_all: operator -> string -> (unit, string) Result.t = "blocking_remove_all"
+external blocking_check: operator -> (unit, string) Result.t = "blocking_check"
+external operator_info: operator -> operator_info = "operator_info"
+external operator_info_capability: operator_info -> capability = "operator_info_capability"
+
+(* file: operator_info.rs *)
+
+external operator_info_name: operator_info -> string = "operator_info_name"
+external operator_info_scheme: operator_info -> string = "operator_info_scheme"
+external operator_info_root: operator_info -> string = "operator_info_root"
(* file: reader.rs *)
external reader_pread: reader -> bytes -> int64 -> (int, string) Result.t = "reader_pread"
+
+(* file: writer.rs *)
+
+external writer_write: writer -> bytes -> (unit, string) Result.t = "writer_write"
+external writer_close: writer -> (metadata, string) Result.t = "writer_close"
diff --git a/bindings/ocaml/src/operator.mli b/bindings/ocaml/src/operator.mli
index 91d8a16e3..9ad8440a6 100644
--- a/bindings/ocaml/src/operator.mli
+++ b/bindings/ocaml/src/operator.mli
@@ -6,8 +6,32 @@ open! Bigarray
type operator
type reader
+type writer
+type lister
type metadata
type entry
+type operator_info
+type capability
+type presigned_request
+
+(* file: capability.rs *)
+
+external capability_stat: capability -> bool = "capability_stat"
+external capability_read: capability -> bool = "capability_read"
+external capability_write: capability -> bool = "capability_write"
+external capability_create_dir: capability -> bool = "capability_create_dir"
+external capability_delete: capability -> bool = "capability_delete"
+external capability_copy: capability -> bool = "capability_copy"
+external capability_rename: capability -> bool = "capability_rename"
+external capability_list: capability -> bool = "capability_list"
+external capability_list_with_limit: capability -> bool = "capability_list_with_limit"
+external capability_list_with_start_after: capability -> bool = "capability_list_with_start_after"
+external capability_list_with_recursive: capability -> bool = "capability_list_with_recursive"
+external capability_presign: capability -> bool = "capability_presign"
+external capability_presign_read: capability -> bool = "capability_presign_read"
+external capability_presign_stat: capability -> bool = "capability_presign_stat"
+external capability_presign_write: capability -> bool = "capability_presign_write"
+external capability_shared: capability -> bool = "capability_shared"
(* file: entry.rs *)
@@ -15,6 +39,10 @@ external entry_path: entry -> string = "entry_path"
external entry_name: entry -> string = "entry_name"
external entry_metadata: entry -> metadata = "entry_metadata"
+(* file: lister.rs *)
+
+external lister_next: lister -> (entry option, string) Result.t = "lister_next"
+
(* file: metadata.rs *)
external metadata_is_file: metadata -> bool = "metadata_is_file"
@@ -30,18 +58,34 @@ external metadata_last_modified: metadata -> int64 option = "metadata_last_modi
external operator: string -> (string * string) list -> (operator, string) Result.t = "operator"
external blocking_list: operator -> string -> (entry array, string) Result.t = "blocking_list"
+external blocking_lister: operator -> string -> (lister, string) Result.t = "blocking_lister"
external blocking_stat: operator -> string -> (metadata, string) Result.t = "blocking_stat"
external blocking_is_exist: operator -> string -> (bool, string) Result.t = "blocking_is_exist"
external blocking_create_dir: operator -> string -> (bool, string) Result.t = "blocking_create_dir"
external blocking_read: operator -> string -> (char array, string) Result.t = "blocking_read"
external blocking_reader: operator -> string -> (reader, string) Result.t = "blocking_reader"
external blocking_write: operator -> string -> bytes -> (unit, string) Result.t = "blocking_write"
+external blocking_writer: operator -> string -> (writer, string) Result.t = "blocking_writer"
external blocking_copy: operator -> string -> string -> (unit, string) Result.t = "blocking_copy"
external blocking_rename: operator -> string -> string -> (unit, string) Result.t = "blocking_rename"
external blocking_delete: operator -> string -> (unit, string) Result.t = "blocking_delete"
external blocking_remove: operator -> string array -> (unit, string) Result.t = "blocking_remove"
external blocking_remove_all: operator -> string -> (unit, string) Result.t = "blocking_remove_all"
+external blocking_check: operator -> (unit, string) Result.t = "blocking_check"
+external operator_info: operator -> operator_info = "operator_info"
+external operator_info_capability: operator_info -> capability = "operator_info_capability"
+
+(* file: operator_info.rs *)
+
+external operator_info_name: operator_info -> string = "operator_info_name"
+external operator_info_scheme: operator_info -> string = "operator_info_scheme"
+external operator_info_root: operator_info -> string = "operator_info_root"
(* file: reader.rs *)
external reader_pread: reader -> bytes -> int64 -> (int, string) Result.t = "reader_pread"
+
+(* file: writer.rs *)
+
+external writer_write: writer -> bytes -> (unit, string) Result.t = "writer_write"
+external writer_close: writer -> (metadata, string) Result.t = "writer_close"
diff --git a/bindings/ocaml/src/operator/_type.rs b/bindings/ocaml/src/operator/_type.rs
index 50bb7a5b1..10d715ba7 100644
--- a/bindings/ocaml/src/operator/_type.rs
+++ b/bindings/ocaml/src/operator/_type.rs
@@ -27,6 +27,14 @@ ocaml::custom!(Operator);
pub struct Reader(pub(crate) od::blocking::Reader);
ocaml::custom!(Reader);
+#[ocaml::sig]
+pub struct Writer(pub(crate) od::blocking::Writer);
+ocaml::custom!(Writer);
+
+#[ocaml::sig]
+pub struct Lister(pub(crate) od::blocking::Lister);
+ocaml::custom!(Lister);
+
#[ocaml::sig]
pub struct Metadata(pub(crate) od::Metadata);
ocaml::custom!(Metadata);
@@ -34,3 +42,16 @@ ocaml::custom!(Metadata);
#[ocaml::sig]
pub struct Entry(pub(crate) od::Entry);
ocaml::custom!(Entry);
+
+#[ocaml::sig]
+pub struct OperatorInfo(pub(crate) od::OperatorInfo);
+ocaml::custom!(OperatorInfo);
+
+#[ocaml::sig]
+pub struct Capability(pub(crate) od::Capability);
+ocaml::custom!(Capability);
+
+#[ocaml::sig]
+#[allow(dead_code)]
+pub struct PresignedRequest(pub(crate) od::raw::PresignedRequest);
+ocaml::custom!(PresignedRequest);
diff --git a/bindings/ocaml/src/operator/capability.rs b/bindings/ocaml/src/operator/capability.rs
new file mode 100644
index 000000000..d0f84eb71
--- /dev/null
+++ b/bindings/ocaml/src/operator/capability.rs
@@ -0,0 +1,114 @@
+// 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 super::*;
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_stat(cap: &Capability) -> bool {
+ cap.0.stat
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_read(cap: &Capability) -> bool {
+ cap.0.read
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_write(cap: &Capability) -> bool {
+ cap.0.write
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_create_dir(cap: &Capability) -> bool {
+ cap.0.create_dir
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_delete(cap: &Capability) -> bool {
+ cap.0.delete
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_copy(cap: &Capability) -> bool {
+ cap.0.copy
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_rename(cap: &Capability) -> bool {
+ cap.0.rename
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_list(cap: &Capability) -> bool {
+ cap.0.list
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_list_with_limit(cap: &Capability) -> bool {
+ cap.0.list_with_limit
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_list_with_start_after(cap: &Capability) -> bool {
+ cap.0.list_with_start_after
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_list_with_recursive(cap: &Capability) -> bool {
+ cap.0.list_with_recursive
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_presign(cap: &Capability) -> bool {
+ cap.0.presign
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_presign_read(cap: &Capability) -> bool {
+ cap.0.presign_read
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_presign_stat(cap: &Capability) -> bool {
+ cap.0.presign_stat
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_presign_write(cap: &Capability) -> bool {
+ cap.0.presign_write
+}
+
+#[ocaml::func]
+#[ocaml::sig("capability -> bool ")]
+pub fn capability_shared(cap: &Capability) -> bool {
+ cap.0.shared
+}
diff --git a/bindings/ocaml/src/operator/_type.rs b/bindings/ocaml/src/operator/lister.rs
similarity index 59%
copy from bindings/ocaml/src/operator/_type.rs
copy to bindings/ocaml/src/operator/lister.rs
index 50bb7a5b1..16dbf2a07 100644
--- a/bindings/ocaml/src/operator/_type.rs
+++ b/bindings/ocaml/src/operator/lister.rs
@@ -15,22 +15,14 @@
// specific language governing permissions and limitations
// under the License.
-// For ocaml-rs, the build order in the same build group is the order of the file names.
-// In order to use the type in the function in the generated ocaml file, it must be defined on the first generated file.
use super::*;
-#[ocaml::sig]
-pub struct Operator(pub(crate) od::blocking::Operator);
-ocaml::custom!(Operator);
-
-#[ocaml::sig]
-pub struct Reader(pub(crate) od::blocking::Reader);
-ocaml::custom!(Reader);
-
-#[ocaml::sig]
-pub struct Metadata(pub(crate) od::Metadata);
-ocaml::custom!(Metadata);
-
-#[ocaml::sig]
-pub struct Entry(pub(crate) od::Entry);
-ocaml::custom!(Entry);
+#[ocaml::func]
+#[ocaml::sig("lister -> (entry option, string) Result.t ")]
+pub fn lister_next(lister: &mut Lister) -> Result<Option<ocaml::Pointer<Entry>>, String> {
+ match lister.0.next() {
+ Some(Ok(entry)) => Ok(Some(Entry(entry).into())),
+ Some(Err(e)) => Err(e.to_string()),
+ None => Ok(None),
+ }
+}
diff --git a/bindings/ocaml/src/operator/mod.rs b/bindings/ocaml/src/operator/mod.rs
index 9cbb51b40..420709966 100644
--- a/bindings/ocaml/src/operator/mod.rs
+++ b/bindings/ocaml/src/operator/mod.rs
@@ -16,9 +16,13 @@
// under the License.
mod _type;
+mod capability;
mod entry;
+mod lister;
mod metadata;
+mod operator_info;
mod reader;
+mod writer;
use _type::*;
@@ -53,6 +57,15 @@ pub fn blocking_list(
)
}
+#[ocaml::func]
+#[ocaml::sig("operator -> string -> (lister, string) Result.t ")]
+pub fn blocking_lister(
+ operator: &mut Operator,
+ path: String,
+) -> Result<ocaml::Pointer<Lister>, String> {
+ map_res_error(operator.0.lister(path.as_str())).map(|op| Lister(op).into())
+}
+
#[ocaml::func]
#[ocaml::sig("operator -> string -> (metadata, string) Result.t ")]
pub fn blocking_stat(
@@ -99,6 +112,15 @@ pub fn blocking_write(
map_res_error(operator.0.write(path.as_str(), bs).map(|_| ()))
}
+#[ocaml::func]
+#[ocaml::sig("operator -> string -> (writer, string) Result.t ")]
+pub fn blocking_writer(
+ operator: &mut Operator,
+ path: String,
+) -> Result<ocaml::Pointer<Writer>, String> {
+ map_res_error(operator.0.writer(path.as_str())).map(|op| Writer(op).into())
+}
+
#[ocaml::func]
#[ocaml::sig("operator -> string -> string -> (unit, string) Result.t ")]
pub fn blocking_copy(operator: &mut Operator, from: String, to: String) -> Result<(), String> {
@@ -128,3 +150,21 @@ pub fn blocking_remove(operator: &mut Operator, path: Vec<String>) -> Result<(),
pub fn blocking_remove_all(operator: &mut Operator, path: String) -> Result<(), String> {
map_res_error(operator.0.remove_all(path.as_str()))
}
+
+#[ocaml::func]
+#[ocaml::sig("operator -> (unit, string) Result.t ")]
+pub fn blocking_check(operator: &mut Operator) -> Result<(), String> {
+ map_res_error(operator.0.check())
+}
+
+#[ocaml::func]
+#[ocaml::sig("operator -> operator_info ")]
+pub fn operator_info(operator: &mut Operator) -> ocaml::Pointer<OperatorInfo> {
+ OperatorInfo(operator.0.info()).into()
+}
+
+#[ocaml::func]
+#[ocaml::sig("operator_info -> capability ")]
+pub fn operator_info_capability(info: &mut OperatorInfo) -> ocaml::Pointer<Capability> {
+ Capability(info.0.full_capability()).into()
+}
diff --git a/bindings/ocaml/src/operator/_type.rs b/bindings/ocaml/src/operator/operator_info.rs
similarity index 59%
copy from bindings/ocaml/src/operator/_type.rs
copy to bindings/ocaml/src/operator/operator_info.rs
index 50bb7a5b1..ee35511b2 100644
--- a/bindings/ocaml/src/operator/_type.rs
+++ b/bindings/ocaml/src/operator/operator_info.rs
@@ -15,22 +15,22 @@
// specific language governing permissions and limitations
// under the License.
-// For ocaml-rs, the build order in the same build group is the order of the file names.
-// In order to use the type in the function in the generated ocaml file, it must be defined on the first generated file.
use super::*;
-#[ocaml::sig]
-pub struct Operator(pub(crate) od::blocking::Operator);
-ocaml::custom!(Operator);
+#[ocaml::func]
+#[ocaml::sig("operator_info -> string ")]
+pub fn operator_info_name(info: &OperatorInfo) -> String {
+ info.0.name().to_string()
+}
-#[ocaml::sig]
-pub struct Reader(pub(crate) od::blocking::Reader);
-ocaml::custom!(Reader);
+#[ocaml::func]
+#[ocaml::sig("operator_info -> string ")]
+pub fn operator_info_scheme(info: &OperatorInfo) -> String {
+ info.0.scheme().to_string()
+}
-#[ocaml::sig]
-pub struct Metadata(pub(crate) od::Metadata);
-ocaml::custom!(Metadata);
-
-#[ocaml::sig]
-pub struct Entry(pub(crate) od::Entry);
-ocaml::custom!(Entry);
+#[ocaml::func]
+#[ocaml::sig("operator_info -> string ")]
+pub fn operator_info_root(info: &OperatorInfo) -> String {
+ info.0.root().to_string()
+}
diff --git a/bindings/ocaml/src/operator/_type.rs b/bindings/ocaml/src/operator/writer.rs
similarity index 59%
copy from bindings/ocaml/src/operator/_type.rs
copy to bindings/ocaml/src/operator/writer.rs
index 50bb7a5b1..e78793bc1 100644
--- a/bindings/ocaml/src/operator/_type.rs
+++ b/bindings/ocaml/src/operator/writer.rs
@@ -15,22 +15,16 @@
// specific language governing permissions and limitations
// under the License.
-// For ocaml-rs, the build order in the same build group is the order of the file names.
-// In order to use the type in the function in the generated ocaml file, it must be defined on the first generated file.
use super::*;
-#[ocaml::sig]
-pub struct Operator(pub(crate) od::blocking::Operator);
-ocaml::custom!(Operator);
+#[ocaml::func]
+#[ocaml::sig("writer -> bytes -> (unit, string) Result.t ")]
+pub fn writer_write(writer: &mut Writer, bs: &'static [u8]) -> Result<(), String> {
+ map_res_error(writer.0.write(bs))
+}
-#[ocaml::sig]
-pub struct Reader(pub(crate) od::blocking::Reader);
-ocaml::custom!(Reader);
-
-#[ocaml::sig]
-pub struct Metadata(pub(crate) od::Metadata);
-ocaml::custom!(Metadata);
-
-#[ocaml::sig]
-pub struct Entry(pub(crate) od::Entry);
-ocaml::custom!(Entry);
+#[ocaml::func]
+#[ocaml::sig("writer -> (metadata, string) Result.t ")]
+pub fn writer_close(writer: &mut Writer) -> Result<ocaml::Pointer<Metadata>, String> {
+ map_res_error(writer.0.close()).map(|m| Metadata(m).into())
+}
diff --git a/bindings/ocaml/test/test.ml b/bindings/ocaml/test/test.ml
index 8e0f12fbd..909fe9915 100644
--- a/bindings/ocaml/test/test.ml
+++ b/bindings/ocaml/test/test.ml
@@ -28,8 +28,10 @@ let new_test_block_operator test_ctxt : operator =
let cfgs = [ ("root", bracket_tmpdir test_ctxt) ] in
test_check_result (Operator.new_operator "fs" cfgs)
+(** Basic operator creation tests *)
let test_new_block_operator _ = ignore new_test_block_operator
+(** Test directory creation and recursive removal *)
let test_create_dir_and_remove_all test_ctxt =
let bo = new_test_block_operator test_ctxt in
ignore (test_check_result (Operator.create_dir bo "/testdir/"));
@@ -41,6 +43,7 @@ let test_create_dir_and_remove_all test_ctxt =
(Operator.write bo "/testdir/bar" (Bytes.of_string "foo")));
ignore (test_check_result (Operator.remove_all bo "/testdir/"))
+(** Test basic write and read operations *)
let test_block_write_and_read test_ctxt =
let bo = new_test_block_operator test_ctxt in
ignore
@@ -50,6 +53,7 @@ let test_block_write_and_read test_ctxt =
assert_equal "helloworld"
(data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string)
+(** Test reader functionality *)
let test_operator_reader test_ctxt =
let bo = new_test_block_operator test_ctxt in
ignore
@@ -61,6 +65,24 @@ let test_operator_reader test_ctxt =
assert_equal 5 i;
assert_equal "world" (Bytes.to_string data)
+(** Test writer functionality *)
+let test_operator_writer test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+ let writer = Operator.writer bo "test_writer_file" |> test_check_result in
+ ignore
+ (test_check_result
+ (Operator.Writer.write writer (Bytes.of_string "Hello ")));
+ ignore
+ (test_check_result
+ (Operator.Writer.write writer (Bytes.of_string "World!")));
+ ignore (test_check_result (Operator.Writer.close writer));
+
+ (* Verify the content was written correctly *)
+ let data = test_check_result (Operator.read bo "test_writer_file") in
+ assert_equal "Hello World!"
+ (data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string)
+
+(** Test metadata functionality *)
let test_operator_stat test_ctxt =
let bo = new_test_block_operator test_ctxt in
ignore
@@ -72,6 +94,7 @@ let test_operator_stat test_ctxt =
assert_equal 10L (Operator.Metadata.content_length metadata);
()
+(** Test list functionality *)
let test_list test_ctxt =
let bo = new_test_block_operator test_ctxt in
ignore (test_check_result (Operator.create_dir bo "/testdir/"));
@@ -89,15 +112,274 @@ let test_list test_ctxt =
assert_equal 3 (Array.length array);
()
+(** Test streaming lister functionality *)
+let test_lister test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+ ignore (test_check_result (Operator.create_dir bo "/testdir/"));
+ ignore
+ (test_check_result
+ (Operator.write bo "/testdir/file1" (Bytes.of_string "content1")));
+ ignore
+ (test_check_result
+ (Operator.write bo "/testdir/file2" (Bytes.of_string "content2")));
+
+ let lister = Operator.lister bo "testdir/" |> test_check_result in
+ let entries = ref [] in
+
+ (* Collect all entries using the lister *)
+ let rec collect_entries () =
+ match Operator.Lister.next lister with
+ | Ok (Some entry) ->
+ entries := Operator.Entry.name entry :: !entries;
+ collect_entries ()
+ | Ok None -> ()
+ | Error err -> assert_failure err
+ in
+ collect_entries ();
+
+ let actual = Array.of_list (List.rev !entries) in
+ let expected = [| "testdir/"; "file1"; "file2" |] in
+ List.iter (Array.sort compare) [ expected; actual ];
+ assert_equal expected actual;
+ assert_equal 3 (Array.length actual)
+
+(** Test operator info functionality *)
+let test_operator_info test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+ let info = Operator.info bo in
+
+ (* Test basic info properties *)
+ let scheme = Operator.OperatorInfo.scheme info in
+ assert_equal "fs" scheme;
+
+ let root = Operator.OperatorInfo.root info in
+ assert_bool "Root should not be empty" (String.length root > 0);
+
+ (* Test capability information *)
+ let cap = Operator.OperatorInfo.capability info in
+ assert_equal true (Operator.Capability.read cap);
+ assert_equal true (Operator.Capability.write cap);
+ assert_equal true (Operator.Capability.stat cap);
+ assert_equal true (Operator.Capability.list cap);
+ assert_equal true (Operator.Capability.create_dir cap);
+ assert_equal true (Operator.Capability.delete cap);
+
+ (* Filesystem should support these operations *)
+ assert_equal true (Operator.Capability.copy cap);
+ assert_equal true (Operator.Capability.rename cap)
+
+(** Test file existence checking *)
+let test_is_exist test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Test non-existent file *)
+ let exists = Operator.is_exist bo "nonexistent.txt" |> test_check_result in
+ assert_equal false exists;
+
+ (* Create a file and test it exists *)
+ ignore
+ (test_check_result
+ (Operator.write bo "testfile.txt" (Bytes.of_string "test")));
+ let exists = Operator.is_exist bo "testfile.txt" |> test_check_result in
+ assert_equal true exists
+
+(** Test file copy operation *)
+let test_copy test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Create source file *)
+ let content = "Hello, copy test!" in
+ ignore
+ (test_check_result
+ (Operator.write bo "source.txt" (Bytes.of_string content)));
+
+ (* Copy the file *)
+ ignore (test_check_result (Operator.copy bo "source.txt" "destination.txt"));
+
+ (* Verify both files exist and have same content *)
+ let source_data = test_check_result (Operator.read bo "source.txt") in
+ let dest_data = test_check_result (Operator.read bo "destination.txt") in
+
+ let source_str =
+ source_data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string
+ in
+ let dest_str = dest_data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
+
+ assert_equal content source_str;
+ assert_equal content dest_str
+
+(** Test file rename operation *)
+let test_rename test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Create source file *)
+ let content = "Hello, rename test!" in
+ ignore
+ (test_check_result
+ (Operator.write bo "old_name.txt" (Bytes.of_string content)));
+
+ (* Rename the file *)
+ ignore (test_check_result (Operator.rename bo "old_name.txt" "new_name.txt"));
+
+ (* Verify old file doesn't exist and new file has correct content *)
+ let old_exists = Operator.is_exist bo "old_name.txt" |> test_check_result in
+ let new_exists = Operator.is_exist bo "new_name.txt" |> test_check_result in
+
+ assert_equal false old_exists;
+ assert_equal true new_exists;
+
+ let new_data = test_check_result (Operator.read bo "new_name.txt") in
+ let new_str = new_data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
+ assert_equal content new_str
+
+(** Test delete operation *)
+let test_delete test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Create a file *)
+ ignore
+ (test_check_result
+ (Operator.write bo "to_delete.txt" (Bytes.of_string "delete me")));
+
+ (* Verify it exists *)
+ let exists_before =
+ Operator.is_exist bo "to_delete.txt" |> test_check_result
+ in
+ assert_equal true exists_before;
+
+ (* Delete it *)
+ ignore (test_check_result (Operator.delete bo "to_delete.txt"));
+
+ (* Verify it's gone *)
+ let exists_after =
+ Operator.is_exist bo "to_delete.txt" |> test_check_result
+ in
+ assert_equal false exists_after
+
+(** Test remove (batch delete) operation *)
+let test_remove test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Create multiple files *)
+ let files = [| "file1.txt"; "file2.txt"; "file3.txt" |] in
+ Array.iter
+ (fun file ->
+ ignore
+ (test_check_result
+ (Operator.write bo file (Bytes.of_string ("content of " ^ file)))))
+ files;
+
+ (* Verify all files exist *)
+ Array.iter
+ (fun file ->
+ let exists = Operator.is_exist bo file |> test_check_result in
+ assert_equal true exists)
+ files;
+
+ (* Remove all files *)
+ ignore (test_check_result (Operator.remove bo files));
+
+ (* Verify all files are gone *)
+ Array.iter
+ (fun file ->
+ let exists = Operator.is_exist bo file |> test_check_result in
+ assert_equal false exists)
+ files
+
+(** Test operator check functionality *)
+let test_check test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Check should succeed for a valid operator *)
+ match Operator.check bo with
+ | Ok () -> () (* Success is expected *)
+ | Error err -> assert_failure ("Check failed: " ^ err)
+
+(** Test multiple operations together *)
+let test_complex_workflow test_ctxt =
+ let bo = new_test_block_operator test_ctxt in
+
+ (* Create directory structure *)
+ ignore (test_check_result (Operator.create_dir bo "project/"));
+ ignore (test_check_result (Operator.create_dir bo "project/src/"));
+ ignore (test_check_result (Operator.create_dir bo "project/docs/"));
+
+ (* Create some files using writer *)
+ let readme_writer =
+ Operator.writer bo "project/README.md" |> test_check_result
+ in
+ ignore
+ (test_check_result
+ (Operator.Writer.write readme_writer (Bytes.of_string "# My Project\n")));
+ ignore
+ (test_check_result
+ (Operator.Writer.write readme_writer
+ (Bytes.of_string "This is a test project.\n")));
+ ignore (test_check_result (Operator.Writer.close readme_writer));
+
+ (* Create a source file *)
+ ignore
+ (test_check_result
+ (Operator.write bo "project/src/main.ml"
+ (Bytes.of_string "let () = print_endline \"Hello, World!\"")));
+
+ (* List the project directory *)
+ let entries = Operator.list bo "project/" |> test_check_result in
+ assert_equal 4 (Array.length entries);
+
+ (* project/, src/, docs/, README.md *)
+
+ (* Verify file contents *)
+ let readme_data = test_check_result (Operator.read bo "project/README.md") in
+ let readme_str =
+ readme_data |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string
+ in
+ let contains_substring s sub =
+ let len_s = String.length s in
+ let len_sub = String.length sub in
+ let rec check i =
+ if i > len_s - len_sub then false
+ else if String.sub s i len_sub = sub then true
+ else check (i + 1)
+ in
+ if len_sub = 0 then true else check 0
+ in
+ assert_bool "README should contain project info"
+ (contains_substring readme_str "My Project");
+
+ (* Copy the main file *)
+ ignore
+ (test_check_result
+ (Operator.copy bo "project/src/main.ml" "project/src/backup.ml"));
+
+ (* Check capabilities *)
+ let info = Operator.info bo in
+ let cap = Operator.OperatorInfo.capability info in
+ assert_equal true (Operator.Capability.write cap);
+ assert_equal true (Operator.Capability.copy cap);
+
+ (* Clean up *)
+ ignore (test_check_result (Operator.remove_all bo "project/"))
+
let suite =
- "suite"
+ "OCaml OpenDAL Test Suite"
>::: [
"test_new_block_operator" >:: test_new_block_operator;
"test_create_dir_and_remove_all" >:: test_create_dir_and_remove_all;
"test_block_write_and_read" >:: test_block_write_and_read;
"test_operator_reader" >:: test_operator_reader;
+ "test_operator_writer" >:: test_operator_writer;
"test_operator_stat" >:: test_operator_stat;
"test_list" >:: test_list;
+ "test_lister" >:: test_lister;
+ "test_operator_info" >:: test_operator_info;
+ "test_is_exist" >:: test_is_exist;
+ "test_copy" >:: test_copy;
+ "test_rename" >:: test_rename;
+ "test_delete" >:: test_delete;
+ "test_remove" >:: test_remove;
+ "test_check" >:: test_check;
+ "test_complex_workflow" >:: test_complex_workflow;
]
let () = run_test_tt_main suite