diff --git a/lib/pinchflat/profiles/media_profile.ex b/lib/pinchflat/profiles/media_profile.ex index 7b72b8c..c8e3aee 100644 --- a/lib/pinchflat/profiles/media_profile.ex +++ b/lib/pinchflat/profiles/media_profile.ex @@ -1,6 +1,6 @@ defmodule Pinchflat.Profiles.MediaProfile do @moduledoc """ - A media profile is a set of settings that can be applied to many media sources + A media profile is a set of configuration options that can be applied to many media sources """ use Ecto.Schema diff --git a/lib/pinchflat/settings.ex b/lib/pinchflat/settings.ex new file mode 100644 index 0000000..14c734e --- /dev/null +++ b/lib/pinchflat/settings.ex @@ -0,0 +1,76 @@ +defmodule Pinchflat.Settings do + @moduledoc """ + The Settings context. + """ + + import Ecto.Query, warn: false + alias Pinchflat.Repo + + alias Pinchflat.Settings.Setting + + @doc """ + Returns the list of settings. + + Returns [%Setting{}, ...] + """ + def list_settings do + Repo.all(Setting) + end + + @doc """ + Creates or updates a setting, returning the parsed value. + Raises if an unsupported datatype is used. + + Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)` + """ + def set!(name, value) do + set!(name, value, infer_datatype(value)) + end + + def set!(name, value, datatype) do + # Only create if doesn't exist + case Repo.get_by(Setting, name: to_string(name)) do + nil -> create_setting!(name, value, datatype) + setting -> update_setting!(setting, value, datatype) + end + end + + @doc """ + Gets the parsed value of a setting. Raises if the setting does not exist. + + Returns value in type of `Ecto.Enum.mappings(Setting, :datatype)` + """ + def get!(name) do + Setting + |> Repo.get_by!(name: to_string(name)) + |> read_setting() + end + + defp change_setting(setting, attrs) do + Setting.changeset(setting, attrs) + end + + defp create_setting!(name, value, datatype) do + %Setting{} + |> change_setting(%{name: to_string(name), value: to_string(value), datatype: datatype}) + |> Repo.insert!() + |> read_setting() + end + + defp update_setting!(setting, value, datatype) do + setting + |> change_setting(%{value: to_string(value), datatype: datatype}) + |> Repo.update!() + |> read_setting() + end + + defp read_setting(%{value: value, datatype: :string}), do: value + defp read_setting(%{value: value, datatype: :boolean}), do: value in ["true", "t", "1"] + defp read_setting(%{value: value, datatype: :integer}), do: String.to_integer(value) + defp read_setting(%{value: value, datatype: :float}), do: String.to_float(value) + + defp infer_datatype(value) when is_boolean(value), do: :boolean + defp infer_datatype(value) when is_integer(value), do: :integer + defp infer_datatype(value) when is_float(value), do: :float + defp infer_datatype(value) when is_binary(value), do: :string +end diff --git a/lib/pinchflat/settings/setting.ex b/lib/pinchflat/settings/setting.ex new file mode 100644 index 0000000..fab97b9 --- /dev/null +++ b/lib/pinchflat/settings/setting.ex @@ -0,0 +1,24 @@ +defmodule Pinchflat.Settings.Setting do + @moduledoc """ + A Setting is a key-value pair with a datatype used to track user-level settings. + """ + + use Ecto.Schema + import Ecto.Changeset + + schema "settings" do + field :name, :string + field :value, :string + field :datatype, Ecto.Enum, values: ~w(boolean string integer float)a + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(setting, attrs) do + setting + |> cast(attrs, [:name, :value, :datatype]) + |> validate_required([:name, :value, :datatype]) + |> unique_constraint([:name]) + end +end diff --git a/priv/repo/migrations/20240306173305_create_settings.exs b/priv/repo/migrations/20240306173305_create_settings.exs new file mode 100644 index 0000000..5d84539 --- /dev/null +++ b/priv/repo/migrations/20240306173305_create_settings.exs @@ -0,0 +1,15 @@ +defmodule Pinchflat.Repo.Migrations.CreateSettings do + use Ecto.Migration + + def change do + create table(:settings) do + add :name, :string, null: false + add :value, :string, null: false + add :datatype, :string, null: false + + timestamps(type: :utc_datetime) + end + + create unique_index(:settings, [:name]) + end +end diff --git a/test/pinchflat/settings_test.exs b/test/pinchflat/settings_test.exs new file mode 100644 index 0000000..055dc35 --- /dev/null +++ b/test/pinchflat/settings_test.exs @@ -0,0 +1,81 @@ +defmodule Pinchflat.SettingsTest do + use Pinchflat.DataCase + + alias Pinchflat.Settings + alias Pinchflat.Settings.Setting + + describe "list_settings/0" do + test "returns all settings" do + Settings.set!("foo", "bar") + assert [_] = Settings.list_settings() + end + end + + describe "set/2" do + test "creates a new setting if one does not exist" do + assert Repo.aggregate(Setting, :count, :id) == 0 + Settings.set!("foo", "bar") + assert Repo.aggregate(Setting, :count, :id) == 1 + end + + test "updates an existing setting if one exists" do + Settings.set!("foo", "bar") + assert Repo.aggregate(Setting, :count, :id) == 1 + Settings.set!("foo", "baz") + assert Repo.aggregate(Setting, :count, :id) == 1 + assert Settings.get!("foo") == "baz" + end + + test "returns the parsed value" do + assert Settings.set!("foo", true) == true + assert Settings.set!("foo", false) == false + assert Settings.set!("foo", 123) == 123 + assert Settings.set!("foo", 12.34) == 12.34 + assert Settings.set!("foo", "bar") == "bar" + end + + test "allows for atom keys" do + assert Settings.set!(:foo, "bar") == "bar" + end + + test "blows up when an unsupported datatype is used" do + assert_raise FunctionClauseError, fn -> + Settings.set!("foo", nil) + end + end + end + + describe "set/3" do + test "allows manual specification of datatype" do + assert Settings.set!("foo", "true", :boolean) == true + assert Settings.set!("foo", "false", :boolean) == false + assert Settings.set!("foo", "123", :integer) == 123 + assert Settings.set!("foo", "12.34", :float) == 12.34 + end + end + + describe "get/1" do + test "returns the value of the setting" do + Settings.set!("str", "bar") + Settings.set!("bool", true) + Settings.set!("int", 123) + Settings.set!("float", 12.34) + + assert Settings.get!("str") == "bar" + assert Settings.get!("bool") == true + assert Settings.get!("int") == 123 + assert Settings.get!("float") == 12.34 + end + + test "allows for atom keys" do + Settings.set!("str", "bar") + assert Settings.get!(:str) == "bar" + end + + test "blows up when the setting does not exist" do + assert_raise Ecto.NoResultsError, fn -> + Settings.get!("foo") + end + end + end +end