├── test
├── test_helper.exs
└── dg_test.exs
├── .formatter.exs
├── renovate.json
├── .github
├── workflows
│ ├── elixir.yml
│ ├── publish-docs.yml
│ └── publish.yml
└── dependabot.yml
├── CHANGELOG.md
├── lib
├── collectable.ex
├── inspect.ex
├── sigil.ex
└── dg.ex
├── .gitignore
├── LICENSE
├── mix.lock
├── mix.exs
└── README.md
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.github/workflows/elixir.yml:
--------------------------------------------------------------------------------
1 | name: Elixir CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | uses: swoosh/actions/.github/workflows/test.yml@main
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: mix
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: '19:00'
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/publish-docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docs
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | publish:
7 | uses: swoosh/actions/.github/workflows/publish.yml@main
8 | with:
9 | mode: 'docs'
10 | secrets:
11 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
12 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | workflow_dispatch:
7 |
8 | jobs:
9 | publish:
10 | uses: swoosh/actions/.github/workflows/publish.yml@main
11 | with:
12 | mode: 'package'
13 | secrets:
14 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v0.4.1
2 |
3 | Support non string labels, thanks @martosaur (#39)
4 |
5 | ## v0.4.0
6 |
7 | `subgraph` now returns a DG struct, thanks @jdewar ! (#25)
8 |
9 | ## v0.3.0
10 |
11 | Add loading from `:libgraph` `Graph`.
12 |
13 | ## v0.2.0
14 |
15 | Add interpolation support.
16 |
17 | ## v0.1.0
18 |
19 | [View highlights](https://github.com/princemaple/dg/blob/v0.1.0/README.md#highlights)
20 |
--------------------------------------------------------------------------------
/lib/collectable.ex:
--------------------------------------------------------------------------------
1 | defimpl Collectable, for: DG do
2 | def into(%DG{} = dg) do
3 | {dg, &push/2}
4 | end
5 |
6 | defp push(%DG{} = g, {:cont, {:vertex, v}}) do
7 | DG.add_vertex(g, v)
8 | g
9 | end
10 |
11 | defp push(%DG{} = g, {:cont, {:vertex, v, label}}) do
12 | DG.add_vertex(g, v, label)
13 | g
14 | end
15 |
16 | defp push(%DG{} = g, {:cont, {:edge, v1, v2}}) do
17 | DG.add_edge(g, v1, v2)
18 | g
19 | end
20 |
21 | defp push(%DG{} = g, {:cont, {:edge, v1, v2, label}}) do
22 | DG.add_edge(g, v1, v2, label)
23 | g
24 | end
25 |
26 | defp push(g, :done), do: g
27 | defp push(_g, :halt), do: :ok
28 | end
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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 | dg-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Po Chen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/inspect.ex:
--------------------------------------------------------------------------------
1 | defimpl Inspect, for: DG do
2 | import Inspect.Algebra
3 |
4 | def inspect(%DG{dg: dg, opts: opts}, _opts) do
5 | vertices = :digraph.vertices(dg)
6 |
7 | direction = Keyword.get(opts, :direction, "LR")
8 |
9 | content =
10 | vertices
11 | |> Enum.map(fn v ->
12 | case {:digraph.out_edges(dg, v), :digraph.in_edges(dg, v)} do
13 | {[], []} ->
14 | [inspect_node(dg, v)]
15 |
16 | {out_edges, _} ->
17 | out_edges
18 | |> Enum.map(&:digraph.edge(dg, &1))
19 | |> Enum.map(fn
20 | {_e, ^v, n, []} ->
21 | [inspect_node(dg, v), "-->", inspect_node(dg, n)]
22 |
23 | {_e, ^v, n, label} ->
24 | [inspect_node(dg, v), "--", inspect_term(label), "-->", inspect_node(dg, n)]
25 | end)
26 | |> Enum.intersperse([line()])
27 | end
28 | end)
29 | |> Enum.reject(&match?([], &1))
30 | |> Enum.intersperse([line()])
31 | |> List.flatten()
32 | |> concat()
33 |
34 | concat([
35 | "graph #{direction}",
36 | nest(
37 | concat([
38 | line(),
39 | content
40 | ]),
41 | 4
42 | )
43 | ])
44 | end
45 |
46 | defp inspect_node(dg, string) when is_binary(string), do: label(dg, string, string)
47 | defp inspect_node(dg, other), do: label(dg, other, inspect(other))
48 |
49 | defp label(dg, v, prefix) do
50 | case :digraph.vertex(dg, v) do
51 | {^v, []} -> [prefix]
52 | {^v, l} -> [prefix, "[", inspect_term(l), "]"]
53 | end
54 | |> concat
55 | end
56 |
57 | defp inspect_term(term) when is_binary(term), do: term
58 | defp inspect_term(term), do: inspect(term)
59 | end
60 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "abnf_parsec": {:hex, :abnf_parsec, "2.1.0", "c4e88d5d089f1698297c0daced12be1fb404e6e577ecf261313ebba5477941f9", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e0ed6290c7cc7e5020c006d1003520390c9bdd20f7c3f776bd49bfe3c5cd362a"},
3 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
4 | "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"},
5 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
6 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
7 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
8 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
10 | }
11 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule DG.MixProject do
2 | use Mix.Project
3 |
4 | @source_url "https://github.com/princemaple/dg"
5 | @version "0.4.1"
6 |
7 | def project do
8 | [
9 | app: :dg,
10 | version: @version,
11 | elixir: "~> 1.12",
12 | start_permanent: Mix.env() == :prod,
13 | deps: deps(),
14 | docs: docs(),
15 | package: package(),
16 | preferred_cli_env: [
17 | docs: :docs,
18 | "hex.publish": :docs
19 | ]
20 | ]
21 | end
22 |
23 | def application do
24 | [extra_applications: [:logger]]
25 | end
26 |
27 | defp deps do
28 | [
29 | {:abnf_parsec, "~> 2.0", runtime: false},
30 | {:libgraph, ">= 0.0.0", optional: true},
31 | {:ex_doc, ">= 0.0.0", only: :docs}
32 | ]
33 | end
34 |
35 | defp package do
36 | [
37 | description: "Elixir wrapper of `:digraph` with a pinch of protocols and sigils",
38 | licenses: ["MIT"],
39 | maintainers: ["Po Chen"],
40 | links: %{
41 | Changelog: "https://hexdocs.pm/dg/changelog.html",
42 | GitHub: @source_url
43 | }
44 | ]
45 | end
46 |
47 | defp docs do
48 | [
49 | extras: [
50 | "CHANGELOG.md": [],
51 | LICENSE: [title: "License"],
52 | "README.md": [title: "Overview"]
53 | ],
54 | assets: "assets",
55 | main: "readme",
56 | canonical: "http://hexdocs.pm/dg",
57 | homepage_url: @source_url,
58 | source_url: @source_url,
59 | source_ref: "v#{@version}",
60 | before_closing_body_tag: &before_closing_body_tag/1
61 | ]
62 | end
63 |
64 | defp before_closing_body_tag(:html) do
65 | """
66 |
67 |
85 | """
86 | end
87 |
88 | defp before_closing_body_tag(_), do: ""
89 | end
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DG
2 |
3 |
4 |
5 | Elixir wrapper of `:digraph` with a pinch of protocols and sigils
6 |
7 | ## Installation
8 |
9 | The package can be installed by adding `dg` to your list of dependencies in `mix.exs`:
10 |
11 | ```elixir
12 | def deps do
13 | [
14 | {:dg, "~> 0.4"},
15 | ...
16 | ]
17 | end
18 | ```
19 |
20 | ## Highlights
21 |
22 | ### Inspect
23 |
24 | ```elixir
25 | dg = DG.new()
26 | DG.add_vertex(dg, "v1")
27 | DG.add_vertex(dg, "v2", "label 2")
28 | DG.add_vertex(dg, "v3")
29 | DG.add_edge(dg, "v1", "v2")
30 | DG.add_edge(dg, "v1", "v3", "1 to 3")
31 |
32 | IO.inspect(dg)
33 | ```
34 |
35 | Outputs `mermaid.js` format flowchart
36 |
37 | ```
38 | graph LR
39 | v1-->v2[label 2]
40 | v1--1 to 3-->v3
41 | ```
42 |
43 | which can be put into livebook `mermaid` block and will display as
44 |
45 | ```mermaid
46 | graph LR
47 | v1-->v2[label 2]
48 | v1--1 to 3-->v3
49 | ```
50 |
51 | ### Collectable
52 |
53 | Easily add
54 |
55 | - vertices `[{:vertex, 1}, {:vertex, 2, "label 2"}, ...]`
56 | - or edges `[{:edge, 1, 2}, {:edge, "a", "b", "label ab"}, ...]`
57 |
58 | to the graph via `Enum.into/2`
59 |
60 | ```elixir
61 | dg = DG.new()
62 |
63 | ~w(a b c d e) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
64 |
65 | ~w(a b c d e)
66 | |> Enum.chunk_every(2, 1, :discard)
67 | |> Enum.map(fn [f, t] -> {:edge, f, t} end)
68 | |> Enum.into(dg)
69 |
70 | IO.inspect(dg)
71 | ```
72 |
73 | outputs
74 |
75 | ```mermaid
76 | graph LR
77 | b-->c
78 | c-->d
79 | a-->b
80 | d-->e
81 | ```
82 |
83 | ### Functions
84 |
85 | - Most functions from `:digraph` and `:digraph_utils` are mirrored under `DG`.
86 | - Some functions have their parameters re-arranged so that the graph is always the first parameter.
87 | (namely `reachable/2`, `reachable_neighbours/2`, `reaching/2` and `reaching_neighbours/2`)
88 | - `new/{0,1,2,3}` returns `%DG{}` instead of an Erlang `digraph`
89 | - `new/2` and `new/3` are shortcuts that can add vertices and edges on creation,
90 | which don't exist on `:digraph`
91 |
92 | ### Sigils
93 |
94 | `~G` and `~g` are provided so you can copy `mermaid.js` flowchart and paste into Elixir to get a `%DG{}`
95 |
96 | ```elixir
97 | import DG.Sigil
98 |
99 | dg = ~G"""
100 | graph LR
101 | a[some label]
102 | b[other label]
103 | 1-->2
104 | 3[three] -- three to four --> 4[four]
105 | a --> b
106 | """
107 | ```
108 |
109 | ```elixir
110 | # With interpolation
111 |
112 | label = "1 2 3"
113 | dg = ~g"""
114 | graph LR
115 | a --#{label}--> b
116 | """
117 | ```
118 |
119 | **caveat:** `:digraph` is stateful (using `ets`), don't use the sigil at compile time,
120 | e.g. as a module attribute, it won't carry over to runtime.
121 | Only use it in runtime code, e.g. function body,
122 | and remember to clean up properly when it's no longer used, with `delete/1`.
123 |
124 | ### Load from `libgraph`
125 |
126 | ```elixir
127 | dg = DG.new()
128 | DG.from({:libgraph, graph})
129 | ```
130 |
--------------------------------------------------------------------------------
/lib/sigil.ex:
--------------------------------------------------------------------------------
1 | defmodule DG.Sigil do
2 | @moduledoc """
3 | Sigils that parse `mermaid.js` format flowchart into `DG`
4 |
5 | iex> import DG.Sigil
6 | ...> dg = ~G"\""
7 | ...> graph LR
8 | ...> a[some label]
9 | ...> b[other label]
10 | ...> 1-->2
11 | ...> 3[three] -- three to four --> 4[four]
12 | ...> a --> b
13 | ...> "\""
14 | iex> DG.vertex(dg, "a")
15 | {"a", "some label"}
16 | iex> Enum.sort(DG.vertices(dg))
17 | ["1", "2", "3", "4", "a", "b"]
18 | iex> length(DG.edges(dg))
19 | 3
20 | """
21 |
22 | use AbnfParsec,
23 | abnf: """
24 | wsp = %x20 / %x09
25 | lwsp = *(wsp / LF / CRLF)
26 | direction = "TB" / "TD" / "LR"
27 | type = "flowchart" / "graph"
28 | label = 1*(ALPHA / DIGIT / %x20)
29 | square-brace-open = "["
30 | square-brace-close = "]"
31 | vertex-id = 1*(ALPHA / DIGIT)
32 | vertex = vertex-id [square-brace-open label square-brace-close]
33 | edge = vertex *wsp ("-->" / "--" label "-->") *wsp vertex
34 | vertex-or-edge = [lwsp] (edge / vertex)
35 | graph = lwsp type 1*wsp direction lwsp *vertex-or-edge
36 | """,
37 | parse: :graph,
38 | unbox: ["vertex-or-edge", "vertex-id"],
39 | ignore: ["wsp", "lwsp", "square-brace-open", "square-brace-close"],
40 | unwrap: ["type", "direction", "label"],
41 | transform: %{
42 | "vertex-id" => {:reduce, {List, :to_string, []}},
43 | "label" => [{:reduce, {List, :to_string, []}}, {:map, {String, :trim, []}}]
44 | }
45 |
46 | defp unwrap_vertex({:vertex, [v]}) do
47 | {:vertex, v}
48 | end
49 |
50 | defp unwrap_vertex({:vertex, [v, label: label]}) do
51 | {:vertex, v, label}
52 | end
53 |
54 | defp extract_vertex({:vertex, [v | _]}), do: v
55 | defp extract_vertex({:vertex, v, _label}), do: v
56 | defp extract_vertex({:vertex, v}), do: v
57 |
58 | defp extract_edge({:edge, v1, v2, _label}), do: {v1, v2}
59 | defp extract_edge({:edge, v1, v2}), do: {v1, v2}
60 |
61 | @doc false
62 | def prepare_gen(string) do
63 | {:ok, [graph: [{:type, _type}, {:direction, direction} | content]], _, _, _, _} =
64 | parse(string)
65 |
66 | vertices =
67 | content
68 | |> Enum.flat_map(fn
69 | {:vertex, _} = v ->
70 | [unwrap_vertex(v)]
71 |
72 | {:edge, [v1, "-->", v2]} ->
73 | [unwrap_vertex(v1), unwrap_vertex(v2)]
74 |
75 | {:edge, [v1, "--", _label, "-->", v2]} ->
76 | [unwrap_vertex(v1), unwrap_vertex(v2)]
77 | end)
78 | |> Enum.uniq_by(&extract_vertex/1)
79 |
80 | edges =
81 | content
82 | |> Enum.filter(fn
83 | {:edge, _} -> true
84 | _ -> false
85 | end)
86 | |> Enum.map(fn
87 | {:edge, [v1, "-->", v2]} ->
88 | {:edge, extract_vertex(v1), extract_vertex(v2)}
89 |
90 | {:edge, [v1, "--", {:label, label}, "-->", v2]} ->
91 | {:edge, extract_vertex(v1), extract_vertex(v2), label}
92 | end)
93 | |> Enum.uniq_by(&extract_edge/1)
94 |
95 | {direction, vertices, edges}
96 | end
97 |
98 | defp gen(string) do
99 | {direction, vertices, edges} = prepare_gen(string)
100 |
101 | quote do
102 | DG.new(
103 | unquote(Macro.escape(vertices)),
104 | unquote(Macro.escape(edges)),
105 | direction: unquote(direction)
106 | )
107 | end
108 | end
109 |
110 | defmacro sigil_g({:<<>>, _, [string]}, _opts), do: gen(string)
111 |
112 | defmacro sigil_g({:<<>>, _, _pieces} = string, _opts) do
113 | quote do
114 | import unquote(__MODULE__), only: [prepare_gen: 1]
115 | {direction, vertices, edges} = prepare_gen(unquote(string))
116 | DG.new(vertices, edges, direction: direction)
117 | end
118 | end
119 |
120 | defmacro sigil_G({:<<>>, _, [string]}, _opts), do: gen(string)
121 | end
122 |
--------------------------------------------------------------------------------
/test/dg_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DG.Test do
2 | use ExUnit.Case
3 | doctest DG
4 | doctest DG.Sigil
5 |
6 | setup do
7 | {:ok, dg: DG.new()}
8 | end
9 |
10 | describe "inspect" do
11 | test "disjoint", %{dg: dg} do
12 | DG.add_vertex(dg, 1)
13 |
14 | assert inspect(dg) ==
15 | String.trim("""
16 | graph LR
17 | 1
18 | """)
19 | end
20 |
21 | test "direction", %{dg: dg} do
22 | DG.add_vertex(dg, 1)
23 | dg = DG.options(dg, direction: "TB")
24 |
25 | assert inspect(dg) ==
26 | String.trim("""
27 | graph TB
28 | 1
29 | """)
30 | end
31 |
32 | test "vertex with label", %{dg: dg} do
33 | DG.add_vertex(dg, 2, "two")
34 |
35 | assert inspect(dg) ==
36 | String.trim("""
37 | graph LR
38 | 2[two]
39 | """)
40 | end
41 |
42 | test "arrow", %{dg: dg} do
43 | DG.add_vertex(dg, 1)
44 | DG.add_vertex(dg, 2)
45 | DG.add_edge(dg, 1, 2)
46 |
47 | assert inspect(dg) ==
48 | String.trim("""
49 | graph LR
50 | 1-->2
51 | """)
52 | end
53 |
54 | test "arrow with label", %{dg: dg} do
55 | DG.add_vertex(dg, 1)
56 | DG.add_vertex(dg, 2)
57 | DG.add_edge(dg, 1, 2, "one to two")
58 |
59 | assert inspect(dg) ==
60 | String.trim("""
61 | graph LR
62 | 1--one to two-->2
63 | """)
64 | end
65 |
66 | test "labels", %{dg: dg} do
67 | DG.add_vertex(dg, 1)
68 | DG.add_vertex(dg, 2, "two")
69 | DG.add_edge(dg, 1, 2, "one to two")
70 |
71 | assert inspect(dg) ==
72 | String.trim("""
73 | graph LR
74 | 1--one to two-->2[two]
75 | """)
76 | end
77 |
78 | test "fancy labels", %{dg: dg} do
79 | DG.add_vertex(dg, 1)
80 | DG.add_vertex(dg, 2, :c.pid(0, 42, 42))
81 | DG.add_edge(dg, 1, 2, :"one to two")
82 |
83 | assert inspect(dg) ==
84 | String.trim("""
85 | graph LR
86 | 1--:\"one to two\"-->2[#PID<0.42.42>]
87 | """)
88 | end
89 | end
90 |
91 | describe "collectable" do
92 | test "add vertices", %{dg: dg} do
93 | ~w(a b c d e) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
94 | assert length(DG.vertices(dg)) == 5
95 | end
96 |
97 | test "add vertices with labels", %{dg: dg} do
98 | ~w(a b c d e) |> Enum.map(&{:vertex, &1, "HI #{&1}"}) |> Enum.into(dg)
99 | assert length(DG.vertices(dg)) == 5
100 | assert {"a", "HI a"} = DG.vertex(dg, "a")
101 | end
102 |
103 | test "add edges", %{dg: dg} do
104 | ~w(a b c d e) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
105 |
106 | ~w(a b c d e)
107 | |> Enum.chunk_every(2, 1, :discard)
108 | |> Enum.map(fn [f, t] -> {:edge, f, t} end)
109 | |> Enum.into(dg)
110 |
111 | assert length(DG.edges(dg)) == 4
112 | end
113 |
114 | test "add edges with labels", %{dg: dg} do
115 | ~w(a b c d e) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
116 |
117 | ~w(a b c d e)
118 | |> Enum.chunk_every(2, 1, :discard)
119 | |> Enum.map(fn [f, t] -> {:edge, f, t, "#{f} -> #{t}"} end)
120 | |> Enum.into(dg)
121 |
122 | assert length(DG.edges(dg)) == 4
123 | assert {_, "a", "b", "a -> b"} = DG.edge(dg, List.first(DG.edges(dg, "a")))
124 | end
125 | end
126 |
127 | describe "subgraph" do
128 | test "returns a DG" do
129 | dg = DG.new(test: true)
130 | ~w(a b c d e) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
131 |
132 | assert %DG{} = DG.subgraph(dg, ~w(a b c))
133 | end
134 |
135 | test "handles digraph options" do
136 | dg = DG.new(test: true)
137 | ~w(a b c) |> Enum.map(&{:vertex, &1}) |> Enum.into(dg)
138 |
139 | label = "a -> b"
140 | Enum.into([{:edge, "a", "b", label}], dg)
141 |
142 | subgraph = DG.subgraph(dg, ~w(a b), digraph_opts: [keep_labels: true])
143 |
144 | assert {_, "a", "b", ^label} = DG.edge(subgraph, List.first(DG.edges(dg, "a")))
145 | end
146 | end
147 |
148 | describe "sigil" do
149 | import DG.Sigil
150 |
151 | test "integration" do
152 | dg = ~g"""
153 | graph LR
154 | a[aaaaa]
155 | b[bb bb bb]
156 | 1--> 2
157 | 3[three] -- 1 2 3 --> 4[four]
158 | c -->a
159 | """
160 |
161 | text = inspect(dg)
162 |
163 | assert text =~ "graph LR"
164 | assert text =~ "3[three]--1 2 3-->4[four]"
165 | assert text =~ "1-->2"
166 | assert text =~ "a[aaaaa]"
167 | assert text =~ "b[bb bb bb]"
168 | end
169 |
170 | test "interpolation" do
171 | label = "1 2 3"
172 |
173 | dg = ~g"""
174 | graph LR
175 | a -- #{label} --> b
176 | """
177 |
178 | text = inspect(dg)
179 | assert text =~ "a--1 2 3-->b"
180 | end
181 | end
182 | end
183 |
--------------------------------------------------------------------------------
/lib/dg.ex:
--------------------------------------------------------------------------------
1 | defmodule DG do
2 | @external_resource "README.md"
3 | @moduledoc File.read!("README.md") |> String.split("") |> List.last()
4 |
5 | defstruct dg: nil, opts: []
6 |
7 | def new(opts \\ []) do
8 | {digraph_opts, opts} = Keyword.pop(opts, :digraph_opts, [])
9 | %__MODULE__{dg: :digraph.new(digraph_opts), opts: opts}
10 | end
11 |
12 | def new(vertices, opts) do
13 | Enum.into(vertices, DG.new(opts))
14 | end
15 |
16 | def new(vertices, edges, opts) do
17 | Enum.into(edges, new(vertices, opts))
18 | end
19 |
20 | def options(%__MODULE__{opts: opts}) do
21 | opts
22 | end
23 |
24 | def options(%__MODULE__{opts: opts} = dg, new_opts) do
25 | %{dg | opts: Keyword.merge(opts, new_opts)}
26 | end
27 |
28 | def add_edge(%__MODULE__{dg: dg}, v1, v2) do
29 | :digraph.add_edge(dg, v1, v2)
30 | end
31 |
32 | def add_edge(%__MODULE__{dg: dg}, v1, v2, label) do
33 | :digraph.add_edge(dg, v1, v2, label)
34 | end
35 |
36 | def add_edge(%__MODULE__{dg: dg}, e, v1, v2, label) do
37 | :digraph.add_edge(dg, e, v1, v2, label)
38 | end
39 |
40 | def add_vertex(%__MODULE__{dg: dg}) do
41 | :digraph.add_vertex(dg)
42 | end
43 |
44 | def add_vertex(%__MODULE__{dg: dg}, v) do
45 | :digraph.add_vertex(dg, v)
46 | end
47 |
48 | def add_vertex(%__MODULE__{dg: dg}, v, label) do
49 | :digraph.add_vertex(dg, v, label)
50 | end
51 |
52 | def del_edge(%__MODULE__{dg: dg}, e) do
53 | :digraph.del_edge(dg, e)
54 | end
55 |
56 | def del_edges(%__MODULE__{dg: dg}, edges) do
57 | :digraph.del_edges(dg, edges)
58 | end
59 |
60 | def del_path(%__MODULE__{dg: dg}, v1, v2) do
61 | :digraph.del_path(dg, v1, v2)
62 | end
63 |
64 | def del_vertex(%__MODULE__{dg: dg}, v) do
65 | :digraph.del_vertex(dg, v)
66 | end
67 |
68 | def del_vertices(%__MODULE__{dg: dg}, vertices) do
69 | :digraph.del_vertices(dg, vertices)
70 | end
71 |
72 | def delete(%__MODULE__{dg: dg}) do
73 | :digraph.delete(dg)
74 | end
75 |
76 | def edge(%__MODULE__{dg: dg}, e) do
77 | :digraph.edge(dg, e)
78 | end
79 |
80 | def edges(%__MODULE__{dg: dg}) do
81 | :digraph.edges(dg)
82 | end
83 |
84 | def edges(%__MODULE__{dg: dg}, v) do
85 | :digraph.edges(dg, v)
86 | end
87 |
88 | def get_cycle(%__MODULE__{dg: dg}, v) do
89 | :digraph.get_cycle(dg, v)
90 | end
91 |
92 | def get_path(%__MODULE__{dg: dg}, v1, v2) do
93 | :digraph.get_path(dg, v1, v2)
94 | end
95 |
96 | def get_short_cycle(%__MODULE__{dg: dg}, v) do
97 | :digraph.get_short_cycle(dg, v)
98 | end
99 |
100 | def get_short_path(%__MODULE__{dg: dg}, v1, v2) do
101 | :digraph.get_short_path(dg, v1, v2)
102 | end
103 |
104 | def in_degree(%__MODULE__{dg: dg}, v) do
105 | :digraph.in_degree(dg, v)
106 | end
107 |
108 | def in_edges(%__MODULE__{dg: dg}, v) do
109 | :digraph.in_edges(dg, v)
110 | end
111 |
112 | def in_neighbours(%__MODULE__{dg: dg}, v) do
113 | :digraph.in_neighbours(dg, v)
114 | end
115 |
116 | def info(%__MODULE__{dg: dg}) do
117 | :digraph.info(dg)
118 | end
119 |
120 | def no_edges(%__MODULE__{dg: dg}) do
121 | :digraph.no_edges(dg)
122 | end
123 |
124 | def no_vertices(%__MODULE__{dg: dg}) do
125 | :digraph.no_vertices(dg)
126 | end
127 |
128 | def out_degree(%__MODULE__{dg: dg}, v) do
129 | :digraph.out_degree(dg, v)
130 | end
131 |
132 | def out_edges(%__MODULE__{dg: dg}, v) do
133 | :digraph.out_edges(dg, v)
134 | end
135 |
136 | def out_neighbours(%__MODULE__{dg: dg}, v) do
137 | :digraph.out_neighbours(dg, v)
138 | end
139 |
140 | def vertex(%__MODULE__{dg: dg}, v) do
141 | :digraph.vertex(dg, v)
142 | end
143 |
144 | def vertices(%__MODULE__{dg: dg}) do
145 | :digraph.vertices(dg)
146 | end
147 |
148 | def arborescence_root(%__MODULE__{dg: dg}) do
149 | :digraph_utils.arborescence_root(dg)
150 | end
151 |
152 | def components(%__MODULE__{dg: dg}) do
153 | :digraph_utils.components(dg)
154 | end
155 |
156 | def condensation(%__MODULE__{dg: dg}) do
157 | :digraph_utils.condensation(dg)
158 | end
159 |
160 | def cyclic_strong_components(%__MODULE__{dg: dg}) do
161 | :digraph_utils.cyclic_strong_components(dg)
162 | end
163 |
164 | def is_acyclic(%__MODULE__{dg: dg}) do
165 | :digraph_utils.is_acyclic(dg)
166 | end
167 |
168 | def is_arborescence(%__MODULE__{dg: dg}) do
169 | :digraph_utils.is_arborescence(dg)
170 | end
171 |
172 | def is_tree(%__MODULE__{dg: dg}) do
173 | :digraph_utils.is_tree(dg)
174 | end
175 |
176 | def loop_vertices(%__MODULE__{dg: dg}) do
177 | :digraph_utils.loop_vertices(dg)
178 | end
179 |
180 | def postorder(%__MODULE__{dg: dg}) do
181 | :digraph_utils.postorder(dg)
182 | end
183 |
184 | def preorder(%__MODULE__{dg: dg}) do
185 | :digraph_utils.preorder(dg)
186 | end
187 |
188 | def reachable(%__MODULE__{dg: dg}, vertices) do
189 | :digraph_utils.reachable(vertices, dg)
190 | end
191 |
192 | def reachable_neighbours(%__MODULE__{dg: dg}, vertices) do
193 | :digraph_utils.reachable_neighbours(vertices, dg)
194 | end
195 |
196 | def reaching(%__MODULE__{dg: dg}, vertices) do
197 | :digraph_utils.reaching(vertices, dg)
198 | end
199 |
200 | def reaching_neighbours(%__MODULE__{dg: dg}, vertices) do
201 | :digraph_utils.reaching_neighbours(vertices, dg)
202 | end
203 |
204 | def strong_components(%__MODULE__{dg: dg}) do
205 | :digraph_utils.strong_components(dg)
206 | end
207 |
208 | def subgraph(%__MODULE__{dg: dg}, vertices, options \\ []) do
209 | {digraph_opts, opts} = Keyword.pop(options, :digraph_opts, [])
210 | subgraph = :digraph_utils.subgraph(dg, vertices, digraph_opts)
211 | %__MODULE__{dg: subgraph, opts: opts}
212 | end
213 |
214 | def topsort(%__MODULE__{dg: dg}) do
215 | :digraph_utils.topsort(dg)
216 | end
217 |
218 | if Code.ensure_loaded?(Graph) do
219 | def from({:libgraph, graph}) do
220 | dg = DG.new()
221 |
222 | graph
223 | |> Graph.vertices()
224 | |> Enum.map(&{:vertex, &1})
225 | |> Enum.into(dg)
226 |
227 | graph
228 | |> Graph.edges()
229 | |> Enum.map(fn
230 | %Graph.Edge{label: nil} = e ->
231 | {:edge, e.v1, e.v2}
232 |
233 | %Graph.Edge{} = e ->
234 | {:edge, e.v1, e.v2, e.label}
235 | end)
236 | |> Enum.into(dg)
237 | end
238 | end
239 | end
240 |
--------------------------------------------------------------------------------