Skip to content

Commit 9240c24

Browse files
committed
[dot-dsl] Implement new exercise with generator
1 parent 70f9431 commit 9240c24

10 files changed

Lines changed: 685 additions & 0 deletions

File tree

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,14 @@
19021902
"prerequisites": [],
19031903
"difficulty": 5
19041904
},
1905+
{
1906+
"slug": "dot-dsl",
1907+
"name": "DOT DSL",
1908+
"uuid": "82c49ab1-270c-425a-a461-377853a13e3b",
1909+
"practices": [],
1910+
"prerequisites": [],
1911+
"difficulty": 5
1912+
},
19051913
{
19061914
"slug": "grade-school",
19071915
"name": "Grade School",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Instructions
2+
3+
A [Domain Specific Language (DSL)][dsl] is a small language optimized for a specific domain.
4+
Since a DSL is targeted, it can greatly impact productivity/understanding by allowing the writer to declare _what_ they want rather than _how_.
5+
6+
One problem area where they are applied are complex customizations/configurations.
7+
8+
For example the [DOT language][dot-language] allows you to write a textual description of a graph which is then transformed into a picture by one of the [Graphviz][graphviz] tools (such as `dot`).
9+
A simple graph looks like this:
10+
11+
graph {
12+
graph [bgcolor="yellow"]
13+
a [color="red"]
14+
b [color="blue"]
15+
a -- b [color="green"]
16+
}
17+
18+
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.
19+
20+
Write a Domain Specific Language similar to the Graphviz dot language.
21+
22+
Our DSL is similar to the Graphviz dot language in that our DSL will be used to create graph data structures.
23+
However, unlike the DOT Language, our DSL will be an internal DSL for use only in our language.
24+
25+
[Learn more about the difference between internal and external DSLs][fowler-dsl].
26+
27+
[dsl]: https://en.wikipedia.org/wiki/Domain-specific_language
28+
[dot-language]: https://en.wikipedia.org/wiki/DOT_(graph_description_language)
29+
[graphviz]: https://graphviz.org/
30+
[fowler-dsl]: https://martinfowler.com/bliki/DomainSpecificLanguage.html
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"authors": [
3+
"IsaacG"
4+
],
5+
"files": {
6+
"solution": [
7+
"dot_dsl.go"
8+
],
9+
"test": [
10+
"dot_dsl_test.go",
11+
"cases_test.go"
12+
],
13+
"example": [
14+
".meta/example.go"
15+
]
16+
},
17+
"blurb": "Write a Domain Specific Language similar to the Graphviz dot language.",
18+
"source": "Wikipedia",
19+
"source_url": "https://en.wikipedia.org/wiki/DOT_(graph_description_language)"
20+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package dotdsl
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
// Properties holds the properties of a node or edge.
10+
// The values can be int, bool or string.
11+
type Properties map[string]any
12+
13+
// Graph stores the parts of a dot graph.
14+
// All entities are stored as a Properties map (`nil` Properties when none set)
15+
// attrs is the Properties for the entire Graph, vs a specific node or edge.
16+
type Graph struct {
17+
nodes map[string]Properties
18+
edges map[string]Properties
19+
attrs Properties
20+
}
21+
22+
// extractPropSection splits a line like "a -- b [prop=val]" into "a -- b" and "prop=val".
23+
func extractPropSection(line string) (string, string, error) {
24+
// Parse the property, if any.
25+
if !strings.ContainsAny(line, "[]") {
26+
return line, "", nil
27+
}
28+
if strings.Count(line, "[") != 1 || strings.Count(line, "]") != 1 {
29+
return "", "", fmt.Errorf("incorrect number of []")
30+
}
31+
if !strings.HasSuffix(line, "]") {
32+
return "", "", fmt.Errorf("incorrectly placed ']'")
33+
}
34+
line, props, ok := strings.Cut(strings.TrimSuffix(line, "]"), "[")
35+
if !ok || len(props) < 3 || !strings.Contains(props[1:len(props)-1], "=") {
36+
return "", "", fmt.Errorf("badly formatted props")
37+
}
38+
return line, props, nil
39+
}
40+
41+
// parseProp turns a prop like `foo=2` into `foo` and `int(5)`.
42+
// It handles bool, int, quoted strings and (as a fallback) unquoted strings.
43+
func parseProp(rawProp string) (string, any, error) {
44+
propName, rawPropVal, hasProp := strings.Cut(rawProp, "=")
45+
if !hasProp || len(propName) == 0 || len(rawPropVal) == 0 {
46+
return "", nil, fmt.Errorf("badly formatted prop")
47+
}
48+
49+
var propVal any
50+
if rawPropVal == "true" {
51+
propVal = true
52+
} else if rawPropVal == "false" {
53+
propVal = false
54+
} else if v, err := strconv.Atoi(rawPropVal); err == nil {
55+
propVal = v
56+
} else if strings.HasPrefix(rawPropVal, `"`) && strings.HasSuffix(rawPropVal, `"`) {
57+
propVal = strings.Trim(rawPropVal, `"`)
58+
} else {
59+
propVal = rawPropVal
60+
}
61+
return propName, propVal, nil
62+
}
63+
64+
// extractNodes parses a line like "a -- b -- c" to return []string{"a", "b", "c"}.
65+
func extractNodes(line string) ([]string, error) {
66+
nodes := strings.Split(line, "--")
67+
if len(nodes) == 0 || len(nodes) == 1 && nodes[0] == "" {
68+
return nil, nil
69+
}
70+
for i, node := range nodes {
71+
node = strings.TrimSpace(node)
72+
nodes[i] = node
73+
if len(node) == 0 {
74+
return nil, fmt.Errorf("badly formatted node, line %q", line)
75+
}
76+
for _, r := range node {
77+
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '1')) {
78+
return nil, fmt.Errorf("node name should be alnum, got %s", node)
79+
}
80+
}
81+
}
82+
return nodes, nil
83+
}
84+
85+
// nodesToEdges takes a slice of nodes eg []string{"a", "b", "c"} and returns edges,
86+
// like []string{"{a b", "{b c}"}
87+
func nodesToEdges(nodes []string) []string {
88+
if len(nodes) < 2 {
89+
return nil
90+
}
91+
edges := make([]string, len(nodes)-1)
92+
for i := range len(nodes) - 1 {
93+
a, b := nodes[i], nodes[i+1]
94+
if a > b {
95+
a, b = b, a
96+
}
97+
edges[i] = fmt.Sprintf("{%s %s}", a, b)
98+
}
99+
return edges
100+
}
101+
102+
// addKeys updates a map, setting missing entries to nil entries.
103+
func addKeys(keys []string, myMap map[string]Properties) {
104+
for _, key := range keys {
105+
if _, ok := myMap[key]; !ok {
106+
myMap[key] = nil
107+
}
108+
}
109+
}
110+
111+
// setProp updates the Properties in a map for a set of keys.
112+
func setProp(keys []string, myMap map[string]Properties, prop string, value any) {
113+
for _, key := range keys {
114+
if myMap[key] == nil {
115+
myMap[key] = Properties{}
116+
}
117+
myMap[key][prop] = value
118+
}
119+
}
120+
121+
// parseLine updates the Graph with a single line.
122+
func (g *Graph) parseLine(line string) error {
123+
// Check the line format.
124+
line = strings.TrimSpace(line)
125+
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
126+
return nil
127+
}
128+
if !strings.HasSuffix(line, ";") {
129+
return fmt.Errorf("line does not end in a semicolon")
130+
}
131+
line = strings.TrimSuffix(line, ";")
132+
133+
// Parse the property, if any.
134+
var propName string
135+
var propVal any
136+
var hasProp bool
137+
138+
if lineSection, propSection, err := extractPropSection(line); err != nil {
139+
return err
140+
} else if propSection != "" {
141+
hasProp = true
142+
line = lineSection
143+
if propName, propVal, err = parseProp(propSection); err != nil {
144+
return err
145+
}
146+
}
147+
148+
nodes, err := extractNodes(line)
149+
if err != nil {
150+
return err
151+
}
152+
edges := nodesToEdges(nodes)
153+
addKeys(nodes, g.nodes)
154+
addKeys(edges, g.edges)
155+
156+
if hasProp {
157+
switch len(nodes) {
158+
case 0:
159+
g.attrs[propName] = propVal
160+
case 1:
161+
setProp(nodes, g.nodes, propName, propVal)
162+
default:
163+
setProp(edges, g.edges, propName, propVal)
164+
}
165+
}
166+
return nil
167+
}
168+
169+
// Parse creates a Graph from a text blob.
170+
func Parse(data string) (*Graph, error) {
171+
g := &Graph{make(map[string]Properties), make(map[string]Properties), Properties{}}
172+
lines := strings.Split(data, "\n")
173+
// Check the graph start/end.
174+
if len(lines) < 2 || lines[0] != "graph {" || lines[len(lines)-1] != "}" {
175+
return nil, fmt.Errorf("not a graph")
176+
}
177+
for i, line := range lines[1 : len(lines)-1] {
178+
if err := g.parseLine(line); err != nil {
179+
return nil, fmt.Errorf("line %d: %q -- parse error %w", i, line, err)
180+
}
181+
}
182+
return g, nil
183+
}

0 commit comments

Comments
 (0)