Current section
Files
Jump to
Current section
Files
src/dream_http_client@storage.erl
-module(dream_http_client@storage).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_http_client/storage.gleam").
-export([load_recordings/1, save_recording_immediately/3, save_recordings/3]).
-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 I/O for recordings\n"
"\n"
" Handles loading and saving recording files to/from the filesystem.\n"
).
-file("src/dream_http_client/storage.gleam", 48).
?DOC(
" Load recordings from a directory\n"
"\n"
" Scans the directory for all `.json` files and loads them as individual recordings.\n"
" This function is used internally by the recorder when starting in Playback mode, but\n"
" can also be called directly to inspect or manipulate recordings.\n"
"\n"
" ## Parameters\n"
"\n"
" - `directory`: The directory containing individual recording files\n"
"\n"
" ## Returns\n"
"\n"
" - `Ok(List(Recording))`: Successfully loaded recordings (empty list if directory doesn't exist)\n"
" - `Error(String)`: Error message if directory exists but files cannot be read or decoded\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Load recordings for inspection\n"
" case storage.load_recordings(\"mocks/api\") {\n"
" Ok(recordings) -> {\n"
" io.println(\"Loaded \" <> int.to_string(list.length(recordings)) <> \" recordings\")\n"
" }\n"
" Error(reason) -> io.println_error(\"Failed to load: \" <> reason)\n"
" }\n"
" ```\n"
"\n"
" ## Notes\n"
"\n"
" - Returns an empty list (not an error) if the directory doesn't exist\n"
" - This is the expected behavior for Playback mode when no recordings have been created yet\n"
" - Each file contains a single recording in the versioned JSON format\n"
).
-spec load_recordings(binary()) -> {ok,
list(dream_http_client@recording:recording())} |
{error, binary()}.
load_recordings(Directory) ->
case simplifile_erl:read_directory(Directory) of
{ok, Files} ->
Json_files = gleam@list:filter(
Files,
fun(File) ->
gleam_stdlib:string_ends_with(File, <<".json"/utf8>>)
end
),
_pipe = Json_files,
gleam@list:try_map(
_pipe,
fun(Filename) ->
File_path = <<<<Directory/binary, "/"/utf8>>/binary,
Filename/binary>>,
case simplifile:read(File_path) of
{ok, Content} ->
case dream_http_client@recording:decode_recording_file(
Content
) of
{ok, File@1} ->
case gleam@list:first(
erlang:element(3, File@1)
) of
{ok, Entry} ->
{ok, Entry};
{error, _} ->
{error,
<<"Recording file contains no entries: "/utf8,
File_path/binary>>}
end;
{error, Reason} ->
{error,
<<<<<<"Failed to decode "/utf8,
File_path/binary>>/binary,
": "/utf8>>/binary,
Reason/binary>>}
end;
{error, Read_error} ->
{error,
<<<<<<"Failed to read "/utf8, File_path/binary>>/binary,
": "/utf8>>/binary,
(gleam@string:inspect(Read_error))/binary>>}
end
end
);
{error, enoent} ->
{ok, []};
{error, Read_error@1} ->
{error,
<<<<<<"Failed to read directory "/utf8, Directory/binary>>/binary,
": "/utf8>>/binary,
(gleam@string:inspect(Read_error@1))/binary>>}
end.
-file("src/dream_http_client/storage.gleam", 295).
-spec method_to_string(gleam@http:method()) -> binary().
method_to_string(Method) ->
case Method of
get ->
<<"GET"/utf8>>;
post ->
<<"POST"/utf8>>;
put ->
<<"PUT"/utf8>>;
delete ->
<<"DELETE"/utf8>>;
patch ->
<<"PATCH"/utf8>>;
head ->
<<"HEAD"/utf8>>;
options ->
<<"OPTIONS"/utf8>>;
trace ->
<<"TRACE"/utf8>>;
connect ->
<<"CONNECT"/utf8>>;
{other, S} ->
string:uppercase(S)
end.
-file("src/dream_http_client/storage.gleam", 314).
?DOC(
" Sanitize a string to be safe for use in filenames\n"
"\n"
" Replaces characters that are problematic in filenames with underscores.\n"
" Allows alphanumeric, hyphens, periods, and underscores.\n"
).
-spec sanitize_for_filename(binary()) -> binary().
sanitize_for_filename(Input) ->
_pipe = Input,
_pipe@1 = gleam@string:to_graphemes(_pipe),
_pipe@2 = gleam@list:map(_pipe@1, fun(Char) -> case Char of
<<"a"/utf8>> ->
Char;
<<"b"/utf8>> ->
Char;
<<"c"/utf8>> ->
Char;
<<"d"/utf8>> ->
Char;
<<"e"/utf8>> ->
Char;
<<"f"/utf8>> ->
Char;
<<"g"/utf8>> ->
Char;
<<"h"/utf8>> ->
Char;
<<"i"/utf8>> ->
Char;
<<"j"/utf8>> ->
Char;
<<"k"/utf8>> ->
Char;
<<"l"/utf8>> ->
Char;
<<"m"/utf8>> ->
Char;
<<"n"/utf8>> ->
Char;
<<"o"/utf8>> ->
Char;
<<"p"/utf8>> ->
Char;
<<"q"/utf8>> ->
Char;
<<"r"/utf8>> ->
Char;
<<"s"/utf8>> ->
Char;
<<"t"/utf8>> ->
Char;
<<"u"/utf8>> ->
Char;
<<"v"/utf8>> ->
Char;
<<"w"/utf8>> ->
Char;
<<"x"/utf8>> ->
Char;
<<"y"/utf8>> ->
Char;
<<"z"/utf8>> ->
Char;
<<"A"/utf8>> ->
Char;
<<"B"/utf8>> ->
Char;
<<"C"/utf8>> ->
Char;
<<"D"/utf8>> ->
Char;
<<"E"/utf8>> ->
Char;
<<"F"/utf8>> ->
Char;
<<"G"/utf8>> ->
Char;
<<"H"/utf8>> ->
Char;
<<"I"/utf8>> ->
Char;
<<"J"/utf8>> ->
Char;
<<"K"/utf8>> ->
Char;
<<"L"/utf8>> ->
Char;
<<"M"/utf8>> ->
Char;
<<"N"/utf8>> ->
Char;
<<"O"/utf8>> ->
Char;
<<"P"/utf8>> ->
Char;
<<"Q"/utf8>> ->
Char;
<<"R"/utf8>> ->
Char;
<<"S"/utf8>> ->
Char;
<<"T"/utf8>> ->
Char;
<<"U"/utf8>> ->
Char;
<<"V"/utf8>> ->
Char;
<<"W"/utf8>> ->
Char;
<<"X"/utf8>> ->
Char;
<<"Y"/utf8>> ->
Char;
<<"Z"/utf8>> ->
Char;
<<"0"/utf8>> ->
Char;
<<"1"/utf8>> ->
Char;
<<"2"/utf8>> ->
Char;
<<"3"/utf8>> ->
Char;
<<"4"/utf8>> ->
Char;
<<"5"/utf8>> ->
Char;
<<"6"/utf8>> ->
Char;
<<"7"/utf8>> ->
Char;
<<"8"/utf8>> ->
Char;
<<"9"/utf8>> ->
Char;
<<"-"/utf8>> ->
Char;
<<"."/utf8>> ->
Char;
<<"_"/utf8>> ->
Char;
_ ->
<<"_"/utf8>>
end end),
gleam@string:join(_pipe@2, <<""/utf8>>).
-file("src/dream_http_client/storage.gleam", 380).
?DOC(" Truncate a string to a maximum length\n").
-spec truncate_string(binary(), integer()) -> binary().
truncate_string(Input, Max_length) ->
case string:length(Input) > Max_length of
true ->
gleam@string:slice(Input, 0, Max_length);
false ->
Input
end.
-file("src/dream_http_client/storage.gleam", 388).
?DOC(" Generate a hash from a string\n").
-spec generate_hash(binary()) -> binary().
generate_hash(Input) ->
Bits = gleam_stdlib:identity(Input),
Hash_bits = gleam@crypto:hash(sha256, Bits),
_pipe = gleam_stdlib:base16_encode(Hash_bits),
string:lowercase(_pipe).
-file("src/dream_http_client/storage.gleam", 266).
?DOC(
" Build a unique filename for a recording based on the request\n"
"\n"
" Creates a filename with the format: `{method}_{host}_{path}_{hash}.json`\n"
" The hash ensures uniqueness even for requests with different query params, headers, or body.\n"
"\n"
" ## Parameters\n"
"\n"
" - `request`: The request to generate a filename for\n"
" - `matching_config`: The matching configuration used to build the signature\n"
"\n"
" ## Returns\n"
"\n"
" A safe filename string ending in `.json`\n"
"\n"
" ## Examples\n"
"\n"
" - `GET_api.example.com_users_a3f5b2.json`\n"
" - `POST_api.example.com_users_c7d8e9.json`\n"
" - `GET_localhost_text_f1a2b3.json`\n"
).
-spec build_filename(
dream_http_client@recording:recorded_request(),
dream_http_client@matching:matching_config()
) -> binary().
build_filename(Request, Matching_config) ->
Method_part = sanitize_for_filename(
method_to_string(erlang:element(2, Request))
),
Host_part = sanitize_for_filename(erlang:element(4, Request)),
Path_part = begin
_pipe = erlang:element(6, Request),
_pipe@1 = gleam@string:replace(_pipe, <<"/"/utf8>>, <<"_"/utf8>>),
_pipe@2 = sanitize_for_filename(_pipe@1),
truncate_string(_pipe@2, 50)
end,
Signature = dream_http_client@matching:build_signature(
Request,
Matching_config
),
Hash = generate_hash(Signature),
Hash_short = gleam@string:slice(Hash, 0, 6),
<<<<<<<<<<<<<<Method_part/binary, "_"/utf8>>/binary, Host_part/binary>>/binary,
"_"/utf8>>/binary,
Path_part/binary>>/binary,
"_"/utf8>>/binary,
Hash_short/binary>>/binary,
".json"/utf8>>.
-file("src/dream_http_client/storage.gleam", 134).
?DOC(
" Save a single recording immediately to its own file\n"
"\n"
" Writes a single recording to an individual file based on the request signature.\n"
" The filename is generated from the request method, host, path, and a hash for uniqueness.\n"
"\n"
" ## Parameters\n"
"\n"
" - `directory`: The directory where the recording file will be written\n"
" - `rec`: The recording to save\n"
" - `matching_config`: The matching configuration used to generate the filename signature\n"
"\n"
" ## Returns\n"
"\n"
" - `Ok(Nil)`: Successfully saved the recording\n"
" - `Error(String)`: Error message if directory creation or file write fails\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let rec = recording.Recording(request: req, response: resp)\n"
" let config = matching.match_url_only()\n"
" case storage.save_recording_immediately(\"mocks/api\", rec, config) {\n"
" Ok(_) -> io.println(\"Saved recording\")\n"
" Error(reason) -> io.println_error(\"Failed to save: \" <> reason)\n"
" }\n"
" ```\n"
"\n"
" ## Notes\n"
"\n"
" - Each recording is saved to its own file for O(1) write performance\n"
" - Filenames include human-readable parts (method, host, path) plus a hash for uniqueness\n"
" - Concurrent tests can record safely without file contention\n"
).
-spec save_recording_immediately(
binary(),
dream_http_client@recording:recording(),
dream_http_client@matching:matching_config()
) -> {ok, nil} | {error, binary()}.
save_recording_immediately(Directory, Rec, Matching_config) ->
Filename = build_filename(erlang:element(2, Rec), Matching_config),
File_path = <<<<Directory/binary, "/"/utf8>>/binary, Filename/binary>>,
_pipe = case simplifile:create_directory_all(Directory) of
{ok, nil} ->
{ok, nil};
{error, eexist} ->
{ok, nil};
{error, Directory_error} ->
{error,
<<<<<<"Failed to create directory "/utf8, Directory/binary>>/binary,
": "/utf8>>/binary,
(gleam@string:inspect(Directory_error))/binary>>}
end,
gleam@result:'try'(
_pipe,
fun(_) ->
Recording_file = {recording_file, <<"1.0"/utf8>>, [Rec]},
Json_value = dream_http_client@recording:encode_recording_file(
Recording_file
),
Json_string = gleam@json:to_string(Json_value),
case simplifile:write(File_path, Json_string) of
{ok, nil} ->
{ok, nil};
{error, Write_error} ->
{error,
<<<<<<"Failed to write recording file "/utf8,
File_path/binary>>/binary,
": "/utf8>>/binary,
(gleam@string:inspect(Write_error))/binary>>}
end
end
).
-file("src/dream_http_client/storage.gleam", 217).
?DOC(
" Save multiple recordings to individual files\n"
"\n"
" Writes each recording to its own file in the directory. Creates the directory if it\n"
" doesn't exist. Each file is named based on the request signature for easy identification.\n"
"\n"
" ## Parameters\n"
"\n"
" - `directory`: The directory where recording files will be written\n"
" - `recordings`: List of recordings to save\n"
" - `matching_config`: The matching configuration used to generate filenames\n"
"\n"
" ## Returns\n"
"\n"
" - `Ok(Nil)`: Successfully saved all recordings\n"
" - `Error(String)`: Error message if directory creation or file write fails\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let recordings = [\n"
" create_test_recording(),\n"
" create_another_recording(),\n"
" ]\n"
" let config = matching.match_url_only()\n"
"\n"
" case storage.save_recordings(\"mocks/api\", recordings, config) {\n"
" Ok(_) -> io.println(\"Saved recordings successfully\")\n"
" Error(reason) -> io.println_error(\"Failed to save: \" <> reason)\n"
" }\n"
" ```\n"
"\n"
" ## Notes\n"
"\n"
" - Directory is created automatically if it doesn't exist\n"
" - Each recording is saved to its own file (no file contention)\n"
" - Existing files with the same name are overwritten\n"
).
-spec save_recordings(
binary(),
list(dream_http_client@recording:recording()),
dream_http_client@matching:matching_config()
) -> {ok, nil} | {error, binary()}.
save_recordings(Directory, Recordings, Matching_config) ->
_pipe = case simplifile:create_directory_all(Directory) of
{ok, nil} ->
{ok, nil};
{error, eexist} ->
{ok, nil};
{error, Directory_error} ->
{error,
<<<<<<"Failed to create directory "/utf8, Directory/binary>>/binary,
": "/utf8>>/binary,
(gleam@string:inspect(Directory_error))/binary>>}
end,
gleam@result:'try'(_pipe, fun(_) -> _pipe@1 = Recordings,
gleam@list:try_each(
_pipe@1,
fun(Rec) ->
save_recording_immediately(Directory, Rec, Matching_config)
end
) end).