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