diff --git a/.iex.exs b/.iex.exs index e7c2cf7..52a238b 100644 --- a/.iex.exs +++ b/.iex.exs @@ -11,6 +11,7 @@ alias Pinchflat.Tasks alias Pinchflat.Media alias Pinchflat.Profiles alias Pinchflat.Sources +alias Pinchflat.Settings alias Pinchflat.MediaClient.{SourceDetails, MediaDownloader} alias Pinchflat.Metadata.{Zipper, ThumbnailFetcher} diff --git a/lib/pinchflat/application.ex b/lib/pinchflat/application.ex index 81ba2f0..1527d7d 100644 --- a/lib/pinchflat/application.ex +++ b/lib/pinchflat/application.ex @@ -10,6 +10,8 @@ defmodule Pinchflat.Application do children = [ PinchflatWeb.Telemetry, Pinchflat.Repo, + # {Task, &run_startup_tasks/0}, + Pinchflat.StartupTasks, {Oban, Application.fetch_env!(:pinchflat, Oban)}, {DNSCluster, query: Application.get_env(:pinchflat, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Pinchflat.PubSub}, diff --git a/lib/pinchflat/settings.ex b/lib/pinchflat/settings.ex index 14c734e..29db085 100644 --- a/lib/pinchflat/settings.ex +++ b/lib/pinchflat/settings.ex @@ -19,7 +19,8 @@ defmodule Pinchflat.Settings do @doc """ Creates or updates a setting, returning the parsed value. - Raises if an unsupported datatype is used. + Raises if an unsupported datatype is used. Optionally allows + specifying the datatype. Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)` """ @@ -46,6 +47,24 @@ defmodule Pinchflat.Settings do |> read_setting() end + @doc """ + Attempts to find a setting by name or creates a setting with value + if one doesn't exist, returning the parsed value. Optionally allows + specifying the datatype. + + Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)` + """ + def fetch!(name, value) do + fetch!(name, value, infer_datatype(value)) + end + + def fetch!(name, value, datatype) do + case Repo.get_by(Setting, name: to_string(name)) do + nil -> create_setting!(name, value, datatype) + setting -> read_setting(setting) + end + end + defp change_setting(setting, attrs) do Setting.changeset(setting, attrs) end diff --git a/lib/pinchflat/startup_tasks.ex b/lib/pinchflat/startup_tasks.ex new file mode 100644 index 0000000..41d0597 --- /dev/null +++ b/lib/pinchflat/startup_tasks.ex @@ -0,0 +1,36 @@ +defmodule Pinchflat.StartupTasks do + @moduledoc """ + This module is responsible for running startup tasks on app boot. + + It's a GenServer because that plays REALLY nicely with the existing + Phoenix supervision tree. + """ + + # restart: :temporary means that this process will never be restarted (ie: will run once and then die) + use GenServer, restart: :temporary + + alias Pinchflat.Settings + + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, %{}, opts) + end + + @doc """ + Runs application startup tasks. + + Any code defined here will run every time the application starts. You must + make sure that the code is idempotent and safe to run multiple times. + + This is a good place to set up default settings, create initial records, stuff like that + """ + @impl true + def init(state) do + apply_default_settings() + + {:ok, state} + end + + defp apply_default_settings do + Settings.fetch!(:onboarding, true) + end +end diff --git a/test/pinchflat/settings_test.exs b/test/pinchflat/settings_test.exs index 055dc35..443538a 100644 --- a/test/pinchflat/settings_test.exs +++ b/test/pinchflat/settings_test.exs @@ -4,25 +4,32 @@ defmodule Pinchflat.SettingsTest do alias Pinchflat.Settings alias Pinchflat.Settings.Setting + # NOTE: We're treating some of these tests differently + # than in other modules because certain settings + # are always created on app boot (including in the test env), + # so we can't treat these like a clean slate. + describe "list_settings/0" do test "returns all settings" do Settings.set!("foo", "bar") - assert [_] = Settings.list_settings() + results = Settings.list_settings() + + assert Enum.all?(results, fn setting -> match?(%Setting{}, setting) end) end end describe "set/2" do test "creates a new setting if one does not exist" do - assert Repo.aggregate(Setting, :count, :id) == 0 + original = Repo.aggregate(Setting, :count, :id) Settings.set!("foo", "bar") - assert Repo.aggregate(Setting, :count, :id) == 1 + assert Repo.aggregate(Setting, :count, :id) == original + 1 end test "updates an existing setting if one exists" do Settings.set!("foo", "bar") - assert Repo.aggregate(Setting, :count, :id) == 1 + original = Repo.aggregate(Setting, :count, :id) Settings.set!("foo", "baz") - assert Repo.aggregate(Setting, :count, :id) == 1 + assert Repo.aggregate(Setting, :count, :id) == original assert Settings.get!("foo") == "baz" end @@ -78,4 +85,24 @@ defmodule Pinchflat.SettingsTest do end end end + + describe "fetch/2" do + test "creates a setting if one doesn't exist" do + original = Repo.aggregate(Setting, :count, :id) + assert Settings.fetch!("foo", "bar") == "bar" + assert Repo.aggregate(Setting, :count, :id) == original + 1 + end + + test "returns an existing setting if one does exist" do + Settings.set!("foo", "bar") + + assert Settings.fetch!("foo", "baz") == "bar" + end + end + + describe "fetch/3" do + test "allows manual specification of datatype" do + assert Settings.fetch!("foo", "true", :boolean) == true + end + end end diff --git a/test/pinchflat/startup_tasks_test.exs b/test/pinchflat/startup_tasks_test.exs new file mode 100644 index 0000000..ef06d0a --- /dev/null +++ b/test/pinchflat/startup_tasks_test.exs @@ -0,0 +1,16 @@ +defmodule Pinchflat.StartupTasksTest do + use Pinchflat.DataCase + + alias Pinchflat.Settings + + # Since this runs on app boot (even in the test env), + # any actions in the `init/1` function will already have + # run. So we can only test the side effects of those actions, + # rather than the actions themselves. + + describe "apply_default_settings" do + test "sets default settings" do + assert Settings.get!(:onboarding) == true + end + end +end