├── 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 | --------------------------------------------------------------------------------