Skip to content

Commit 1fa908d

Browse files
committed
feat: add OnePiece.Ecto package
1 parent bbe6189 commit 1fa908d

File tree

8 files changed

+309
-0
lines changed

8 files changed

+309
-0
lines changed

apps/one_piece_ecto/.formatter.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
line_length: 120,
3+
import_deps: [:ecto],
4+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
5+
]

apps/one_piece_ecto/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
one_piece_ecto-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

apps/one_piece_ecto/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# OnePieceEcto
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `one_piece_ecto` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:one_piece_ecto, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at <https://hexdocs.pm/one_piece_ecto>.
21+
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
defmodule OnePiece.Ecto.Schema do
2+
@moduledoc """
3+
Extends a `Ecto.Schema` module with functionality.
4+
"""
5+
6+
alias Ecto.Changeset
7+
8+
@doc """
9+
Extends a `Ecto.Schema` module with some functionality.
10+
11+
defmodule MyValueObject do
12+
use Ecto.Schema
13+
use OnePiece.Ecto.Schema
14+
15+
embedded_schema do
16+
field :title, :string
17+
# ...
18+
end
19+
end
20+
21+
The following functions are available in the module now:
22+
23+
`new/1`: **overridable** struct factory function. It takes an attribute map
24+
and runs the `changeset/2`.
25+
26+
`new!/1`: **overridable** like `new/1` raising an error when the validation
27+
fails.
28+
29+
`changeset/2`: **overridable** function. It takes a struct and the attributes
30+
and returns a `Ecto.Changeset`.
31+
The default implementation apply a deeply-nested casting over all the fields
32+
using `Ecto.Changeset.cast/4` and `Ecto.Changeset.cast_embed/4`.
33+
When `@enforce_keys` is defined, it will apply `Ecto.Changeset.validate_required/3`
34+
to the list of fields.
35+
When overriding the function, allows you have full control over the validation
36+
layer, deactivating all the nested-casting.
37+
"""
38+
@spec __using__(opts :: []) :: any()
39+
defmacro __using__(_opts \\ []) do
40+
quote do
41+
alias OnePiece.Ecto.Schema
42+
43+
@before_compile OnePiece.Ecto.Schema
44+
45+
@doc """
46+
Creates a `t:t/0`.
47+
"""
48+
@spec new(attrs :: map()) :: {:ok, %__MODULE__{}}
49+
def new(attrs) do
50+
ValueObject.__new__(__MODULE__, attrs)
51+
end
52+
53+
@doc """
54+
Creates a `t:t/0`.
55+
"""
56+
@spec new!(attrs :: map()) :: %__MODULE__{}
57+
def new!(attrs) do
58+
ValueObject.__new__!(__MODULE__, attrs)
59+
end
60+
61+
@doc """
62+
Returns an `t:Ecto.Changeset.t/0` for a given `t:t/0` model.
63+
"""
64+
@spec changeset(model :: %__MODULE__{}, attrs :: map()) :: Ecto.Changeset.t()
65+
def changeset(model, attrs) do
66+
ValueObject.__changeset__(model, attrs)
67+
end
68+
69+
defoverridable new: 1, new!: 1, changeset: 2
70+
end
71+
end
72+
73+
defmacro __before_compile__(env) do
74+
enforced_keys = get_enforced_keys(env)
75+
76+
quote unquote: false, bind_quoted: [enforced_keys: enforced_keys] do
77+
def __enforced_keys__ do
78+
unquote(enforced_keys)
79+
end
80+
81+
for the_key <- enforced_keys do
82+
def __enforced_keys__?(unquote(the_key)) do
83+
true
84+
end
85+
end
86+
87+
def __enforced_keys__?(_) do
88+
false
89+
end
90+
end
91+
end
92+
93+
defp get_enforced_keys(env) do
94+
enforce_keys = Module.get_attribute(env.module, :enforce_keys) || []
95+
enforce_keys ++ get_primary_key_name(env)
96+
end
97+
98+
defp get_primary_key_name(env) do
99+
case Module.get_attribute(env.module, :primary_key) do
100+
{field_name, _, _} -> [field_name]
101+
_ -> []
102+
end
103+
end
104+
105+
def __new__(struct_module, attrs) do
106+
struct_module
107+
|> apply_changeset(attrs)
108+
|> Changeset.apply_action(:new)
109+
end
110+
111+
def __new__!(struct_module, attrs) do
112+
struct_module
113+
|> apply_changeset(attrs)
114+
|> Changeset.apply_action!(:new!)
115+
end
116+
117+
def __changeset__(%struct_module{} = model, attrs) do
118+
embeds = struct_module.__schema__(:embeds)
119+
allowed = struct_module.__schema__(:fields) -- embeds
120+
121+
changeset =
122+
model
123+
|> Changeset.cast(attrs, allowed)
124+
|> Changeset.validate_required(struct_module.__enforced_keys__() -- embeds)
125+
126+
Enum.reduce(
127+
embeds,
128+
changeset,
129+
&Changeset.cast_embed(&2, &1, required: struct_module.__enforced_keys__?(&1))
130+
)
131+
end
132+
133+
defp apply_changeset(struct_module, attrs) do
134+
struct_module
135+
|> struct()
136+
|> struct_module.changeset(attrs)
137+
end
138+
end

apps/one_piece_ecto/mix.exs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
defmodule OnePiece.Ecto.MixProject do
2+
use Mix.Project
3+
4+
@app :one_piece_ecto
5+
@version "0.1.0"
6+
@elixir_version "~> 1.13"
7+
@source_url "https://github.com/straw-hat-team/beam-monorepo"
8+
9+
def project do
10+
[
11+
build_path: "../../_build",
12+
config_path: "../../config/config.exs",
13+
deps_path: "../../deps",
14+
lockfile: "../../mix.lock",
15+
name: "OnePiece.Ecto",
16+
description: "Extend Ecto package",
17+
app: @app,
18+
version: @version,
19+
elixir: @elixir_version,
20+
elixir: "~> 1.14",
21+
start_permanent: Mix.env() == :prod,
22+
deps: deps(),
23+
aliases: aliases(),
24+
test_coverage: test_coverage(),
25+
preferred_cli_env: preferred_cli_env(),
26+
package: package(),
27+
docs: docs(),
28+
dialyzer: dialyzer()
29+
]
30+
end
31+
32+
def application do
33+
[
34+
extra_applications: [:logger]
35+
]
36+
end
37+
38+
defp deps do
39+
[
40+
{:ecto, "~> 3.6"},
41+
42+
# Tools
43+
{:dialyxir, ">= 0.0.0", only: [:dev], runtime: false},
44+
{:credo, ">= 0.0.0", only: [:dev, :test], runtime: false},
45+
{:excoveralls, ">= 0.0.0", only: [:test], runtime: false},
46+
{:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}
47+
]
48+
end
49+
50+
defp aliases do
51+
[
52+
test: ["test --trace"]
53+
]
54+
end
55+
56+
defp test_coverage do
57+
[tool: ExCoveralls]
58+
end
59+
60+
defp preferred_cli_env do
61+
[
62+
"coveralls.html": :test,
63+
"coveralls.json": :test,
64+
coveralls: :test
65+
]
66+
end
67+
68+
defp dialyzer do
69+
[
70+
plt_core_path: "priv/plts",
71+
ignore_warnings: ".dialyzer_ignore.exs"
72+
]
73+
end
74+
75+
defp package do
76+
[
77+
name: @app,
78+
files: [
79+
".formatter.exs",
80+
"lib",
81+
"mix.exs",
82+
"README*",
83+
"LICENSE*"
84+
],
85+
maintainers: ["Yordis Prieto"],
86+
licenses: ["MIT"],
87+
links: %{
88+
"GitHub" => @source_url
89+
}
90+
]
91+
end
92+
93+
defp docs do
94+
[
95+
main: "readme",
96+
homepage_url: @source_url,
97+
source_url_pattern: "#{@source_url}/blob/#{@app}@v#{@version}/apps/#{@app}/%{path}#L%{line}",
98+
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
99+
extras: [
100+
"README.md",
101+
"CHANGELOG.md"
102+
],
103+
groups_for_extras: [
104+
"How-to": ~r/docs\/how-to\/.?/,
105+
Explanations: ~r/docs\/explanations\/.?/,
106+
References: ~r/docs\/references\/.?/
107+
]
108+
]
109+
end
110+
end

apps/one_piece_ecto/priv/plts/.gitkeep

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule OnePieceEctoTest do
2+
use ExUnit.Case
3+
doctest OnePieceEcto
4+
5+
test "greets the world" do
6+
assert OnePieceEcto.hello() == :world
7+
end
8+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)