Skip to content

Commit 72dc1a5

Browse files
[CODE-34] Pretty print plan (#25)
* [CODE-34] Added pretty print plan function + tests * [CODE-34] Added pretty print to default reporter * [CODE-34] Improved validation error * [CODE-34] Added common method to print out plan array. Updated validation error message
1 parent a215bbe commit 72dc1a5

6 files changed

Lines changed: 323 additions & 36 deletions

File tree

src/plugins/plugin-manager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ctx, SubProcessName } from '../events/context.js';
66
import { groupBy } from '../utils/index.js';
77
import { Plugin } from './plugin.js';
88
import { PluginResolver } from './resolver.js';
9+
import { prettyFormatPlan } from '../ui/plan-pretty-printer.js';
910

1011
type PluginName = string;
1112
type ResourceTypeId = string;
@@ -141,9 +142,10 @@ export class PluginManager {
141142
private async validateApply(pluginName: string, desired: ResourceConfig): Promise<void> {
142143
const validationPlan = await this.plugins.get(pluginName)!.plan(desired);
143144
if (validationPlan.operation !== ResourceOperation.NOOP) {
144-
throw new Error(`Plugin: '${pluginName}'. Resource: '${desired.type}'. Apply validation was not successful (additional changes are needed to match the desired plan).
145+
throw new Error(`Plugin: '${pluginName}'. Resource: '${desired.type}'. Additional changes are needed to match the desired plan.
145146
146-
Validation plan returned: ${validationPlan.operation}.
147+
Validation returned: "${validationPlan.operation}" instead of "${ResourceOperation.NOOP}". These changes are remaining.
148+
${prettyFormatPlan(validationPlan)}
147149
`)
148150
}
149151
}

src/ui/components/plan/plan.tsx

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { OrderedList } from '@inkjs/ui';
21
import { PlanResponseData, ResourceOperation } from 'codify-schemas';
32
import { Box, Text } from 'ink';
43
import React from 'react';
54

5+
import { prettyFormatPlan } from '../../plan-pretty-printer.js';
66
import { ResourceText } from './resource-text.js';
77

88
export function PlanComponent(props: {
@@ -11,39 +11,18 @@ export function PlanComponent(props: {
1111
const filteredPlan = props.plan.filter((p) => p.operation !== ResourceOperation.NOOP);
1212

1313
return <Box flexDirection="column">
14-
<Box borderStyle="round" borderColor="green">
14+
<Box borderColor="green" borderStyle="round">
1515
<Text>Codify Plan</Text>
1616
</Box>
1717
<Text>The following actions will be performed: </Text>
1818
<Text> </Text>
19-
<Box marginLeft={1}>
20-
<OrderedList>{
19+
<Box flexDirection="column" marginLeft={1}>{
2120
filteredPlan.map((p, idx) =>
22-
<OrderedList.Item key={idx}>
23-
<Box flexDirection="column" marginBottom={1}>
24-
<ResourceText plan={p}/>
25-
<Text>
26-
<Text>Parameters: </Text>
27-
<Text>{JSON.stringify(p.parameters, null, 2)}</Text>
28-
{/* <Box flexDirection='column' marginLeft={2} width={300}>{ */}
29-
{/* p.parameters.map((parameter, idx2) => */}
30-
{/* <Box flexDirection = 'row' justifyContent='space-between' key={idx2}> */}
31-
{/* <ParameterOperationSymbol parameterOperation={parameter.operation}/> */}
32-
{/* <Text>{parameter.name}</Text> */}
33-
{/* <Text> */}
34-
{/* <Text>{JSON.stringify(parameter.previousValue, null, 2)}</Text> */}
35-
{/* <Text>{' -> '}</Text> */}
36-
{/* <Text>{JSON.stringify(parameter.newValue, null, 2)}</Text> */}
37-
{/* </Text> */}
38-
{/* /!* <Text>{JSON.stringify(parameter, null, 2)}</Text> *!/ */}
39-
{/* </Box> */}
40-
{/* ) */}
41-
{/* }</Box> */}
42-
</Text>
43-
</Box>
44-
</OrderedList.Item>
21+
<Box flexDirection="column" key={idx} marginBottom={1}>
22+
<ResourceText plan={p}/>
23+
<Text>{prettyFormatPlan(p)}</Text>
24+
</Box>
4525
)
46-
}</OrderedList>
47-
</Box>
26+
}</Box>
4827
</Box>
4928
}

src/ui/components/plan/resource-text.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ export function ResourceText(props: {
4848

4949
return <Box>
5050
<Text backgroundColor={backgroundColor}>
51+
<ResourceOperationSymbol resourceOperation={operation}/>
52+
<Text> </Text>
5153
<Text bold>{fullyQualifiedName}</Text>
52-
<Text> resource will {operationName}</Text>
54+
<Text> will {operationName}</Text>
5355
</Text>
54-
<Text> </Text>
55-
<ResourceOperationSymbol resourceOperation={operation}/>
5656
</Box>
5757
}

src/ui/plan-pretty-printer.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, it } from 'vitest';
2+
import { prettyFormatPlan } from './plan-pretty-printer.js';
3+
import { ParameterOperation, PlanResponseData, ResourceOperation } from 'codify-schemas';
4+
5+
describe('Plan pretty printer', () => {
6+
it('Can print create plans', () => {
7+
const plan: PlanResponseData = {
8+
planId: 'id',
9+
operation: ResourceOperation.CREATE,
10+
parameters: [
11+
{ name: 'propC', previousValue: null, newValue: 'yui', operation: ParameterOperation.ADD },
12+
{ name: 'propD', previousValue: null, newValue: 'qwe', operation: ParameterOperation.ADD },
13+
{
14+
name: 'propE',
15+
previousValue: null,
16+
newValue: ['10.0.0', '11.0.0', '9.0.0'],
17+
operation: ParameterOperation.ADD
18+
},
19+
{ name: 'propF', previousValue: null, newValue: ['abc', 'def'], operation: ParameterOperation.ADD },
20+
]
21+
}
22+
23+
console.log(prettyFormatPlan(plan))
24+
})
25+
26+
it('Can print destroy plans', () => {
27+
const plan: PlanResponseData = {
28+
planId: 'id',
29+
operation: ResourceOperation.DESTROY,
30+
parameters: [
31+
{ name: 'propC', previousValue: 'yui', newValue: null, operation: ParameterOperation.REMOVE },
32+
{ name: 'propD', previousValue: 'qwe', newValue: null, operation: ParameterOperation.REMOVE },
33+
{
34+
name: 'propE',
35+
previousValue: ['10.0.0', '11.0.0', '9.0.0'],
36+
newValue: null,
37+
operation: ParameterOperation.REMOVE
38+
},
39+
{ name: 'propF', previousValue: ['abc', 'def'], newValue: null, operation: ParameterOperation.REMOVE },
40+
]
41+
}
42+
43+
console.log(prettyFormatPlan(plan))
44+
})
45+
46+
it('Can print modify and re-create plans', () => {
47+
const plan: PlanResponseData = {
48+
planId: 'id',
49+
operation: ResourceOperation.RECREATE,
50+
parameters: [
51+
{ name: 'propA', previousValue: 'abc', newValue: 'def', operation: ParameterOperation.MODIFY },
52+
{
53+
name: 'propALong',
54+
previousValue: 'abc\ndef',
55+
newValue: 'def\nteoewriu',
56+
operation: ParameterOperation.MODIFY
57+
},
58+
{ name: 'propB', previousValue: 'xzy', newValue: 'xzy', operation: ParameterOperation.NOOP },
59+
{ name: 'propC', previousValue: null, newValue: 'yui', operation: ParameterOperation.ADD },
60+
{ name: 'propD', previousValue: 'qwe', newValue: null, operation: ParameterOperation.REMOVE },
61+
{
62+
name: 'propE',
63+
previousValue: ['10.0.0', '9.0.0'],
64+
newValue: ['10.0.0', '11.0.0'],
65+
operation: ParameterOperation.MODIFY
66+
},
67+
{ name: 'propF', previousValue: null, newValue: ['abc', 'def'], operation: ParameterOperation.ADD },
68+
]
69+
}
70+
71+
console.log(prettyFormatPlan(plan))
72+
})
73+
});

src/ui/plan-pretty-printer.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import chalk from 'chalk';
2+
import { ParameterOperation, PlanResponseData, ResourceOperation } from 'codify-schemas';
3+
4+
export function prettyFormatPlans(plans: PlanResponseData[]) {
5+
const builder = [
6+
'',
7+
'',
8+
chalk.bold('Codify Plan'),
9+
'The following actions will be performed',
10+
'',
11+
];
12+
13+
plans.forEach((plan: PlanResponseData) => {
14+
const formattedPlan = prettyFormatPlan(plan);
15+
16+
builder.push(chalk.bold(plan.resourceType + (plan.resourceName ? `.${plan.resourceName}` : '')) + ' will ' + resourceOperationText(plan.operation));
17+
builder.push(formattedPlan)
18+
})
19+
20+
return builder.join('\n')
21+
}
22+
23+
export function prettyFormatPlan(plan: PlanResponseData): string {
24+
switch (plan.operation) {
25+
case ResourceOperation.CREATE: {
26+
return prettyFormatCreatePlan(plan);
27+
}
28+
29+
case ResourceOperation.DESTROY: {
30+
return prettyFormatDestroyPlan(plan);
31+
}
32+
33+
case ResourceOperation.MODIFY:
34+
case ResourceOperation.RECREATE: {
35+
return prettyFormatModifyPlan(plan);
36+
}
37+
}
38+
39+
return '';
40+
}
41+
42+
function prettyFormatCreatePlan(plan: PlanResponseData): string {
43+
const parameters = plan.parameters
44+
.reduce((result, parameter) => {
45+
if (parameter.newValue === null || parameter.newValue === undefined) {
46+
return result;
47+
}
48+
49+
result[parameter.name] = typeof parameter.newValue === 'string'
50+
? escapeNewlines(parameter.newValue)
51+
: parameter.newValue;
52+
53+
return result;
54+
}, {} as Record<string, unknown>)
55+
56+
const json = JSON.stringify(parameters, null, 4)
57+
.split(/\n/g)
58+
.map((l) => ` ${l}`)
59+
.join('\n')
60+
return chalk.green(json);
61+
}
62+
63+
function prettyFormatDestroyPlan(plan: PlanResponseData): string {
64+
const parameters = plan.parameters
65+
.reduce((result, parameter) => {
66+
if (parameter.previousValue === null || parameter.previousValue === undefined) {
67+
return result;
68+
}
69+
70+
result[parameter.name] = typeof parameter.previousValue === 'string'
71+
? escapeNewlines(parameter.previousValue)
72+
: parameter.previousValue;
73+
74+
return result;
75+
}, {} as Record<string, unknown>)
76+
77+
const json = JSON.stringify(parameters, null, 4)
78+
.split(/\n/g)
79+
.map((l) => ` ${l}`)
80+
.join('\n')
81+
return chalk.red(json);
82+
}
83+
84+
function prettyFormatModifyPlan(plan: PlanResponseData): string {
85+
const builder = [
86+
' {'
87+
];
88+
89+
for (const parameter of plan.parameters) {
90+
91+
// TODO: Add support for object types as well in the future
92+
if ((Array.isArray(parameter.previousValue) || parameter.previousValue === null)
93+
&& (Array.isArray(parameter.newValue) || parameter.newValue === null)
94+
&& !(parameter.previousValue === null && parameter.newValue === null)
95+
) {
96+
const line = formatArray(parameter);
97+
builder.push(line);
98+
} else {
99+
const formattedParameter = formatParameter(parameter);
100+
101+
const line = formattedParameter.split(/\n/g)
102+
.map((l) => ` ${l}`)
103+
.map((l, idx) => idx === 0 ? operationSymbol(parameter.operation) + l : ` ${l}`)
104+
.join('\n')
105+
106+
builder.push(line);
107+
}
108+
}
109+
110+
builder.push(' }')
111+
return builder.join('\n');
112+
}
113+
114+
function escapeNewlines(str: string): string {
115+
return str.replaceAll('\n', '\\n');
116+
}
117+
118+
119+
function formatParameter(parameter: PlanResponseData['parameters'][0]): string {
120+
switch (parameter.operation) {
121+
case ParameterOperation.NOOP: {
122+
return typeof parameter.newValue === 'string'
123+
? `"${parameter.name}": "${escapeNewlines(parameter.newValue)}",`
124+
: `"${parameter.name}": ${parameter.newValue},`
125+
}
126+
127+
case ParameterOperation.ADD: {
128+
return typeof parameter.newValue === 'string'
129+
? chalk.green(`"${parameter.name}": "${escapeNewlines(parameter.newValue)}",`)
130+
: chalk.green(`"${parameter.name}": ${parameter.newValue},`)
131+
}
132+
133+
case ParameterOperation.REMOVE: {
134+
return typeof parameter.previousValue === 'string'
135+
? chalk.red(`"${parameter.name}": "${escapeNewlines(parameter.previousValue)}",`)
136+
: chalk.red(`"${parameter.name}": ${parameter.previousValue},`)
137+
}
138+
139+
case ParameterOperation.MODIFY: {
140+
return typeof parameter.newValue === 'string' && typeof parameter.previousValue === 'string'
141+
? `"${parameter.name}": "${escapeNewlines(parameter.previousValue)}" -> "${escapeNewlines(parameter.newValue)}",`
142+
: `"${parameter.name}": ${parameter.previousValue} -> ${parameter.newValue},`
143+
}
144+
}
145+
}
146+
147+
function resourceOperationText(operation: ResourceOperation): string {
148+
switch (operation) {
149+
case ResourceOperation.CREATE:
150+
return 'be created'
151+
case ResourceOperation.MODIFY:
152+
return 'be modified'
153+
case ResourceOperation.RECREATE:
154+
return 'be recreated'
155+
case ResourceOperation.DESTROY:
156+
return 'be destroyed'
157+
case ResourceOperation.NOOP:
158+
return 'not be changed'
159+
}
160+
}
161+
162+
function operationSymbol(operation: ParameterOperation): string {
163+
switch (operation) {
164+
case ParameterOperation.ADD: {
165+
return chalk.green('+')
166+
}
167+
168+
case ParameterOperation.NOOP: {
169+
return ' '
170+
}
171+
172+
case ParameterOperation.MODIFY: {
173+
return chalk.yellow('~')
174+
}
175+
176+
case ParameterOperation.REMOVE: {
177+
return chalk.red('-')
178+
}
179+
}
180+
}
181+
182+
function formatArray(parameter: PlanResponseData['parameters'][0]): string {
183+
const { name, newValue, operation, previousValue } = parameter;
184+
const a = previousValue as null | unknown[];
185+
const b = newValue as null | unknown[];
186+
187+
const mappedA = a?.map((l) =>
188+
typeof l === 'object' ? JSON.stringify(l) : l
189+
) ?? [];
190+
const mappedB = b?.map((l) =>
191+
typeof l === 'object' ? JSON.stringify(l) : l
192+
) ?? [];
193+
194+
if (operation === ParameterOperation.ADD) {
195+
return JSON.stringify(mappedB, null, 4)
196+
.split(/\n/g)
197+
.map((l, idx) => idx === 0 ? `"${name}": ${l}` : l)
198+
.map((l) => ` ${chalk.green(l)}`)
199+
.map((l, idx) => idx === 0 ? operationSymbol(operation) + l : ` ${l}`)
200+
.join('\n') + ','
201+
}
202+
203+
if (operation === ParameterOperation.REMOVE) {
204+
return JSON.stringify(mappedA, null, 4)
205+
.split(/\n/g)
206+
.map((l, idx) => idx === 0 ? `"${name}": ${l}` : l)
207+
.map((l) => ` ${chalk.red(l)}`)
208+
.map((l, idx) => idx === 0 ? operationSymbol(operation) + l : ` ${l}`)
209+
.join('\n') + ','
210+
}
211+
212+
if (operation === ParameterOperation.NOOP) {
213+
return JSON.stringify(mappedB, null, 4)
214+
.split(/\n/g)
215+
.map((l) => ` ${l}`)
216+
.join('\n') + ','
217+
}
218+
219+
const noop = mappedA.filter((l) => mappedB.includes(l))
220+
const remove = mappedA.filter((l) => !mappedB.includes(l));
221+
const add = mappedB.filter((l) => !mappedA.includes(l));
222+
223+
return [
224+
`${operationSymbol(operation)} "${name}": [`,
225+
...noop.map((l) => ` ${l},`),
226+
...add.map((l) => `${operationSymbol(ParameterOperation.ADD)} ${chalk.green(l + ',')}`),
227+
...remove.map((l) => `${operationSymbol(ParameterOperation.REMOVE)} ${chalk.red(l + ',')}`),
228+
' ],'
229+
].join('\n')
230+
}

0 commit comments

Comments
 (0)