From e516edc6e22a45850e25a28397ff951163b8f837 Mon Sep 17 00:00:00 2001 From: czh <2565523901@qq.com> Date: Mon, 9 Mar 2026 19:04:55 +0800 Subject: [PATCH] feat: add boundary checks and error handling for Rating and State enums Add proper input validation for enum conversion functions: - from_int/1 now returns {:ok, value} | :error tuple - Add from_int!/1 with explicit ArgumentError on invalid input - Add valid?/1 for quick validation checks - Update @spec types for better dialyzer support - Add comprehensive @doc with examples This improves API robustness and provides better error messages for invalid enum values instead of FunctionClauseError. --- lib/lulucat/fsrs_rating.ex | 65 ++++++++++++++++++++++++++++++++++---- lib/lulucat/fsrs_state.ex | 62 +++++++++++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/lib/lulucat/fsrs_rating.ex b/lib/lulucat/fsrs_rating.ex index 933befb..3bb3c66 100644 --- a/lib/lulucat/fsrs_rating.ex +++ b/lib/lulucat/fsrs_rating.ex @@ -34,12 +34,65 @@ defmodule Fsrs.Rating do @doc """ Converts integer rating to atom representation. + Returns `{:ok, rating}` on success, `:error` on invalid input. - 中文说明:整数评分转原子。 + 中文说明:整数评分转原子,成功返回 `{:ok, rating}`,无效输入返回 `:error`。 + + ## Examples + + iex> Fsrs.Rating.from_int(1) + {:ok, :again} + + iex> Fsrs.Rating.from_int(5) + :error + """ + @spec from_int(integer()) :: {:ok, t()} | :error + def from_int(1), do: {:ok, :again} + def from_int(2), do: {:ok, :hard} + def from_int(3), do: {:ok, :good} + def from_int(4), do: {:ok, :easy} + def from_int(_), do: :error + + @doc """ + Converts integer rating to atom representation. + Raises `ArgumentError` on invalid input. + + 中文说明:整数评分转原子,无效输入抛出 `ArgumentError`。 + + ## Examples + + iex> Fsrs.Rating.from_int!(1) + :again + + iex> Fsrs.Rating.from_int!(5) + ** (ArgumentError) invalid rating: 5, expected 1-4 + """ + @spec from_int!(integer()) :: t() + def from_int!(n) do + case from_int(n) do + {:ok, rating} -> rating + :error -> raise ArgumentError, "invalid rating: #{n}, expected 1-4" + end + end + + @doc """ + Checks if the given value is a valid rating. + + 中文说明:检查给定值是否为有效评分。 + + ## Examples + + iex> Fsrs.Rating.valid?(:good) + true + + iex> Fsrs.Rating.valid?(5) + false """ - @spec from_int(integer()) :: t() - def from_int(1), do: :again - def from_int(2), do: :hard - def from_int(3), do: :good - def from_int(4), do: :easy + @spec valid?(t() | integer()) :: boolean() + def valid?(:again), do: true + def valid?(:hard), do: true + def valid?(:good), do: true + def valid?(:easy), do: true + def valid?(n) when is_integer(n) and n in 1..4, do: true + def valid?(_), do: false end diff --git a/lib/lulucat/fsrs_state.ex b/lib/lulucat/fsrs_state.ex index c80bfe5..9404d02 100644 --- a/lib/lulucat/fsrs_state.ex +++ b/lib/lulucat/fsrs_state.ex @@ -32,11 +32,63 @@ defmodule Fsrs.State do @doc """ Converts integer state to atom representation. + Returns `{:ok, state}` on success, `:error` on invalid input. - 中文说明:整数状态转原子。 + 中文说明:整数状态转原子,成功返回 `{:ok, state}`,无效输入返回 `:error`。 + + ## Examples + + iex> Fsrs.State.from_int(1) + {:ok, :learning} + + iex> Fsrs.State.from_int(5) + :error + """ + @spec from_int(integer()) :: {:ok, t()} | :error + def from_int(1), do: {:ok, :learning} + def from_int(2), do: {:ok, :review} + def from_int(3), do: {:ok, :relearning} + def from_int(_), do: :error + + @doc """ + Converts integer state to atom representation. + Raises `ArgumentError` on invalid input. + + 中文说明:整数状态转原子,无效输入抛出 `ArgumentError`。 + + ## Examples + + iex> Fsrs.State.from_int!(1) + :learning + + iex> Fsrs.State.from_int!(5) + ** (ArgumentError) invalid state: 5, expected 1-3 + """ + @spec from_int!(integer()) :: t() + def from_int!(n) do + case from_int(n) do + {:ok, state} -> state + :error -> raise ArgumentError, "invalid state: #{n}, expected 1-3" + end + end + + @doc """ + Checks if the given value is a valid state. + + 中文说明:检查给定值是否为有效状态。 + + ## Examples + + iex> Fsrs.State.valid?(:learning) + true + + iex> Fsrs.State.valid?(5) + false """ - @spec from_int(integer()) :: t() - def from_int(1), do: :learning - def from_int(2), do: :review - def from_int(3), do: :relearning + @spec valid?(t() | integer()) :: boolean() + def valid?(:learning), do: true + def valid?(:review), do: true + def valid?(:relearning), do: true + def valid?(n) when is_integer(n) and n in 1..3, do: true + def valid?(_), do: false end