Avatar of elvanja
0
0
Genius
0
0

elvanja's solution

to DOT DSL in the Elixir Track

Instructions
Test suite
Solution

Write a Domain Specific Language similar to the Graphviz dot language.

A Domain Specific Language (DSL) is a small language optimized for a specific domain.

For example the dot language of Graphviz allows you to write a textual description of a graph which is then transformed into a picture by one of the graphviz tools (such as dot). A simple graph looks like this:

graph {
    graph [bgcolor="yellow"]
    a [color="red"]
    b [color="blue"]
    a -- b [color="green"]
}

Putting this in a file example.dot and running dot example.dot -T png -o example.png creates an image example.png with red and blue circle connected by a green line on a yellow background.

Create a DSL similar to the dot language.

Running tests

Execute the tests with:

$ elixir dot_dsl_test.exs

Pending tests

In the test suites, all but the first test have been skipped.

Once you get a test passing, you can unskip the next one by commenting out the relevant @tag :pending with a # symbol.

For example:

# @tag :pending
test "shouting" do
  assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end

Or, you can enable all the tests by commenting out the ExUnit.configure line in the test suite.

# ExUnit.configure exclude: :pending, trace: true

For more detailed information about the Elixir track, please see the help page.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

dot_dsl_test.exs

if !System.get_env("EXERCISM_TEST_EXAMPLES") do
  Code.load_file("dot.exs", __DIR__)
end

ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)

defmodule DotTest do
  use ExUnit.Case
  require Dot

  # Expand at RunTime, used to avoid invalid macro calls preventing compilation
  # of the tests.
  #
  # Inspired by (read: clone of) Support.CompileHelpers.delay_compile in Ecto.
  defmacrop exprt(ast) do
    escaped = Macro.escape(ast)

    quote do
      Code.eval_quoted(unquote(escaped), [], __ENV__) |> elem(0)
    end
  end

  # @tag :pending
  test "empty graph" do
    assert %Graph{} ==
             exprt(
               Dot.graph do
               end
             )
  end

  @tag :pending
  test "graph with one node" do
    assert %Graph{nodes: [{:a, []}]} ==
             exprt(
               Dot.graph do
                 a
               end
             )
  end

  @tag :pending
  test "graph with one node with keywords" do
    assert %Graph{nodes: [{:a, [color: :green]}]} ==
             exprt(
               Dot.graph do
                 a(color: :green)
               end
             )
  end

  @tag :pending
  test "graph with one edge" do
    assert %Graph{edges: [{:a, :b, []}]} ==
             exprt(
               Dot.graph do
                 a -- b
               end
             )
  end

  @tag :pending
  test "graph with just attribute" do
    assert %Graph{attrs: [foo: 1]} ==
             exprt(
               Dot.graph do
                 graph(foo: 1)
               end
             )
  end

  @tag :pending
  test "graph with attributes" do
    assert %Graph{
             attrs: [bar: true, foo: 1, title: "Testing Attrs"],
             nodes: [{:a, [color: :green]}, {:b, [label: "Beta!"]}, {:c, []}],
             edges: [{:a, :b, [color: :blue]}, {:b, :c, []}]
           } ==
             exprt(
               Dot.graph do
                 graph(foo: 1)
                 graph(title: "Testing Attrs")
                 graph([])
                 a(color: :green)
                 c([])
                 b(label: "Beta!")
                 b -- c([])
                 a -- b(color: :blue)
                 graph(bar: true)
               end
             )
  end

  @tag :pending
  test "keywords stuck to graph without space" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          graph[[title: "Bad"]]
        end
      )
    end
  end

  @tag :pending
  test "keywords stuck to node without space" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a[[label: "Alpha!"]]
        end
      )
    end
  end

  @tag :pending
  test "keywords stuck to edge without space" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a -- b[[label: "Bad"]]
        end
      )
    end
  end

  @tag :pending
  test "invalid statement: int" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a
          2
        end
      )
    end
  end

  @tag :pending
  test "invalid statement: list" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          [title: "Testing invalid"]
        end
      )
    end
  end

  @tag :pending
  test "invalid statement: qualified atom" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          Enum.map()
        end
      )
    end
  end

  @tag :pending
  test "invalid statement: graph with no keywords" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          Enum.map()
        end
      )
    end
  end

  @tag :pending
  test "two attribute lists" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a([color: green][[label: "Alpha!"]])
        end
      )
      |> IO.inspect()
    end
  end

  @tag :pending
  test "non-keyword attribute list" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a(["Alpha!", color: green])
        end
      )
    end
  end

  @tag :pending
  test "int edge" do
    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          1 -- b
        end
      )
    end

    assert_raise ArgumentError, fn ->
      exprt(
        Dot.graph do
          a -- 2
        end
      )
    end
  end
end
defmodule Graph do
  defstruct attrs: [], nodes: [], edges: []
end

defmodule Dot do
  defmacro graph(do: ast) do
    build_graph(ast)
  end

  defp build_graph({:__block__, _, graphs}) do
    graphs
    |> Enum.map(&(&1 |> build_graph |> Code.eval_quoted |> elem(0)))
    |> Enum.reduce(%Graph{}, &merge_graphs_sorted/2)
    |> Macro.escape
  end

  defp build_graph({:--, _, [{a, _, _}, {b, _, nil}]}) do
    quote do: %Graph{edges: [{unquote(a), unquote(b), []}]}
  end

  defp build_graph({:--, _, [{a, _, _}, {b, _, [attrs]}]}) do
    if Macro.escape(attrs) != attrs do; raise ArgumentError end
    quote do: %Graph{edges: [{unquote(a), unquote(b), unquote(attrs)}]}
  end

  defp build_graph({:graph, _, [attrs]}) do
    if Macro.escape(attrs) != attrs do; raise ArgumentError end
    quote do: %Graph{attrs: unquote(attrs)}
  end

  defp build_graph({node, _, nil}) do
    quote do: %Graph{nodes: [{unquote(node), []}]}
  end

  defp build_graph({node, _, [keywords]}) do
    if Macro.escape(keywords) != keywords do; raise ArgumentError end
    quote do: %Graph{nodes: [{unquote(node), unquote(keywords)}]}
  end

  defp build_graph(nil) do
    quote do: %Graph{}
  end

  defp build_graph(_) do
    raise ArgumentError
  end

  defp merge_graphs_sorted(first, second) do
    Map.merge(first, second, fn(k, v1, v2) ->
      if k == :__struct__ do
        v1
      else
        v1 ++ v2 |> Enum.sort
      end
    end)
  end
end

What can you learn from this solution?

A huge amount can be learnt from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that I could read more about to develop my understanding?