Skip to content

Commit 79b2d95

Browse files
authored
feat: add support for optional custom error messages by using ajv-errors (#44)
1 parent fbf2281 commit 79b2d95

File tree

8 files changed

+511
-12
lines changed

8 files changed

+511
-12
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,57 @@ jobs:
3434
files: .github/workflows/**.yml
3535
```
3636
37+
### Example with Custom Error Messages
38+
39+
When `custom-errors` is enabled, you can use the `errorMessage` keyword in your
40+
JSON schema to provide more user-friendly error messages. This is powered by the
41+
[ajv-errors](https://github.com/ajv-validator/ajv-errors) library.
42+
43+
```yaml
44+
jobs:
45+
validate-config:
46+
name: Validate configuration files
47+
runs-on: ubuntu-latest
48+
steps:
49+
- name: Validate config with custom errors
50+
uses: dsanders11/[email protected]
51+
with:
52+
schema: ./config.schema.json
53+
files: ./config/*.yml
54+
custom-errors: true
55+
```
56+
57+
Example schema with custom error messages:
58+
59+
```json
60+
{
61+
"$schema": "http://json-schema.org/draft-07/schema#",
62+
"type": "object",
63+
"properties": {
64+
"name": {
65+
"type": "string",
66+
"minLength": 1
67+
},
68+
"version": {
69+
"type": "string",
70+
"pattern": "^\\d+\\.\\d+\\.\\d+$"
71+
}
72+
},
73+
"required": ["name", "version"],
74+
"errorMessage": {
75+
"type": "Configuration must be an object",
76+
"required": {
77+
"name": "Configuration must have a 'name' property",
78+
"version": "Configuration must have a 'version' property"
79+
},
80+
"properties": {
81+
"name": "Name must be a non-empty string",
82+
"version": "Version must follow semantic versioning (e.g., '1.0.0')"
83+
}
84+
}
85+
}
86+
```
87+
3788
### Validating Schema
3889

3990
Schemas can be validated by setting the `schema` input to the string literal
@@ -56,6 +107,8 @@ simply set a URL fragment (e.g. `#bust-cache`) on the schema URL.
56107
`true`)
57108
- `all-errors` - Whether to report all errors or stop after the first (default:
58109
`false`)
110+
- `custom-errors` - Enable support for custom error messages using ajv-errors
111+
(default: `false`)
59112

60113
### Outputs
61114

__tests__/main.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,4 +595,106 @@ describe('action', () => {
595595
);
596596
});
597597
});
598+
599+
describe('custom error messages', () => {
600+
it('forces allErrors to true when custom-errors is enabled', async () => {
601+
mockGetBooleanInput({ 'custom-errors': true, 'all-errors': false });
602+
mockGetInput({ schema });
603+
mockGetMultilineInput({ files });
604+
605+
vi.mocked(fs.readFile)
606+
.mockResolvedValueOnce(schemaContents)
607+
.mockResolvedValueOnce('invalid content');
608+
mockGlobGenerator(['/foo/bar/baz/config.yml']);
609+
610+
await main.run();
611+
expect(runSpy).toHaveReturned();
612+
expect(process.exitCode).not.toBeDefined();
613+
614+
// Should report multiple errors even though all-errors was false
615+
expect(core.error).toHaveBeenCalledTimes(4);
616+
expect(core.setOutput).toHaveBeenCalledTimes(1);
617+
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
618+
});
619+
620+
it('provides custom error messages when validation fails', async () => {
621+
mockGetBooleanInput({ 'custom-errors': true, 'fail-on-invalid': true });
622+
mockGetInput({ schema });
623+
mockGetMultilineInput({ files });
624+
625+
// Create a schema with custom error messages
626+
const customErrorSchemaContents = JSON.stringify({
627+
title: 'Test schema with custom errors',
628+
$schema: 'http://json-schema.org/draft-07/schema#',
629+
type: 'object',
630+
properties: {
631+
name: { type: 'string', minLength: 1 }
632+
},
633+
required: ['name'],
634+
errorMessage: {
635+
properties: {
636+
name: 'Name must be a non-empty string'
637+
}
638+
}
639+
});
640+
641+
const invalidInstanceContents = JSON.stringify({ name: '' });
642+
643+
vi.mocked(fs.readFile)
644+
.mockResolvedValueOnce(customErrorSchemaContents)
645+
.mockResolvedValueOnce(invalidInstanceContents);
646+
mockGlobGenerator(['/foo/bar/baz/config.yml']);
647+
648+
await main.run();
649+
expect(runSpy).toHaveReturned();
650+
expect(process.exitCode).toEqual(1);
651+
652+
expect(core.error).toHaveBeenCalledWith(
653+
'Error while validating file: /foo/bar/baz/config.yml'
654+
);
655+
656+
// Check that we get our custom error message in the JSON output
657+
const errorCalls = vi.mocked(core.error).mock.calls;
658+
const hasCustomMessage = errorCalls.some(
659+
call =>
660+
typeof call[0] === 'string' &&
661+
call[0].includes('Name must be a non-empty string')
662+
);
663+
expect(hasCustomMessage).toBe(true);
664+
665+
expect(core.setOutput).toHaveBeenCalledTimes(1);
666+
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
667+
});
668+
669+
it('works without custom-errors when disabled', async () => {
670+
mockGetBooleanInput({ 'custom-errors': false, 'fail-on-invalid': false });
671+
mockGetInput({ schema });
672+
mockGetMultilineInput({ files });
673+
674+
vi.mocked(fs.readFile)
675+
.mockResolvedValueOnce(schemaContents)
676+
.mockResolvedValueOnce('invalid content');
677+
mockGlobGenerator(['/foo/bar/baz/config.yml']);
678+
679+
await main.run();
680+
expect(runSpy).toHaveReturned();
681+
expect(process.exitCode).not.toBeDefined();
682+
683+
expect(core.error).toHaveBeenCalledWith(
684+
'Error while validating file: /foo/bar/baz/config.yml'
685+
);
686+
687+
// Should NOT have any custom error messages (no errorMessage keyword)
688+
const errorCalls = vi.mocked(core.error).mock.calls;
689+
const hasCustomErrors = errorCalls.some(
690+
call =>
691+
typeof call[0] === 'string' &&
692+
call[0].includes('"keyword":"errorMessage"')
693+
);
694+
expect(hasCustomErrors).toBe(false);
695+
696+
expect(core.setOutput).toHaveBeenCalledTimes(1);
697+
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
698+
});
699+
});
598700
});

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ inputs:
2525
description: Report all errors instead of stopping at the first
2626
required: false
2727
default: false
28+
custom-errors:
29+
description: Enable support for custom error messages using ajv-errors
30+
required: false
31+
default: false
2832

2933
outputs:
3034
valid:

0 commit comments

Comments
 (0)