Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,38 @@ on:

jobs:
test:
runs-on: ubuntu-22.04
runs-on: ${{matrix.version.os}}
env:
MIX_ENV: test
name: Elixir ${{matrix.version.elixir}} / OTP ${{matrix.version.otp}}
strategy:
matrix:
version:
[
{ elixir: "1.14", otp: "24" },
{ elixir: "1.14", otp: "25" },
{ elixir: "1.14", otp: "24", os: "ubuntu-22.04" },
{ elixir: "1.14", otp: "25", os: "ubuntu-22.04" },

{ elixir: "1.15", otp: "24" },
{ elixir: "1.15", otp: "25" },
{ elixir: "1.15", otp: "26" },
{ elixir: "1.15", otp: "24", os: "ubuntu-22.04" },
{ elixir: "1.15", otp: "25", os: "ubuntu-22.04" },
{ elixir: "1.15", otp: "26", os: "ubuntu-22.04" },

{ elixir: "1.16", otp: "24" },
{ elixir: "1.16", otp: "25" },
{ elixir: "1.16", otp: "26" },
{ elixir: "1.16", otp: "24", os: "ubuntu-22.04" },
{ elixir: "1.16", otp: "25", os: "ubuntu-22.04" },
{ elixir: "1.16", otp: "26", os: "ubuntu-22.04" },

{ elixir: "1.17", otp: "25" },
{ elixir: "1.17", otp: "26" },
{ elixir: "1.17", otp: "27" },
{ elixir: "1.17", otp: "25", os: "ubuntu-22.04" },
{ elixir: "1.17", otp: "26", os: "ubuntu-22.04" },
{ elixir: "1.17", otp: "27", os: "ubuntu-22.04" },

{ elixir: "1.18", otp: "26", os: "ubuntu-24.04" },
{ elixir: "1.18", otp: "27", os: "ubuntu-24.04" },

{ elixir: "1.19", otp: "27", os: "ubuntu-24.04" },
{ elixir: "1.19", otp: "28", os: "ubuntu-24.04" },

{ elixir: "1.20", otp: "27", os: "ubuntu-24.04" },
{ elixir: "1.20", otp: "28", os: "ubuntu-24.04" },
{ elixir: "1.20", otp: "29", os: "ubuntu-24.04" },
]
steps:
- uses: actions/checkout@v4
Expand All @@ -54,5 +64,8 @@ jobs:
mix local.rebar --force
mix local.hex --force
mix deps.get
mix deps.compile
- name: Compile
run: mix compile --warnings-as-errors
- name: Run Tests
run: mix test
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# storex

## 0.6.2

- Fix type warnings emitted by `use Storex.Store` on Elixir 1.18 and newer

## 0.6.1

- Add missing `cast` to `Storex.Message`
Expand Down
111 changes: 66 additions & 45 deletions lib/storex/store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,56 @@ defmodule Storex.Store do
@callback terminate(session_id :: binary(), params :: %{binary() => any()}, state :: any()) :: any()
@optional_callbacks terminate: 3

@doc false
def __init__(store, session, params) do
apply(store, :init, [session, params])
|> case do
{:ok, state} ->
{:ok, state, nil}

{:ok, state, key} ->
{:ok, state, key}

{:error, reason} ->
{:error, reason}

_ ->
raise "Return value of store init should be {:ok, state}, {:ok, state, key} or {:error, reason}"
end
end

@doc false
def __mutation__(store, name, data, session, params, state) do
try do
apply(store, :mutation, [name, data, session, params, state])
|> case do
{:reply, message, result} ->
{:reply, message, result}

{:noreply, result} ->
{:noreply, result}

{:error, error} ->
{:error, error}

_ ->
{:error,
"Return value of mutation should be {:reply, message, state}, {:noreply, state} or {:error, error}"}
end
rescue
FunctionClauseError ->
{:error,
"No mutation matching #{inspect(name)} with data #{inspect(data)} in store #{inspect(store)}"}
end
Comment on lines +62 to +66
end

@doc false
def __terminate__(store, session, params, state) do
if :erlang.function_exported(store, :terminate, 3) do
apply(store, :terminate, [session, params, state])
end
end

defmacro __using__(_opts) do
quote do
@behaviour Storex.Store
Expand Down Expand Up @@ -59,42 +109,26 @@ defmodule Storex.Store do
end

def handle_cast(:session_ended, state) do
if :erlang.function_exported(@store, :terminate, 3) do
Kernel.apply(@store, :terminate, [state.session, state.params, state.state])
end
Storex.Store.__terminate__(@store, state.session, state.params, state.state)

{:stop, :normal, state}
end

def handle_call({name, data}, _, state) do
try do
Kernel.apply(@store, :mutation, [name, data, state.session, state.params, state.state])
|> case do
{:reply, message, result} ->
diff = Storex.Diff.check(state.state, result)
state = Map.put(state, :state, result)
{:reply, {:ok, message, diff}, state}

{:noreply, result} ->
diff = Storex.Diff.check(state.state, result)
state = Map.put(state, :state, result)
{:reply, {:ok, diff}, state}

{:error, error} ->
{:reply, {:error, error}, state}

_ ->
{:reply,
{:error,
"Return value of mutation should be {:reply, message, state}, {:noreply, state} or {:error, error}"},
state}
end
rescue
e in FunctionClauseError ->
{:reply,
{:error,
"No mutation matching #{inspect(name)} with data #{inspect(data)} in store #{inspect(@store)}"},
state}
Storex.Store.__mutation__(@store, name, data, state.session, state.params, state.state)
|> case do
{:reply, message, result} ->
diff = Storex.Diff.check(state.state, result)
state = Map.put(state, :state, result)
{:reply, {:ok, message, diff}, state}

{:noreply, result} ->
diff = Storex.Diff.check(state.state, result)
state = Map.put(state, :state, result)
{:reply, {:ok, diff}, state}

{:error, error} ->
{:reply, {:error, error}, state}
end
end

Expand All @@ -103,20 +137,7 @@ defmodule Storex.Store do
end

defp init_store(session, params) do
@store.init(session, params)
|> case do
{:ok, state} ->
{:ok, state, nil}

{:ok, state, key} ->
{:ok, state, key}

{:error, reason} ->
{:error, reason}

_ ->
raise "Return value of store init should be {:ok, state}, {:ok, state, key} or {:error, reason}"
end
Storex.Store.__init__(@store, session, params)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Storex.MixProject do
use Mix.Project

@version "0.6.1"
@version "0.6.2"

def project do
[
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "storex",
"version": "0.6.1",
"version": "0.6.2",
"main": "./priv/static/storex.umd.js",
"module": "./priv/static/storex.esm.js",
"types": "./priv/static/storex.d.ts",
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/stores/invalid_init.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule StorexTest.Store.InvalidInit do
use Storex.Store

def init(_session, _params) do
:not_a_valid_return
end

def mutation(_mutation, _data, _session_id, _params, state) do
{:noreply, state}
end
end
15 changes: 15 additions & 0 deletions test/fixtures/stores/invalid_mutation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule StorexTest.Store.InvalidMutation do
use Storex.Store

def init(_session, _params) do
{:ok, %{counter: 0}}
end

def mutation("invalid", _data, _session_id, _params, _state) do
:not_a_valid_return
end

def mutation("error", _data, _session_id, _params, _state) do
{:error, "Not allowed"}
end
end
103 changes: 103 additions & 0 deletions test/storex/store_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule StorexTest.StoreTest do
use ExUnit.Case

alias StorexTest.Store.Counter
alias StorexTest.Store.ErrorInit
alias StorexTest.Store.InvalidInit
alias StorexTest.Store.InvalidMutation
alias StorexTest.Store.KeyInit
alias StorexTest.Store.Text

describe "init dispatch" do
test "{:ok, state} is normalized with a nil key" do
assert Storex.Store.__init__(Counter, "session", %{}) == {:ok, %{counter: 0}, nil}
end

test "{:ok, state, key} keeps the key" do
assert Storex.Store.__init__(KeyInit, "session", %{}) == {:ok, %{counter: 0}, "user_id"}
end

test "{:error, reason} is passed through" do
assert Storex.Store.__init__(ErrorInit, "session", %{}) == {:error, "Unauthorized"}
end

test "params are forwarded to the store" do
assert Storex.Store.__init__(Text, "session", %{"initial_value" => "custom"}) ==
{:ok, "custom", nil}
end

test "an unsupported return value raises" do
assert_raise RuntimeError,
"Return value of store init should be {:ok, state}, {:ok, state, key} or {:error, reason}",
fn -> Storex.Store.__init__(InvalidInit, "session", %{}) end
end
end

describe "mutation dispatch" do
test "{:noreply, state} is passed through" do
assert Storex.Store.__mutation__(Counter, "increase", [], "session", %{}, %{counter: 0}) ==
{:noreply, %{counter: 1}}
end

test "{:reply, message, state} is passed through" do
assert Storex.Store.__mutation__(Counter, "decrease", [], "session", %{}, %{counter: 0}) ==
{:reply, "decreased", %{counter: -1}}
end

test "{:error, reason} is passed through" do
assert Storex.Store.__mutation__(InvalidMutation, "error", [], "session", %{}, %{}) ==
{:error, "Not allowed"}
end

test "an unsupported return value is reported as an error" do
assert Storex.Store.__mutation__(InvalidMutation, "invalid", [], "session", %{}, %{}) ==
{:error,
"Return value of mutation should be {:reply, message, state}, {:noreply, state} or {:error, error}"}
end

test "an unmatched mutation name is reported as an error" do
assert Storex.Store.__mutation__(Counter, "unknown", [1], "session", %{}, %{counter: 0}) ==
{:error,
"No mutation matching \"unknown\" with data [1] in store StorexTest.Store.Counter"}
end
end

describe "store server" do
setup do
session = "session-#{System.unique_integer([:positive])}"

on_exit(fn ->
Storex.Registry.session_stores(session)
|> Enum.each(fn {store, _, _, _, _} -> Storex.Supervisor.remove_store(session, store) end)
end)

%{session: session}
end

test "starts a store returning {:ok, state}", %{session: session} do
assert {:ok, nil} =
Storex.Supervisor.add_store("StorexTest.Store.Counter", session, self(), %{})

assert Storex.Supervisor.get_store_state(session, "StorexTest.Store.Counter") == %{
counter: 0
}
end

test "starts a store returning {:ok, state, key}", %{session: session} do
assert {:ok, "user_id"} =
Storex.Supervisor.add_store("StorexTest.Store.KeyInit", session, self(), %{})
end

test "does not start a store returning {:error, reason}", %{session: session} do
assert {:error, "Unauthorized"} =
Storex.Supervisor.add_store("StorexTest.Store.ErrorInit", session, self(), %{})
end

test "mutating through the server returns the state diff", %{session: session} do
{:ok, _} = Storex.Supervisor.add_store("StorexTest.Store.Counter", session, self(), %{})

assert {:ok, [%{a: "u", p: [:counter], t: 1}]} =
Storex.Supervisor.mutate_store(session, "StorexTest.Store.Counter", "increase", [])
end
end
end
Loading