Skip to content

Commit

Permalink
Baseline implementation of MermaidJS Output Formatter - qossmic/deptr…
Browse files Browse the repository at this point in the history
…ac#1372 (#20)

Feat: base implementation of MermaidJS Output Formatter

---------

Co-authored-by: simbera <jan.simbera@onlineprinters.com>
  • Loading branch information
jan-simbera and simbera authored Mar 20, 2024
1 parent 7788ac6 commit 842a8c6
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 0 deletions.
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
use Qossmic\Deptrac\Supportive\OutputFormatter\GraphVizOutputImageFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\JsonOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\JUnitOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\MermaidJSOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\TableOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\XMLOutputFormatter;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand Down Expand Up @@ -417,6 +418,9 @@
$services
->set(CodeclimateOutputFormatter::class)
->tag('output_formatter');
$services
->set(MermaidJSOutputFormatter::class)
->tag('output_formatter');

/*
* Console
Expand Down
6 changes: 6 additions & 0 deletions deptrac.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Qossmic\Deptrac\Contract\Config\DeptracConfig;
use Qossmic\Deptrac\Contract\Config\EmitterType;
use Qossmic\Deptrac\Contract\Config\Formatter\GraphvizConfig;
use Qossmic\Deptrac\Contract\Config\Formatter\MermaidJsConfig;
use Qossmic\Deptrac\Contract\Config\Layer;
use Qossmic\Deptrac\Contract\Config\Ruleset;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand Down Expand Up @@ -106,6 +107,11 @@
->pointsToGroup(true)
->groups('Contract', $contract)
->groups('Supportive', $supportive, $file, $symfony, $console, $dependencyInjection, $outputFormatter, $time)
->groups('Core', $analyser, $ast, $dependency, $inputCollector, $layer),
MermaidJsConfig::create()
->direction('TD')
->groups('Contract', $contract)
->groups('Supportive', $supportive, $file, $symfony, $console, $dependencyInjection, $outputFormatter, $time)
->groups('Core', $analyser, $ast, $dependency, $inputCollector, $layer)
);
};
18 changes: 18 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ deptrac:
- Dependency
- InputCollector
- Layer
mermaidjs:
direction: TD
groups:
Contract:
- Contract
Supportive:
- Supportive
- File
Symfony:
- Console
- DependencyInjection
- OutputFormatter
Core:
- Analyser
- Ast
- Dependency
- InputCollector
- Layer

layers:
# Domains
Expand Down
81 changes: 81 additions & 0 deletions docs/formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,87 @@ Will produce the following graph:
#### Pointing to groups instead of nodes
With `formatters.graphviz.pointToGroups` set to `true`, when you have a node inside a groups with the same name as the group itself, edges pointing to that node will point to the group instead. This might be useful for example if you want to provide a "public API" for a module defined by a group.

## MermaidJS Formatter

The MermaidJS formatter is a console formatter, which generates a mermaid.js compatible graph definition. It can be activated with `--formatter=mermaidjs`.
With the -o option you can specify the output file.

Available options:

```
--formatter=mermaidjs
--output= path to a dumped file
```
With this example
Yaml Config:

```yaml
deptrac:
layers:
- User Frontend
- User Backend
- Admin Frontend
- Admin Backend
formatters:
mermaidjs:
direction: TD
groups:
User:
- User Frontend
- User Backend
Admin:
- Admin Frontend
- Admin Backend
```

This will produce the following graph:

```mermaid
flowchart TD;
subgraph ContractGroup;
Contract;
end;
subgraph SupportiveGroup;
Supportive;
File;
end;
subgraph SymfonyGroup;
Console;
DependencyInjection;
OutputFormatter;
end;
subgraph CoreGroup;
Analyser;
Ast;
Dependency;
InputCollector;
Layer;
end;
Contract -->|6| Symfony;
InputCollector -->|3| File;
InputCollector -->|7| Symfony;
Dependency -->|36| Ast;
Layer -->|68| Ast;
Layer -->|8| Symfony;
Analyser -->|18| Ast;
Analyser -->|23| Dependency;
Analyser -->|6| Layer;
Analyser -->|10| Symfony;
Ast -->|3| Symfony;
Ast -->|3| InputCollector;
Ast -->|7| File;
OutputFormatter -->|5| Symfony;
OutputFormatter -->|1| DependencyInjection;
File -->|9| Symfony;
DependencyInjection -->|37| Symfony;
Console -->|66| Symfony;
Console -->|2| DependencyInjection;
Console -->|16| Analyser;
Console -->|5| File;
Console -->|4| OutputFormatter;
Console -->|4| Time;
```

## JSON Formatter

By default, Json formatter dumps information to *STDOUT*. It can be activated
Expand Down
59 changes: 59 additions & 0 deletions src/Contract/Config/Formatter/MermaidJsConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Contract\Config\Formatter;

use Qossmic\Deptrac\Contract\Config\Layer;

final class MermaidJsConfig implements FormatterConfigInterface
{
private string $name = 'mermaidjs';

private string $direction = 'TD';

/** @var array<string, Layer[]> */
private array $groups = [];

public static function create(): self
{
return new self();
}

public function getName(): string
{
return $this->name;
}

public function direction(string $direction): self
{
$this->direction = $direction;

return $this;
}

public function groups(string $name, Layer ...$layerConfigs): self
{
foreach ($layerConfigs as $layerConfig) {
$this->groups[$name][] = $layerConfig;
}

return $this;
}

public function toArray(): array
{
$output = [];

if ([] !== $this->groups) {
$output['groups'] = array_map(
static fn (array $configs) => array_map(static fn (Layer $layer) => $layer->name, $configs),
$this->groups
);
}

$output['direction'] = $this->direction;

return $output;
}
}
15 changes: 15 additions & 0 deletions src/Supportive/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ private function appendFormatters(ArrayNodeDefinition $node): void
})
->end()
->end()
->arrayNode('mermaidjs')
->info('Configure MermaidJS output formatter')
->addDefaultsIfNotSet()
->children()
->scalarNode('direction')->defaultValue('TD')
->end()
->arrayNode('groups')
->info('Combine multiple layers to a group')
->useAttributeAsKey('name')
->arrayPrototype()
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->arrayNode('codeclimate')
->addDefaultsIfNotSet()
->info('Configure Codeclimate output formatters')
Expand Down
117 changes: 117 additions & 0 deletions src/Supportive/OutputFormatter/MermaidJSOutputFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Supportive\OutputFormatter;

use Qossmic\Deptrac\Contract\OutputFormatter\OutputFormatterInput;
use Qossmic\Deptrac\Contract\OutputFormatter\OutputFormatterInterface;
use Qossmic\Deptrac\Contract\OutputFormatter\OutputInterface;
use Qossmic\Deptrac\Contract\Result\OutputResult;
use Qossmic\Deptrac\Supportive\OutputFormatter\Configuration\FormatterConfiguration;

/**
* @internal
*/
final class MermaidJSOutputFormatter implements OutputFormatterInterface
{
/** @var array{direction: string, groups: array<string, string[]>} */
private array $config;
private const GRAPH_TYPE = 'flowchart %s;';

private const GRAPH_END = ' end;';
private const SUBGRAPH = ' subgraph %sGroup;';
private const LAYER = ' %s;';
private const GRAPH_NODE_FORMAT = ' %s -->|%d| %s;';
private const VIOLATION_STYLE_FORMAT = ' linkStyle %d stroke:red,stroke-width:4px;';

public function __construct(FormatterConfiguration $config)
{
/** @var array{direction: string, groups: array<string, string[]>} $extractedConfig */
$extractedConfig = $config->getConfigFor('mermaidjs');
$this->config = $extractedConfig;
}

public static function getName(): string
{
return 'mermaidjs';
}

public function finish(
OutputResult $result,
OutputInterface $output,
OutputFormatterInput $outputFormatterInput
): void {
$graph = $this->parseResults($result);
$violations = $result->violations();
$buffer = '';

$buffer .= sprintf(self::GRAPH_TYPE.PHP_EOL, $this->config['direction']);

foreach ($this->config['groups'] as $subGraphName => $layers) {
$buffer .= sprintf(self::SUBGRAPH.PHP_EOL, $subGraphName);

foreach ($layers as $layer) {
$buffer .= sprintf(self::LAYER.PHP_EOL, $layer);
}

$buffer .= self::GRAPH_END.PHP_EOL;
}

$linkCount = 0;
$violationsLinks = [];
$violationGraphLinks = [];

foreach ($violations as $violation) {
if (!isset($violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()])) {
$violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()] = 1;
} else {
++$violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()];
}
}

foreach ($violationsLinks as $dependerLayer => $layers) {
foreach ($layers as $dependentLayer => $count) {
$buffer .= sprintf(self::GRAPH_NODE_FORMAT.PHP_EOL, $dependerLayer, $count, $dependentLayer);
$violationGraphLinks[] = $linkCount;
++$linkCount;
}
}

foreach ($graph as $dependerLayer => $layers) {
foreach ($layers as $dependentLayer => $count) {
if (!isset($violationsLinks[$dependerLayer][$dependentLayer])) {
$buffer .= sprintf(self::GRAPH_NODE_FORMAT.PHP_EOL, $dependerLayer, $count, $dependentLayer);
}
}
}

foreach ($violationGraphLinks as $linkNumber) {
$buffer .= sprintf(self::VIOLATION_STYLE_FORMAT.PHP_EOL, $linkNumber);
}

if (null !== $outputFormatterInput->outputPath) {
file_put_contents($outputFormatterInput->outputPath, $buffer);
} else {
$output->writeRaw($buffer);
}
}

/**
* @return array<string, array<string, int<1, max>>>
*/
protected function parseResults(OutputResult $result): array
{
$graph = [];

foreach ($result->allowed() as $rule) {
if (!isset($graph[$rule->getDependerLayer()][$rule->getDependentLayer()])) {
$graph[$rule->getDependerLayer()][$rule->getDependentLayer()] = 1;
} else {
++$graph[$rule->getDependerLayer()][$rule->getDependentLayer()];
}
}

return $graph;
}
}
4 changes: 4 additions & 0 deletions tests/Supportive/DependencyInjection/DeptracExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class DeptracExtensionTest extends TestCase
'groups' => [],
'point_to_groups' => false,
],
'mermaidjs' => [
'direction' => 'TD',
'groups' => [],
],
'codeclimate' => [
'severity' => [
'failure' => 'major',
Expand Down
Loading

0 comments on commit 842a8c6

Please sign in to comment.