Created new settings table, schema, and context

This commit is contained in:
Kieran Eglin 2024-04-04 11:40:27 -07:00
parent 4ff2cc445f
commit 3a6930457b
No known key found for this signature in database
GPG key ID: 193984967FCF432D
5 changed files with 253 additions and 80 deletions

View 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

View 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

View 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

View 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

View file

@ -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