Skip to content
Merged
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
54 changes: 52 additions & 2 deletions lib/ecto_command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ defmodule EctoCommand do
:subset
]

@valid_embed_validators [
:required,
:change
]

@doc false
defmacro __using__(_) do
quote do
Expand All @@ -67,6 +72,7 @@ defmodule EctoCommand do
internal: 2,
internal: 3,
embeds_one: 3,
embeds_one: 4,
cast_embedded_fields: 2
]

Expand Down Expand Up @@ -259,7 +265,7 @@ defmodule EctoCommand do
__MODULE__,
unquote(name),
unquote(type),
opts |> Keyword.drop(unquote(@command_options ++ @valid_validators))
Keyword.drop(opts, unquote(@command_options ++ @valid_validators))
)
end
end
Expand Down Expand Up @@ -314,6 +320,20 @@ defmodule EctoCommand do
end
end

defmacro embeds_one(name, schema, opts, do: block) when is_list(opts) do
quote do
defmodule unquote(schema) do
use EctoCommand

command do
unquote(block)
end
end

unquote(embed_submodule(name, schema, opts))
end
end

defmacro embeds_one(name, schema, do: block) do
quote do
defmodule unquote(schema) do
Expand All @@ -324,10 +344,14 @@ defmodule EctoCommand do
end
end

Ecto.Schema.embeds_one(unquote(name), unquote(schema))
unquote(embed_submodule(name, schema, []))
end
end

defmacro embeds_one(name, schema, opts) when is_list(opts) do
embed_submodule(name, schema, opts)
end

@doc false
@spec trim_fields(map(), [atom()]) :: map()
def trim_fields(params, trim_fields) do
Expand Down Expand Up @@ -366,4 +390,30 @@ defmodule EctoCommand do
|> Pipeline.halt()
end
end

def embed_submodule(name, schema, opts) do
quote do
opts = unquote(opts)

Module.put_attribute(__MODULE__, :command_fields, {unquote(name), unquote(schema), opts})

Enum.each(unquote(@valid_embed_validators), fn validator ->
if opts[validator] !== nil && opts[validator] !== false do
parsed_opts = if opts[validator] == true, do: [], else: opts[validator]

Module.put_attribute(
__MODULE__,
:validators,
{unquote(name), validator, Macro.escape(parsed_opts)}
)
end
end)

Ecto.Schema.embeds_one(
unquote(name),
unquote(schema),
Keyword.drop(opts, unquote(@command_options ++ @valid_embed_validators))
)
end
end
end
8 changes: 7 additions & 1 deletion lib/open_api/open_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ defmodule EctoCommand.OpenApi do
%{type: :array, items: schema_for(inner_type, Keyword.drop(opts, [:doc, :default]))}
end

defp base_schema(type, _opts), do: base_schema(type)
defp base_schema(type, _opts) do
if is_atom(type) and function_exported?(type, :schema, 0) do
Map.from_struct(type.schema())
else
base_schema(type)
end
end

defp base_schema(:id), do: %{type: :integer}
defp base_schema(type) when type in [:float, :decimal], do: %{type: :number}
Expand Down
73 changes: 73 additions & 0 deletions test/unit/command/creation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,41 @@ defmodule Unit.EctoCommand.CreationTest do
})} == module_name.new(%{address: %{street: "piazzale loreto", city: "it/milano", zip: "20142"}})
end

test "a param could have subparams - they accept options" do
module_name = String.to_atom("Sample#{:rand.uniform(999_999)}")

define_a_module_with_fields module_name do
embeds_one :address, Address, required: true do
param :street, :string
param :city, :string
param :zip, :string
end

embeds_one :phone, Phone, required: false do
param :number, :string
param :country_code, :string
param :prefix, :string
end
end

address_module = String.to_atom("Elixir.#{module_name}.Address")

assert {:ok,
struct!(module_name, %{
address:
struct!(address_module, %{
street: "piazzale loreto",
city: "it/milano",
zip: "20142"
}),
phone: nil
})} ==
module_name.new(%{
address: %{street: "piazzale loreto", city: "it/milano", zip: "20142"},
phone: nil
})
end

test "when a subparam is invalid an invalid changeset is returned" do
module_name = String.to_atom("Sample#{:rand.uniform(999_999)}")

Expand Down Expand Up @@ -96,5 +131,43 @@ defmodule Unit.EctoCommand.CreationTest do
assert %{address: %{city: ["can't be blank"], street: ["should be at least 10 character(s)"]}} ==
errors_on(changeset)
end

test "an already existing module could be embedded - it accept options" do
embedded_module = String.to_atom("Sample#{:rand.uniform(999_999)}")

define_a_module_with_fields embedded_module do
param :street, :string, required: true, length: [min: 10]
param :city, :string, required: true
param :zip, :string
end

module_name = String.to_atom("Sample#{:rand.uniform(999_999)}")

define_a_module_with_fields module_name do
embeds_one :address, embedded_module, required: true
embeds_one :address_2, embedded_module, required: false

embeds_one :address_3, embedded_module,
required: false,
change: &unquote(__MODULE__).always_invalid/2
end

assert {:error, changeset} =
module_name.new(%{
address: nil,
address_2: %{street: "foo", city: "New York"},
address_3: %{street: "Madison avenue", city: "New York"}
})

assert %{
address: ["can't be blank"],
address_2: %{street: ["should be at least 10 character(s)"]},
address_3: ["is invalid"]
} == errors_on(changeset)
end
end

def always_invalid(field, _value) do
[{field, "is invalid"}]
end
end
33 changes: 32 additions & 1 deletion test/unit/command/open_api/open_api_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ defmodule Unit.EctoCommand.OpenApi.OpenApiTest do
use ExUnit.Case, async: true
use EctoCommand.Test.CommandCase

defmodule EmbeddedSample do
@moduledoc false

use EctoCommand
use EctoCommand.OpenApi, title: "EmbeddedSample"

command do
param :id, :string, doc: Type.uuid()
param :title, :string, required: true, length: [min: 5], doc: [example: "A title"]
end
end

defmodule Sample do
@moduledoc false

Expand Down Expand Up @@ -43,6 +55,8 @@ defmodule Unit.EctoCommand.OpenApi.OpenApiTest do
param :a_map, :map, doc: [description: "A map"], default: %{}
param :a_map_with_int_values, {:map, :integer}, doc: [description: "A map with integer values"], default: %{a: 1}

embeds_one :embedded_sample, EmbeddedSample, doc: [description: "A embedded sample"]

internal :triggered_by, :map
internal :uploaded_by, :string
end
Expand Down Expand Up @@ -156,6 +170,22 @@ defmodule Unit.EctoCommand.OpenApi.OpenApiTest do
description: "A map with integer values",
default: %{a: 1},
example: %{a: 1}
},
embedded_sample: %OpenApiSpex.Schema{
title: "EmbeddedSample",
required: [:title],
type: :object,
description: "A embedded sample",
properties: %{
id: %OpenApiSpex.Schema{
type: :string,
description: "UUID",
format: :uuid,
example: "02ef9c5f-29e6-48fc-9ec3-7ed57ed351f6"
},
title: %OpenApiSpex.Schema{minLength: 5, type: :string, example: "A title"}
},
example: %{id: "02ef9c5f-29e6-48fc-9ec3-7ed57ed351f6", title: "A title"}
}
} == Sample.schema().properties

Expand Down Expand Up @@ -188,7 +218,8 @@ defmodule Unit.EctoCommand.OpenApi.OpenApiTest do
name: "Mario",
phone: "(425) 123-4567",
type_id: "",
uploaded_at: "2020-04-20T16:20:00Z"
uploaded_at: "2020-04-20T16:20:00Z",
embedded_sample: %{id: "02ef9c5f-29e6-48fc-9ec3-7ed57ed351f6", title: "A title"}
} == Sample.schema().example

refute Map.has_key?(Sample.schema().example, :hidden_field)
Expand Down