Current section

Files

Jump to
sorcery lib spec_db spec_db.ex
Raw

lib/spec_db/spec_db.ex

defmodule Sorcery.SpecDb do
@moduledoc """
## Introduction
In Sorcery, we keep data sync'd across nodes. So it naturally follows that we should keep the metadata sync'd across codes.
## Schema module
In a typical phoenix app, you might have a Player schema for a game.
You can replace part, or all of that module with Sorcery. Observe:
```elixir
defmodule MyApp.Player do
require Sorcery.SpecDb
@spec_table %{
user_id: %{t: :id, require_update: false},
name: %{t: :string, default: "Player", min: 3, max: 45},
age: %{t: :integer, min: 0, max: 200, bump: true}
}
Sorcery.SpecDb.build_schema_module("player")
# Alternately:
#
# def spec_table, do: @spec_table
# Sorcery.SpecDb.build_ecto_schema(name)
# Sorcery.SpecDb.build_norm_schema()
# Sorcery.SpecDb.build_streamdata_generator()
# Sorcery.SpecDb.build_changesets()
end
```
## Exploring the results
What does this achieve? Well, I think the test module actually speaks for itself.
Read the comments carefully
```elixir
defmodule MyApp.PlayerTest do
use ExUnit.Case
use ExUnitProperties
use Norm
alias MyApp.Player
test "build_schema macro" do
# Builds the struct via Ecto.Schema
assert Map.get(%Player{}, :name) == "Player"
# Builds the Norm schema
assert !valid?(%{name: "hello"}, Player.t())
assert valid?(%Player{user_id: 1, age: 23, name: "hello"}, Player.t())
assert valid?(%{user_id: 1, age: 23, name: "hello"}, Player.t())
end
property "Generates Players" do
# First, we generate a player with random data that matches the spec_table
# The map you pass into gen/1 will override any random values.
check all player <- Player.gen(%{user_id: 24}) do
assert valid?(player, Player.t())
# We get a valid changeset
cs = Player.sorcery_update(%Player{id: player.id}, player)
assert cs.valid?
# If we use numbers outside the min/max, using `bump: true` will set it the same min/max value.
# Remember our spec_table included:
# age: %{t: :integer, min: 0, max: 200, bump: true}
player = Map.put(player, :age, 9999999)
cs = Player.sorcery_update(%Player{id: player.id}, player)
assert cs.changes.age == 200
assert cs.valid?
# Instead of causing an invalid changeset, it just 'bumped' it to the :max value.
end
end
end
```
## The table_spec
To see more about the options to pass into each field, see the types section.
"""
@typedoc "Required. The type of this field"
@type t :: :integer | :float | :boolean | :string
@typedoc "Default: true. Whether Norm should treat this as a required field. Does nothing if :default field is set."
@type required :: boolean
@typedoc "The default value if nothing else set."
@type default :: any
@typedoc "The lowest possible number, or string length"
@type min :: integer | float
@typedoc "The highest possible number, or string length"
@type max :: integer | float
@typedoc "Whether the value should be coerced to its min/max if beyond that range."
@type bump :: boolean
@typedoc "Whether this field should be cast inside sorcery_update/2"
@type cast_update :: boolean
@typedoc "Whether this field should be in validate_required inside sorcery_update/2"
@type require_update :: boolean
@typedoc "Whether this field should be cast inside sorcery_insert/2"
@type cast_insert :: boolean
@typedoc "Whether this field should be in validate_required inside sorcery_insert/2"
@type require_insert :: boolean
@typedoc "A list of possible options"
@type one_of :: [any]
@typedoc "If t: :list, then every item must be of this type"
@type coll_of :: atom
@typedoc "Sets the length of a list type."
@type length :: integer
@doc """
```
# Injects this
use Ecto.Schema
schema "name" do
field :fieldname, :type
end
# While passing in all the data from @spec_table
```
"""
defmacro build_ecto_schema(name) do
quote do
require Sorcery.SpecDb.EctoHelpers
Sorcery.SpecDb.EctoHelpers.build_ecto_schema(unquote(name), @spec_table)
end
end
@doc """
Adds the __MODULE__.t() function, for usage with Norm.
"""
defmacro build_norm_schema() do
quote do
def t() do
Sorcery.SpecDb.NormHelpers.build_schema(@spec_table)
end
end
end
@doc """
Adds the __MODULE__.gen() function, for generating data in property based testing with StreamData
"""
defmacro build_streamdata_generator() do
quote do
def gen(attrs \\ %{}) do
Sorcery.SpecDb.SdHelpers.gen(@spec_table, attrs)
end
end
end
@doc """
Adds `sorcery_insert/2` and `sorcery_update/2`, based on the @spec_table
"""
defmacro build_changesets() do
quote do
import Ecto.Changeset
import Sorcery.SpecDb.CsHelpers
def sorcery_insert(strct, attrs \\ %{}) do
attrs = bump(@spec_table, attrs)
strct
|> cast(attrs, get_cast_insert(@spec_table))
|> validate_required(get_require_insert(@spec_table))
end
def sorcery_update(strct, attrs \\ %{}) do
attrs = bump(@spec_table, attrs)
strct
|> cast(attrs, get_cast_update(@spec_table))
|> validate_required(get_require_update(@spec_table))
end
end
end
@doc """
```elixir
# Macro which automatically adds to your module:
def spec_table, do: @spec_table
Sorcery.SpecDb.build_ecto_schema(name)
Sorcery.SpecDb.build_norm_schema()
Sorcery.SpecDb.build_streamdata_generator()
Sorcery.SpecDb.build_changesets()
```
"""
defmacro build_schema_module(name) do
quote do
def spec_table, do: @spec_table
Sorcery.SpecDb.build_ecto_schema(unquote(name))
Sorcery.SpecDb.build_norm_schema()
Sorcery.SpecDb.build_streamdata_generator()
Sorcery.SpecDb.build_changesets()
end
end
end