-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
440 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[ | ||
line_length: 120, | ||
import_deps: [:ecto], | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# The directory Mix will write compiled artifacts to. | ||
/_build/ | ||
|
||
# If you run "mix test --cover", coverage assets end up here. | ||
/cover/ | ||
|
||
# The directory Mix downloads your dependencies sources to. | ||
/deps/ | ||
|
||
# Where third-party dependencies like ExDoc output generated docs. | ||
/doc/ | ||
|
||
# Ignore .fetch files in case you like to edit your project deps locally. | ||
/.fetch | ||
|
||
# If the VM crashes, it generates a dump, let's ignore it too. | ||
erl_crash.dump | ||
|
||
# Also ignore archive artifacts (built via "mix archive.build"). | ||
*.ez | ||
|
||
# Ignore package tarball (built via "mix hex.build"). | ||
one_piece_ecto-*.tar | ||
|
||
# Temporary files, for example, from tests. | ||
/tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# OnePieceEcto | ||
|
||
**TODO: Add description** | ||
|
||
## Installation | ||
|
||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed | ||
by adding `one_piece_ecto` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:one_piece_ecto, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | ||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | ||
be found at <https://hexdocs.pm/one_piece_ecto>. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
defmodule OnePiece.Ecto.Schema do | ||
@moduledoc """ | ||
Extends a `Ecto.Schema` module with functionality. | ||
""" | ||
|
||
alias Ecto.Changeset | ||
|
||
@doc """ | ||
Extends a `Ecto.Schema` module with some functionality. | ||
defmodule MySchema do | ||
use Ecto.Schema | ||
use OnePiece.Ecto.Schema | ||
embedded_schema do | ||
field :title, :string | ||
# ... | ||
end | ||
end | ||
The following functions are available in the module now: | ||
`new/1`: **overridable** struct factory function. It takes an attribute map | ||
and runs the `changeset/2`. | ||
`new!/1`: **overridable** like `new/1` raising an error when the validation | ||
fails. | ||
`changeset/2`: **overridable** function. It takes a struct and the attributes | ||
and returns a `Ecto.Changeset`. | ||
The default implementation apply a deeply-nested casting over all the fields | ||
using `Ecto.Changeset.cast/4` and `Ecto.Changeset.cast_embed/4`. | ||
When `@enforce_keys` is defined, it will apply `Ecto.Changeset.validate_required/3` | ||
to the list of fields. | ||
When overriding the function, allows you have full control over the validation | ||
layer, deactivating all the nested-casting. | ||
""" | ||
@spec __using__(opts :: []) :: any() | ||
defmacro __using__(_opts \\ []) do | ||
quote do | ||
@before_compile OnePiece.Ecto.Schema | ||
|
||
@doc """ | ||
Creates a `t:t/0`. | ||
""" | ||
@spec new(attrs :: map()) :: {:ok, %__MODULE__{}} | ||
def new(attrs) do | ||
OnePiece.Ecto.Schema.__new__(__MODULE__, attrs) | ||
end | ||
|
||
@doc """ | ||
Creates a `t:t/0`. | ||
""" | ||
@spec new!(attrs :: map()) :: %__MODULE__{} | ||
def new!(attrs) do | ||
OnePiece.Ecto.Schema.__new__!(__MODULE__, attrs) | ||
end | ||
|
||
@doc """ | ||
Returns an `t:Ecto.Changeset.t/0` for a given `t:t/0` model. | ||
""" | ||
@spec changeset(model :: %__MODULE__{}, attrs :: map()) :: Ecto.Changeset.t() | ||
def changeset(model, attrs) do | ||
OnePiece.Ecto.Schema.__changeset__(model, attrs) | ||
end | ||
|
||
defoverridable new: 1, new!: 1, changeset: 2 | ||
end | ||
end | ||
|
||
defmacro __before_compile__(env) do | ||
enforced_keys = get_enforced_keys(env) | ||
|
||
quote unquote: false, bind_quoted: [enforced_keys: enforced_keys] do | ||
def __enforced_keys__ do | ||
unquote(enforced_keys) | ||
end | ||
|
||
for the_key <- enforced_keys do | ||
def __enforced_keys__?(unquote(the_key)) do | ||
true | ||
end | ||
end | ||
|
||
def __enforced_keys__?(_) do | ||
false | ||
end | ||
end | ||
end | ||
|
||
defp get_enforced_keys(env) do | ||
enforce_keys = Module.get_attribute(env.module, :enforce_keys) || [] | ||
enforce_keys ++ get_primary_key_name(env) | ||
end | ||
|
||
defp get_primary_key_name(env) do | ||
case Module.get_attribute(env.module, :primary_key) do | ||
{field_name, _, _} -> [field_name] | ||
_ -> [] | ||
end | ||
end | ||
|
||
def __new__(struct_module, attrs) do | ||
struct_module | ||
|> apply_changeset(attrs) | ||
|> Changeset.apply_action(:new) | ||
end | ||
|
||
def __new__!(struct_module, attrs) do | ||
struct_module | ||
|> apply_changeset(attrs) | ||
|> Changeset.apply_action!(:new!) | ||
end | ||
|
||
def __changeset__(%struct_module{} = model, attrs) do | ||
embeds = struct_module.__schema__(:embeds) | ||
fields = struct_module.__schema__(:fields) | ||
|
||
changeset = | ||
message | ||
|> Changeset.cast(attrs, fields -- embeds) | ||
|> Changeset.validate_required(struct_module.__enforced_keys__() -- embeds) | ||
|
||
Enum.reduce( | ||
embeds, | ||
changeset, | ||
&cast_embed(&1, &2, struct_module, attrs) | ||
) | ||
end | ||
|
||
defp cast_embed(field, changeset, struct_module, attrs) do | ||
case is_struct(attrs[field]) do | ||
false -> | ||
Changeset.cast_embed(changeset, field, required: struct_module.__enforced_keys__?(field)) | ||
|
||
true -> | ||
# credo:disable-for-next-line Credo.Check.Design.TagTODO | ||
# TODO: Validate that the struct is of the correct type. | ||
# It may be the case that you passed a completely different struct as the value. We could `cast_embed` | ||
# always and fix the `Changeset.cast(attrs, fields -- embeds)` by converting the `attrs` into a map. But it | ||
# would be a bit more expensive since it will run the casting for a field that was already casted. | ||
# Checking the struct types MAY be enough but taking into consideration `embeds_many` could complicated | ||
# things. For now, we'll just assume that the user knows what they're doing. | ||
Changeset.put_change(changeset, field, attrs[field]) | ||
end | ||
end | ||
|
||
defp apply_changeset(struct_module, attrs) do | ||
struct_module | ||
|> struct() | ||
|> struct_module.changeset(attrs) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
defmodule OnePiece.Ecto.MixProject do | ||
use Mix.Project | ||
|
||
@app :one_piece_ecto | ||
@version "0.1.0" | ||
@elixir_version "~> 1.13" | ||
@source_url "https://github.com/straw-hat-team/beam-monorepo" | ||
|
||
def project do | ||
[ | ||
build_path: "../../_build", | ||
config_path: "../../config/config.exs", | ||
deps_path: "../../deps", | ||
lockfile: "../../mix.lock", | ||
name: "OnePiece.Ecto", | ||
description: "Extend Ecto package", | ||
app: @app, | ||
version: @version, | ||
elixir: @elixir_version, | ||
elixir: "~> 1.14", | ||
start_permanent: Mix.env() == :prod, | ||
deps: deps(), | ||
aliases: aliases(), | ||
test_coverage: test_coverage(), | ||
preferred_cli_env: preferred_cli_env(), | ||
package: package(), | ||
docs: docs(), | ||
dialyzer: dialyzer() | ||
] | ||
end | ||
|
||
def application do | ||
[ | ||
extra_applications: [:logger] | ||
] | ||
end | ||
|
||
defp deps do | ||
[ | ||
{:ecto, "~> 3.6"}, | ||
|
||
# Tools | ||
{:dialyxir, ">= 0.0.0", only: [:dev], runtime: false}, | ||
{:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, | ||
{:excoveralls, ">= 0.0.0", only: [:test], runtime: false}, | ||
{:ex_doc, ">= 0.0.0", only: [:dev], runtime: false} | ||
] | ||
end | ||
|
||
defp aliases do | ||
[ | ||
test: ["test --trace"] | ||
] | ||
end | ||
|
||
defp test_coverage do | ||
[tool: ExCoveralls] | ||
end | ||
|
||
defp preferred_cli_env do | ||
[ | ||
"coveralls.html": :test, | ||
"coveralls.json": :test, | ||
coveralls: :test | ||
] | ||
end | ||
|
||
defp dialyzer do | ||
[ | ||
plt_core_path: "priv/plts", | ||
ignore_warnings: ".dialyzer_ignore.exs" | ||
] | ||
end | ||
|
||
defp package do | ||
[ | ||
name: @app, | ||
files: [ | ||
".formatter.exs", | ||
"lib", | ||
"mix.exs", | ||
"README*", | ||
"LICENSE*" | ||
], | ||
maintainers: ["Yordis Prieto"], | ||
licenses: ["MIT"], | ||
links: %{ | ||
"GitHub" => @source_url | ||
} | ||
] | ||
end | ||
|
||
defp docs do | ||
[ | ||
main: "readme", | ||
homepage_url: @source_url, | ||
source_url_pattern: "#{@source_url}/blob/#{@app}@v#{@version}/apps/#{@app}/%{path}#L%{line}", | ||
skip_undefined_reference_warnings_on: ["CHANGELOG.md"], | ||
extras: [ | ||
"README.md", | ||
"CHANGELOG.md" | ||
], | ||
groups_for_extras: [ | ||
"How-to": ~r/docs\/how-to\/.?/, | ||
Explanations: ~r/docs\/explanations\/.?/, | ||
References: ~r/docs\/references\/.?/ | ||
] | ||
] | ||
end | ||
end |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
defmodule OnePiece.Ecto.SchemaTest do | ||
use ExUnit.Case, async: true | ||
|
||
describe "new/1" do | ||
test "creates a struct" do | ||
assert {:ok, %TestSupport.MessageOne{title: nil}} = TestSupport.MessageOne.new(%{}) | ||
end | ||
|
||
test "validates a key enforce" do | ||
{:error, changeset} = TestSupport.MessageTwo.new(%{}) | ||
assert %{title: ["can't be blank"]} = TestSupport.errors_on(changeset) | ||
end | ||
|
||
test "validates a key enforce for embed fields" do | ||
{:error, changeset} = TestSupport.MessageThree.new(%{}) | ||
assert %{target: ["can't be blank"]} = TestSupport.errors_on(changeset) | ||
end | ||
|
||
test "validates casting embed fields" do | ||
assert {:ok, %TestSupport.MessageThree{target: %TestSupport.MessageOne{title: "Hello, World!"}}} = | ||
TestSupport.MessageThree.new(%{target: %{title: "Hello, World!"}}) | ||
end | ||
|
||
test "bypass casting structs" do | ||
assert {:ok, %TestSupport.MessageThree{target: %TestSupport.MessageOne{title: "Hello, World!"}}} = | ||
TestSupport.MessageThree.new(%{target: %TestSupport.MessageOne{title: "Hello, World!"}}) | ||
end | ||
|
||
test "validates casting embed fields with a wrong value" do | ||
{:error, changeset} = TestSupport.MessageThree.new(%{target: "a wrong value"}) | ||
assert %{target: ["is invalid"]} = TestSupport.errors_on(changeset) | ||
end | ||
end | ||
|
||
describe "new!/1" do | ||
test "raises an error when a validation fails" do | ||
assert_raise Ecto.InvalidChangesetError, fn -> | ||
TestSupport.MessageTwo.new!(%{}) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.