From cbfa2f8891cb6d2716f01bc461287067965ca1ee Mon Sep 17 00:00:00 2001 From: Tyrone Taylor <28680107+ttaylor92@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:35:18 -0500 Subject: [PATCH 1/2] feat: implemented list objects functionality --- lib/cloud_cache/adapters/s3.ex | 26 +++++++++++++ lib/cloud_cache/adapters/s3/sandbox.ex | 39 +++++++++++++++++++ test/cloud_cache/adapters/s3_sandbox_test.exs | 37 ++++++++++++++++++ test/cloud_cache/adapters/s3_test.exs | 19 +++++++++ 4 files changed, 121 insertions(+) diff --git a/lib/cloud_cache/adapters/s3.ex b/lib/cloud_cache/adapters/s3.ex index 38ab4fd..034bf09 100644 --- a/lib/cloud_cache/adapters/s3.ex +++ b/lib/cloud_cache/adapters/s3.ex @@ -197,6 +197,28 @@ defmodule CloudCache.Adapters.S3 do end end + def list_objects(bucket, opts \\ []) do + opts = Keyword.merge(@default_options, opts) + + sandbox? = opts[:s3][:sandbox_enabled] === true + + if not sandbox? or sandbox_disabled?() do + case bucket |> S3.list_objects(opts) |> perform(opts) do + {:ok, _} = result -> + result + + {:error, reason} -> + {:error, + ErrorMessage.service_unavailable("service temporarily unavailable", %{ + bucket: bucket, + reason: reason + })} + end + else + sandbox_list_objects_response(bucket, opts) + end + end + @impl true @doc """ ... @@ -953,6 +975,10 @@ defmodule CloudCache.Adapters.S3 do to: CloudCache.Adapters.S3.Testing.S3Sandbox, as: :put_object_response + defdelegate sandbox_list_objects_response(bucket, opts), + to: CloudCache.Adapters.S3.Testing.S3Sandbox, + as: :list_objects_response + defdelegate sandbox_copy_object_response( dest_bucket, dest_object, diff --git a/lib/cloud_cache/adapters/s3/sandbox.ex b/lib/cloud_cache/adapters/s3/sandbox.ex index 39ac236..2622e22 100644 --- a/lib/cloud_cache/adapters/s3/sandbox.ex +++ b/lib/cloud_cache/adapters/s3/sandbox.ex @@ -92,6 +92,41 @@ if Code.ensure_loaded?(SandboxRegistry) do end end + @doc """ + Returns the registered response function for `list_objects/2` in the + context of the calling process. + """ + def list_objects_response(bucket, opts \\ []) do + doc_examples = + [ + "fn -> ...", + "fn (object) -> ...", + "fn (object, options) -> ..." + ] + + func = find!(:list_objects, bucket, doc_examples) + + case :erlang.fun_info(func)[:arity] do + 0 -> + func.() + + 1 -> + func.(bucket) + + 2 -> + func.(bucket, opts) + + _ -> + raise """ + This function's signature is not supported: #{inspect(func)} + + Please provide a function with one of the following arities (0-#{length(doc_examples) - 1}): + + #{Enum.map_join(doc_examples, "\n", &(" " <> &1))} + """ + end + end + @doc """ Returns the registered response function for `copy_object/5` in the context of the calling process. @@ -660,6 +695,10 @@ if Code.ensure_loaded?(SandboxRegistry) do set_responses(:put_object, tuples) end + def set_list_objects_responses(tuples) do + set_responses(:list_objects, tuples) + end + def set_copy_object_responses(tuples) do set_responses(:copy_object, tuples) end diff --git a/test/cloud_cache/adapters/s3_sandbox_test.exs b/test/cloud_cache/adapters/s3_sandbox_test.exs index 63fae24..440740a 100644 --- a/test/cloud_cache/adapters/s3_sandbox_test.exs +++ b/test/cloud_cache/adapters/s3_sandbox_test.exs @@ -92,6 +92,43 @@ defmodule CloudCache.Adapters.S3.Testing.S3SandboxTest do end end + describe "list_objects/2" do + test "returns list of objects on success" do + S3Sandbox.set_list_objects_responses([ + {@bucket, + fn -> + {:ok, + %{ + body: %{ + contents: [ + %{ + key: "test-object", + last_modified: ~U[2025-08-30 01:00:00.000000Z], + etag: "etag" + } + ] + }, + headers: %{} + }} + end} + ]) + + assert {:ok, + %{ + body: %{ + contents: [ + %{ + key: "test-object", + last_modified: ~U[2025-08-30 01:00:00.000000Z], + etag: "etag" + } + ] + }, + headers: %{} + }} = S3.list_objects(@bucket, @options) + end + end + describe "copy_object/3" do test "returns object metadata on success" do S3Sandbox.set_copy_object_responses([ diff --git a/test/cloud_cache/adapters/s3_test.exs b/test/cloud_cache/adapters/s3_test.exs index ba38eab..bcbf1ea 100644 --- a/test/cloud_cache/adapters/s3_test.exs +++ b/test/cloud_cache/adapters/s3_test.exs @@ -78,6 +78,25 @@ defmodule CloudCache.Adapters.S3Test do end end + describe "list_objects/2" do + test "returns list of objects on success" do + src_object = "test_#{:erlang.unique_integer()}.txt" + + assert {:ok, _} = LocalStack.put_object(@bucket, src_object, "content", []) + + assert {:ok, + %{ + body: %{ + contents: contents + }, + headers: headers + }} = S3.list_objects(@bucket, @options) + + assert Enum.any?(contents, fn content -> content.key === src_object end) + assert headers + end + end + describe "pre_sign/3" do test "returns a presigned URL and metadata on success" do assert {:ok, From 4933970410f14165b2500b70e6203aadda60f11b Mon Sep 17 00:00:00 2001 From: Tyrone Taylor <28680107+ttaylor92@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:00:48 -0500 Subject: [PATCH 2/2] chore: updated behavior module and adapter with doc and impl annotation --- lib/cloud_cache/adapter.ex | 9 +++++++++ lib/cloud_cache/adapters/s3.ex | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/lib/cloud_cache/adapter.ex b/lib/cloud_cache/adapter.ex index 4c28581..ec98e9d 100644 --- a/lib/cloud_cache/adapter.ex +++ b/lib/cloud_cache/adapter.ex @@ -31,6 +31,11 @@ defmodule CloudCache.Adapter do opts :: options() ) :: {:ok, term()} | {:error, term()} + @callback list_objects( + bucket :: bucket(), + opts :: options() + ) :: {:ok, term()} | {:error, term()} + @callback pre_sign( bucket :: bucket(), object :: object(), @@ -131,6 +136,10 @@ defmodule CloudCache.Adapter do adapter.copy_object(dest_bucket, dest_object, src_bucket, src_object, opts) end + def list_objects(adapter, bucket, opts \\ []) do + adapter.list_objects(bucket, opts) + end + # Multipart Upload API def pre_sign_part(adapter, bucket, object, upload_id, part_number, opts \\ []) do diff --git a/lib/cloud_cache/adapters/s3.ex b/lib/cloud_cache/adapters/s3.ex index 034bf09..fc95e95 100644 --- a/lib/cloud_cache/adapters/s3.ex +++ b/lib/cloud_cache/adapters/s3.ex @@ -197,6 +197,10 @@ defmodule CloudCache.Adapters.S3 do end end + @impl true + @doc """ + ... + """ def list_objects(bucket, opts \\ []) do opts = Keyword.merge(@default_options, opts)