-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" " Dream Test uses this module internally for snapshot testing and Gherkin file\n" " parsing. It wraps Erlang file operations with a small, structured error type\n" " (`FileError`) so callers can decide how to handle failures.\n" "\n" " ## Example\n" "\n" " Use this in test code (for example inside a snippet `tests()` function).\n" "\n" " ```gleam\n" " let path = tmp_path()\n" "\n" " // Setup: create the file (no assertions during setup)\n" " use _ <- result.try(\n" " write(path, \"hello\") |> result.map_error(error_to_string),\n" " )\n" "\n" " read(path)\n" " |> should\n" " |> be_equal(Ok(\"hello\"))\n" " |> or_fail_with(\"expected to read back written content\")\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", 107). ?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" " ## Parameters\n" "\n" " - `error`: the `FileError` value to format\n" "\n" " ## Returns\n" "\n" " A human-readable message string. This function is pure (no I/O).\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " error_to_string(NotFound(\"/x\"))\n" " |> should\n" " |> be_equal(\"File not found: /x\")\n" " |> or_fail_with(\"expected NotFound formatting\")\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", 155). ?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" " let path = tmp_path()\n" "\n" " // Setup: create the file (no assertions during setup)\n" " use _ <- result.try(\n" " write(path, \"hello\") |> result.map_error(error_to_string),\n" " )\n" "\n" " read(path)\n" " |> should\n" " |> be_equal(Ok(\"hello\"))\n" " |> or_fail_with(\"expected to read back written content\")\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", 187). ?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" " let path = tmp_path()\n" "\n" " // Setup: create the file (no assertions during setup)\n" " use _ <- result.try(\n" " write(path, \"hello\") |> result.map_error(error_to_string),\n" " )\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", 226). ?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" " let path = tmp_path()\n" "\n" " // Setup: create the file, then delete it (no assertions during setup)\n" " use _ <- result.try(\n" " write(path, \"hello\") |> result.map_error(error_to_string),\n" " )\n" " use _ <- result.try(delete(path) |> result.map_error(error_to_string))\n" "\n" " read(path)\n" " |> should\n" " |> be_equal(Error(NotFound(path)))\n" " |> or_fail_with(\"expected deleted file to be NotFound\")\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", 272). ?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(FileError)` - Directory access failed (the specific variant depends on the underlying file system error)\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let directory = \"./test/tmp/file_helpers_\" <> int.to_string(unique_port())\n" " let a = directory <> \"/a.snap\"\n" " let b = directory <> \"/b.snap\"\n" " let keep = directory <> \"/keep.txt\"\n" "\n" " // Setup: create 2 matching files and 1 non-matching file\n" " use _ <- result.try(write(a, \"a\") |> result.map_error(error_to_string))\n" " use _ <- result.try(write(b, \"b\") |> result.map_error(error_to_string))\n" " use _ <- result.try(\n" " write(keep, \"keep\") |> result.map_error(error_to_string),\n" " )\n" "\n" " delete_files_matching(directory, \".snap\")\n" " |> should\n" " |> be_equal(Ok(2))\n" " |> or_fail_with(\"expected two deleted snapshots\")\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).