Repository: ViacomInc/data-point (js)

Issue:
implement mermaid tracing support
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!
Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Please fill/un-comment out this template with all the relevant information so we can
understand what's going on and fix the issue.
-->

**Problem description**:

Feature to show a visual graph of the tracing of a datapoint transformation.

**Suggested solution**:

Return a mermaid output with the tree reducer structure created off of a transformation.

**Environment**:

- `data-point` version: canary

Pull request description:
feat(data-point-tracers): add mermaid support
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!

BREAKING CHANGES:

If your PR includes a breaking change, please submit it with a codemod under
data-point-codemods that will help users upgrade their codebase.

Breaking changes without a codemod will not be accepted unless a codemod is not
viable or does not apply to the specific situation.
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:

closes #419

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Has Breaking changes
- [ ] Documentation
- [X] Tests
- [X] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [ ] Added username to **all-contributors** list

<!-- feel free to add additional comments -->

Patch:
diff --git a/documentation/examples/mermaid-graph/mermaid-example.js b/documentation/examples/mermaid-graph/mermaid-example.js
new file mode 100644
index 0000000..6bd8026
--- /dev/null
+++ b/documentation/examples/mermaid-graph/mermaid-example.js
@@ -0,0 +1,66 @@
+/* eslint-disable */
+
+const path = require("path");
+
+const DataPoint = require("@data-point/core");
+const DPModel = require("@data-point/core/model");
+const DPIfThenElse = require("@data-point/core/ifThenElse");
+const DPMap = require("@data-point/core/map");
+
+const DPMermaidTracer = require("@data-point/tracers/mermaid");
+
+const myModel = DPModel({
+  name: "myModel",
+
+  uid: acc => `${acc.reducer.id}${acc.value.a.b}`,
+
+  value: [
+    "$a.b",
+    input => input.toUpperCase(),
+    DPIfThenElse({
+      if: input => input === "FOO",
+      then: () => {
+        // return "yes foo!!";
+        throw new Error("ohh");
+      },
+      else: input => `foo no! got ${input}`
+    })
+  ],
+
+  catch(acc) {
+    return "its all ok";
+  }
+});
+
+async function main() {
+  const datapoint = DataPoint();
+
+  const input = [
+    {
+      a: {
+        b: "foo"
+      }
+    },
+    {
+      a: {
+        b: "bar"
+      }
+    },
+    {
+      a: {
+        b: "baz"
+      }
+    }
+  ];
+
+  const tracer = DPMermaidTracer();
+
+  result = await datapoint.resolve(input, DPMap(myModel), {
+    tracer
+  });
+
+  // save to disk
+  tracer.report(path.join(__dirname, "datapoint-trace-example.mermaid"));
+}
+
+main();
diff --git a/documentation/examples/mermaid-graph/package.json b/documentation/examples/mermaid-graph/package.json
new file mode 100644
index 0000000..c3b5539
--- /dev/null
+++ b/documentation/examples/mermaid-graph/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "@data-point/mermaid-example",
+  "main": "mermaid-example.js",
+  "private": true,
+  "version": "1.0.0",
+  "devDependencies": {
+    "@data-point/core": "^6.0.0",
+    "@data-point/tracers": "^1.0.0"
+  }
+}
diff --git a/packages/data-point-tracers/mermaid/index.js b/packages/data-point-tracers/mermaid/index.js
new file mode 100644
index 0000000..4f9e278
--- /dev/null
+++ b/packages/data-point-tracers/mermaid/index.js
@@ -0,0 +1,1 @@
+module.exports = require("./mermaid").Mermaid.create;
diff --git a/packages/data-point-tracers/mermaid/mermaid.js b/packages/data-point-tracers/mermaid/mermaid.js
new file mode 100644
index 0000000..a9df753
--- /dev/null
+++ b/packages/data-point-tracers/mermaid/mermaid.js
@@ -0,0 +1,48 @@
+const fs = require("fs").promises;
+
+function getParent(span) {
+  return span.root ? "root" : span.parent;
+}
+
+function getName(span) {
+  return span === "root" ? "root" : `${span.name}${span.context.pid}`;
+}
+
+function graphTD(spans) {
+  const nodes = spans.map(span => {
+    return `  ${getName(getParent(span))}-->${getName(span)}`;
+  });
+
+  return `graph TD;\n${nodes.join("\n")}`;
+}
+
+class Mermaid {
+  constructor() {
+    this.spans = [];
+  }
+
+  static create() {
+    return new Mermaid();
+  }
+
+  start(dpSpan) {
+    this.spans.push(dpSpan);
+  }
+
+  async report(destinationPath) {
+    const report = graphTD(this.spans);
+
+    if (destinationPath) {
+      await fs.writeFile(destinationPath, report);
+    }
+
+    return report;
+  }
+}
+
+module.exports = {
+  getParent,
+  getName,
+  graphTD,
+  Mermaid
+};


Test patch:
diff --git a/packages/data-point-tracers/mermaid/mermaid.test.js b/packages/data-point-tracers/mermaid/mermaid.test.js
new file mode 100644
index 0000000..12a4875
--- /dev/null
+++ b/packages/data-point-tracers/mermaid/mermaid.test.js
@@ -0,0 +1,98 @@
+const fs = require("fs").promises;
+const { getParent, getName, graphTD, Mermaid } = require("./mermaid");
+
+function createChildSpan(name, pid, parent) {
+  return {
+    name,
+    parent,
+    context: {
+      pid
+    }
+  };
+}
+
+function createSampleSpans() {
+  const aa = createChildSpan("aa", 1);
+  aa.root = true;
+  const bb = createChildSpan("bb", 2, aa);
+  const cc = createChildSpan("cc", 3, aa);
+  const dd = createChildSpan("dd", 4, bb);
+
+  const spans = [aa, bb, cc, dd];
+
+  return spans;
+}
+
+const graphResult = `graph TD;
+  root-->aa1
+  aa1-->bb2
+  aa1-->cc3
+  bb2-->dd4`;
+
+describe("getParent", () => {
+  it("should return `root` if root flag is true", () => {
+    expect(getParent({ root: true })).toEqual("root");
+  });
+  it("should return parent if root flag not true", () => {
+    expect(getParent({ parent: "parent" })).toEqual("parent");
+  });
+});
+
+describe("getName", () => {
+  it("should return 'root' if span is root", () => {
+    expect(getName("root")).toEqual("root");
+  });
+  it("should return constructed name if span is not root", () => {
+    const span = createChildSpan("name", 1);
+    expect(getName(span)).toEqual("name1");
+  });
+});
+
+describe("graphTD", () => {
+  it("should create graph tree", () => {
+    const result = graphTD(createSampleSpans());
+    expect(result).toEqual(graphResult);
+  });
+});
+
+describe("Mermaid", () => {
+  describe("constructor", () => {
+    it("should create spans array", () => {
+      const result = new Mermaid();
+      expect(result).toHaveProperty("spans", []);
+    });
+  });
+
+  describe("create", () => {
+    it("should have static method to create new instance", () => {
+      const result = Mermaid.create();
+      expect(result).toBeInstanceOf(Mermaid);
+    });
+  });
+
+  describe("start", () => {
+    it("should track spans", () => {
+      const result = new Mermaid();
+      result.start("span");
+      expect(result.spans).toEqual(["span"]);
+    });
+  });
+
+  describe("report", () => {
+    it("should create and return mermaid graph", async () => {
+      const result = new Mermaid();
+      result.spans = createSampleSpans();
+      expect(await result.report()).toEqual(graphResult);
+    });
+
+    it("should save to file", async () => {
+      const result = new Mermaid();
+      result.spans = createSampleSpans();
+
+      const spyWriteFile = jest.spyOn(fs, "writeFile").mockResolvedValue(true);
+
+      await result.report("/test.mer");
+      expect(spyWriteFile).toBeCalledWith("/test.mer", graphResult);
+    });
+  });
+});