From 8a09c30d4d61cd7fcea7ae68980a501d65480690 Mon Sep 17 00:00:00 2001 From: mrkaspa Date: Sun, 13 Mar 2016 12:03:31 -0500 Subject: [PATCH] graph validator added --- lib/exstreme/graph.ex | 83 ++++++++++++++ .../{stream_graph.ex => graph_creator.ex} | 52 ++++++--- lib/exstreme/graph_validator.ex | 62 ++++++++++ mix.exs | 4 +- mix.lock | 1 + test/exstreme/graph_creator_test.exs | 101 ++++++++++++++++ test/exstreme/graph_validator_test.exs | 84 ++++++++++++++ test/exstreme/stream_graph_test.exs | 108 ------------------ test/exstreme_test.exs | 3 - 9 files changed, 371 insertions(+), 127 deletions(-) create mode 100644 lib/exstreme/graph.ex rename lib/exstreme/{stream_graph.ex => graph_creator.ex} (64%) create mode 100644 lib/exstreme/graph_validator.ex create mode 100644 mix.lock create mode 100644 test/exstreme/graph_creator_test.exs create mode 100644 test/exstreme/graph_validator_test.exs delete mode 100644 test/exstreme/stream_graph_test.exs diff --git a/lib/exstreme/graph.ex b/lib/exstreme/graph.ex new file mode 100644 index 0000000..889f1c7 --- /dev/null +++ b/lib/exstreme/graph.ex @@ -0,0 +1,83 @@ +defmodule Exstreme.Graph do + @moduledoc """ + """ + alias __MODULE__ + + @type t :: %Graph{params: [key: term], nodes: %{key: atom}, connections: %{key: atom}} + defstruct params: [], nodes: %{}, connections: %{} + + @doc """ + """ + @spec count_nodes(t) :: non_neg_integer + def count_nodes(%Graph{nodes: nodes}) do + nodes + |> Map.keys + |> Enum.count + end + + @doc """ + """ + @spec count_connections(t) :: non_neg_integer + def count_connections(%Graph{connections: connections}) do + connections + |> Map.keys + |> Enum.count + end + + @doc """ + """ + @spec connections_stats(t) :: %{key: integer} + def connections_stats(graph) do + graph + |> map_to_connections + |> Enum.reduce(Map.new, fn(key, map) -> + Map.update(map, key, 1, &(&1 + 1)) + end) + end + + @doc """ + """ + @spec find_start_node(t) :: [atom] + def find_start_node(%Graph{nodes: nodes, connections: connections}) do + is_first? = + fn(node) -> + at_first?(connections, node) and not(at_last?(connections, node)) + end + + nodes + |> Map.keys + |> Enum.filter(is_first?) + end + + # private + + @spec map_to_connections(t) :: [atom] + defp map_to_connections(%Graph{nodes: nodes, connections: connections}) do + to_connections = + fn(node) -> + case {at_first?(connections, node), at_last?(connections, node)} do + {true, true} -> :connected + {true, false} -> :begin + {false, true} -> :end + {false, false} -> :unconnected + end + end + + nodes + |> Map.keys + |> Enum.map(to_connections) + end + + @spec at_first?(%{key: atom}, atom) :: boolean + defp at_first?(connections, node) do + Map.has_key?(connections, node) + end + + @spec at_last?(%{key: atom}, atom) :: boolean + defp at_last?(connections, node) do + connections + |> Map.values + |> List.flatten + |> Enum.member?(node) + end +end diff --git a/lib/exstreme/stream_graph.ex b/lib/exstreme/graph_creator.ex similarity index 64% rename from lib/exstreme/stream_graph.ex rename to lib/exstreme/graph_creator.ex index 14fb9b4..357528d 100644 --- a/lib/exstreme/stream_graph.ex +++ b/lib/exstreme/graph_creator.ex @@ -1,16 +1,18 @@ -defmodule Exstreme.StreamGraph do +defmodule Exstreme.GraphCreator do @moduledoc """ """ - defmodule Graph do - defstruct params: [], nodes: %{}, connections: %{} - end + alias Exstreme.Graph + + @type update_map_func :: (%{key: atom} -> %{key: atom}) @doc """ """ + @spec create_graph([key: term]) :: Graph.t def create_graph(params \\ []), do: %Graph{params: params} @doc """ """ + @spec create_node(Graph.t, [key: term]) :: {Graph.t, atom} def create_node(graph = %Graph{nodes: nodes}, params \\ []) do key = next_node_key(nodes) @@ -20,6 +22,7 @@ defmodule Exstreme.StreamGraph do @doc """ """ + @spec create_broadcast(Graph.t, [key: term]) :: {Graph.t, atom} def create_broadcast(graph = %Graph{nodes: nodes}, params \\ []) do key = next_broadcast_key(nodes) @@ -29,6 +32,7 @@ defmodule Exstreme.StreamGraph do @doc """ """ + @spec create_funnel(Graph.t, [key: term]) :: {Graph.t, atom} def create_funnel(graph = %Graph{nodes: nodes}, params \\ []) do key = next_funnel_key(nodes) @@ -38,14 +42,16 @@ defmodule Exstreme.StreamGraph do @doc """ """ - def add_connection(graph = %Graph{nodes: nodes}, start, finish) when start != finish do - if has_node(nodes, start) && has_node(nodes, finish) do + @spec add_connection(Graph.t, atom, atom) :: Graph.t + def add_connection(graph = %Graph{nodes: nodes}, start, finish) when start !== finish do + if has_node(nodes, start) and has_node(nodes, finish) do update_in(graph.connections, store_connection_fn(start, finish)) end end - #private + # private + @spec store_connection_fn(atom, atom) :: update_map_func defp store_connection_fn(start, finish) do fn(connections) -> add_connection = fn(keywords, start, finish) -> @@ -58,6 +64,7 @@ defmodule Exstreme.StreamGraph do end) end + # validates before adding connections |> validate_repeated(start, finish) |> validate_repeated(finish, start) @@ -67,16 +74,20 @@ defmodule Exstreme.StreamGraph do end end - def validate_repeated(connections, start, finish) do - if connections |> Enum.any?(&(&1 == {start, finish})) do + # validations before adding a node + + @spec validate_repeated(%{key: atom}, atom, atom) :: %{key: atom} + defp validate_repeated(connections, start, finish) do + if Enum.any?(connections, &(&1 == {start, finish})) do raise ArgumentError, message: "there is already a connection like that" else connections end end + @spec validate_position(%{key: atom}, atom, :start | :end) :: %{key: atom} defp validate_position(connections, node, position) do - case node|> Atom.to_string |> String.first do + case node |> Atom.to_string |> String.first do "n" -> validate_position_node(connections, node, position) "b" -> validate_position_broadcast(connections, node, position) "f" -> validate_position_funnel(connections, node, position) @@ -84,30 +95,35 @@ defmodule Exstreme.StreamGraph do end end + @spec validate_position_node(%{key: atom}, atom, :start) :: %{key: atom} defp validate_position_node(connections, node, :start) do validate_position_start(connections, node,"the node can't be twice at start position #{node}") end + @spec validate_position_node(%{key: atom}, atom, :end) :: %{key: atom} defp validate_position_node(connections, node, :end) do validate_position_end(connections, node,"the node can't be twice at end position") end - defp validate_position_broadcast(connections, _, :start), do: connections + @spec validate_position_broadcast(%{key: atom}, atom, :start) :: %{key: atom} + defp validate_position_broadcast(connections, _node, :start), do: connections + @spec validate_position_broadcast(%{key: atom}, atom, :end) :: %{key: atom} defp validate_position_broadcast(connections, bct, :end) do validate_position_end(connections, bct, "the broadcast can't be twice at end position") end + @spec validate_position_funnel(%{key: atom}, atom, :start) :: %{key: atom} defp validate_position_funnel(connections, node, :start) do validate_position_start(connections, node,"the funnel can't be twice at start position #{node}") end - defp validate_position_funnel(connections, node, :end), do: connections + @spec validate_position_funnel(%{key: atom}, atom, :end) :: %{key: atom} + defp validate_position_funnel(connections, _node, :end), do: connections + @spec validate_position_start(%{key: atom}, atom, String.t) :: %{key: atom} defp validate_position_start(connections, node, msg) do - exist = - connections - |> Map.has_key?(node) + exist = Map.has_key?(connections, node) if exist do raise ArgumentError, message: msg else @@ -115,6 +131,7 @@ defmodule Exstreme.StreamGraph do end end + @spec validate_position_end(%{key: atom}, atom, String.t) :: %{key: atom} defp validate_position_end(connections, node, msg) do exist = connections @@ -127,6 +144,7 @@ defmodule Exstreme.StreamGraph do end end + @spec has_node(%{key: atom}, atom) :: true defp has_node(nodes, node) do if Map.has_key?(nodes, node) do true @@ -135,18 +153,22 @@ defmodule Exstreme.StreamGraph do end end + @spec next_node_key(%{key: atom}) :: atom defp next_node_key(nodes) do next_key(nodes, "n") end + @spec next_broadcast_key(%{key: atom}) :: atom defp next_broadcast_key(nodes) do next_key(nodes, "b") end + @spec next_funnel_key(%{key: atom}) :: atom defp next_funnel_key(nodes) do next_key(nodes, "f") end + @spec next_key(%{key: atom}, String.t) :: atom defp next_key(map, letter) do count = map diff --git a/lib/exstreme/graph_validator.ex b/lib/exstreme/graph_validator.ex new file mode 100644 index 0000000..07d9883 --- /dev/null +++ b/lib/exstreme/graph_validator.ex @@ -0,0 +1,62 @@ +defmodule Exstreme.GraphValidator do + alias Exstreme.Graph + @moduledoc """ + """ + + @doc """ + """ + @spec validate(Graph.t) :: :ok | {:error, []} + def validate(graph) do + with :ok <- validate_must_have_connections(graph), + :ok <- validate_start_nodes(graph), + :ok <- validate_connectivity(graph), + do: :ok + end + + # private + + @spec validate_must_have_connections(Graph.t) :: :ok | {:error, []} + defp validate_must_have_connections(graph) do + nodes_amount = Graph.count_nodes(graph) + connections_amount = Graph.count_connections(graph) + if nodes_amount > 0 and connections_amount == 0 do + {:error, []} + else + :ok + end + end + + @spec validate_start_nodes(Graph.t) :: :ok | {:error, []} + defp validate_start_nodes(graph) do + start_nodes = Graph.find_start_node(graph) + + with :ok <- validate_should_start_with_one_node(start_nodes), + :ok <- validate_should_start_with_node(start_nodes), + do: :ok + end + + @spec validate_should_start_with_one_node([atom, ...]) :: :ok | {:error, []} + defp validate_should_start_with_one_node([_]), do: :ok + + defp validate_should_start_with_one_node(_), do: {:error, []} + + @spec validate_should_start_with_node([atom, ...]) :: :ok | {:error, []} + defp validate_should_start_with_node([start_node]) do + start_char = start_node |> Atom.to_string |> String.first + if start_char == "n" do + :ok + else + {:error, []} + end + end + + @spec validate_connectivity(Graph.t) :: :ok | {:error, []} + defp validate_connectivity(graph) do + stats = Graph.connections_stats(graph) + if stats[:unconnected] == nil do + :ok + else + {:error, []} + end + end +end diff --git a/mix.exs b/mix.exs index 53da30e..0d8adf8 100644 --- a/mix.exs +++ b/mix.exs @@ -27,6 +27,8 @@ defmodule Exstreme.Mixfile do # # Type "mix help deps" for more examples and options defp deps do - [] + [ + {:dialyxir, "~> 0.3", only: [:dev]} + ] end end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..4e40a5a --- /dev/null +++ b/mix.lock @@ -0,0 +1 @@ +%{"dialyxir": {:hex, :dialyxir, "0.3.3"}} diff --git a/test/exstreme/graph_creator_test.exs b/test/exstreme/graph_creator_test.exs new file mode 100644 index 0000000..44e80bb --- /dev/null +++ b/test/exstreme/graph_creator_test.exs @@ -0,0 +1,101 @@ +defmodule Exstreme.GraphCreatorTest do + use ExUnit.Case + alias Exstreme.GraphCreator + alias Exstreme.Graph + doctest Exstreme.GraphCreator + + test "creates a valid graph struct" do + compare_graph = %Graph{ + nodes: %{n1: [], n2: []}, + connections: %{n1: :n2} + } + assert create_graph == compare_graph + end + + test "throws an error when adding again the relation" do + assert_raise ArgumentError, fn -> + create_graph + |> GraphCreator.add_connection(:n1, :n2) + end + end + + test "throws an error when adding again a self relation" do + assert_raise FunctionClauseError, fn -> + create_graph + |> GraphCreator.add_connection(:n1, :n1) + end + end + + test "throws an error when adding a cicle relation" do + assert_raise ArgumentError, "there is already a connection like that", fn -> + create_graph + |> GraphCreator.add_connection(:n2, :n1) + end + end + + test "can create n3 and add a relation between n2 and n3" do + compare_graph = %Graph{ + nodes: %{n1: [], n2: [], n3: []}, + connections: %{n1: :n2, n2: :n3} + } + + {graph, n3} = GraphCreator.create_node(create_graph, params) + new_graph = GraphCreator.add_connection(graph, :n2, n3) + + assert new_graph == compare_graph + end + + test "can add a broadcast an many nodes to the broadcast" do + compare_graph = %Graph{ + nodes: %{n1: [], n2: [], b1: [], n3: [], n4: []}, + connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3]} + } + + {graph, b1} = GraphCreator.create_broadcast(create_graph, params) + {graph, n3} = GraphCreator.create_node(graph, params) + {graph, n4} = GraphCreator.create_node(graph, params) + + new_graph = + graph + |> GraphCreator.add_connection(:n2, b1) + |> GraphCreator.add_connection(b1, n3) + |> GraphCreator.add_connection(b1, n4) + + assert new_graph == compare_graph + end + + test "can add a funnel" do + compare_graph = %Graph{ + nodes: %{n1: [], n2: [], b1: [], n3: [], n4: [], f1: [], n5: []}, + connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3], n3: :f1, n4: :f1, f1: :n5} + } + + {graph, b1} = GraphCreator.create_broadcast(create_graph, params) + {graph, n3} = GraphCreator.create_node(graph, params) + {graph, n4} = GraphCreator.create_node(graph, params) + {graph, f1} = GraphCreator.create_funnel(graph, params) + {graph, n5} = GraphCreator.create_node(graph, params) + + new_graph = + graph + |> GraphCreator.add_connection(:n2, b1) + |> GraphCreator.add_connection(b1, n3) + |> GraphCreator.add_connection(b1, n4) + |> GraphCreator.add_connection(n3, f1) + |> GraphCreator.add_connection(n4, f1) + |> GraphCreator.add_connection(f1, n5) + + assert new_graph == compare_graph + end + + # private + + defp params, do: [] + + defp create_graph do + graph = GraphCreator.create_graph(params) + {graph, n1} = GraphCreator.create_node(graph, params) + {graph, n2} = GraphCreator.create_node(graph, params) + GraphCreator.add_connection(graph, n1, n2) + end +end diff --git a/test/exstreme/graph_validator_test.exs b/test/exstreme/graph_validator_test.exs new file mode 100644 index 0000000..e46be28 --- /dev/null +++ b/test/exstreme/graph_validator_test.exs @@ -0,0 +1,84 @@ +defmodule Exstreme.GraphValidatorTest do + use ExUnit.Case + alias Exstreme.{GraphCreator, GraphValidator} + doctest Exstreme.GraphValidator + + test "the graph with many nodes must be valid" do + assert :ok = GraphValidator.validate(graph_many_nodes) + end + + test "the graph with one node must be invalid" do + assert {:error, _} = GraphValidator.validate(graph_one_node_no_connections) + end + + test "the graph without connections is invalid" do + assert {:error, _} = GraphValidator.validate(graph_no_connections) + end + + test "the graph should start in a node" do + assert {:error, _} = GraphValidator.validate(graph_start_with_broadcast) + assert {:error, _} = GraphValidator.validate(graph_start_with_funnnel) + end + + test "all the nodes in the graph has to be connected" do + assert {:error, _} = GraphValidator.validate(graph_unconnected_nodes) + end + + # private + + # valid graphs + + defp graph_many_nodes do + graph = GraphCreator.create_graph(params) + {graph, n1} = GraphCreator.create_node(graph, params) + {graph, n2} = GraphCreator.create_node(graph, params) + {graph, b1} = GraphCreator.create_broadcast(graph, params) + {graph, n3} = GraphCreator.create_node(graph, params) + {graph, n4} = GraphCreator.create_node(graph, params) + {graph, f1} = GraphCreator.create_funnel(graph, params) + {graph, n5} = GraphCreator.create_node(graph, params) + + graph + |> GraphCreator.add_connection(n1, n2) + |> GraphCreator.add_connection(n2, b1) + |> GraphCreator.add_connection(b1, n3) + |> GraphCreator.add_connection(b1, n4) + |> GraphCreator.add_connection(n3, f1) + |> GraphCreator.add_connection(n4, f1) + |> GraphCreator.add_connection(f1, n5) + end + + # invalid graphs + + defp graph_one_node_no_connections do + graph = GraphCreator.create_graph(params) + {graph, _n1} = GraphCreator.create_node(graph, params) + graph + end + + defp graph_no_connections, do: GraphCreator.create_graph(params) + + defp graph_start_with_broadcast do + graph = GraphCreator.create_graph(params) + {graph, b1} = GraphCreator.create_broadcast(graph, params) + {graph, n1} = GraphCreator.create_node(graph, params) + GraphCreator.add_connection(graph, b1, n1) + end + + defp graph_start_with_funnnel do + graph = GraphCreator.create_graph(params) + {graph, f1} = GraphCreator.create_funnel(graph, params) + {graph, n1} = GraphCreator.create_node(graph, params) + GraphCreator.add_connection(graph, f1, n1) + end + + defp graph_unconnected_nodes do + graph = GraphCreator.create_graph(params) + {graph, n1} = GraphCreator.create_node(graph, params) + {graph, n2} = GraphCreator.create_node(graph, params) + {graph, _n3} = GraphCreator.create_node(graph, params) + GraphCreator.add_connection(graph, n1, n2) + end + + defp params, do: [] +end diff --git a/test/exstreme/stream_graph_test.exs b/test/exstreme/stream_graph_test.exs deleted file mode 100644 index e7f7526..0000000 --- a/test/exstreme/stream_graph_test.exs +++ /dev/null @@ -1,108 +0,0 @@ -defmodule Exstreme.StreamGraphTest do - use ExUnit.Case - alias Exstreme.StreamGraph - alias Exstreme.StreamGraph.Graph - doctest Exstreme.StreamGraph - - def params, do: [] - - def create_graph do - with( - graph <- StreamGraph.create_graph(params), - {graph, n1} <- StreamGraph.create_node(graph, params), - {graph, n2} <- StreamGraph.create_node(graph, params), - do: StreamGraph.add_connection(graph, n1, n2) - ) - end - - test "creates a valid graph struct" do - compare_graph = %Graph{ - nodes: %{n1: [], n2: []}, - connections: %{n1: :n2} - } - assert create_graph == compare_graph - end - - test "throws an error when adding again the relation" do - assert_raise ArgumentError, fn -> - create_graph - |> StreamGraph.add_connection(:n1, :n2) - end - end - - test "throws an error when adding again a self relation" do - assert_raise FunctionClauseError, fn -> - create_graph - |> StreamGraph.add_connection(:n1, :n1) - end - end - - test "throws an error when adding a cicle relation" do - assert_raise ArgumentError, "there is already a connection like that", fn -> - create_graph - |> StreamGraph.add_connection(:n2, :n1) - end - end - - test "can create n3 and add a relation between n2 and n3" do - compare_graph = %Graph{ - nodes: %{n1: [], n2: [], n3: []}, - connections: %{n1: :n2, n2: :n3} - } - - new_graph = - with( - {graph, n3} <- StreamGraph.create_node(create_graph, params), - do: graph |> StreamGraph.add_connection(:n2, n3) - ) - - assert new_graph == compare_graph - end - - test "can add a broadcast an many nodes to the broadcast" do - compare_graph = %Graph{ - nodes: %{n1: [], n2: [], b1: [], n3: [], n4: []}, - connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3]} - } - - new_graph = - with( - {graph, b1} <- StreamGraph.create_broadcast(create_graph, params), - {graph, n3} <- StreamGraph.create_node(graph, params), - {graph, n4} <- StreamGraph.create_node(graph, params) - ) do - graph - |> StreamGraph.add_connection(:n2, b1) - |> StreamGraph.add_connection(b1, n3) - |> StreamGraph.add_connection(b1, n4) - end - - assert new_graph == compare_graph - end - - test "can add a funnel" do - compare_graph = %Graph{ - nodes: %{n1: [], n2: [], b1: [], n3: [], n4: [], f1: [], n5: []}, - connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3], n3: :f1, n4: :f1, f1: :n5} - } - - new_graph = - with( - {graph, b1} <- StreamGraph.create_broadcast(create_graph, params), - {graph, n3} <- StreamGraph.create_node(graph, params), - {graph, n4} <- StreamGraph.create_node(graph, params), - {graph, f1} <- StreamGraph.create_funnel(graph, params), - {graph, n5} <- StreamGraph.create_node(graph, params) - ) do - graph - |> StreamGraph.add_connection(:n2, b1) - |> StreamGraph.add_connection(b1, n3) - |> StreamGraph.add_connection(b1, n4) - |> StreamGraph.add_connection(n3, f1) - |> StreamGraph.add_connection(n4, f1) - |> StreamGraph.add_connection(f1, n5) - end - - assert new_graph == compare_graph - end -end diff --git a/test/exstreme_test.exs b/test/exstreme_test.exs index 6115594..50ef46e 100644 --- a/test/exstreme_test.exs +++ b/test/exstreme_test.exs @@ -2,7 +2,4 @@ defmodule ExstremeTest do use ExUnit.Case doctest Exstreme - test "the truth" do - assert 1 + 1 == 2 - end end