From 4eac7de50191ad8c4c5e0092269c4e745c242230 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 6 May 2021 10:51:19 -0400 Subject: [PATCH 1/2] update ecto --- mix.exs | 2 +- mix.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index c619f86..45cd255 100644 --- a/mix.exs +++ b/mix.exs @@ -30,7 +30,7 @@ defmodule EctoDot.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ecto, "~> 2.1"}, + {:ecto, "~> 3.0"}, {:mix_test_watch, "~> 0.5", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index a63487c..ae44311 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,8 @@ %{ - "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [], [], "hexpm"}, - "ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [], [], "hexpm"}, - "mix_test_watch": {:hex, :mix_test_watch, "0.6.0", "5e206ed04860555a455de2983937efd3ce79f42bd8536fc6b900cc286f5bb830", [], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "ecto": {:hex, :ecto, "3.6.1", "7bb317e3fd0179ad725069fd0fe8a28ebe48fec6282e964ea502e4deccb0bd0f", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbb3294a990447b19f0725488a749f8cf806374e0d9d0dffc45d61e7aeaf6553"}, + "file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm", "d3d5ee3a1d656cb1efa0d0446df2aeb230c55d0d5fa2ab2840082f7ace50d04a"}, + "mix_test_watch": {:hex, :mix_test_watch, "0.6.0", "5e206ed04860555a455de2983937efd3ce79f42bd8536fc6b900cc286f5bb830", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "ea6f2a3766f18c2f53ca5b2d40b623ce2831c1646f36ff2b608607e20fc6c63c"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, } From 90fdd4a50e8a71614ce9b970f74f830ff82e43f1 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 6 May 2021 16:43:49 -0400 Subject: [PATCH 2/2] feat: add embedded schemas --- lib/ecto_dot/association.ex | 25 ++++++++----- lib/ecto_dot/schema.ex | 6 +++- test/ecto_dot/diagram_test.exs | 39 ++++++++++++++++++++ test/ecto_dot/schema_test.exs | 7 ++++ test/ecto_dot_test.exs | 61 ++++++++++++++++++++++++++++++++ test/support/embedded_comment.ex | 11 ++++++ test/support/embedded_post.ex | 12 +++++++ test/support/embedded_user.ex | 12 +++++++ 8 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 test/support/embedded_comment.ex create mode 100644 test/support/embedded_post.ex create mode 100644 test/support/embedded_user.ex diff --git a/lib/ecto_dot/association.ex b/lib/ecto_dot/association.ex index b87db48..9f47895 100644 --- a/lib/ecto_dot/association.ex +++ b/lib/ecto_dot/association.ex @@ -3,12 +3,9 @@ defmodule EctoDot.Association do defstruct [:name, :from, :to, :cardinality] def from_ecto(mod) do - mod.__schema__(:associations) - |> Enum.map(fn assoc -> - mod.__schema__(:association, assoc) - end) - |> Enum.flat_map(fn - %Ecto.Association.Has{} = assoc -> + associations_and_embeds(mod) + |> Enum.flat_map(fn assoc -> + if is_struct(assoc, Ecto.Association.Has) || is_struct(assoc, Ecto.Embedded) do [ %__MODULE__{ name: assoc.field, @@ -17,12 +14,24 @@ defmodule EctoDot.Association do cardinality: assoc.cardinality } ] - - _ -> + else [] + end end) end + defp associations_and_embeds(mod) do + associations = + mod.__schema__(:associations) + |> Enum.map(& mod.__schema__(:association, &1)) + + embeds = + mod.__schema__(:embeds) + |> Enum.map(& mod.__schema__(:embed, &1)) + + associations ++ embeds + end + def to_dot(%__MODULE__{} = assoc, opts \\ []) do indent = String.duplicate(" ", Keyword.get(opts, :indentation, 0)) diff --git a/lib/ecto_dot/schema.ex b/lib/ecto_dot/schema.ex index 5defc73..faac321 100644 --- a/lib/ecto_dot/schema.ex +++ b/lib/ecto_dot/schema.ex @@ -8,8 +8,12 @@ defmodule EctoDot.Schema do fields = mod.__schema__(:fields) |> Enum.map(fn field -> - %Field{name: field, type: mod.__schema__(:type, field)} + case mod.__schema__(:type, field) do + {:parameterized, _, _} -> nil + type -> %Field{name: field, type: type} + end end) + |> Enum.reject(& &1 == nil) %__MODULE__{mod: mod, name: Macro.to_string(mod), fields: fields} end diff --git a/test/ecto_dot/diagram_test.exs b/test/ecto_dot/diagram_test.exs index 8e508e9..a9b13aa 100644 --- a/test/ecto_dot/diagram_test.exs +++ b/test/ecto_dot/diagram_test.exs @@ -16,6 +16,16 @@ defmodule EctoDot.DiagramTest do assert EctoDot.diagram(User) |> Diagram.to_dot() == expected end + test "only one schema with embedded schemas and no self associations" do + expected = ~s""" + digraph "Diagram" { + #{Schema.from_ecto(EmbeddedUser) |> Schema.to_dot()} + } + """ + + assert EctoDot.diagram(EmbeddedUser) |> Diagram.to_dot() == expected + end + test "only one schema with self associations" do expected = ~s""" digraph "Diagram" { @@ -28,6 +38,18 @@ defmodule EctoDot.DiagramTest do assert EctoDot.diagram(Post) |> Diagram.to_dot() == expected end + test "only one embedded schema with self associations" do + expected = ~s""" + digraph "Diagram" { + #{Schema.from_ecto(EmbeddedPost) |> Schema.to_dot()} + + #{assoc_dot(EmbeddedPost, :related)} + } + """ + + assert EctoDot.diagram(EmbeddedPost) |> Diagram.to_dot() == expected + end + test "many schemas" do expected = ~s""" digraph "Diagram" { @@ -44,6 +66,23 @@ defmodule EctoDot.DiagramTest do assert EctoDot.diagram([User, Post, Comment]) |> Diagram.to_dot() == expected end + + test "many embedded schemas" do + expected = ~s""" + digraph "Diagram" { + #{Schema.from_ecto(EmbeddedUser) |> Schema.to_dot()} + #{Schema.from_ecto(EmbeddedPost) |> Schema.to_dot()} + #{Schema.from_ecto(EmbeddedComment) |> Schema.to_dot()} + + #{assoc_dot(EmbeddedUser, :posts)} + #{assoc_dot(EmbeddedUser, :comments)} + #{assoc_dot(EmbeddedPost, :comments)} + #{assoc_dot(EmbeddedPost, :related)} + } + """ + + assert EctoDot.diagram([EmbeddedUser, EmbeddedPost, EmbeddedComment]) |> Diagram.to_dot() == expected + end end defp assoc_dot(mod, name) do diff --git a/test/ecto_dot/schema_test.exs b/test/ecto_dot/schema_test.exs index f0d8c7e..f5708c5 100644 --- a/test/ecto_dot/schema_test.exs +++ b/test/ecto_dot/schema_test.exs @@ -9,4 +9,11 @@ defmodule EctoDot.SchemaTest do assert Schema.from_ecto(User) |> Schema.to_dot() == expected end + + test "end to end for embedded schemas" do + expected = + ~s("EmbeddedUser" [shape="record", label="{EmbeddedUser|id: binary_id\\lfirst_name: string\\lsurname: string\\lemail: string\\l}"]) + + assert Schema.from_ecto(EmbeddedUser) |> Schema.to_dot() == expected + end end diff --git a/test/ecto_dot_test.exs b/test/ecto_dot_test.exs index 71742c8..ad4fd5d 100644 --- a/test/ecto_dot_test.exs +++ b/test/ecto_dot_test.exs @@ -65,5 +65,66 @@ defmodule EctoDotTest do %Association{name: :related, from: Post, to: Post, cardinality: :many} ] end + + test "one module with an embedded schema" do + diag = EctoDot.diagram(EmbeddedUser) + + assert diag.name == "Diagram" + assert diag.schemas == [Schema.from_ecto(EmbeddedUser)] + assert diag.associations == [] + end + + test "one module with an embedded schema and self associations" do + diag = EctoDot.diagram(EmbeddedPost) + + assert diag.name == "Diagram" + assert diag.schemas == [Schema.from_ecto(EmbeddedPost)] + + assert diag.associations == [ + %Association{name: :related, from: EmbeddedPost, to: EmbeddedPost, cardinality: :many} + ] + end + + test "many modules with embedded schemas and no self associations" do + diag = EctoDot.diagram([EmbeddedUser, EmbeddedComment]) + + assert diag.name == "Diagram" + assert diag.schemas == [Schema.from_ecto(EmbeddedUser), Schema.from_ecto(EmbeddedComment)] + + assert diag.associations == [ + %Association{name: :comments, from: EmbeddedUser, to: EmbeddedComment, cardinality: :many} + ] + end + + test "many modules with embedded schemas and self associations" do + diag = EctoDot.diagram([EmbeddedUser, EmbeddedPost]) + + assert diag.name == "Diagram" + assert diag.schemas == [Schema.from_ecto(EmbeddedUser), Schema.from_ecto(EmbeddedPost)] + + assert diag.associations == [ + %Association{name: :posts, from: EmbeddedUser, to: EmbeddedPost, cardinality: :many}, + %Association{name: :related, from: EmbeddedPost, to: EmbeddedPost, cardinality: :many} + ] + end + + test "all modules with embedded schemas" do + diag = EctoDot.diagram([EmbeddedUser, EmbeddedPost, EmbeddedComment]) + + assert diag.name == "Diagram" + + assert diag.schemas == [ + Schema.from_ecto(EmbeddedUser), + Schema.from_ecto(EmbeddedPost), + Schema.from_ecto(EmbeddedComment) + ] + + assert diag.associations == [ + %Association{name: :posts, from: EmbeddedUser, to: EmbeddedPost, cardinality: :many}, + %Association{name: :comments, from: EmbeddedUser, to: EmbeddedComment, cardinality: :many}, + %Association{name: :comments, from: EmbeddedPost, to: EmbeddedComment, cardinality: :many}, + %Association{name: :related, from: EmbeddedPost, to: EmbeddedPost, cardinality: :many} + ] + end end end diff --git a/test/support/embedded_comment.ex b/test/support/embedded_comment.ex new file mode 100644 index 0000000..8d9a662 --- /dev/null +++ b/test/support/embedded_comment.ex @@ -0,0 +1,11 @@ +defmodule EmbeddedComment do + use Ecto.Schema + + embedded_schema do + field(:title, :string) + field(:body, :string) + + belongs_to(:author, EmbeddedUser) + belongs_to(:post, Post) + end +end diff --git a/test/support/embedded_post.ex b/test/support/embedded_post.ex new file mode 100644 index 0000000..4152f5d --- /dev/null +++ b/test/support/embedded_post.ex @@ -0,0 +1,12 @@ +defmodule EmbeddedPost do + use Ecto.Schema + + embedded_schema do + field(:title, :string) + field(:body, :string) + + embeds_many(:comments, EmbeddedComment) + embeds_many(:related, EmbeddedPost) + belongs_to(:author, EmbeddedUser) + end +end diff --git a/test/support/embedded_user.ex b/test/support/embedded_user.ex new file mode 100644 index 0000000..80fb111 --- /dev/null +++ b/test/support/embedded_user.ex @@ -0,0 +1,12 @@ +defmodule EmbeddedUser do + use Ecto.Schema + + embedded_schema do + field(:first_name, :string) + field(:surname, :string) + field(:email, :string) + + embeds_many(:posts, EmbeddedPost) + embeds_many(:comments, EmbeddedComment) + end +end