Skip to content

Commit

Permalink
#112 Added failFast argument that governs whether createReport should…
Browse files Browse the repository at this point in the history
… fail on the first error (failFast=true), or collect as many of the errors in the template as possible before failing (failFast=false). It defaults to true.
  • Loading branch information
jjhbw committed Apr 18, 2020
1 parent 2de63d0 commit b5e46ca
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 20 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,31 @@ Define a name for a complete command (especially useful for formatting tables):
----------------------------------------------------------
```

## Error handling

By default, `createReport` will throw an error the moment it encounters a problem with the template, such as a bad command (i.e. it 'fails fast'). In some cases, however, you may want to collect all errors that may exist in the template before failing. For example, this is useful when you are letting your users create templates interactively. You can disable fast-failing by providing the `failFast: false` parameter as shown below. This will make `createReport` throw an array of errors instead of a single error so you can get a more complete picture of what is wrong with the template.

```typescript
try {
createReport({
template,
data: {
name: 'John',
surname: 'Appleseed',
},
failFast: false,
});
} catch (errors) {
if (Array.isArray(errors)) {
// An array of errors likely caused by bad commands in the template.
console.log(errors);
} else {
// Not an array of template errors, indicating something more serious.
throw errors;
}
}
```


## [Changelog](https://github.com/guigrpa/docx-templates/blob/master/CHANGELOG.md)

Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/__snapshots__/indexNode.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23938,6 +23938,16 @@ Object {
}
`;

exports[`noSandbox Template processing 112a failFast: false lists all errors in the document before failing. 1`] = `
Array [
[Error: Error executing command: INS notavailable notavailable is not defined],
[Error: Error executing command: INS something something is not defined],
[Error: Error executing command: END-FOR company Invalid command: END-FOR company],
]
`;

exports[`noSandbox Template processing 112b failFast: true has the same behaviour as when failFast is undefined 1`] = `[Error: Error executing command: INS notavailable notavailable is not defined]`;

exports[`sandbox Template processing 03 Uses the resolver's response to produce the report 1`] = `
Object {
"_attrs": Object {
Expand Down Expand Up @@ -47875,3 +47885,13 @@ Object {
"_tag": "w:document",
}
`;

exports[`sandbox Template processing 112a failFast: false lists all errors in the document before failing. 1`] = `
Array [
[Error: Error executing command: INS notavailable notavailable is not defined],
[Error: Error executing command: INS something something is not defined],
[Error: Error executing command: END-FOR company Invalid command: END-FOR company],
]
`;

exports[`sandbox Template processing 112b failFast: true has the same behaviour as when failFast is undefined 1`] = `[Error: Error executing command: INS notavailable notavailable is not defined]`;
Binary file added src/__tests__/fixtures/invalidMultipleErrors.docx
Binary file not shown.
44 changes: 44 additions & 0 deletions src/__tests__/indexNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,50 @@ Morbi dignissim consequat ex, non finibus est faucibus sodales. Integer sed just
// Render to an object and compare with snapshot.
expect(await createReport(opts, 'JS')).toMatchSnapshot();
});

it('112a failFast: false lists all errors in the document before failing.', async () => {
const template = await fs.promises.readFile(
path.join(__dirname, 'fixtures', 'invalidMultipleErrors.docx')
);
return expect(
createReport(
{
template,
data: {
companies: [
{ name: 'FIRST' },
{ name: 'SECOND' },
{ name: 'THIRD' },
],
},
failFast: false,
},
'JS'
)
).rejects.toMatchSnapshot();
});

it('112b failFast: true has the same behaviour as when failFast is undefined', async () => {
const template = await fs.promises.readFile(
path.join(__dirname, 'fixtures', 'invalidMultipleErrors.docx')
);
return expect(
createReport(
{
template,
data: {
companies: [
{ name: 'FIRST' },
{ name: 'SECOND' },
{ name: 'THIRD' },
],
},
failFast: true,
},
'JS'
)
).rejects.toMatchSnapshot();
});
});
});
});
10 changes: 9 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async function createReport(
noSandbox: options.noSandbox || false,
runJs: options.runJs,
additionalJsContext: options.additionalJsContext || {},
failFast: options.failFast == null ? true : options.failFast,
};
const xmlOptions = { literalXmlDelimiter };

Expand Down Expand Up @@ -123,6 +124,9 @@ async function createReport(
finalTemplate,
createOptions
);
if (result.status === 'errors') {
throw result.errors;
}
const {
report: report1,
images: images1,
Expand Down Expand Up @@ -174,12 +178,16 @@ async function createReport(
if (raw == null) throw new Error(`${filePath} could not be read`);
const js0 = await parseXml(raw);
const js = preprocessTemplate(js0, createOptions);
const result = await produceJsReport(queryResult, js, createOptions);
if (result.status === 'errors') {
throw result.errors;
}
const {
report: report2,
images: images2,
links: links2,
htmls: htmls2,
} = await produceJsReport(queryResult, js, createOptions);
} = result;
images = merge(images, images2) as Images;
links = merge(links, links2) as Links;
htmls = merge(htmls, htmls2) as Htmls;
Expand Down
65 changes: 46 additions & 19 deletions src/processTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,25 @@ const extractQuery = async (
!parent._fTextNode && // Flow, don't complain
parent._tag === 'w:t'
) {
await processText(null, nodeIn, ctx);
await processText(null, nodeIn, ctx, options.failFast);
}
if (ctx.query != null) break;
}
return ctx.query;
};

type ReportOutput = {
report: Node;
images: Images;
links: Links;
htmls: Htmls;
};
type ReportOutput =
| {
status: 'success';
report: Node;
images: Images;
links: Links;
htmls: Htmls;
}
| {
status: 'errors';
errors: Error[];
};

const produceJsReport = async (
data: ReportData | undefined,
Expand Down Expand Up @@ -113,6 +119,7 @@ const produceJsReport = async (
let nodeOut: Node = out;
let move;
let deltaJump = 0;
const errors: Error[] = [];

while (true) {
// eslint-disable-line no-constant-condition
Expand Down Expand Up @@ -326,9 +333,14 @@ const produceJsReport = async (
!parent._fTextNode && // Flow-prevention
parent._tag === 'w:t'
) {
// TODO: use a discriminated union here instead of a type assertion to distinguish TextNodes from NonTextNodes.
const newNodeAsTextNode: TextNode = newNode as TextNode;
newNodeAsTextNode._text = await processText(data, nodeIn, ctx);
const result = await processText(data, nodeIn, ctx, options.failFast);
if (typeof result === 'string') {
// TODO: use a discriminated union here instead of a type assertion to distinguish TextNodes from NonTextNodes.
const newNodeAsTextNode: TextNode = newNode as TextNode;
newNodeAsTextNode._text = result;
} else {
errors.push(...result);
}
}

// Execute the move in the output tree
Expand All @@ -346,7 +358,14 @@ const produceJsReport = async (
}
}

if (errors.length > 0)
return {
status: 'errors',
errors,
};

return {
status: 'success',
report: out,
images: ctx.images,
links: ctx.links,
Expand All @@ -357,8 +376,9 @@ const produceJsReport = async (
const processText = async (
data: ReportData | undefined,
node: TextNode,
ctx: Context
): Promise<string> => {
ctx: Context,
failFast: boolean
): Promise<string | Error[]> => {
const { cmdDelimiter } = ctx.options;
const text = node._text;
if (text == null || text === '') return '';
Expand All @@ -367,6 +387,7 @@ const processText = async (
.map(s => s.split(cmdDelimiter[1]))
.reduce((x, y) => x.concat(y));
let outText = '';
const errors: Error[] = [];
for (let idx = 0; idx < segments.length; idx++) {
// Include the separators in the `buffers` field (used for deleting paragraphs if appropriate)
if (idx > 0) appendTextToTagBuffers(cmdDelimiter[0], ctx, { fCmd: true });
Expand All @@ -385,16 +406,22 @@ const processText = async (
if (ctx.fCmd) {
const cmdResultText = await processCmd(data, node, ctx);
if (cmdResultText != null) {
outText += cmdResultText;
appendTextToTagBuffers(cmdResultText, ctx, {
fCmd: false,
fInsertedText: true,
});
if (cmdResultText instanceof Error) {
if (failFast) throw cmdResultText;
errors.push(cmdResultText);
} else {
outText += cmdResultText;
appendTextToTagBuffers(cmdResultText, ctx, {
fCmd: false,
fInsertedText: true,
});
}
}
}
ctx.fCmd = !ctx.fCmd;
}
}
if (errors.length > 0) return errors;
return outText;
};

Expand All @@ -405,7 +432,7 @@ const processCmd = async (
data: ReportData | undefined,
node: Node,
ctx: Context
): Promise<null | undefined | string> => {
): Promise<null | undefined | string | Error> => {
const cmd = getCommand(ctx);
DEBUG && log.debug(`Processing cmd: ${cmd}`);
try {
Expand Down Expand Up @@ -506,7 +533,7 @@ const processCmd = async (
} else throw new Error(`Invalid command syntax: '${cmd}'`);
return out;
} catch (err) {
throw new Error(`Error executing command: ${cmd} ${err.message}`);
return new Error(`Error executing command: ${cmd} ${err.message}`);
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type UserOptions = {
noSandbox?: boolean;
runJs?: RunJSFunc;
additionalJsContext?: Object;
failFast?: boolean;
};

export type CreateReportOptions = {
Expand All @@ -61,6 +62,7 @@ export type CreateReportOptions = {
noSandbox: boolean;
runJs?: RunJSFunc;
additionalJsContext: Object;
failFast: boolean;
};

export type Context = {
Expand Down

0 comments on commit b5e46ca

Please sign in to comment.