Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 426 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"@oclif/dev-cli": "^1.26.6",
"@types/jest": "^27.0.3",
"@types/jest-expect-message": "^1.0.3",
"@types/node": "^10.17.60",
"@types/lodash": "^4.14.183",
"@types/node": "^14.18.11",
"@types/object-path": "^0.11.1",
"@types/uuid": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^5.5.0",
Expand Down Expand Up @@ -77,10 +78,15 @@
"@oclif/command": "1.8.7",
"@oclif/config": "1.18.1",
"@oclif/errors": "1.3.5",
"@oclif/help": "^1.0.1",
"@oclif/plugin-help": "3.2.10",
"@types/fs-extra": "^9.0.13",
"ajv": "8.8.2",
"fs-extra": "^10.1.0",
"glob": "7.2.0",
"lodash": "^4.17.21",
"object-path": "0.11.8",
"path": "^0.12.7",
"tslib": "1.14.1",
"yaml": "1.10.2"
}
Expand Down
43 changes: 43 additions & 0 deletions src/commands/export/c4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Command, flags } from '@oclif/command';
import { C4Docs } from '../../exporters/c4/c4';
import { YAAN } from '../../yaan/yaan';

export default class C4Plantuml extends Command {
static description = 'Export to C4 PlantUML';

static examples = [
`$ yaan export c4 -p presentation path-to-my-yaan-project path-to-result-directory`,
];

static flags = {
help: flags.help({ char: 'h' }),
presentation: flags.string({
char: 'p',
description: 'path to relative project presentation to export',
required: true,
}),
};

static args = [{ name: 'projectPath' }, { name: 'resultPath' }];

async run() {
try {
const { args, flags } = this.parse(C4Plantuml);
if (args.projectPath && args.resultPath) {
const yaan = new YAAN();
const project = yaan.loadProjectFromDir(args.projectPath);
const c4 = new C4Docs(project, flags.presentation);

await c4.print(args.resultPath);
} else {
this.error(
'Please specify path to project directory and path to write result',
);
return -1;
}
} catch (e) {
console.error(e);
this.error(e as Error);
}
}
}
43 changes: 43 additions & 0 deletions src/commands/export/split.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Command, flags } from '@oclif/command';
import { SplitDocs } from '../../exporters/split/splitUml';
import { YAAN } from '../../yaan/yaan';

export default class SplitPlantuml extends Command {
static description = 'Export to PlantUMLs splitted by components';

static examples = [
`$ yaan export split -p presentation path-to-my-yaan-project path-to-result-directory`,
];

static flags = {
help: flags.help({ char: 'h' }),
presentation: flags.string({
char: 'p',
description: 'path to relative project presentation to export',
required: true,
}),
};

static args = [{ name: 'projectPath' }, { name: 'resultPath' }];

async run() {
try {
const { args, flags } = this.parse(SplitPlantuml);
if (args.projectPath && args.resultPath) {
const yaan = new YAAN();
const project = yaan.loadProjectFromDir(args.projectPath);
const split = new SplitDocs(project, flags.presentation);

await split.print(args.resultPath);
} else {
this.error(
'Please specify path to project directory and path to write result',
);
return -1;
}
} catch (e) {
console.error(e);
this.error(e as Error);
}
}
}
198 changes: 198 additions & 0 deletions src/exporters/c4/c4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import _ from 'lodash';
import path from 'path';
import fs from 'fs-extra';
import { ProjectContainer } from '../../yaan/types';
import { PlantUml } from '../plantUml/plantUml';
import { PlantUmlObject } from '../plantUml/plantUmlObject';
import { PlantUmlGroup } from '../plantUml/plantUmlGroup';
import { PlantUmlComponentGroup } from '../plantUml/plantUmlComponentGroup';
import { PlantUmlDeploymentGroup } from '../plantUml/plantUmlDeploymentGroup';

interface ContainerLevel {
data: PlantUmlObject;
components: PlantUmlObject[];
}

interface SystemLevel {
data: PlantUmlObject;
containers: ContainerLevel[];
}

interface C4Uml {
title: string;
context: PlantUmlObject;
systems: SystemLevel[];
}

export class C4Docs {
public readonly docC4: C4Uml;
constructor(
private readonly project: ProjectContainer,
private readonly presentation: string,
) {
const plantUml = new PlantUml(project, presentation);

const systems: PlantUmlObject[] = [];
for (const puml of plantUml.children[0].children) {
if (puml.children.length > 0) {
systems.push(_.cloneDeep(puml));
}
}
const contextPuml = plantUml;
const systemsPuml: Record<string, any>[] = [];
for (const system of systems) {
for (const sysItem of system.children) {
systemsPuml.push({
data: sysItem,
containers: [],
});
}
}
for (const systemPuml of systemsPuml) {
if (systemPuml.data.children.length > 0) {
for (const container of systemPuml.data.children) {
systemPuml.containers.push({
data: _.cloneDeep(container),
components: [],
});
if (container.children.length > 0) {
for (const component of container.children) {
systemPuml.containers[
systemPuml.containers.length - 1
].components.push(_.cloneDeep(component));
}
container.children.length = 0;
}
}
systemPuml.data.children.length = 0;
}
}

this.docC4 = {
title: presentation,
context: contextPuml,
systems: systemsPuml as SystemLevel[],
};
}

private _getGroupInfo(groupObject: PlantUmlObject): {
title: string;
description?: string;
} {
const defaultInfo = `${this.presentation}-group#${groupObject.id}`;
if (groupObject instanceof PlantUmlGroup) {
return {
title: groupObject.title || defaultInfo,
};
} else if (groupObject instanceof PlantUmlComponentGroup) {
return {
title: groupObject.title || defaultInfo,
description: groupObject.group.title,
};
} else if (groupObject instanceof PlantUmlDeploymentGroup) {
return {
title: groupObject.deploymentGroup.title || defaultInfo,
description: groupObject.deploymentGroup.solution,
};
}
return { title: defaultInfo };
}

public async print(resultPath: string): Promise<void> {
const resPath = path.join(resultPath, this.docC4.title);
await fs.emptyDir(resPath);
await fs.outputFile(path.join(resPath, 'context.md'), this.docC4.title);
await fs.outputFile(
path.join(resPath, 'context.puml'),
`${this.header}${this.docC4.context.print()}${this.footer}`,
);

for (const system of this.docC4.systems) {
const { title, description } = this._getGroupInfo(system.data);
const sysPath = path.join(resPath, title);
await fs.outputFile(
path.join(sysPath, 'system.md'),
description || title,
);
await fs.outputFile(
path.join(sysPath, 'system.puml'),
`${this.header}${system.data.print()}${this.footer}`,
);
for (const container of system.containers) {
const { title, description } = this._getGroupInfo(
container.data,
);
const contPath = path.join(sysPath, title);
await fs.outputFile(
path.join(contPath, 'container.md'),
description || title,
);
await fs.outputFile(
path.join(contPath, 'container.puml'),
`${this.header}${container.data.print()}${this.footer}`,
);
for (const component of container.components) {
const { title, description } =
this._getGroupInfo(component);
const compPath = path.join(contPath, title);
await fs.outputFile(
path.join(compPath, 'component.md'),
description || title,
);
await fs.outputFile(
path.join(compPath, 'component.puml'),
`${this.header}${component.print()}${this.footer}`,
);
}
}
}
}

protected get header(): string {
return `
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
!define FONTAWESOME https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome-5
!define FONTAWESOME1 https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome
!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons2
!include FONTAWESOME/server.puml
!include FONTAWESOME/envelope.puml
!include FONTAWESOME1/microchip.puml
!include FONTAWESOME1/hdd_o.puml
!include FONTAWESOME1/lock.puml
!include DEVICONS/kubernetes.puml

AddElementTag("hiddenGroup", $bgColor = "transparent", $borderColor="transparent")
AddElementTag("visibleGroup", $bgColor = "transparent", $fontColor="#3B3A2F")
AddElementTag("organization", $bgColor = "#FAF8C9", $shadowing="true", $shape=RoundedBoxShape())
AddElementTag("software", $bgColor = "#BAB995")
AddElementTag("infra", $bgColor = "#BAB995")
AddElementTag("fallback", $bgColor="#c0c0c0")

AddRelTag("fallback", $textColor="#c0c0c0", $lineColor="#438DD5")
AddRelTag("uses-external", $textColor="#ff0000", $lineColor="#ff0000", $lineStyle=BoldLine())
AddRelTag("uses-internal", $textColor="#3B3A2F", $lineColor="#3B3A2F", $lineStyle=DashedLine())
AddRelTag("deployed-on", $textColor="#3B3A2F", $lineColor="#3B3A2F", $lineStyle=DashedLine())
AddRelTag("clustered-on", $textColor="#3B3A2F", $lineColor="#3B3A2F", $lineStyle=DashedLine())

AddElementTag("deploymentGroup", $bgColor="#BAB995")
AddElementTag("deployment", $shadowing = true, $bgColor = "transparent")
AddElementTag("server", $shadowing = true, $bgColor="#E0DFB4")
AddElementTag("kubernetesCluster", $shadowing = true)
AddElementTag("hidden", $shadowing = false, $shape = EightSidedShape(), $bgColor="#444444")


WithoutPropertyHeader()

`;
}

protected get footer(): string {
return `
SHOW_LEGEND()
@enduml
`;
}
}
35 changes: 35 additions & 0 deletions src/exporters/split/plantUmlComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PlantUmlObject } from './plantUmlObject';
import { SolutionComponent } from '../../yaan/schemas/solution';

export class PlantUmlComponent extends PlantUmlObject {
constructor(
public readonly id: string,
public readonly component: SolutionComponent,
) {
super(id);
}

private getContainerFigure() {
switch (this.component.kind) {
case 'db':
return 'ContainerDb';
case 'queue':
return 'ContainerQueue';
default:
return 'Container';
}
}

protected get header(): string {
return `
${this.getContainerFigure()}("${this.id}", "${
this.component.title || ''
}", "${this.component.description || 'Component'}", "", "") {
`;
}

protected get footer(): string {
return `
}`;
}
}
11 changes: 11 additions & 0 deletions src/exporters/split/plantUmlComponentGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SolutionComponentGroup } from '../../yaan/schemas/solution';
import { GroupVisibility, PlantUmlGroup } from './plantUmlGroup';

export class PlantUmlComponentGroup extends PlantUmlGroup {
constructor(
public readonly id: string,
public readonly group: SolutionComponentGroup,
) {
super(id, GroupVisibility.HiddenIfEmpty, group.title);
}
}
37 changes: 37 additions & 0 deletions src/exporters/split/plantUmlComponentPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PlantUmlObject } from './plantUmlObject';
import {
SolutionPort,
SolutionPortDetailed,
} from '../../yaan/schemas/solution';

export class PlantUmlComponentPort extends PlantUmlObject {
constructor(
public readonly id: string,
public readonly port: SolutionPort,
public readonly portName: string,
) {
super(id);
}

protected get header(): string {
const port: SolutionPortDetailed =
typeof this.port === 'object'
? this.port
: {
number: this.port,
description: '',
protocol: 'TCP',
};
return `
AddProperty("${port.protocol || 'TCP'}", "${port.number}")
Deployment_Node("${this.id}", "${
this.portName || port.description
}", "Port", "${port.description || ''}") {
`;
}

protected get footer(): string {
return `
}`;
}
}
Loading