cover/Elixir.WalletJourney.JourneyStore.html

1 defmodule WalletJourney.JourneyStore do
2 @moduledoc """
3 ETS-backed GenServer for wallet journey persistence.
4
5 Stores journey structs keyed by journey_id.
6 Supports lookup by user_id via a secondary bag index.
7
8 No database is required; this is an in-memory store for Phase 1-3 CI.
9 """
10
11 use GenServer
12
13 alias WalletJourney.Journey
14
15 @table :wallet_journey_store
16 @user_idx :wallet_journey_user_idx
17
18 # --- Client API ---
19
20 def start_link(opts) do
21
:-(
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
22 end
23
24 @doc "Stores a new journey. Returns :ok."
25 @spec store(Journey.t()) :: :ok
26 def store(%Journey{} = journey) do
27 19 GenServer.call(__MODULE__, {:store, journey})
28 end
29
30 @doc "Retrieves a journey by journey_id. Returns `{:ok, journey}` or `{:error, :not_found}`."
31 @spec get(journey_id :: String.t()) :: {:ok, Journey.t()} | {:error, :not_found}
32 def get(journey_id) do
33 11 GenServer.call(__MODULE__, {:get, journey_id})
34 end
35
36 @doc "Updates an existing journey. Returns `:ok` or `{:error, :not_found}`."
37 @spec update(Journey.t()) :: :ok | {:error, :not_found}
38 def update(%Journey{} = journey) do
39 130 GenServer.call(__MODULE__, {:update, journey})
40 end
41
42 @doc "Lists all journeys for a given user_id."
43 @spec list_by_user(user_id :: String.t()) :: [Journey.t()]
44 def list_by_user(user_id) do
45
:-(
GenServer.call(__MODULE__, {:list_by_user, user_id})
46 end
47
48 @doc "Resets all store state. For test use only."
49 21 def reset, do: GenServer.call(__MODULE__, :reset)
50
51 # --- Server Callbacks ---
52
53 @impl true
54 def init(_opts) do
55
:-(
table = :ets.new(@table, [:set, :protected, :named_table])
56
:-(
user_idx = :ets.new(@user_idx, [:bag, :protected, :named_table])
57 {:ok, %{table: table, user_idx: user_idx}}
58 end
59
60 @impl true
61 def handle_call({:store, journey}, _from, state) do
62 19 :ets.insert(@table, {journey.journey_id, journey})
63 19 :ets.insert(@user_idx, {journey.user_id, journey.journey_id})
64 19 {:reply, :ok, state}
65 end
66
67 @impl true
68 def handle_call({:get, journey_id}, _from, state) do
69 11 result =
70 case :ets.lookup(@table, journey_id) do
71 2 [] -> {:error, :not_found}
72 9 [{_key, journey}] -> {:ok, journey}
73 end
74
75 11 {:reply, result, state}
76 end
77
78 @impl true
79 def handle_call({:update, journey}, _from, state) do
80 130 result =
81 130 case :ets.lookup(@table, journey.journey_id) do
82
:-(
[] ->
83 {:error, :not_found}
84
85 [{_key, _existing}] ->
86 130 :ets.insert(@table, {journey.journey_id, journey})
87 :ok
88 end
89
90 130 {:reply, result, state}
91 end
92
93 @impl true
94 def handle_call({:list_by_user, user_id}, _from, state) do
95
:-(
journey_ids =
96 :ets.lookup(@user_idx, user_id)
97
:-(
|> Enum.map(fn {_uid, jid} -> jid end)
98
99
:-(
journeys =
100 Enum.flat_map(journey_ids, fn journey_id ->
101
:-(
case :ets.lookup(@table, journey_id) do
102
:-(
[{_key, journey}] -> [journey]
103
:-(
[] -> []
104 end
105 end)
106
107
:-(
{:reply, journeys, state}
108 end
109
110 @impl true
111 def handle_call(:reset, _from, state) do
112 21 :ets.delete_all_objects(@table)
113 21 :ets.delete_all_objects(@user_idx)
114 21 {:reply, :ok, state}
115 end
116 end
Line Hits Source