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.
Execute the tests with:
$ elixir dot_dsl_test.exs
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.
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
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
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.
Level up your programming skills with 3,450 exercises across 52 languages, and insightful discussion with our volunteer team of welcoming mentors. Exercism is 100% free forever.
Sign up Learn More
Community comments