1 module dgraphviz;
2 
3 import std.format : format;
4 
5 
6 private struct Option {
7     @safe:
8     string[string] option;
9     alias option this;
10 
11     auto toString() pure {
12         if (option.length == 0) return "";
13         auto s = " [ ";
14         foreach (k, v; option) {
15             s ~= "%s = \"%s\", ".format(k, v);
16         }
17         s = s[0 .. $-2] ~ " ]";
18         return s;
19     }
20 }
21 
22 private struct Edge {
23     @safe:
24     string ark;
25     Node src, dst;
26     Option option;
27 
28     auto toString() pure {
29         return "\"%s\" %s \"%s\" %s;\n".format(src.label, ark, dst.label, option);
30     }
31 }
32 
33 private class Node {
34     @safe:
35     string label;
36     Option option;
37     size_t nIn = 0, nOut = 0;
38 
39     this(string label) pure {
40         import std.string : replace;
41         this.label = label.replace("\"", "\\\"");
42     }
43 
44     this(string label, Option option) pure {
45         this(label);
46         this.option = option;
47     }
48 
49     auto info() pure {
50         if (option.length == 0) return "";
51         auto s = "\"%s\" %s;\n".format(label, option);
52         return s;
53     }
54 }
55 
56 
57 abstract class Graph {
58     @safe:
59     import std.conv : to;
60 
61     // TODO use Set
62     private Node[string] nodes;
63     private Edge[string] edges;
64     private Option graphOpt, nodeOpt, edgeOpt;
65 
66     ref auto node(ref Node d) pure { return d; }
67 
68     ref auto node(T)(T t) pure {
69         string[string] opt;
70         return node(t, opt);
71     }
72 
73     ref auto node(T)(T t, string[string] option) pure {
74         auto key = t.to!string;
75         if (key !in this.nodes) {
76             this.nodes[key] = new Node(t.to!string, Option(option));
77         }
78         return this.nodes[key];
79     }
80 
81     auto edge(S, D)(S src, D dst,) pure {
82         string[string] opt;
83         return edge(src, dst, opt);
84     }
85 
86     auto edge(S, D)(S src, D dst, string[string] option) pure {
87         auto s = node(src);
88         auto d = node(dst);
89         auto e = Edge(this.ark, s, d, Option(option));
90         ++s.nOut;
91         ++d.nIn;
92         this.edges[e.to!string] = e;
93         return e;
94     }
95 
96     protected abstract string typename() pure;
97     protected abstract string ark() pure;
98 
99     override string toString() pure {
100         import std.array : array;
101         import std.algorithm : uniq, map, sort;
102         auto s = this.typename ~ " g{\n";
103 
104         if (graphOpt.length > 0) s ~= "graph %s;\n".format(graphOpt);
105         if (nodeOpt.length > 0) s ~= "node %s;\n".format(nodeOpt);
106         if (edgeOpt.length > 0) s ~= "edge %s;\n".format(edgeOpt);
107 
108         foreach (k, n; this.nodes) {
109             s ~= n.info;
110         }
111         foreach (k, e; this.edges) {
112             s ~= k;
113         }
114         s ~= "}\n";
115         return s;
116     }
117 
118     void save(string path) {
119         import std.stdio : File;
120         auto f = File(path, "w");
121         f.write(this.toString());
122         f.detach();
123     }
124 }
125 
126 class Undirected : Graph {
127     @safe:
128     protected override string typename() pure { return "graph"; }
129     protected override string ark() pure { return "--"; }
130 }
131 
132 class Directed : Graph {
133     @safe:
134     protected override string typename() pure { return "digraph"; }
135     protected override string ark() pure { return "->"; }
136 }
137 
138 
139 ///
140 unittest {
141     import std.stdio;
142     import std.format;
143     import dgraphviz;
144 
145     struct A {
146         auto toString() {
147             return "A\n\"struct\"";
148         }
149     }
150 
151     auto g = new Directed;
152     A a;
153     with (g) {
154         node(a, ["shape": "box", "color": "#ff0000"]);
155         edge(a, true);
156         edge(a, 1, ["style": "dashed", "label": "a-to-1"]);
157         edge(true, "foo");
158     }
159     g.save("simple.dot");
160 }
161 
162 version(unittest) Directed libraryDependency(string root, string prefix="",
163                            bool verbose=false, size_t maxDepth=3) {
164     import std.file : dirEntries, SpanMode, readText;
165     import std.format : formattedRead;
166     import std.string : split, strip, join, endsWith, replace, startsWith;
167     import std.algorithm : map, canFind, min, any, filter;
168     import std.stdio : writefln;
169 
170     auto g = new Directed;
171 
172     with (g) {
173         enum invalidTokens = ["\"", "$", "/", "\\"];
174         auto removeSub(string s) {
175             return s.split(".")[0..min($, maxDepth)].join(".");
176         }
177 
178         void registerEdge(string src, string dst) {
179             dst = dst.strip;
180             // FIXME follow import expr spec.
181             if (invalidTokens.map!(i => dst.canFind(i)).any) {
182                 return;
183             } else if (dst.canFind(":")) {
184                 registerEdge(src, dst.split(":")[0]);
185             } else if (dst.canFind(",")) {
186                 foreach (d; split(dst, ",")) {
187                     registerEdge(src, d);
188                 }
189             } else if (dst.canFind(" ")) {
190                 return;
191             } else if (dst.canFind("std.")) {
192                 if (verbose) writefln("%s -> %s", src, dst);
193                 edge(removeSub(src), removeSub(dst));
194             }
195         }
196 
197         auto dfiles = dirEntries(root, SpanMode.depth)
198             .filter!(f => f.name.startsWith(root ~ prefix) && f.name.endsWith(".d"));
199         foreach (dpath; dfiles) {
200             auto src = dpath[root.length .. $].replace("/", ".")[0 .. $-2];
201             try {
202                 foreach (txt; dpath.readText.split("import")[1..$]) {
203                     txt = "import " ~ txt;
204                     string dst, rest;
205                     txt.formattedRead!"import %s;%s"(dst, rest);
206                     if (verbose) writefln("%s ---------> %s", src, dst);
207                     registerEdge(src, dst);
208                 }
209             } catch (Exception e) {
210                 // FIXME display warnings
211             }
212         }
213     }
214     return g;
215 }
216 
217 ///
218 unittest {
219     import std.path;
220     import std.process;
221 
222     auto dc = environment.get("DC");
223     assert(dc != "", "use DUB or set DC enviroment variable");
224     auto which = executeShell("which " ~ dc);
225     assert(which.status == 0);
226     version(DigitalMars) {
227         auto root = which.output.dirName ~ "/../../src/phobos/";
228     }
229     version(LDC) {
230         auto root = which.output.dirName ~ "/../import/";
231     }
232 
233     auto g = libraryDependency(root, "std/range", true);
234     g.save("range.dot");
235 }