Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/lulucat/fsrs/ecto.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Fsrs.Ecto do
@moduledoc """
Ecto integration for FSRS types.

This module provides Ecto.Type implementations for Fsrs structs,
allowing them to be stored in database fields (typically as JSON).

## Available Types

- `Fsrs.Ecto.CardType` - For storing Fsrs.Card structs
- `Fsrs.Ecto.SchedulerType` - For storing Fsrs.Scheduler configurations

## Installation

To use Ecto types, add `:ecto` to your dependencies:

def deps do
[
{:fsrs_ex, "~> 0.1.2"},
{:ecto, "~> 3.0"} # Optional, for database integration
]
end

## Example Schema

defmodule MyApp.Card do
use Ecto.Schema

schema "cards" do
field :content, :string
field :fsrs_data, Fsrs.Ecto.CardType
timestamps()
end
end

## Migration Example

defmodule MyApp.Repo.Migrations.CreateCards do
use Ecto.Migration

def change do
create table(:cards) do
add :content, :text
add :fsrs_data, :map # JSON/JSONB column
timestamps()
end
end
end

中文说明:为 FSRS 类型提供 Ecto 集成,支持数据库存储。
"""
end
66 changes: 66 additions & 0 deletions lib/lulucat/fsrs/ecto/card_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Fsrs.Ecto.CardType do
@moduledoc """
Ecto type for Fsrs.Card serialization.

This module implements the `Ecto.Type` behaviour to allow storing
Fsrs.Card structs in database fields (typically as JSON/BSON).

## Usage

schema "cards" do
field :fsrs_data, Fsrs.Ecto.CardType
end

The card is stored as a JSON object with all fields serialized.

中文说明:为 Fsrs.Card 实现的 Ecto 类型,支持数据库存储。
"""

use Ecto.Type

alias Fsrs.Card

@impl true
def type, do: :map

@impl true
def cast(%Card{} = card) do
{:ok, Card.to_dict(card)}
end

def cast(%{} = map) do
try do
{:ok, Card.from_dict(map)}
rescue
_ -> :error
end
end

def cast(_), do: :error

@impl true
def load(data) when is_map(data) do
try do
{:ok, Card.from_dict(data)}
rescue
_ -> :error
end
end

def load(_), do: :error

@impl true
def dump(%Card{} = card) do
{:ok, Card.to_dict(card)}
end

def dump(_), do: :error

@impl true
def embed_as(_format), do: :self

@impl true
def equal?(term1, term2) do
term1 == term2
end
end
64 changes: 64 additions & 0 deletions lib/lulucat/fsrs/ecto/scheduler_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule Fsrs.Ecto.SchedulerType do
@moduledoc """
Ecto type for Fsrs.Scheduler serialization.

This module implements the `Ecto.Type` behaviour to allow storing
Fsrs.Scheduler configurations in database fields.

## Usage

schema "users" do
field :scheduler_config, Fsrs.Ecto.SchedulerType
end

中文说明:为 Fsrs.Scheduler 实现的 Ecto 类型,支持数据库存储。
"""

use Ecto.Type

alias Fsrs.Scheduler

@impl true
def type, do: :map

@impl true
def cast(%Scheduler{} = scheduler) do
{:ok, Scheduler.to_dict(scheduler)}
end

def cast(%{} = map) do
try do
{:ok, Scheduler.from_dict(map)}
rescue
_ -> :error
end
end

def cast(_), do: :error

@impl true
def load(data) when is_map(data) do
try do
{:ok, Scheduler.from_dict(data)}
rescue
_ -> :error
end
end

def load(_), do: :error

@impl true
def dump(%Scheduler{} = scheduler) do
{:ok, Scheduler.to_dict(scheduler)}
end

def dump(_), do: :error

@impl true
def embed_as(_format), do: :self

@impl true
def equal?(term1, term2) do
term1 == term2
end
end
77 changes: 77 additions & 0 deletions test/lulucat/fsrs_ecto/card_type_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Fsrs.Ecto.CardTypeTest do
@moduledoc """
Tests for Fsrs.Ecto.CardType.
These tests are only run if Ecto is available.
"""

use ExUnit.Case, async: true

# Check if Ecto is available
@ecto_available Code.ensure_loaded?(Ecto.Type)

if @ecto_available do
alias Fsrs.Ecto.CardType

describe "CardType Ecto type" do
test "type returns :map" do
assert CardType.type() == :map
end

test "cast accepts Card structs" do
card = Fsrs.new_card()
assert {:ok, _} = CardType.cast(card)
end

test "cast accepts valid maps" do
card = Fsrs.new_card()
map = Fsrs.Card.to_dict(card)
assert {:ok, _} = CardType.cast(map)
end

test "cast rejects invalid data" do
assert :error = CardType.cast("invalid")
assert :error = CardType.cast(123)
end

test "load converts map to Card" do
card = Fsrs.new_card()
map = Fsrs.Card.to_dict(card)
assert {:ok, loaded_card} = CardType.load(map)
assert %Fsrs.Card{} = loaded_card
assert loaded_card.card_id == card.card_id
end

test "load returns error for invalid data" do
assert :error = CardType.load("invalid")
assert :error = CardType.load(%{invalid: "data"})
end

test "dump converts Card to map" do
card = Fsrs.new_card()
assert {:ok, map} = CardType.dump(card)
assert is_map(map)
assert map["card_id"] == card.card_id
end

test "dump returns error for non-Card" do
assert :error = CardType.dump("invalid")
assert :error = CardType.dump(%{card_id: 123})
end

test "embed_as returns :self" do
assert CardType.embed_as(nil) == :self
end

test "equal? compares Cards correctly" do
card1 = Fsrs.new_card(card_id: 1)
card2 = Fsrs.new_card(card_id: 1)
card3 = Fsrs.new_card(card_id: 2)

assert CardType.equal?(card1, card2)
refute CardType.equal?(card1, card3)
end
end
else
IO.puts("Skipping Ecto tests - Ecto not available")
end
end