Created new settings table, schema, and context
This commit is contained in:
parent
4ff2cc445f
commit
3a6930457b
5 changed files with 253 additions and 80 deletions
28
lib/pinchflat/settings/setting.ex
Normal file
28
lib/pinchflat/settings/setting.ex
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule Pinchflat.Settings.Setting do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@allowed_fields [
|
||||||
|
:onboarding,
|
||||||
|
:pro_enabled,
|
||||||
|
:yt_dlp_version
|
||||||
|
]
|
||||||
|
|
||||||
|
@required_fields ~w(
|
||||||
|
onboarding
|
||||||
|
pro_enabled
|
||||||
|
)a
|
||||||
|
|
||||||
|
schema "settings" do
|
||||||
|
field :onboarding, :boolean, default: false
|
||||||
|
field :pro_enabled, :boolean, default: false
|
||||||
|
field :yt_dlp_version, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(setting, attrs) do
|
||||||
|
setting
|
||||||
|
|> cast(attrs, @allowed_fields)
|
||||||
|
|> validate_required(@required_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
64
lib/pinchflat/settings/settings.ex
Normal file
64
lib/pinchflat/settings/settings.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule Pinchflat.Settings do
|
||||||
|
@moduledoc """
|
||||||
|
The Settings context.
|
||||||
|
"""
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Settings.Setting
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the only setting record. It _should_ be impossible
|
||||||
|
to create or delete this record, so it's assertive about
|
||||||
|
assuming it's the only one.
|
||||||
|
|
||||||
|
Returns %Setting{}
|
||||||
|
"""
|
||||||
|
def record do
|
||||||
|
Setting
|
||||||
|
|> limit(1)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a setting, returning the new value.
|
||||||
|
Is setup to take a keyword list argument so you
|
||||||
|
can call it like `Settings.set(onboarding: true)`
|
||||||
|
|
||||||
|
Returns {:ok, value} | {:error, :invalid_key} | {:error, %Ecto.Changeset{}}
|
||||||
|
"""
|
||||||
|
def set([{attr, value}]) do
|
||||||
|
record()
|
||||||
|
|> Setting.changeset(%{attr => value})
|
||||||
|
|> Repo.update()
|
||||||
|
|> case do
|
||||||
|
{:ok, %{^attr => _}} -> {:ok, value}
|
||||||
|
{:ok, _} -> {:error, :invalid_key}
|
||||||
|
{:error, changeset} -> {:error, changeset}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the value of a setting.
|
||||||
|
|
||||||
|
Returns {:ok, value} | {:error, :invalid_key}
|
||||||
|
"""
|
||||||
|
def get(name) do
|
||||||
|
case Map.fetch(record(), name) do
|
||||||
|
{:ok, value} -> {:ok, value}
|
||||||
|
:error -> {:error, :invalid_key}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the value of a setting, raising if it doesn't exist.
|
||||||
|
|
||||||
|
Returns value
|
||||||
|
"""
|
||||||
|
def get!(name) do
|
||||||
|
case get(name) do
|
||||||
|
{:ok, value} -> value
|
||||||
|
{:error, _} -> raise "Setting `#{name}` not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
priv/repo/migrations/20240404174144_create_settings.exs
Normal file
18
priv/repo/migrations/20240404174144_create_settings.exs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Pinchflat.Repo.Migrations.CreateSettings do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
create table(:settings) do
|
||||||
|
add :onboarding, :boolean, default: false, null: false
|
||||||
|
add :pro_enabled, :boolean, default: false, null: false
|
||||||
|
add :yt_dlp_version, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make an initial record because this will be the only one ever inserted
|
||||||
|
execute "INSERT INTO settings (onboarding, pro_enabled, yt_dlp_version) VALUES (false, false, NULL)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop table(:settings)
|
||||||
|
end
|
||||||
|
end
|
||||||
108
test/pinchflat/settings_backup_test.exs
Normal file
108
test/pinchflat/settings_backup_test.exs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
defmodule Pinchflat.SettingsBackupTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.SettingsBackup
|
||||||
|
alias Pinchflat.SettingsBackup.SettingBackup
|
||||||
|
|
||||||
|
# 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
|
||||||
|
SettingsBackup.set!("foo", "bar")
|
||||||
|
results = SettingsBackup.list_settings()
|
||||||
|
|
||||||
|
assert Enum.all?(results, fn setting -> match?(%SettingBackup{}, setting) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "set/2" do
|
||||||
|
test "creates a new setting if one does not exist" do
|
||||||
|
original = Repo.aggregate(SettingBackup, :count, :id)
|
||||||
|
SettingsBackup.set!("foo", "bar")
|
||||||
|
assert Repo.aggregate(SettingBackup, :count, :id) == original + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates an existing setting if one exists" do
|
||||||
|
SettingsBackup.set!("foo", "bar")
|
||||||
|
original = Repo.aggregate(SettingBackup, :count, :id)
|
||||||
|
SettingsBackup.set!("foo", "baz")
|
||||||
|
assert Repo.aggregate(SettingBackup, :count, :id) == original
|
||||||
|
assert SettingsBackup.get!("foo") == "baz"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the parsed value" do
|
||||||
|
assert SettingsBackup.set!("foo", true) == true
|
||||||
|
assert SettingsBackup.set!("foo", false) == false
|
||||||
|
assert SettingsBackup.set!("foo", 123) == 123
|
||||||
|
assert SettingsBackup.set!("foo", 12.34) == 12.34
|
||||||
|
assert SettingsBackup.set!("foo", "bar") == "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows for atom keys" do
|
||||||
|
assert SettingsBackup.set!(:foo, "bar") == "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blows up when an unsupported datatype is used" do
|
||||||
|
assert_raise FunctionClauseError, fn ->
|
||||||
|
SettingsBackup.set!("foo", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "set/3" do
|
||||||
|
test "allows manual specification of datatype" do
|
||||||
|
assert SettingsBackup.set!("foo", "true", :boolean) == true
|
||||||
|
assert SettingsBackup.set!("foo", "false", :boolean) == false
|
||||||
|
assert SettingsBackup.set!("foo", "123", :integer) == 123
|
||||||
|
assert SettingsBackup.set!("foo", "12.34", :float) == 12.34
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get/1" do
|
||||||
|
test "returns the value of the setting" do
|
||||||
|
SettingsBackup.set!("str", "bar")
|
||||||
|
SettingsBackup.set!("bool", true)
|
||||||
|
SettingsBackup.set!("int", 123)
|
||||||
|
SettingsBackup.set!("float", 12.34)
|
||||||
|
|
||||||
|
assert SettingsBackup.get!("str") == "bar"
|
||||||
|
assert SettingsBackup.get!("bool") == true
|
||||||
|
assert SettingsBackup.get!("int") == 123
|
||||||
|
assert SettingsBackup.get!("float") == 12.34
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows for atom keys" do
|
||||||
|
SettingsBackup.set!("str", "bar")
|
||||||
|
assert SettingsBackup.get!(:str) == "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blows up when the setting does not exist" do
|
||||||
|
assert_raise Ecto.NoResultsError, fn ->
|
||||||
|
SettingsBackup.get!("foo")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch/2" do
|
||||||
|
test "creates a setting if one doesn't exist" do
|
||||||
|
original = Repo.aggregate(SettingBackup, :count, :id)
|
||||||
|
assert SettingsBackup.fetch!("foo", "bar") == "bar"
|
||||||
|
assert Repo.aggregate(SettingBackup, :count, :id) == original + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an existing setting if one does exist" do
|
||||||
|
SettingsBackup.set!("foo", "bar")
|
||||||
|
|
||||||
|
assert SettingsBackup.fetch!("foo", "baz") == "bar"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "fetch/3" do
|
||||||
|
test "allows manual specification of datatype" do
|
||||||
|
assert SettingsBackup.fetch!("foo", "true", :boolean) == true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,108 +1,63 @@
|
||||||
defmodule Pinchflat.SettingsBackupTest do
|
defmodule Pinchflat.SettingsTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
alias Pinchflat.SettingsBackup
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.SettingsBackup.SettingBackup
|
alias Pinchflat.Settings.Setting
|
||||||
|
|
||||||
# NOTE: We're treating some of these tests differently
|
# NOTE: We're treating some of these tests differently
|
||||||
# than in other modules because certain settings
|
# than in other modules because certain settings
|
||||||
# are always created on app boot (including in the test env),
|
# are always created on app boot (including in the test env),
|
||||||
# so we can't treat these like a clean slate.
|
# so we can't treat these like a clean slate.
|
||||||
|
|
||||||
describe "list_settings/0" do
|
setup do
|
||||||
test "returns all settings" do
|
# Ensure we have a clean slate
|
||||||
SettingsBackup.set!("foo", "bar")
|
Settings.set(onboarding: false)
|
||||||
results = SettingsBackup.list_settings()
|
Settings.set(pro_enabled: false)
|
||||||
|
Settings.set(yt_dlp_version: nil)
|
||||||
|
|
||||||
assert Enum.all?(results, fn setting -> match?(%SettingBackup{}, setting) end)
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "record/0" do
|
||||||
|
test "returns the only setting" do
|
||||||
|
assert %Setting{} = Settings.record()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "set/2" do
|
describe "set/1" do
|
||||||
test "creates a new setting if one does not exist" do
|
test "updates the setting" do
|
||||||
original = Repo.aggregate(SettingBackup, :count, :id)
|
assert {:ok, true} = Settings.set(onboarding: true)
|
||||||
SettingsBackup.set!("foo", "bar")
|
assert {:ok, true} = Settings.get(:onboarding)
|
||||||
assert Repo.aggregate(SettingBackup, :count, :id) == original + 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates an existing setting if one exists" do
|
test "returns an error if the setting key doesn't exist" do
|
||||||
SettingsBackup.set!("foo", "bar")
|
assert {:error, :invalid_key} = Settings.set(foo: "bar")
|
||||||
original = Repo.aggregate(SettingBackup, :count, :id)
|
|
||||||
SettingsBackup.set!("foo", "baz")
|
|
||||||
assert Repo.aggregate(SettingBackup, :count, :id) == original
|
|
||||||
assert SettingsBackup.get!("foo") == "baz"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the parsed value" do
|
test "returns an error if the setting value is invalid" do
|
||||||
assert SettingsBackup.set!("foo", true) == true
|
assert {:error, %Ecto.Changeset{}} = Settings.set(onboarding: "bar")
|
||||||
assert SettingsBackup.set!("foo", false) == false
|
|
||||||
assert SettingsBackup.set!("foo", 123) == 123
|
|
||||||
assert SettingsBackup.set!("foo", 12.34) == 12.34
|
|
||||||
assert SettingsBackup.set!("foo", "bar") == "bar"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows for atom keys" do
|
|
||||||
assert SettingsBackup.set!(:foo, "bar") == "bar"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "blows up when an unsupported datatype is used" do
|
|
||||||
assert_raise FunctionClauseError, fn ->
|
|
||||||
SettingsBackup.set!("foo", nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "set/3" do
|
|
||||||
test "allows manual specification of datatype" do
|
|
||||||
assert SettingsBackup.set!("foo", "true", :boolean) == true
|
|
||||||
assert SettingsBackup.set!("foo", "false", :boolean) == false
|
|
||||||
assert SettingsBackup.set!("foo", "123", :integer) == 123
|
|
||||||
assert SettingsBackup.set!("foo", "12.34", :float) == 12.34
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get/1" do
|
describe "get/1" do
|
||||||
test "returns the value of the setting" do
|
test "returns the setting value" do
|
||||||
SettingsBackup.set!("str", "bar")
|
assert {:ok, false} = Settings.get(:onboarding)
|
||||||
SettingsBackup.set!("bool", true)
|
|
||||||
SettingsBackup.set!("int", 123)
|
|
||||||
SettingsBackup.set!("float", 12.34)
|
|
||||||
|
|
||||||
assert SettingsBackup.get!("str") == "bar"
|
|
||||||
assert SettingsBackup.get!("bool") == true
|
|
||||||
assert SettingsBackup.get!("int") == 123
|
|
||||||
assert SettingsBackup.get!("float") == 12.34
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "allows for atom keys" do
|
test "returns an error if the setting key doesn't exist" do
|
||||||
SettingsBackup.set!("str", "bar")
|
assert {:error, :invalid_key} = Settings.get(:foo)
|
||||||
assert SettingsBackup.get!(:str) == "bar"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "blows up when the setting does not exist" do
|
|
||||||
assert_raise Ecto.NoResultsError, fn ->
|
|
||||||
SettingsBackup.get!("foo")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "fetch/2" do
|
describe "get!/1" do
|
||||||
test "creates a setting if one doesn't exist" do
|
test "returns the setting value" do
|
||||||
original = Repo.aggregate(SettingBackup, :count, :id)
|
assert Settings.get!(:onboarding) == false
|
||||||
assert SettingsBackup.fetch!("foo", "bar") == "bar"
|
|
||||||
assert Repo.aggregate(SettingBackup, :count, :id) == original + 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an existing setting if one does exist" do
|
test "raises an error if the setting key doesn't exist" do
|
||||||
SettingsBackup.set!("foo", "bar")
|
assert_raise RuntimeError, "Setting `foo` not found", fn ->
|
||||||
|
Settings.get!(:foo)
|
||||||
assert SettingsBackup.fetch!("foo", "baz") == "bar"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "fetch/3" do
|
|
||||||
test "allows manual specification of datatype" do
|
|
||||||
assert SettingsBackup.fetch!("foo", "true", :boolean) == true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue