Skip to content

Commit

Permalink
feat: add OnePiece.Ecto package
Browse files Browse the repository at this point in the history
  • Loading branch information
yordis committed Mar 7, 2023
1 parent bbe6189 commit b93b418
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 0 deletions.
5 changes: 5 additions & 0 deletions apps/one_piece_ecto/.formatter.exs
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}"]
]
26 changes: 26 additions & 0 deletions apps/one_piece_ecto/.gitignore
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/
21 changes: 21 additions & 0 deletions apps/one_piece_ecto/README.md
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>.

138 changes: 138 additions & 0 deletions apps/one_piece_ecto/lib/one_piece/ecto/schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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 MyValueObject 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
alias OnePiece.Ecto.Schema

@before_compile OnePiece.Ecto.Schema

@doc """
Creates a `t:t/0`.
"""
@spec new(attrs :: map()) :: {:ok, %__MODULE__{}}
def new(attrs) do
ValueObject.__new__(__MODULE__, attrs)
end

@doc """
Creates a `t:t/0`.
"""
@spec new!(attrs :: map()) :: %__MODULE__{}
def new!(attrs) do
ValueObject.__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
ValueObject.__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)
allowed = struct_module.__schema__(:fields) -- embeds

changeset =
model
|> Changeset.cast(attrs, allowed)
|> Changeset.validate_required(struct_module.__enforced_keys__() -- embeds)

Enum.reduce(
embeds,
changeset,
&Changeset.cast_embed(&2, &1, required: struct_module.__enforced_keys__?(&1))
)
end

defp apply_changeset(struct_module, attrs) do
struct_module
|> struct()
|> struct_module.changeset(attrs)
end
end
110 changes: 110 additions & 0 deletions apps/one_piece_ecto/mix.exs
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.
8 changes: 8 additions & 0 deletions apps/one_piece_ecto/test/one_piece_ecto_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule OnePieceEctoTest do
use ExUnit.Case
doctest OnePieceEcto

test "greets the world" do
assert OnePieceEcto.hello() == :world
end
end
1 change: 1 addition & 0 deletions apps/one_piece_ecto/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit b93b418

Please sign in to comment.