diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14d89db..ec31fb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,14 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install erlang - run: sudo apt update && sudo apt install -y erlang + - uses: erlef/setup-beam@v1 + with: + otp-version: "28.0.2" + rebar3-version: "3.24.0" + - name: Verify OTP + run: erl -noshell -eval 'io:format("~s~n",[erlang:system_info(otp_release)]), halt().' - name: Install mond - run: cargo install --git https://github.com/benjaminjellis/mond.git --tag 0.0.9 bahn + run: cargo install --git https://github.com/benjaminjellis/mond.git --tag 0.0.12 bahn - name: Check formatting run: bahn format --check - name: Build diff --git a/bahn.toml b/bahn.toml index 97d62ce..7e6749a 100644 --- a/bahn.toml +++ b/bahn.toml @@ -1,6 +1,6 @@ [package] name = "std" -version = "0.0.7" +version = "0.0.8" min_mond_version = "0.0.7" [dependencies] diff --git a/src/bit_array.mond b/src/bit_array.mond new file mode 100644 index 0000000..a88b0f9 --- /dev/null +++ b/src/bit_array.mond @@ -0,0 +1,68 @@ +(use result [Result]) +(use order [Order]) + +;;; BitArray is a sequence of binary data of any length. +(pub extern type BitArray) + +;;; Converts a UTF-8 String into a BitArray. +(pub extern let from_string ~ (String -> BitArray) mond_bit_array_helpers/identity) + +;;; Returns the number of bits in a bit array. +(pub extern let bit_size ~ (BitArray -> Int) erlang/bit_size) + +;;; Returns the number of bytes in a bit array. +(pub extern let byte_size ~ (BitArray -> Int) erlang/byte_size) + +;;; Pads a bit array with zeros so that it is a whole number of bytes. +(pub extern let pad_to_bytes ~ (BitArray -> BitArray) mond_bit_array_helpers/pad_to_bytes) + +;;; Creates a new bit array by joining two bit arrays. +(pub let append {first second} + (concat [first second])) + +;;; Extracts a sub-section of a bit array. +;;; Position and length are in bits. +;;; Negative length slices relative to the end of the input. +(pub extern let slice ~ (BitArray -> Int -> Int -> Result BitArray Unit) mond_bit_array_helpers/slice) + +;;; Tests whether a bit array is valid UTF-8. +(pub let is_utf8 {bits} + (match (to_string bits) + (Ok _) ~> True + (Error _) ~> False)) + +;;; Converts a bit array to a UTF-8 string. +;;; Returns Error Unit for invalid UTF-8. +(pub extern let to_string ~ (BitArray -> Result String Unit) mond_bit_array_helpers/bit_array_to_string) + +;;; Creates a new bit array by joining multiple bit arrays. +(pub extern let concat ~ ((List BitArray) -> BitArray) mond_bit_array_helpers/concat) + +;;; Encodes a BitArray into base64. +;;; If input is not byte-aligned it is padded with zero bits first. +(pub extern let base64_encode ~ (BitArray -> Bool -> String) mond_bit_array_helpers/base64_encode) + +;;; Decodes a base64 string into a BitArray. +(pub extern let base64_decode ~ (String -> Result BitArray Unit) mond_bit_array_helpers/base64_decode) + +;;; Encodes a BitArray into URL-safe base64. +(pub extern let base64_url_encode ~ (BitArray -> Bool -> String) mond_bit_array_helpers/base64_url_encode) + +;;; Decodes a URL-safe base64 string into a BitArray. +(pub extern let base64_url_decode ~ (String -> Result BitArray Unit) mond_bit_array_helpers/base64_url_decode) + +;;; Encodes a BitArray into lowercase base16. +;;; If input is not byte-aligned it is padded with zero bits first. +(pub extern let base16_encode ~ (BitArray -> String) mond_bit_array_helpers/base16_encode) + +;;; Decodes a base16 string into a BitArray. +(pub extern let base16_decode ~ (String -> Result BitArray Unit) mond_bit_array_helpers/base16_decode) + +;;; Converts a BitArray to printable decimal-byte syntax. +(pub extern let inspect ~ (BitArray -> String) mond_bit_array_helpers/inspect) + +;;; Compare two bit arrays as sequences of bytes. +(pub extern let compare ~ (BitArray -> BitArray -> Order) mond_bit_array_helpers/compare) + +;;; Checks whether the first BitArray starts with the second one. +(pub extern let starts_with ~ (BitArray -> BitArray -> Bool) mond_bit_array_helpers/starts_with) diff --git a/src/filepath.mond b/src/filepath.mond new file mode 100644 index 0000000..9d3e036 --- /dev/null +++ b/src/filepath.mond @@ -0,0 +1,85 @@ +(use string) +(use list) +(use result [Result]) +(use option [Option]) +(pub extern let is_windows ~ (Unit -> Bool) mond_filepath_helpers/is_windows) + +(let relative {path} + (match (= (string/slice path 0 1) "/") + True ~> (relative (string/drop_start path 1)) + False ~> path)) + +(pub let join {left right} + (remove_trailing_slash + (match left right + _ "/" ~> left + "" _ ~> (relative right) + "/" candidate if (= (string/slice candidate 0 1) "/") ~> candidate + "/" _ ~> (string/concat left right) + _ _ ~> (string/concat + (string/concat (remove_trailing_slash left) "/") + (relative right))))) + +(pub let remove_trailing_slash {path} + (match (string/ends_with path "/") + True ~> (string/drop_end path 1) + False ~> path)) + +(pub let split_unix {path} + (|> (match (string/split path "/") + [""] ~> [] + ["" | rest] ~> (list/append ["/"] rest) + rest ~> rest) + (list/filter (f {x} -> (!= x ""))))) + +;;; Warning there is currently no windows support +(pub let split {path} + (split_unix path)) + +;;; Get the base name of a path, i.e. the last path segment. +;;; Returns an empty string for the root path "/". +(pub let base_name {path} + (match (= path "/") + True ~> "" + False ~> (let [segments (split path) + last_index (- (list/length segments) 1)] + (match (list/nth last_index segments) + (Some name) ~> name + None ~> "")))) + +(pub let extension {path} + (let [file (base_name path)] + (match (string/split file ".") + ["" _] ~> (Error ()) + [_ ext] ~> (Ok ext) + [_ | rest] ~> (list/last rest) + _ ~> (Error ())))) + +(pub let strip_extension {path} + (match (extension path) + (Ok ext) ~> (string/drop_end path (+ (string/length ext) 1)) + (Error _) ~> path)) + +(let get_directory_name {path acc segment} + (match path + ["/" | rest] ~> (get_directory_name rest (string/concat acc segment) "/") + [first | rest] ~> (get_directory_name + rest + acc + (string/concat segment first)) + [] ~> acc)) + +(pub let directory_name {path} + (let [path (remove_trailing_slash path) + graphemes (string/to_graphemes path)] + (match graphemes + ["/" | rest] ~> (get_directory_name rest "/" "") + _ ~> (get_directory_name graphemes "" "")))) + +;; fn permission_to_integer(permission: Permission) -> Int { +;; case permission { +;; Read -> 0o4 +;; Write -> 0o2 +;; Execute -> 0o1 +;; } +;; } diff --git a/src/fs.mond b/src/fs.mond new file mode 100644 index 0000000..7564a3d --- /dev/null +++ b/src/fs.mond @@ -0,0 +1,404 @@ +(use filepath) +(use bit_array) +(use bit_array [BitArray]) +(use list) +(use string) +(use result [Result]) +(use int) +(use set [Set]) +(use set) + +;;; This type represents all of the reasons for why a file system operation could fail. +(pub type FileError + [Eacces + Eagain + Ebadf + Ebadmsg + Ebusy + Edeadlk + Edeadlock + Edquot + Eexist + Efault + Efbig + Eftype + Eintr + Einval + Eio + Eisdir + Eloop + Emfile + Emlink + Emultihop + Enametoolong + Enfile + Enobufs + Enodev + Enolck + Enolink + Enoent + Enomem + Enospc + Enosr + Enostr + Enosys + Enotblk + Enotdir + Enotsup + Enxio + Eopnotsupp + Eoverflow + Eperm + Epipe + Erange + Erofs + Espipe + Esrch + Estale + Etxtbsy + Exdev + NotUtf8 + (Unknown ~ String)]) + +;;; Convert an error into a human-readable description. +(pub let describe_error {error} + (match error + Eperm ~> "Operation not permitted" + Enoent ~> "No such file or directory" + Esrch ~> "No such process" + Eintr ~> "Interrupted system call" + Eio ~> "Input/output error" + Enxio ~> "Device not configured" + Ebadf ~> "Bad file descriptor" + Edeadlk ~> "Resource deadlock avoided" + Edeadlock ~> "Resource deadlock avoided" + Enomem ~> "Cannot allocate memory" + Eacces ~> "Permission denied" + Efault ~> "Bad address" + Enotblk ~> "Block device required" + Ebusy ~> "Resource busy" + Eexist ~> "File exists" + Exdev ~> "Cross-device link" + Enodev ~> "Operation not supported by device" + Enotdir ~> "Not a directory" + Eisdir ~> "Is a directory" + Einval ~> "Invalid argument" + Enfile ~> "Too many open files in system" + Emfile ~> "Too many open files" + Etxtbsy ~> "Text file busy" + Efbig ~> "File too large" + Enospc ~> "No space left on device" + Espipe ~> "Illegal seek" + Erofs ~> "Read-only file system" + Emlink ~> "Too many links" + Epipe ~> "Broken pipe" + Erange ~> "Result too large" + Eagain ~> "Resource temporarily unavailable" + Enotsup ~> "Operation not supported" + Enobufs ~> "No buffer space available" + Eloop ~> "Too many levels of symbolic links" + Enametoolong ~> "File name too long" + Edquot ~> "Disc quota exceeded" + Estale ~> "Stale NFS file handle" + Enolck ~> "No locks available" + Enosys ~> "Function not implemented" + Eftype ~> "Inappropriate file type or format" + Eoverflow ~> "Value too large to be stored in data type" + Ebadmsg ~> "Bad message" + Emultihop ~> "Multihop attempted" + Enolink ~> "Link has been severed" + Enosr ~> "No STREAM resources" + Enostr ~> "Not a STREAM" + Eopnotsupp ~> "Operation not supported on socket" + NotUtf8 ~> "File not UTF-8 encoded" + (Unknown inner) ~> (string/concat "Unknown error: " inner))) + +;;; Represents file information from read_file_info/stat. +(pub type FileInfo + [(:size ~ Int) + (:mode ~ Int) + (:nlink ~ Int) + (:inode ~ Int) + (:user_id ~ Int) + (:group_id ~ Int) + (:dev ~ Int) + (:atime_seconds ~ Int) + (:mtime_seconds ~ Int) + (:ctime_seconds ~ Int)]) + +;;; Represents a file permission. +(pub type Permission + [Read + Write + Execute]) + +;;; Represents a set of file permissions for a given file. +(pub type FilePermissions + [(:user ~ Set Permission) + (:group ~ Set Permission) + (:other ~ Set Permission)]) + +;;; An enumeration of different types of files. +(pub type FileType + [File + Directory + Symlink + Other]) + +(let permission_digit_from_list {permissions} + (match permissions + [] ~> 0 + [permission | rest] ~> (+ + (permission_to_integer permission) + (permission_digit_from_list rest)))) + +(let make_permission_digit {permissions} + (permission_digit_from_list (set/to_list permissions))) + +(let integer_to_permissions {integer} + (match (int/bitwise_and integer 7) + 7 ~> (set/from_list [Read Write Execute]) + 6 ~> (set/from_list [Read Write]) + 5 ~> (set/from_list [Read Execute]) + 4 ~> (set/from_list [Read]) + 3 ~> (set/from_list [Write Execute]) + 2 ~> (set/from_list [Write]) + 1 ~> (set/from_list [Execute]) + 0 ~> (set/new) + ;; This branch is unreachable due to the bitwise mask, but Mond has no panic. + _ ~> (set/new))) + +;; NOTE: this uses decimal Ints (Mond currently has no octal literal syntax). +(pub let permission_to_integer {permission} + (match permission + Read ~> 4 + Write ~> 2 + Execute ~> 1)) + +(let octal_to_file_permissions {octal} + (FilePermissions + :user (integer_to_permissions (int/bitwise_shift_right octal 6)) + :group (integer_to_permissions (int/bitwise_shift_right octal 3)) + :other (integer_to_permissions octal))) + +;;; Extract the file permissions from a given FileInfo value in octal representation. +(pub let file_info_permissions_octal {info} + ;; NOTE: 0o777 = 511 + (int/bitwise_and (:mode info) 511)) + +;;; Extract the FilePermissions from a given FileInfo value. +(pub let file_info_permissions {info} + (octal_to_file_permissions (file_info_permissions_octal info))) + +;;; Convert FilePermissions to their octal integer representation. +(pub let file_permissions_to_octal {permissions} + (+ + (+ + (* (make_permission_digit (:user permissions)) 64) + (* (make_permission_digit (:group permissions)) 8)) + (make_permission_digit (:other permissions)))) + +;;; Extract the file type from a given FileInfo value. +(pub let file_info_type {info} + ;; S_IFMT and related constants. + ;; 0o170000 = 61440, 0o100000 = 32768, 0o40000 = 16384, 0o120000 = 40960 + (match (int/bitwise_and (:mode info) 61440) + 32768 ~> File + 16384 ~> Directory + 40960 ~> Symlink + _ ~> Other)) + +;;; Get information about a file. Follows symlinks. +(pub extern let file_info ~ (String -> Result FileInfo FileError) mond_fs_helpers/file_info) + +;;; Get information about a symlink itself. +(pub extern let link_info ~ (String -> Result FileInfo FileError) mond_fs_helpers/link_info) + +;;; Read a file's contents as a string. +(pub let read {filepath} + (match (read_bits filepath) + (Ok bits) ~> + (match (bit_array/to_string bits) + (Ok str) ~> (Ok str) + (Error _) ~> (Error NotUtf8)) + (Error err) ~> (Error err))) + +;;; Write a string to a file. +(pub let write {filepath contents} + (write_bits filepath (bit_array/from_string contents))) + +;;; Delete a file or directory recursively. +(pub extern let delete ~ (String -> Result Unit FileError) mond_fs_helpers/delete) + +;;; Delete a single file. +(pub extern let delete_file ~ (String -> Result Unit FileError) mond_fs_helpers/delete_file) + +;;; Delete all files/directories in a list of paths, ignoring missing paths. +(pub let delete_all {paths} + (match paths + [] ~> (Ok ()) + [path | rest] ~> + (match (delete path) + (Ok _) ~> (delete_all rest) + (Error Enoent) ~> (delete_all rest) + (Error err) ~> (Error err)))) + +;;; Append a string to a file. +(pub let append {filepath contents} + (append_bits filepath (bit_array/from_string contents))) + +;;; Read a file's contents as a bit-array. +(pub extern let read_bits ~ (String -> Result BitArray FileError) mond_fs_helpers/read_bits) + +;;; Write a bit-array to a file. +(pub extern let write_bits ~ (String -> BitArray -> Result Unit FileError) mond_fs_helpers/write_bits) + +;;; Append a bit-array to a file. +(pub extern let append_bits ~ (String -> BitArray -> Result Unit FileError) mond_fs_helpers/append_bits) + +;;; Checks if the path exists and is a directory. +(pub extern let is_directory ~ (String -> Result Bool FileError) mond_fs_helpers/is_directory) + +;;; Create a directory at a path. +(pub extern let create_directory ~ (String -> Result Unit FileError) mond_fs_helpers/create_directory) + +;;; Create a symbolic link called `symlink` pointing to `target`. +(pub extern let create_symlink ~ (String -> String -> Result Unit FileError) mond_fs_helpers/create_symlink) + +;;; Create a hard link called `link` pointing to `target`. +(pub extern let create_link ~ (String -> String -> Result Unit FileError) mond_fs_helpers/create_link) + +;;; List directory contents (non-recursive). +(pub extern let read_directory ~ (String -> Result (List String) FileError) mond_fs_helpers/read_directory) + +;;; Checks if the path exists and is a file. +(pub extern let is_file ~ (String -> Result Bool FileError) mond_fs_helpers/is_file) + +;;; Checks if the path exists and is a symbolic link. +(pub extern let is_symlink ~ (String -> Result Bool FileError) mond_fs_helpers/is_symlink) + +;;; Create an empty file. Returns Error Eexist if file/directory already exists. +(pub let create_file {filepath} + (match (is_file filepath) + (Ok True) ~> (Error Eexist) + (Ok False) ~> + (match (is_directory filepath) + (Ok True) ~> (Error Eexist) + (Ok False) ~> (write_bits filepath (bit_array/from_string "")) + (Error err) ~> (Error err)) + (Error err) ~> (Error err))) + +;;; Recursively creates directories for a given path. +(pub let create_directory_all {dirpath} + (do_create_dir_all (string/concat dirpath "/"))) + +(extern let do_create_dir_all ~ (String -> Result Unit FileError) mond_fs_helpers/create_dir_all) + +;;; Copy a file or directory to a new path. +(pub let copy {src dest} + (let? [src_info (file_info src)] + (match (file_info_type src_info) + File ~> (copy_file src dest) + Directory ~> (copy_directory src dest) + Symlink ~> (Error + (Unknown + "Internal bug: file_info returned symlink info unexpectedly.")) + Other ~> (Error + (Unknown "Unknown file type (not file, directory, or symlink)"))))) + +;;; Copy a file from src to dest. +(pub let copy_file {src dest} + (match (do_copy_file src dest) + (Ok _) ~> (Ok ()) + (Error err) ~> (Error err))) + +(extern let do_copy_file ~ (String -> String -> Result Int FileError) mond_fs_helpers/copy_file) + +;;; Rename/move a file or directory. +(pub extern let rename_file ~ (String -> String -> Result Unit FileError) mond_fs_helpers/rename_file) + +;;; Rename/move a file or directory. +(pub let rename {src dest} + (rename_file src dest)) + +(let copy_directory_segments {src dest segments} + (match segments + [] ~> (Ok ()) + [segment | rest] ~> (let [src_path (filepath/join src segment) + dest_path (filepath/join dest segment)] + (let? [src_info (file_info src_path)] + (match (file_info_type src_info) + File ~> (let? [content (read_bits src_path) + _ (write_bits dest_path content)] + (copy_directory_segments src dest rest)) + Directory ~> (let? [_ (create_directory dest_path) + _ (do_copy_directory + src_path + dest_path)] + (copy_directory_segments + src + dest + rest)) + Symlink ~> (Error + (Unknown + "Internal bug: file_info returned symlink info unexpectedly.")) + Other ~> (Error + (Unknown + "Unknown file type (not file, directory, or symlink)"))))))) + +;;; Copy a directory recursively. +(pub let copy_directory {src dest} + (let? [_ (create_directory_all dest)] + (do_copy_directory src dest))) + +(let do_copy_directory {src dest} + (let? [segments (read_directory src)] + (copy_directory_segments src dest segments))) + +;;; Copy a directory recursively and then delete the old one. +(pub let rename_directory {src dest} + (let? [_ (copy_directory src dest)] + (delete src))) + +;;; Clear all contents from a directory, leaving the top-level directory in place. +(pub let clear_directory {path} + (let? [paths (read_directory path)] + (delete_all (list/map (f {segment} -> (filepath/join path segment)) paths)))) + +(let get_files_from_contents {directory contents acc} + (match contents + [] ~> (Ok acc) + [content | rest] ~> (let [path (filepath/join directory content)] + (let? [info (file_info path)] + (match (file_info_type info) + File ~> (get_files_from_contents + directory + rest + (list/append [path] acc)) + Directory ~> (let? [nested_files (get_files path)] + (get_files_from_contents + directory + rest + (list/append acc nested_files))) + _ ~> (get_files_from_contents directory rest acc)))))) + +;;; Returns a list of filepaths for every file in a directory, including nested files. +(pub let get_files {directory} + (let? [contents (read_directory directory)] + (get_files_from_contents directory contents []))) + +;;; Set permissions for a file. +(pub let set_permissions {filepath permissions} + (set_permissions_octal filepath (file_permissions_to_octal permissions))) + +;;; Set permissions for a file in octal integer representation. +(pub extern let set_permissions_octal ~ (String -> Int -> Result Unit FileError) mond_fs_helpers/set_permissions_octal) + +;;; Returns the current working directory. +(pub extern let current_directory ~ (Unit -> Result String FileError) mond_fs_helpers/current_directory) + +;;; Convert a relative path to an absolute path. +(pub let resolve {path} + (do_resolve path)) + +(extern let do_resolve ~ (String -> Result String FileError) mond_fs_helpers/resolve) diff --git a/src/int.mond b/src/int.mond new file mode 100644 index 0000000..583b75a --- /dev/null +++ b/src/int.mond @@ -0,0 +1,2 @@ +(pub extern let bitwise_and ~ (Int -> Int -> Int) erlang/band) +(pub extern let bitwise_shift_right ~ (Int -> Int -> Int) erlang/bsr) diff --git a/src/list.mond b/src/list.mond index b4426ac..e4eab30 100644 --- a/src/list.mond +++ b/src/list.mond @@ -1,5 +1,6 @@ ;;; list.mond — list helpers backed by Erlang lists (use option [Option]) +(use result [Result]) ;;; Returns the number of elements in the list. (pub extern let length ~ ((List 'a) -> Int) erlang/length) @@ -33,3 +34,11 @@ ;;; Flattens a list of lists into a single list. (pub extern let flatten ~ ((List (List 'a)) -> (List 'a)) lists/flatten) + +;;; Returns the last element in the given list. +;;; Returns `Error ()` if the list is empty. +(pub let last {list} + (match list + [] ~> (Error ()) + [last] ~> (Ok last) + [_ | t] ~> (last t))) diff --git a/src/map.mond b/src/map.mond index f2cf2cb..6d03a2b 100644 --- a/src/map.mond +++ b/src/map.mond @@ -25,6 +25,9 @@ ;;; Number of entries. (pub extern let size ~ (Map 'k 'v -> Int) maps/size) +;;; Return the list of keys in the map. +(pub extern let keys ~ (Map 'k 'v -> (List 'k)) maps/keys) + ;;; Result of a take operation: the updated map and the removed value (if present). (pub type ['k 'v] TakeResult [(:map ~ Map 'k 'v) diff --git a/src/mond_bit_array_helpers.erl b/src/mond_bit_array_helpers.erl new file mode 100644 index 0000000..be92ab6 --- /dev/null +++ b/src/mond_bit_array_helpers.erl @@ -0,0 +1,264 @@ +-module(mond_bit_array_helpers). + +-export([ + identity/1, + pad_to_bytes/1, + slice/3, + bit_array_to_string/1, + concat/1, + base64_encode/2, + base64_decode/1, + base64_url_encode/2, + base64_url_decode/1, + base16_encode/1, + base16_decode/1, + inspect/1, + compare/2, + bit_array_to_int_and_size/1, + starts_with/2 +]). + +identity(Value) -> + Value. + +pad_to_bytes(Bits) when is_bitstring(Bits) -> + case bit_size(Bits) rem 8 of + 0 -> + Bits; + Rem -> + Padding = 8 - Rem, + <> + end. + +slice(Bits, Position, Length) + when is_bitstring(Bits), is_integer(Position), is_integer(Length) -> + try + TotalSize = bit_size(Bits), + Start = if + Position < 0 -> TotalSize + Position; + true -> Position + end, + End = if + Length < 0 -> TotalSize + Length; + true -> Start + Length + end, + if + Start < 0 -> + {error, unit}; + End < Start -> + {error, unit}; + End > TotalSize -> + {error, unit}; + true -> + Take = End - Start, + <<_:Start/bitstring, Out:Take/bitstring, _/bitstring>> = Bits, + {ok, Out} + end + catch + _:_ -> + {error, unit} + end; +slice(_, _, _) -> + {error, unit}. + +bit_array_to_string(Bits) when is_binary(Bits) -> + try unicode:characters_to_binary(Bits, utf8, utf8) of + Utf8 when is_binary(Utf8) -> + {ok, Utf8} + catch + _:_ -> + {error, unit} + end; +bit_array_to_string(_) -> + {error, unit}. + +concat(BitArrays) when is_list(BitArrays) -> + lists:foldl( + fun(Bits, Acc) -> + <> + end, + <<>>, + BitArrays + ). + +base64_encode(Input, Padding) + when is_bitstring(Input), is_boolean(Padding) -> + Encoded = base64:encode(pad_to_bytes(Input)), + case Padding of + true -> Encoded; + false -> trim_base64_padding(Encoded) + end. + +base64_decode(Encoded) when is_binary(Encoded) -> + try + {ok, base64:decode(pad_base64(Encoded))} + catch + _:_ -> + {error, unit} + end; +base64_decode(_) -> + {error, unit}. + +base64_url_encode(Input, Padding) -> + Encoded = base64_encode(Input, Padding), + UrlA = binary:replace(Encoded, <<"+">>, <<"-">>, [global]), + binary:replace(UrlA, <<"/">>, <<"_">>, [global]). + +base64_url_decode(Encoded) when is_binary(Encoded) -> + StdA = binary:replace(Encoded, <<"-">>, <<"+">>, [global]), + StdB = binary:replace(StdA, <<"_">>, <<"/">>, [global]), + base64_decode(StdB); +base64_url_decode(_) -> + {error, unit}. + +base16_encode(Input) when is_bitstring(Input) -> + Padded = pad_to_bytes(Input), + << + <<(hex_digit(Byte bsr 4)), (hex_digit(Byte band 16#0F))>> + || <> <= Padded + >>. + +base16_decode(Input) when is_binary(Input) -> + decode_hex(Input, <<>>); +base16_decode(_) -> + {error, unit}. + +decode_hex(<<>>, Acc) -> + {ok, Acc}; +decode_hex(<<_OnlyNibble>>, _Acc) -> + {error, unit}; +decode_hex(<>, Acc) -> + case {hex_value(Hi), hex_value(Lo)} of + {{ok, H}, {ok, L}} -> + decode_hex(Rest, <>); + _ -> + {error, unit} + end. + +hex_digit(N) when N >= 0, N =< 9 -> + $0 + N; +hex_digit(N) -> + $a + (N - 10). + +hex_value(C) when C >= $0, C =< $9 -> + {ok, C - $0}; +hex_value(C) when C >= $a, C =< $f -> + {ok, C - $a + 10}; +hex_value(C) when C >= $A, C =< $F -> + {ok, C - $A + 10}; +hex_value(_) -> + error. + +inspect(Input) when is_bitstring(Input) -> + TotalBits = bit_size(Input), + FullBytes = TotalBits div 8, + RemBits = TotalBits rem 8, + Prefix = <<"<<">>, + Suffix = <<">>">>, + BytePart = inspect_bytes(Input, FullBytes), + case RemBits of + 0 -> + <>; + _ -> + <> = Input, + TailInt = bits_to_int(Tail), + TailBin = integer_to_binary(TailInt), + RemBin = integer_to_binary(RemBits), + case BytePart of + <<>> -> + <>; + _ -> + <> + end + end. + +inspect_bytes(_Bits, 0) -> + <<>>; +inspect_bytes(Bits, Count) -> + <> = Bits, + inspect_byte_list(binary_to_list(Bytes), <<>>). + +inspect_byte_list([], Acc) -> + Acc; +inspect_byte_list([Byte], Acc) -> + append_piece(Acc, integer_to_binary(Byte)); +inspect_byte_list([Byte | Rest], Acc) -> + Next = append_piece(Acc, integer_to_binary(Byte)), + inspect_byte_list(Rest, <>). + +append_piece(<<>>, Piece) -> + Piece; +append_piece(Acc, Piece) -> + <>. + +compare(A, B) when is_bitstring(A), is_bitstring(B) -> + compare_loop(A, B). + +compare_loop(<>, <>) -> + if + First > Second -> gt; + First < Second -> lt; + true -> compare_loop(FirstRest, SecondRest) + end; +compare_loop(<<>>, <<>>) -> + eq; +compare_loop(_, <<>>) -> + gt; +compare_loop(<<>>, _) -> + lt; +compare_loop(First, Second) -> + {AInt, ASize} = bit_array_to_int_and_size(First), + {BInt, BSize} = bit_array_to_int_and_size(Second), + if + AInt > BInt -> gt; + AInt < BInt -> lt; + ASize > BSize -> gt; + ASize < BSize -> lt; + true -> eq + end. + +bit_array_to_int_and_size(Bits) when is_bitstring(Bits) -> + {bits_to_int(Bits), bit_size(Bits)}. + +bits_to_int(Bits) -> + bits_to_int(Bits, 0). + +bits_to_int(<<>>, Acc) -> + Acc; +bits_to_int(<>, Acc) -> + bits_to_int(Rest, (Acc bsl 1) bor Bit). + +starts_with(Bits, Prefix) when is_bitstring(Bits), is_bitstring(Prefix) -> + PrefixSize = bit_size(Prefix), + BitsSize = bit_size(Bits), + if + PrefixSize > BitsSize -> + false; + true -> + <> = Bits, + Pref =:= Prefix + end. + +trim_base64_padding(Encoded) -> + trim_base64_padding(Encoded, byte_size(Encoded)). + +trim_base64_padding(_Encoded, 0) -> + <<>>; +trim_base64_padding(Encoded, Size) -> + case binary:last(Encoded) of + $= -> + trim_base64_padding(binary:part(Encoded, 0, Size - 1), Size - 1); + _ -> + Encoded + end. + +pad_base64(Encoded) -> + case byte_size(Encoded) rem 4 of + 0 -> Encoded; + N -> <> + end. + +padding_equals(0) -> + <<>>; +padding_equals(N) -> + <<$=, (padding_equals(N - 1))/binary>>. diff --git a/src/mond_filepath_helpers.erl b/src/mond_filepath_helpers.erl new file mode 100644 index 0000000..8fa783f --- /dev/null +++ b/src/mond_filepath_helpers.erl @@ -0,0 +1,9 @@ +-module(mond_filepath_helpers). + +-export([is_windows/0]). + +is_windows() -> + case os:type() of + {win32, _} -> true; + _ -> false + end. diff --git a/src/mond_fs_helpers.erl b/src/mond_fs_helpers.erl new file mode 100644 index 0000000..33ab150 --- /dev/null +++ b/src/mond_fs_helpers.erl @@ -0,0 +1,260 @@ +-module(mond_fs_helpers). + +-compile({no_auto_import,[link/2]}). + +-export([ + append_bits/2, + bit_array_to_string/1, + copy_file/2, + create_directory/1, + create_dir_all/1, + create_link/2, + create_symlink/2, + current_directory/0, + delete/1, + delete_file/1, + file_info/1, + identity/1, + is_directory/1, + is_file/1, + is_symlink/1, + link_info/1, + read_bits/1, + read_directory/1, + rename_file/2, + resolve/1, + set_permissions_octal/2, + write_bits/2 +]). + +-include_lib("kernel/include/file.hrl"). + +-define(is_posix_error(Error), + Error =:= eacces + orelse Error =:= eagain + orelse Error =:= ebadf + orelse Error =:= ebadmsg + orelse Error =:= ebusy + orelse Error =:= edeadlk + orelse Error =:= edeadlock + orelse Error =:= edquot + orelse Error =:= eexist + orelse Error =:= efault + orelse Error =:= efbig + orelse Error =:= eftype + orelse Error =:= eintr + orelse Error =:= einval + orelse Error =:= eio + orelse Error =:= eisdir + orelse Error =:= eloop + orelse Error =:= emfile + orelse Error =:= emlink + orelse Error =:= emultihop + orelse Error =:= enametoolong + orelse Error =:= enfile + orelse Error =:= enobufs + orelse Error =:= enodev + orelse Error =:= enolck + orelse Error =:= enolink + orelse Error =:= enoent + orelse Error =:= enomem + orelse Error =:= enospc + orelse Error =:= enosr + orelse Error =:= enostr + orelse Error =:= enosys + orelse Error =:= enotblk + orelse Error =:= enotdir + orelse Error =:= enotsup + orelse Error =:= enxio + orelse Error =:= eopnotsupp + orelse Error =:= eoverflow + orelse Error =:= eperm + orelse Error =:= epipe + orelse Error =:= erange + orelse Error =:= erofs + orelse Error =:= espipe + orelse Error =:= esrch + orelse Error =:= estale + orelse Error =:= etxtbsy + orelse Error =:= exdev). + +identity(Value) -> + Value. + +bit_array_to_string(Bits) when is_binary(Bits) -> + try unicode:characters_to_binary(Bits, utf8, utf8) of + Utf8 when is_binary(Utf8) -> + {ok, Utf8} + catch + _:_ -> + {error, unit} + end; +bit_array_to_string(_) -> + {error, unit}. + +read_bits(Filename) -> + posix_result(file:read_file(Filename)). + +write_bits(Filename, Contents) -> + case bit_size(Contents) rem 8 of + 0 -> posix_result(file:write_file(Filename, Contents, [raw])); + _ -> {error, einval} + end. + +append_bits(Filename, Contents) -> + case bit_size(Contents) rem 8 of + 0 -> posix_result(file:write_file(Filename, Contents, [append, raw])); + _ -> {error, einval} + end. + +delete_file(Filename) -> + posix_result(file:delete(Filename, [raw])). + +create_directory(Dir) -> + posix_result(file:make_dir(Dir)). + +create_symlink(Existing, New) -> + posix_result(file:make_symlink(Existing, New)). + +create_link(Existing, New) -> + posix_result(file:make_link(Existing, New)). + +read_directory(Dir) -> + case file:list_dir(Dir) of + {ok, Filenames} -> + {ok, [unicode:characters_to_binary(Filename) || Filename <- Filenames]}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +delete(Dir) -> + posix_result(file:del_dir_r(Dir)). + +create_dir_all(Filename) -> + posix_result(filelib:ensure_path(Filename)). + +rename_file(Source, Destination) -> + posix_result(file:rename(Source, Destination)). + +copy_file(Source, Destination) -> + posix_result(file:copy(Source, Destination)). + +set_permissions_octal(Filename, Permissions) -> + posix_result(file:change_mode(Filename, Permissions)). + +is_directory(Path) -> + case file:read_file_info(Path) of + {ok, FileInfo} -> + case FileInfo#file_info.type of + directory -> + {ok, true}; + _ -> + {ok, false} + end; + {error, enoent} -> + {ok, false}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +is_file(Path) -> + case file:read_file_info(Path) of + {ok, FileInfo} -> + case FileInfo#file_info.type of + regular -> + {ok, true}; + _ -> + {ok, false} + end; + {error, enoent} -> + {ok, false}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +is_symlink(Path) -> + case file:read_link_info(Path) of + {ok, FileInfo} -> + case FileInfo#file_info.type of + symlink -> + {ok, true}; + _ -> + {ok, false} + end; + {error, enoent} -> + {ok, false}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +file_info_result(Result) -> + case Result of + {ok, + {file_info, + Size, + _Type, + _Access, + Atime, + Mtime, + Ctime, + Mode, + Links, + MajorDevice, + _MinorDevice, + Inode, + Uid, + Gid}} -> + {ok, + {fileinfo, + Size, + Mode, + Links, + Inode, + Uid, + Gid, + MajorDevice, + Atime, + Mtime, + Ctime}}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +file_info(Filename) -> + file_info_result(file:read_file_info(Filename, [{time, posix}])). + +link_info(Filename) -> + file_info_result(file:read_link_info(Filename, [{time, posix}])). + +current_directory() -> + case file:get_cwd() of + {ok, Dir} -> + {ok, path_to_binary(Dir)}; + {error, Reason} -> + posix_result({error, Reason}) + end. + +resolve(Path) -> + {ok, path_to_binary(filename:absname(Path))}. + +path_to_binary(Path) when is_binary(Path) -> + Path; +path_to_binary(Path) when is_list(Path) -> + unicode:characters_to_binary(Path). + +reason_to_binary(Reason) when is_atom(Reason) -> + string:uppercase(atom_to_binary(Reason, utf8)); +reason_to_binary(Reason) -> + iolist_to_binary(io_lib:format("~p", [Reason])). + +posix_result(Result) -> + case Result of + ok -> + {ok, unit}; + {ok, Value} -> + {ok, Value}; + {error, Reason} when ?is_posix_error(Reason) -> + {error, Reason}; + {error, Reason} -> + {error, {unknown, reason_to_binary(Reason)}} + end. diff --git a/src/mond_string_helpers.erl b/src/mond_string_helpers.erl index 34fe427..2877735 100644 --- a/src/mond_string_helpers.erl +++ b/src/mond_string_helpers.erl @@ -1,5 +1,5 @@ -module(mond_string_helpers). --export([contains/2, concat/2, split/2]). +-export([contains/2, concat/2, split/2, string_ends_with/2, slice/3, to_graphemes/1]). contains(Haystack, Needle) -> string:find(Haystack, Needle) =/= nomatch. @@ -7,3 +7,25 @@ contains(Haystack, Needle) -> concat(A, B) -> <>. split(Str, Sep) -> string:split(Str, Sep, all). + +string_ends_with(_, <<>>) -> true; +string_ends_with(String, Suffix) when byte_size(Suffix) > byte_size(String) -> false; +string_ends_with(String, Suffix) -> + SuffixSize = byte_size(Suffix), + Suffix == binary_part(String, byte_size(String) - SuffixSize, SuffixSize). + +slice(String, Index, Length) -> + case string:slice(String, Index, Length) of + X when is_binary(X) -> X; + X when is_list(X) -> unicode:characters_to_binary(X) + end. + +to_graphemes(Str) -> + [grapheme_to_binary(G) || G <- string:to_graphemes(Str)]. + +grapheme_to_binary(G) when is_binary(G) -> + G; +grapheme_to_binary(G) when is_integer(G) -> + <>; +grapheme_to_binary(G) when is_list(G) -> + unicode:characters_to_binary(G). diff --git a/src/order.mond b/src/order.mond new file mode 100644 index 0000000..aefa26e --- /dev/null +++ b/src/order.mond @@ -0,0 +1,45 @@ +;;; Ordering relation used by comparison functions. +(pub type Order + [Lt + Eq + Gt]) + +;;; Invert an order (`Lt` <-> `Gt`, `Eq` unchanged). +(pub let negate {order} + (match order + Lt ~> Gt + Eq ~> Eq + Gt ~> Lt)) + +;;; Convert an order to -1/0/1. +(pub let to_int {order} + (match order + Lt ~> -1 + Eq ~> 0 + Gt ~> 1)) + +;;; Compare two Order values. +(pub let compare {a b} + (match a b + x y if (= x y) ~> Eq + Lt _ ~> Lt + Eq Gt ~> Lt + _ _ ~> Gt)) + +;;; Reverse an ordering function. +(pub let reverse {orderer} + (f {a b} -> (orderer b a))) + +;;; Return a fallback order when the first is Eq. +(pub let break_tie {order other} + (match order + Lt ~> order + Gt ~> order + Eq ~> other)) + +;;; Lazily compute a fallback order when the first is Eq. +(pub let lazy_break_tie {order comparison} + (match order + Lt ~> order + Gt ~> order + Eq ~> (comparison))) diff --git a/src/pair.mond b/src/pair.mond index 584e984..2109dc2 100644 --- a/src/pair.mond +++ b/src/pair.mond @@ -1,3 +1,27 @@ (pub type ['a 'b] Pair [(:first ~ 'a) (:second ~ 'b)]) + +;;; Returns the first element of a Pair +(pub let first {pair} + (:first pair)) + +;;; Returns the secomnd element of a Pair +(pub let second {pair} + (:second pair)) + +;;; Returns a new pair with the elements swapped. +(pub let swap {pair} + (Pair :first (:second pair) :second (:first pair))) + +;;; Returns a new pair with the first element having had `function` applied to +(pub let map_first {pair function} + (Pair :first (function (:first pair)) :second (:second pair))) + +;;; Returns a new pair with the second element having had `function` applied to +(pub let map_second {pair function} + (Pair :first (:first pair) :second (function (:second pair)))) + +;;; Create a new pair with first elemnt a and second element b +(pub let new {a b} + (Pair :first a :second b)) diff --git a/src/set.mond b/src/set.mond new file mode 100644 index 0000000..83f2554 --- /dev/null +++ b/src/set.mond @@ -0,0 +1,112 @@ +(use list) +(use map [Map]) +(use option [Option]) +(use pair [Pair]) + +;;; A set is a collection of unique members. +(pub type ['member] Set + [(:dict ~ (Map 'member Unit))]) + +;;; Create a new empty set. +(pub let new {} + (Set :dict (map/new))) + +;;; Number of members in a set. +(pub let size {set} + (map/size (:dict set))) + +;;; True if the set has no members. +(pub let is_empty {set} + (= set (new))) + +;;; Insert a member into a set. +(pub let insert {set member} + (Set :dict (map/put member () (:dict set)))) + +;;; True if a member exists in a set. +(pub let contains {set member} + (match (map/get member (:dict set)) + (Some _) ~> True + None ~> False)) + +;;; Delete a member from a set. +(pub let delete {set member} + (Set :dict (map/remove member (:dict set)))) + +;;; Convert a set to a list of members. +(pub let to_list {set} + (map/keys (:dict set))) + +;;; Create a set from a list. +(pub let from_list {members} + (list/foldl (f {member acc} -> (insert acc member)) (new) members)) + +;;; Fold over all members in a set. +(pub let fold {set initial reducer} + (list/foldl (f {member acc} -> (reducer acc member)) initial (to_list set))) + +;;; Keep only members that satisfy a predicate. +(pub let filter {set predicate} + (fold + set + (new) + (f {acc member} -> (if (predicate member) (insert acc member) acc)))) + +;;; Map members to a new set. +(pub let map {set fun} + (fold set (new) (f {acc member} -> (insert acc (fun member))))) + +;;; Remove all members present in the given list. +(pub let drop {set disallowed} + (list/foldl (f {member acc} -> (delete acc member)) set disallowed)) + +;;; Keep only members present in the given list. +(pub let take {set desired} + (list/foldl + (f {member acc} -> (if (contains set member) (insert acc member) acc)) + (new) + desired)) + +(let order {first second} + (if (> (size first) (size second)) + (Pair :first first :second second) + (Pair :first second :second first))) + +;;; Union of two sets. +(pub let union {first second} + (let [ordered (order first second) + larger (:first ordered) + smaller (:second ordered)] + (fold smaller larger insert))) + +;;; Intersection of two sets. +(pub let intersection {first second} + (let [ordered (order first second) + larger (:first ordered) + smaller (:second ordered)] + (take larger (to_list smaller)))) + +;;; Members in first set that are not in second set. +(pub let difference {first second} + (drop first (to_list second))) + +;;; True if first is a subset of second. +(pub let is_subset {first second} + (= (intersection first second) first)) + +;;; True if two sets have no shared members. +(pub let is_disjoint {first second} + (= (intersection first second) (new))) + +;;; Members present in either set, but not both. +(pub let symmetric_difference {first second} + (difference (union first second) (intersection first second))) + +;;; Call a function for each member. +(pub let each {set fun} + (fold + set + () + (f {nil member} -> + (do (fun member) + nil)))) diff --git a/src/string.mond b/src/string.mond index aa999e5..75ef268 100644 --- a/src/string.mond +++ b/src/string.mond @@ -25,3 +25,33 @@ ;;; Returns true if Needle is found anywhere within Haystack. (pub extern let contains ~ (String -> String -> Bool) mond_string_helpers/contains) + +;;; Checks whether the first `String` ends with the second one. +(pub extern let ends_with ~ (String -> String -> Bool) mond_string_helpers/string_ends_with) +(pub extern let grapheme_slice ~ (String -> Int -> Int -> String) mond_string_helpers/slice) +(pub extern let to_graphemes ~ (String -> (List String)) mond_string_helpers/to_graphemes) + +(pub let slice {string idx len} + (match (<= len 0) + True ~> "" + False ~> + (match (< idx 0) + True ~> (let [translated_idx (+ (length string) idx)] + (match (< translated_idx 0) + True ~> "" + False ~> (grapheme_slice string translated_idx len))) + False ~> (grapheme_slice string idx len)))) + +;;; Drops *n* graphemes from the end of a `String`. +;;; This function traverses the full string, so it runs in linear time with the +;;; size of the string. Avoid using this in a loop. +(pub let drop_end {from up_to} + (match (<= up_to 0) + True ~> from + False ~> (slice from 0 (- (length from) up_to)))) + +;;; Drops *n* graphemes from the start of a `String`. +(pub let drop_start {from up_to} + (match (<= up_to 0) + True ~> from + False ~> (slice from up_to (length from)))) diff --git a/tests/bit_array_test.mond b/tests/bit_array_test.mond new file mode 100644 index 0000000..18f5f4d --- /dev/null +++ b/tests/bit_array_test.mond @@ -0,0 +1,70 @@ +(use std/bit_array) +(use std/result) +(use std/order [Order]) +(use std/result [Result]) +(use std/testing [assert assert_eq]) + +(test + "bit_array/from_string to_string size" + (let [bits (bit_array/from_string "hello")] + (let? [_ (assert_eq (bit_array/bit_size bits) 40)] + (let? [_ (assert_eq (bit_array/byte_size bits) 5)] + (let? [_ (assert (bit_array/is_utf8 bits))] + (assert_eq (bit_array/to_string bits) (Ok "hello"))))))) + +(test + "bit_array/append concat" + (let [a (bit_array/from_string "butter") + b (bit_array/from_string "fly")] + (let? [_ (assert_eq + (bit_array/to_string (bit_array/append a b)) + (Ok "butterfly"))] + (assert_eq + (bit_array/to_string (bit_array/concat [a b])) + (Ok "butterfly"))))) + +(test + "bit_array/slice" + (let [bits (bit_array/from_string "butterfly")] + (let? [prefix (result/map_err + (f {_} -> "slice prefix failed") + (bit_array/slice bits 0 48)) + suffix (result/map_err + (f {_} -> "slice suffix failed") + (bit_array/slice bits 48 24)) + without_tail (result/map_err + (f {_} -> "slice negative-length failed") + (bit_array/slice bits 0 -24))] + (let? [_ (assert_eq (bit_array/to_string prefix) (Ok "butter"))] + (let? [_ (assert_eq (bit_array/to_string suffix) (Ok "fly"))] + (assert_eq (bit_array/to_string without_tail) (Ok "butter"))))))) + +(test + "bit_array/base64 roundtrip" + (let [bits (bit_array/from_string "hello") + encoded (bit_array/base64_encode bits True) + encoded_no_pad (bit_array/base64_encode bits False)] + (let? [_ (assert_eq encoded "aGVsbG8=")] + (let? [_ (assert_eq encoded_no_pad "aGVsbG8")] + (match (bit_array/base64_decode encoded_no_pad) + (Error _) ~> (Error "base64 decode failed") + (Ok decoded) ~> (assert_eq (bit_array/to_string decoded) (Ok "hello"))))))) + +(test + "bit_array/base16 roundtrip" + (let [bits (bit_array/from_string "hello")] + (let? [_ (assert_eq (bit_array/base16_encode bits) "68656c6c6f")] + (match (bit_array/base16_decode "68656c6c6f") + (Error _) ~> (Error "base16 decode failed") + (Ok decoded) ~> (assert_eq (bit_array/to_string decoded) (Ok "hello")))))) + +(test + "bit_array/starts_with and compare" + (let [a (bit_array/from_string "A") + b (bit_array/from_string "B") + butterfly (bit_array/from_string "butterfly") + but (bit_array/from_string "but")] + (let? [_ (assert (bit_array/starts_with butterfly but))] + (let? [_ (assert_eq (bit_array/compare a b) Lt)] + (let? [_ (assert_eq (bit_array/compare b a) Gt)] + (assert_eq (bit_array/compare a a) Eq)))))) diff --git a/tests/filepath_test.mond b/tests/filepath_test.mond new file mode 100644 index 0000000..7b2c9d5 --- /dev/null +++ b/tests/filepath_test.mond @@ -0,0 +1,123 @@ +(use std/filepath) +(use std/result [Result]) +(use std/testing [assert_eq]) + +(test + "filepath/extension returns simple extension" + (assert_eq (filepath/extension "/usr/local/bin/mond.txt") (Ok "txt"))) + +(test + "filepath/extension returns last segment for multi-dot file" + (assert_eq (filepath/extension "/tmp/archive.tar.gz") (Ok "gz"))) + +(test + "filepath/extension returns error for hidden file without extension" + (assert_eq (filepath/extension ".bashrc") (Error ()))) + +(test + "filepath/extension returns error when no extension exists" + (assert_eq (filepath/extension "/usr/local/bin/mond") (Error ()))) + +(test + "filepath/join combines regular paths" + (assert_eq (filepath/join "/usr/local" "bin") "/usr/local/bin")) + +(test + "filepath/join handles absolute right on root left" + (assert_eq (filepath/join "/" "/bin") "/bin")) + +(test + "filepath/join normalises right when left is empty" + (assert_eq (filepath/join "" "/bin") "bin")) + +(test + "filepath/join strips leading slash from right for non-root left" + (assert_eq (filepath/join "/usr/local" "/bin") "/usr/local/bin")) + +(test + "filepath/join normalises trailing slash on left" + (assert_eq (filepath/join "/usr/local/" "bin") "/usr/local/bin")) + +(test + "filepath/join with right slash returns left then trims" + (assert_eq (filepath/join "/" "/") "")) + +(test + "filepath/remove_trailing_slash trims final slash" + (assert_eq (filepath/remove_trailing_slash "/usr/local/") "/usr/local")) + +(test + "filepath/remove_trailing_slash on root follows current semantics" + (assert_eq (filepath/remove_trailing_slash "/") "")) + +(test + "filepath/split_unix absolute path" + (assert_eq (filepath/split_unix "/usr/local/bin/") ["/" "usr" "local" "bin"])) + +(test + "filepath/split_unix root path" + (assert_eq (filepath/split_unix "/") ["/"])) + +(test "filepath/split_unix empty path" (assert_eq (filepath/split_unix "") [])) + +(test + "filepath/split_unix collapses repeated separators" + (assert_eq (filepath/split_unix "usr//local///bin/") ["usr" "local" "bin"])) + +(test + "filepath/split delegates to split_unix" + (assert_eq (filepath/split "usr/local/bin") ["usr" "local" "bin"])) + +(test + "filepath/base_name returns final segment" + (assert_eq (filepath/base_name "/usr/local/bin/") "bin")) + +(test + "filepath/base_name root path is empty" + (assert_eq (filepath/base_name "/") "")) + +(test + "filepath/base_name empty path is empty" + (assert_eq (filepath/base_name "") "")) + +(test + "filepath/strip_extension removes only last extension" + (assert_eq + (filepath/strip_extension "/tmp/archive.tar.gz") + "/tmp/archive.tar")) + +(test + "filepath/strip_extension keeps path when no extension" + (assert_eq (filepath/strip_extension "/tmp/archive") "/tmp/archive")) + +(test + "filepath/strip_extension keeps hidden file without extension" + (assert_eq (filepath/strip_extension ".bashrc") ".bashrc")) + +(test + "filepath/directory_name absolute path" + (assert_eq (filepath/directory_name "/usr/local/bin") "/usr/local")) + +(test + "filepath/directory_name relative path" + (assert_eq (filepath/directory_name "usr/local/bin") "usr/local")) + +(test + "filepath/directory_name file in cwd" + (assert_eq (filepath/directory_name "mond.txt") "")) + +(test + "filepath/directory_name root path is empty" + (assert_eq (filepath/directory_name "/") "")) + +(test + "filepath/directory_name ignores trailing slash" + (assert_eq (filepath/directory_name "/usr/local/bin/") "/usr/local")) + +(test + "filepath/extension of hidden multi-dot file returns last segment" + (assert_eq (filepath/extension ".config.toml") (Ok "toml"))) + +(test + "filepath/extension for trailing dot is empty string" + (assert_eq (filepath/extension "file.") (Ok ""))) diff --git a/tests/fs_test.mond b/tests/fs_test.mond new file mode 100644 index 0000000..847d692 --- /dev/null +++ b/tests/fs_test.mond @@ -0,0 +1,37 @@ +(use std/fs [FileInfo + FileType + FilePermissions + Permission + file_info_permissions + file_info_permissions_octal + file_info_type + file_permissions_to_octal]) +(use std/result [Result]) +(use std/set) +(use std/testing [assert_eq]) + +(let mk_info {mode} + (FileInfo 0 mode 0 0 0 0 0 0 0 0)) + +(test + "fs/file_info_type" + (let? [_ (assert_eq (file_info_type (mk_info 33188)) File)] + (let? [_ (assert_eq (file_info_type (mk_info 16877)) Directory)] + (let? [_ (assert_eq (file_info_type (mk_info 41471)) Symlink)] + (assert_eq (file_info_type (mk_info 49663)) Other))))) + +(test + "fs/file_info_permissions_octal" + (assert_eq (file_info_permissions_octal (mk_info 33188)) 420)) + +(test + "fs/file_permissions_roundtrip" + (let [read_write (set/from_list [Read Write]) + read_only (set/from_list [Read])] + (let? [_ (assert_eq + (file_info_permissions (mk_info 33188)) + (FilePermissions read_write read_only read_only))] + (assert_eq + (file_permissions_to_octal + (FilePermissions read_write read_only read_only)) + 420)))) diff --git a/tests/int_test.mond b/tests/int_test.mond new file mode 100644 index 0000000..3400c61 --- /dev/null +++ b/tests/int_test.mond @@ -0,0 +1,6 @@ +(use std/int) +(use std/testing [assert_eq]) + +(test "int/bitwise_and" (assert_eq (int/bitwise_and 6 3) 2)) + +(test "int/bitwise_shift_right" (assert_eq (int/bitwise_shift_right 8 2) 2)) diff --git a/tests/list_test.mond b/tests/list_test.mond index 85a4b33..1afdd7c 100644 --- a/tests/list_test.mond +++ b/tests/list_test.mond @@ -74,3 +74,15 @@ (test "list/flatten handles empty nested lists" (assert_eq (list/flatten [[] [1 2] [] [3]]) [1 2 3])) + +(test + "list/last returns error for empty list" + (assert_eq (list/last []) (Error ()))) + +(test + "list/last returns only element for singleton list" + (assert_eq (list/last [42]) (Ok 42))) + +(test + "list/last returns final element for non-empty list" + (assert_eq (list/last [1 2 3 4]) (Ok 4))) diff --git a/tests/order_test.mond b/tests/order_test.mond new file mode 100644 index 0000000..900e92d --- /dev/null +++ b/tests/order_test.mond @@ -0,0 +1,32 @@ +(use std/order [Order]) +(use std/order) +(use std/result [Result]) +(use std/testing [assert_eq]) + +(test + "order/negate" + (let? [_ (assert_eq (order/negate Lt) Gt)] + (let? [_ (assert_eq (order/negate Eq) Eq)] + (assert_eq (order/negate Gt) Lt)))) + +(test + "order/to_int" + (let? [_ (assert_eq (order/to_int Lt) -1)] + (let? [_ (assert_eq (order/to_int Eq) 0)] + (assert_eq (order/to_int Gt) 1)))) + +(test + "order/compare" + (let? [_ (assert_eq (order/compare Eq Lt) Gt)] + (let? [_ (assert_eq (order/compare Lt Eq) Lt)] + (assert_eq (order/compare Gt Lt) Gt)))) + +(test + "order/break_tie" + (let? [_ (assert_eq (order/break_tie Lt Eq) Lt)] + (assert_eq (order/break_tie Eq Gt) Gt))) + +(test + "order/lazy_break_tie" + (let? [_ (assert_eq (order/lazy_break_tie Gt (f {_} -> Lt)) Gt)] + (assert_eq (order/lazy_break_tie Eq (f {_} -> Lt)) Lt))) diff --git a/tests/set_test.mond b/tests/set_test.mond new file mode 100644 index 0000000..c7d7655 --- /dev/null +++ b/tests/set_test.mond @@ -0,0 +1,97 @@ +(use std/list) +(use std/result [Result]) +(use std/set) +(use std/testing [assert assert_eq]) + +(let sample_set {} + (set/from_list [1 2 3])) + +(test "set/new starts empty" (assert_eq (set/is_empty (set/new)) True)) + +(test + "set/insert deduplicates and contains member" + (let [s (set/insert (set/insert (set/new) 1) 1)] + (let? [_ (assert (set/contains s 1))] + (assert_eq (set/size s) 1)))) + +(test + "set/delete removes a member" + (assert_eq (set/contains (set/delete (sample_set) 2) 2) False)) + +(test + "set/from_list and set/to_list" + (assert_eq (list/sort (set/to_list (set/from_list [1 1 2 4 3 2]))) [1 2 3 4])) + +(test + "set/fold combines all members" + (assert_eq (set/fold (sample_set) 0 (f {acc member} -> (+ acc member))) 6)) + +(test + "set/filter keeps matching members" + (assert_eq + (list/sort (set/to_list (set/filter (sample_set) (f {x} -> (> x 1))))) + [2 3])) + +(test + "set/map transforms members" + (assert_eq + (list/sort (set/to_list (set/map (sample_set) (f {x} -> (* x 2))))) + [2 4 6])) + +(test + "set/drop removes members in list" + (assert_eq (list/sort (set/to_list (set/drop (sample_set) [1 3 9]))) [2])) + +(test + "set/take keeps only listed members" + (assert_eq (list/sort (set/to_list (set/take (sample_set) [1 3 9]))) [1 3])) + +(test + "set/union includes members from both sets" + (assert_eq + (list/sort + (set/to_list (set/union (set/from_list [1 2]) (set/from_list [2 3])))) + [1 2 3])) + +(test + "set/intersection keeps shared members" + (assert_eq + (list/sort + (set/to_list + (set/intersection (set/from_list [1 2]) (set/from_list [2 3])))) + [2])) + +(test + "set/difference keeps only first-exclusive members" + (assert_eq + (list/sort + (set/to_list + (set/difference (set/from_list [1 2]) (set/from_list [2 3 4])))) + [1])) + +(test + "set/is_subset works for positive and negative cases" + (let? [_ (assert (set/is_subset (set/from_list [1]) (set/from_list [1 2])))] + (assert_eq + (set/is_subset (set/from_list [1 2 3]) (set/from_list [3 4 5])) + False))) + +(test + "set/is_disjoint works for positive and negative cases" + (let? [_ (assert + (set/is_disjoint (set/from_list [1 2 3]) (set/from_list [4 5 6])))] + (assert_eq + (set/is_disjoint (set/from_list [1 2 3]) (set/from_list [3 4 5])) + False))) + +(test + "set/symmetric_difference keeps members not in both" + (assert_eq + (list/sort + (set/to_list + (set/symmetric_difference (set/from_list [1 2 3]) (set/from_list [3 4])))) + [1 2 4])) + +(test + "set/each returns unit" + (assert_eq (set/each (sample_set) (f {x} -> x)) ())) diff --git a/tests/string_test.mond b/tests/string_test.mond index 9fa8057..a00d23e 100644 --- a/tests/string_test.mond +++ b/tests/string_test.mond @@ -30,3 +30,21 @@ (assert_ne (string/contains "hello world" "xyz") True))) (test "string/split" (assert_eq (string/split "a,b,c" ",") ["a" "b" "c"])) + +(test + "string/drop_end" + (assert_eq (string/drop_end "Cigarette Smoking Man" 2) "Cigarette Smoking M")) + +(test + "string/drop_start" + (assert_eq + (string/drop_start "Cigarette Smoking Man" 2) + "garette Smoking Man")) + +(test + "string/to_graphemes ascii" + (assert_eq (string/to_graphemes "abc") ["a" "b" "c"])) + +(test + "string/to_graphemes unicode grapheme cluster" + (assert_eq (string/to_graphemes "🏳️‍🌈a") ["🏳️‍🌈" "a"]))