-module(dream_test@file). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream_test/file.gleam"). -export([error_to_string/1, read/1, write/2, delete/1, delete_files_matching/2]). -export_type([file_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " File operations for dream_test.\n" "\n" " This module provides file I/O operations for internal use by dream_test,\n" " particularly for snapshot testing and Gherkin file parsing. It wraps\n" " Erlang's file operations with proper error handling.\n" "\n" " ## Error Handling\n" "\n" " Unlike many file libraries that return opaque errors, this module provides\n" " structured `FileError` types that tell you exactly what went wrong:\n" "\n" " ```gleam\n" " case file.read(\"config.json\") {\n" " Ok(content) -> parse(content)\n" " Error(NotFound(_)) -> use_defaults()\n" " Error(PermissionDenied(path)) -> panic as \"Cannot read \" <> path\n" " Error(error) -> panic as file.error_to_string(error)\n" " }\n" " ```\n" "\n" " ## Usage Examples\n" "\n" " ### Reading Files\n" "\n" " ```gleam\n" " import dream_test/file\n" "\n" " case file.read(\"./test/fixtures/expected.json\") {\n" " Ok(content) -> content\n" " Error(error) -> {\n" " io.println(\"Error: \" <> file.error_to_string(error))\n" " \"\"\n" " }\n" " }\n" " ```\n" "\n" " ### Writing Files\n" "\n" " ```gleam\n" " // Creates parent directories automatically\n" " case file.write(\"./test/snapshots/output.snap\", result) {\n" " Ok(Nil) -> io.println(\"Saved!\")\n" " Error(NoSpace(_)) -> io.println(\"Disk full!\")\n" " Error(error) -> io.println(file.error_to_string(error))\n" " }\n" " ```\n" "\n" " ### Deleting Files\n" "\n" " ```gleam\n" " // Safe to call even if file doesn't exist\n" " let _ = file.delete(\"./test/snapshots/old.snap\")\n" "\n" " // Delete all snapshots\n" " case file.delete_files_matching(\"./test/snapshots\", \".snap\") {\n" " Ok(count) -> io.println(\"Deleted \" <> int.to_string(count) <> \" files\")\n" " Error(error) -> io.println(file.error_to_string(error))\n" " }\n" " ```\n" ). -type file_error() :: {not_found, binary()} | {permission_denied, binary()} | {is_directory, binary()} | {no_space, binary()} | {file_system_error, binary(), binary()}. -file("src/dream_test/file.gleam", 148). ?DOC( " Convert a `FileError` to a human-readable string.\n" "\n" " Formats the error with both the error type and the affected path,\n" " suitable for logging or displaying to users.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " error_to_string(NotFound(\"/app/config.json\"))\n" " // -> \"File not found: /app/config.json\"\n" "\n" " error_to_string(PermissionDenied(\"/etc/shadow\"))\n" " // -> \"Permission denied: /etc/shadow\"\n" "\n" " error_to_string(FileSystemError(\"/dev/null\", \"ebusy\"))\n" " // -> \"File error (ebusy): /dev/null\"\n" " ```\n" ). -spec error_to_string(file_error()) -> binary(). error_to_string(Error) -> case Error of {not_found, Path} -> <<"File not found: "/utf8, Path/binary>>; {permission_denied, Path@1} -> <<"Permission denied: "/utf8, Path@1/binary>>; {is_directory, Path@2} -> <<"Is a directory: "/utf8, Path@2/binary>>; {no_space, Path@3} -> <<"No space left on device: "/utf8, Path@3/binary>>; {file_system_error, Path@4, Reason} -> <<<<<<"File error ("/utf8, Reason/binary>>/binary, "): "/utf8>>/binary, Path@4/binary>> end. -file("src/dream_test/file.gleam", 201). ?DOC( " Read the entire contents of a file as a UTF-8 string.\n" "\n" " Returns the file contents on success, or a `FileError` describing\n" " what went wrong.\n" "\n" " ## Parameters\n" "\n" " - `path` - Path to the file (absolute or relative to cwd)\n" "\n" " ## Returns\n" "\n" " - `Ok(String)` - The file contents\n" " - `Error(NotFound)` - File doesn't exist\n" " - `Error(PermissionDenied)` - Can't read the file\n" " - `Error(IsDirectory)` - Path is a directory\n" " - `Error(FileSystemError)` - Other OS error\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Read a configuration file\n" " case file.read(\"gleam.toml\") {\n" " Ok(toml) -> parse_config(toml)\n" " Error(NotFound(_)) -> default_config()\n" " Error(error) -> panic as file.error_to_string(error)\n" " }\n" " ```\n" "\n" " ```gleam\n" " // Read with full error handling\n" " case file.read(path) {\n" " Ok(content) -> Ok(content)\n" " Error(NotFound(_)) -> Error(\"Config file missing\")\n" " Error(PermissionDenied(_)) -> Error(\"Cannot read config (permission denied)\")\n" " Error(error) -> Error(file.error_to_string(error))\n" " }\n" " ```\n" ). -spec read(binary()) -> {ok, binary()} | {error, file_error()}. read(Path) -> dream_test_file_ffi:read_file(Path). -file("src/dream_test/file.gleam", 238). ?DOC( " Write a string to a file, creating parent directories if needed.\n" "\n" " If the file exists, it will be completely overwritten.\n" " If parent directories don't exist, they will be created automatically.\n" "\n" " ## Parameters\n" "\n" " - `path` - Destination path (absolute or relative to cwd)\n" " - `content` - String content to write\n" "\n" " ## Returns\n" "\n" " - `Ok(Nil)` - File written successfully\n" " - `Error(PermissionDenied)` - Can't write to the path\n" " - `Error(NoSpace)` - Disk is full\n" " - `Error(IsDirectory)` - Path is a directory\n" " - `Error(FileSystemError)` - Other OS error\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Write a snapshot file\n" " case file.write(\"./test/snapshots/user.snap\", json_output) {\n" " Ok(Nil) -> io.println(\"Snapshot saved\")\n" " Error(error) -> io.println(\"Failed: \" <> file.error_to_string(error))\n" " }\n" " ```\n" "\n" " ```gleam\n" " // Creates nested directories automatically\n" " file.write(\"./deep/nested/path/file.txt\", \"content\")\n" " // Creates ./deep/nested/path/ if it doesn't exist\n" " ```\n" ). -spec write(binary(), binary()) -> {ok, nil} | {error, file_error()}. write(Path, Content) -> dream_test_file_ffi:write_file(Path, Content). -file("src/dream_test/file.gleam", 273). ?DOC( " Delete a file.\n" "\n" " This operation is **idempotent**: deleting a file that doesn't exist\n" " returns `Ok(Nil)`, not an error. This makes cleanup code simpler.\n" "\n" " ## Parameters\n" "\n" " - `path` - Path to the file to delete\n" "\n" " ## Returns\n" "\n" " - `Ok(Nil)` - File was deleted (or didn't exist)\n" " - `Error(PermissionDenied)` - Can't delete the file\n" " - `Error(IsDirectory)` - Path is a directory (use rmdir)\n" " - `Error(FileSystemError)` - Other OS error\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Safe cleanup - doesn't fail if already deleted\n" " let _ = file.delete(\"./test/temp/output.txt\")\n" " ```\n" "\n" " ```gleam\n" " // Delete with error handling\n" " case file.delete(snapshot_path) {\n" " Ok(Nil) -> io.println(\"Snapshot cleared\")\n" " Error(PermissionDenied(_)) -> io.println(\"Cannot delete (permission denied)\")\n" " Error(error) -> io.println(file.error_to_string(error))\n" " }\n" " ```\n" ). -spec delete(binary()) -> {ok, nil} | {error, file_error()}. delete(Path) -> dream_test_file_ffi:delete_file(Path). -file("src/dream_test/file.gleam", 314). ?DOC( " Delete all files in a directory that have a specific extension.\n" "\n" " Searches the given directory (non-recursively) for files matching\n" " the extension pattern and deletes them. Returns the count of files\n" " actually deleted.\n" "\n" " ## Parameters\n" "\n" " - `directory` - Path to the directory to search\n" " - `extension` - File extension including the dot (e.g., \".snap\", \".tmp\")\n" "\n" " ## Returns\n" "\n" " - `Ok(Int)` - Number of files deleted\n" " - `Error(FileSystemError)` - Directory access failed\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Clear all snapshot files\n" " case file.delete_files_matching(\"./test/snapshots\", \".snap\") {\n" " Ok(0) -> io.println(\"No snapshots to delete\")\n" " Ok(n) -> io.println(\"Deleted \" <> int.to_string(n) <> \" snapshots\")\n" " Error(error) -> io.println(file.error_to_string(error))\n" " }\n" " ```\n" "\n" " ```gleam\n" " // Clean up temporary files before test run\n" " let _ = file.delete_files_matching(\"./test/temp\", \".tmp\")\n" " ```\n" "\n" " ## Notes\n" "\n" " - Only searches the immediate directory (not subdirectories)\n" " - Files that can't be deleted are silently skipped\n" " - The count reflects only successfully deleted files\n" ). -spec delete_files_matching(binary(), binary()) -> {ok, integer()} | {error, file_error()}. delete_files_matching(Directory, Extension) -> dream_test_file_ffi:delete_files_matching(Directory, Extension).