1 /** 2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 XML utility for generating GraphML content. 11 Contains data structures and serializers. 12 */ 13 module dextool.plugin.graphml.backend.xml; 14 15 import std.format : FormatSpec; 16 import logger = std.experimental.logger; 17 18 import dextool.hash : makeHash; 19 20 version (unittest) { 21 import unit_threaded : shouldEqual; 22 } 23 24 private ulong nextEdgeId()() { 25 static ulong next = 0; 26 return next++; 27 } 28 29 package ulong nextGraphId()() { 30 static ulong next = 0; 31 return next++; 32 } 33 34 /// Write the XML header for graphml with needed key definitions. 35 void xmlHeader(RecvT)(ref RecvT recv) { 36 import std.conv : to; 37 import std.format : formattedWrite; 38 import std.range.primitives : put; 39 40 put(recv, `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n"); 41 put(recv, `<graphml` ~ "\n"); 42 put(recv, ` xmlns="http://graphml.graphdrawing.org/xmlns"` ~ "\n"); 43 put(recv, ` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"` ~ "\n"); 44 put(recv, ` xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns` ~ "\n"); 45 put(recv, ` http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"` ~ "\n"); 46 put(recv, ` xmlns:y="http://www.yworks.com/xml/graphml">` ~ "\n"); 47 48 formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portgraphics"/>` ~ "\n", 49 cast(int) IdT.portgraphics); 50 formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portgeometry"/>` ~ "\n", 51 cast(int) IdT.portgeometry); 52 formattedWrite(recv, `<key for="port" id="d%s" yfiles.type="portuserdata"/>` ~ "\n", 53 cast(int) IdT.portuserdata); 54 formattedWrite(recv, 55 `<key for="node" attr.name="url" attr.type="string" id="d%s"/>` ~ "\n", 56 cast(int) IdT.url); 57 formattedWrite(recv, `<key for="node" attr.name="description" attr.type="string" id="d%s"/>` ~ "\n", 58 cast(int) IdT.description); 59 formattedWrite(recv, `<key for="node" yfiles.type="nodegraphics" id="d%s"/>` ~ "\n", 60 cast(int) IdT.nodegraphics); 61 formattedWrite(recv, `<key for="graphml" id="d%s" yfiles.type="resources"/>` ~ "\n", 62 cast(int) IdT.graphml); 63 formattedWrite(recv, `<key for="edge" yfiles.type="edgegraphics" id="d%s"/>` ~ "\n", 64 cast(int) IdT.edgegraphics); 65 formattedWrite(recv, `<key for="node" attr.name="position" attr.type="string" id="d%s"/>` ~ "\n", 66 cast(int) IdT.position); 67 formattedWrite(recv, `<key for="node" attr.name="kind" attr.type="string" id="d%s"/>` ~ "\n", 68 cast(int) IdT.kind); 69 formattedWrite(recv, `<key for="node" attr.name="typeAttr" attr.type="string" id="d%s"/>` ~ "\n", 70 cast(int) IdT.typeAttr); 71 formattedWrite(recv, `<key for="node" attr.name="signature" attr.type="string" id="d%s"/>` ~ "\n", 72 cast(int) IdT.signature); 73 74 formattedWrite(recv, `<graph id="G%s" edgedefault="directed">` ~ "\n", nextGraphId); 75 } 76 77 @("Should be enum IDs converted to int strings") 78 unittest { 79 // Even though the test seems stupid it exists to ensure that the first and 80 // last ID is as expected. If they mismatch then the header generator is 81 // probably wrong. 82 import std.format : format; 83 84 format("%s", cast(int) IdT.url).shouldEqual("3"); 85 format("%s", cast(int) IdT.signature).shouldEqual("11"); 86 } 87 88 /// Write the xml footer as required by GraphML. 89 void xmlFooter(RecvT)(ref RecvT recv) { 90 import std.range.primitives : put; 91 92 put(recv, "</graph>\n"); 93 put(recv, "</graphml>\n"); 94 } 95 96 package enum ColorKind { 97 none, 98 file, 99 global, 100 globalConst, 101 globalStatic, 102 namespace, 103 func, 104 class_, 105 field, 106 fallback 107 } 108 109 private struct FillColor { 110 string color1; 111 string color2; 112 113 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 114 import std.format : formattedWrite; 115 116 if (color1.length > 0) { 117 formattedWrite(w, `color="#%s"`, color1); 118 } 119 120 if (color2.length > 0) { 121 formattedWrite(w, ` color2="#%s"`, color2); 122 } 123 } 124 } 125 126 private FillColor toInternal(ColorKind kind) @safe pure nothrow @nogc { 127 final switch (kind) { 128 case ColorKind.none: 129 return FillColor(null, null); 130 case ColorKind.file: 131 return FillColor("CCFFCC", null); 132 case ColorKind.global: 133 return FillColor("FF33FF", null); 134 case ColorKind.globalConst: 135 return FillColor("FFCCFF", null); 136 case ColorKind.globalStatic: 137 return FillColor("FFD9D2", "FF66FF"); 138 case ColorKind.namespace: 139 return FillColor("FFCCCC", null); 140 case ColorKind.func: 141 return FillColor("FF6600", null); 142 case ColorKind.class_: 143 return FillColor("FFCC33", null); 144 case ColorKind.field: 145 return FillColor("FFFF99", null); 146 case ColorKind.fallback: 147 return FillColor("99FF99", null); 148 } 149 } 150 151 package @safe struct ValidNodeId { 152 import cpptooling.data.type : USRType; 153 154 size_t payload; 155 alias payload this; 156 157 this(size_t id) { 158 payload = id; 159 } 160 161 this(string usr) { 162 payload = makeHash(cast(string) usr); 163 debug logger.tracef("usr:%s -> hash:%s", usr, payload); 164 } 165 166 this(USRType usr) { 167 this(cast(string) usr); 168 } 169 170 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 171 import std.format : formatValue; 172 173 formatValue(w, payload, fmt); 174 } 175 } 176 177 package enum StereoType { 178 None, 179 Abstract, 180 Interface, 181 } 182 183 package struct ShapeNode { 184 string label; 185 ColorKind color; 186 187 private enum baseHeight = 20; 188 private enum baseWidth = 140; 189 private enum graphNode = "ShapeNode"; 190 191 void toString(Writer, Char)(scope Writer w, FormatSpec!Char = "%s") const @safe { 192 import std.format : formattedWrite; 193 194 formattedWrite(w, `<y:Geometry height="%s" width="%s"/>`, baseHeight, baseWidth); 195 if (color != ColorKind.none) { 196 formattedWrite(w, `<y:Fill %s transparent="false"/>`, color.toInternal); 197 } 198 formattedWrite(w, 199 `<y:NodeLabel autoSizePolicy="node_size" configuration="CroppingLabel"><![CDATA[%s]]></y:NodeLabel>`, 200 label); 201 } 202 } 203 204 package struct UMLClassNode { 205 string label; 206 StereoType stereotype; 207 string[] attributes; 208 string[] methods; 209 210 private enum baseHeight = 28; 211 private enum graphNode = "UMLClassNode"; 212 213 void toString(Writer)(scope Writer w) const { 214 import std.conv : to; 215 import std.format : formattedWrite; 216 import std.range.primitives : put; 217 218 formattedWrite(w, `<y:NodeLabel><![CDATA[%s]]></y:NodeLabel>`, label); 219 220 formattedWrite(w, `<y:Geometry height="%s" width="100.0"/>`, 221 stereotype == StereoType.None ? baseHeight : baseHeight * 2); 222 put(w, `<y:Fill color="#FFCC00" transparent="false"/>`); 223 224 formattedWrite(w, `<y:UML clipContent="true" omitDetails="false" stereotype="%s">`, 225 stereotype == StereoType.None ? "" : stereotype.to!string()); 226 227 if (attributes.length > 0) { 228 put(w, `<y:AttributeLabel>`); 229 } 230 foreach (attr; attributes) { 231 ccdataWrap(w, attr); 232 put(w, "\n"); 233 } 234 if (attributes.length > 0) { 235 put(w, `</y:AttributeLabel>`); 236 } 237 238 if (methods.length > 0) { 239 put(w, `<y:MethodLabel>`); 240 } 241 foreach (meth; methods) { 242 ccdataWrap(w, meth); 243 put(w, "\n"); 244 } 245 if (methods.length > 0) { 246 put(w, `</y:MethodLabel>`); 247 } 248 249 put(w, `</y:UML>`); 250 } 251 } 252 253 @("Should instantiate all NodeStyles") 254 unittest { 255 import std.array : appender; 256 import std.meta : AliasSeq; 257 258 auto app = appender!string(); 259 260 foreach (T; AliasSeq!(ShapeNode, UMLClassNode)) { 261 { 262 NodeStyle!T node; 263 node.toString(app, FormatSpec!char("%s")); 264 } 265 } 266 } 267 268 package auto makeShapeNode(string label, ColorKind color = ColorKind.none) { 269 return NodeStyle!ShapeNode(ShapeNode(label, color)); 270 } 271 272 package auto makeUMLClassNode(string label) { 273 return NodeStyle!UMLClassNode(UMLClassNode(label)); 274 } 275 276 /** Node style in GraphML. 277 * 278 * Intented to carry metadata and formatting besides a generic label. 279 */ 280 package struct NodeStyle(PayloadT) { 281 PayloadT payload; 282 alias payload this; 283 284 void toString(Writer, Char)(scope Writer w, FormatSpec!Char spec) const { 285 import std.format : formattedWrite, formatValue; 286 287 enum graph_node = PayloadT.graphNode; 288 289 formattedWrite(w, "<y:%s>", graph_node); 290 formatValue(w, payload, spec); 291 formattedWrite(w, "</y:%s>", graph_node); 292 } 293 } 294 295 /** Render a FolderNode. 296 * 297 * The node must have, besides an id, the folder attribute: 298 * yfiles.foldertype="folder" 299 */ 300 package @safe struct FolderNode { 301 import std.format : FormatSpec; 302 303 string label; 304 305 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 306 import std.range.primitives : put; 307 import std.format : formattedWrite; 308 309 put(w, `<y:ProxyAutoBoundsNode><y:Realizers active="1">`); 310 311 // Open 312 put(w, `<y:GroupNode>`); 313 put(w, `<y:Geometry height="50" width="85.0" x="0.0" y="0.0"/>`); 314 put(w, `<y:Fill color="#F5F5F5" transparent="false"/>`); 315 put(w, `<y:BorderStyle color="#000000" type="dashed" width="1.0"/>`); 316 formattedWrite(w, `<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.0" x="0.0" y="0.0"><![CDATA[%s]]></y:NodeLabel>`, 317 label); 318 put(w, `<y:Shape type="roundrectangle"/>`); 319 put(w, 320 `<y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>`); 321 put(w, `<y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>`); 322 put(w, `<y:BorderInsets bottom="21" bottomF="21.4609375" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>`); 323 put(w, `</y:GroupNode>`); 324 325 // Close 326 put(w, `<y:GroupNode>`); 327 put(w, `<y:Geometry height="20.0" width="85.0" x="0.0" y="0.0"/>`); 328 put(w, `<y:Fill color="#F5F5F5" transparent="false"/>`); 329 put(w, `<y:BorderStyle color="#000000" type="dashed" width="1.0"/>`); 330 formattedWrite(w, `<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.0" x="0.0" y="0.0"><![CDATA[%s]]></y:NodeLabel>`, 331 label); 332 put(w, `<y:Shape type="roundrectangle"/>`); 333 put(w, 334 `<y:State closed="true" closedHeight="20.0" closedWidth="85.0" innerGraphDisplayEnabled="false"/>`); 335 put(w, 336 `<y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>`); 337 put(w, 338 `<y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>`); 339 put(w, `</y:GroupNode>`); 340 341 put(w, `</y:Realizers></y:ProxyAutoBoundsNode>`); 342 } 343 } 344 345 package void ccdataWrap(Writer, ARGS...)(scope Writer w, auto ref ARGS args) { 346 import std.range.primitives : put; 347 348 put(w, `<![CDATA[`); 349 foreach (arg; args) { 350 static if (__traits(hasMember, typeof(arg), "toString")) { 351 arg.toString(w, FormatSpec!char("%s")); 352 } else { 353 put(w, arg); 354 } 355 } 356 put(w, `]]>`); 357 } 358 359 package void xmlComment(RecvT, CharT)(ref RecvT recv, CharT v) { 360 import std.format : formattedWrite; 361 362 formattedWrite(recv, "<!-- %s -->\n", v); 363 } 364 365 package enum IdT { 366 portgraphics, 367 portgeometry, 368 portuserdata, 369 url, /// URL such as a path 370 description, 371 nodegraphics, 372 graphml, 373 edgegraphics, 374 position, /// position in a file 375 kind, 376 typeAttr, 377 signature, 378 } 379 380 package struct Attr { 381 IdT id; 382 } 383 384 /// Stream to put attribute data into for complex member methods that handle 385 /// the serialization themself. 386 package alias StreamChar = void delegate(const(char)[]) @safe; 387 388 /** Serialize a struct into the writer. 389 * 390 * Only those fields and functions tagged with the UDA Attr are serialized into 391 * xml elements. 392 * The "id" as required by GraphML for custom data is derived from Attr. 393 * 394 * Params: 395 * RecvT = an OutputRange of char 396 * T = type to analyse for UDA's 397 * recv = ? 398 * bundle = ? 399 */ 400 package void attrToXml(T, Writer)(ref T bundle, scope Writer recv) { 401 import std.traits : isSomeFunction, stdParameters = Parameters, getUDAs; 402 import std.meta : Alias; 403 404 static void dataTag(Writer, T)(scope Writer recv, Attr attr, T data) { 405 import std.format : formattedWrite; 406 import std.range.primitives : put; 407 408 formattedWrite(recv, `<data key="d%s">`, cast(int) attr.id); 409 410 static if (isSomeFunction!T) { 411 data((const(char)[] buf) @trusted{ put(recv, buf); }); 412 } else static if (__traits(hasMember, T, "get")) { 413 // for Nullable etc 414 ccdataWrap(recv, data.get); 415 } else { 416 ccdataWrap(recv, data); 417 } 418 419 put(recv, "</data>"); 420 } 421 422 // TODO block when trying to stream multiple key's with same id 423 424 foreach (member_name; __traits(allMembers, T)) { 425 alias memberType = Alias!(__traits(getMember, T, member_name)); 426 alias res = getUDAs!(memberType, Attr); 427 // lazy helper for retrieving the compose `bundle.<field>` 428 enum member = "__traits(getMember, bundle, member_name)"; 429 430 static if (res.length == 0) { 431 // ignore those without the UDA Attr 432 } else static if (isSomeFunction!memberType) { 433 // process functions 434 // may only have one parameter and it must accept the delegate StreamChar 435 static if (stdParameters!(memberType).length == 1 436 && is(stdParameters!(memberType)[0] == StreamChar)) { 437 dataTag(recv, res[0], &mixin(member)); 438 } else { 439 static assert(0, 440 "member function tagged with Attr may only take one argument. The argument must be of type " 441 ~ typeof(StreamChar).stringof ~ " but is of type " ~ stdParameters!(memberType) 442 .stringof); 443 } 444 } else static if (is(typeof(memberType.init))) { 445 // process basic types 446 dataTag(recv, res[0], mixin(member)); 447 } 448 } 449 } 450 451 @("Should serialize those fields and methods of the struct that has the UDA Attr") 452 unittest { 453 static struct Foo { 454 int ignore; 455 @Attr(IdT.kind) string value; 456 @Attr(IdT.url) void f(StreamChar stream) { 457 stream("f"); 458 } 459 } 460 461 char[] buf; 462 struct Recv { 463 void put(const(char)[] s) { 464 buf ~= s; 465 } 466 } 467 468 Recv recv; 469 auto s = Foo(1, "value_"); 470 attrToXml(s, recv); 471 (cast(string) buf).shouldEqual( 472 `<data key="d9"><![CDATA[value_]]></data><data key="d3">f</data>`); 473 } 474 475 package enum NodeId; 476 package enum NodeExtra; 477 package enum NodeAttribute; 478 479 package void nodeToXml(T, Writer)(ref T bundle, scope Writer recv) { 480 import std.format : formattedWrite; 481 import std.range.primitives : put; 482 import std.traits : isSomeFunction, getUDAs; 483 import std.meta : Alias; 484 485 // lazy helper for retrieving the compose `bundle.<field>` 486 enum member = "__traits(getMember, bundle, member_name)"; 487 488 put(recv, "<node "); 489 scope (success) 490 put(recv, "</node>\n"); 491 492 // Node ID. Can only have one ID. 493 foreach (member_name; __traits(allMembers, T)) { 494 alias memberType = Alias!(__traits(getMember, T, member_name)); 495 alias res = getUDAs!(memberType, NodeId); 496 497 static if (res.length == 0) { 498 // ignore those without the UDA Attr 499 } else static if (isSomeFunction!memberType) { 500 put(recv, `id="`); 501 mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); }); 502 put(recv, `"`); 503 break; 504 } else { 505 formattedWrite(recv, `id="%s"`, mixin(member)); 506 break; 507 } 508 } 509 510 // Node xml attribute. Can be more than one. 511 foreach (member_name; __traits(allMembers, T)) { 512 alias memberType = Alias!(__traits(getMember, T, member_name)); 513 alias res = getUDAs!(memberType, NodeAttribute); 514 515 static if (res.length == 0) { 516 // ignore those without the UDA Attr 517 } else static if (isSomeFunction!memberType) { 518 put(recv, " "); 519 mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); }); 520 } else { 521 put(recv, " "); 522 formattedWrite(recv, `%s`, mixin(member)); 523 } 524 } 525 put(recv, ">"); 526 attrToXml(bundle, recv); 527 528 // Extra node stuff after the attributes 529 foreach (member_name; __traits(allMembers, T)) { 530 alias memberType = Alias!(__traits(getMember, T, member_name)); 531 alias res = getUDAs!(memberType, NodeExtra); 532 533 static if (res.length == 0) { 534 // ignore those without the UDA Attr 535 } else static if (isSomeFunction!memberType) { 536 mixin(member)((const(char)[] buf) @trusted{ put(recv, buf); }); 537 } else static if (is(typeof(memberType.init))) { 538 static if (isSomeString!(typeof(memberType))) { 539 ccdataWrap(recv, mixin(member)); 540 } else { 541 import std.conv : to; 542 543 ccdataWrap(recv, mixin(member).to!string()); 544 } 545 } 546 } 547 } 548 549 @("shall serialize a node by the UDA's") 550 unittest { 551 static struct Foo { 552 @NodeId int id; 553 @Attr(IdT.description) string desc; 554 555 @NodeExtra void extra(StreamChar s) { 556 s("extra"); 557 } 558 } 559 560 char[] buf; 561 struct Recv { 562 void put(const(char)[] s) { 563 buf ~= s; 564 } 565 } 566 567 Recv recv; 568 auto s = Foo(3, "desc"); 569 nodeToXml(s, recv); 570 (cast(string) buf).shouldEqual( 571 `<node id="3"><data key="d4"><![CDATA[desc]]></data>extra</node> 572 `); 573 } 574 575 package enum EdgeKind { 576 Directed, 577 Generalization 578 } 579 580 package void xmlEdge(RecvT, SourceT, TargetT)(ref RecvT recv, SourceT src, 581 TargetT target, EdgeKind kind) @safe { 582 import std.conv : to; 583 import std.format : formattedWrite; 584 import std.range.primitives : put; 585 586 auto src_ = ValidNodeId(src); 587 auto target_ = ValidNodeId(target); 588 589 debug { 590 import std..string : replace; 591 592 // printing the raw identifiers to make it easier to debug 593 formattedWrite(recv, `<!-- %s - %s -->`, src.replace("-", "_"), target.replace("-", "_")); 594 } 595 596 final switch (kind) with (EdgeKind) { 597 case Directed: 598 formattedWrite(recv, `<edge id="e%s" source="%s" target="%s"/>`, 599 nextEdgeId.to!string, src_, target_); 600 break; 601 case Generalization: 602 formattedWrite(recv, `<edge id="e%s" source="%s" target="%s">`, 603 nextEdgeId.to!string, src_, target_); 604 formattedWrite(recv, 605 `<data key="d%s"><y:PolyLineEdge><y:Arrows source="none" target="white_delta"/></y:PolyLineEdge></data>`, 606 cast(int) IdT.edgegraphics); 607 put(recv, `</edge>`); 608 break; 609 } 610 611 put(recv, "\n"); 612 }