🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of angelikatyborska

angelikatyborska's solution

to DOT DSL in the Elixir Track

Published at Nov 18 2018 · 0 comments
Instructions
Test suite
Solution

Note:

This exercise has changed since this solution was written.

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

If you're stuck on something, it may help to look at some of the available resources out there where answers might be found.

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: []

  def add_node(graph, node) do
    %Graph{graph | nodes: [node | graph.nodes] |> List.keysort(0)}
  end

  def add_attrs(graph, attrs) do
    %Graph{graph | attrs: Keyword.merge(attrs, graph.attrs) |> List.keysort(0)}
  end

  def add_edge(graph, edge) do
    %Graph{graph | edges: [edge | graph.edges]}
  end
end

defmodule Dot do
  defmacro graph(ast) do
    [do: block] = ast

    block
    |> process(%Graph{})
    |> Macro.escape()
  end

  defp process({:__block__, _, args}, graph) do
    Enum.reduce(args, graph, &process/2)
  end

  defp process({:--, _, args}, graph) do
    case args do
      [{node1, _, _}, {node2, _, args}] when is_atom(node1) and is_atom(node2) ->
        Graph.add_edge(graph, {node1, node2, get_opts(args)})

      _ ->
        raise ArgumentError
    end
  end

  defp process({:graph, _, args}, graph) do
    Graph.add_attrs(graph, get_opts(args))
  end

  defp process({name, _, args}, graph) when is_atom(name) do
    Graph.add_node(graph, {name, get_opts(args)})
  end

  defp process(_, _) do
    raise ArgumentError
  end

  defp get_opts(args) do
    case args do
      nil -> []
      [opts] -> if Keyword.keyword?(opts), do: opts, else: raise(ArgumentError)
      _ -> raise ArgumentError
    end
  end
end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned 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 you could read more about to improve your understanding?