Skip to content

Commit d9dcfbb

Browse files
Schema validation final merge (#1672)
* Add configuration schema validator and CI checks (#1667) Add a new schema validation script (scripts/validate-configuration-schemas.mjs) that finds packages/*/*configuration-schema.json and validates them against JSON Schema draft-07 using Ajv. The script prints human-friendly errors (or a markdown table with --markdown) and exits non-zero on failures. Wire this check into CI: run pnpm validate:schemas in .github workflows (ci, docs, publish). Update package.json to add the validate:schemas script and Ajv dependency, and update README, templates, and the wiki to document JSON Schema draft-07 and instruct maintainers to run pnpm validate:schemas after editing configuration-schema.json. * don't validate schemas while publishing docs docs workflow depends on publish workflow, and publish has already validated --------- Co-authored-by: Alassane Ndoye <aloutndoye.an@gmail.com>
1 parent 9361b95 commit d9dcfbb

9 files changed

Lines changed: 246 additions & 3 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ jobs:
2525
- name: Setup pnpm
2626
run: pnpm install --frozen-lockfile
2727

28+
- name: Validate configuration schemas
29+
run: pnpm validate:schemas
2830
- name: Build
2931
run: pnpm run build
3032
- name: Git Status

.github/workflows/publish.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
node-version: '22.20.0'
1919
- uses: pnpm/action-setup@v4
2020
- run: pnpm install --frozen-lockfile
21+
- name: Validate configuration schemas
22+
run: pnpm validate:schemas
2123
- run: pnpm build
2224
- run: pnpm test:git
2325
- run: pnpm test

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,22 @@ to help you build your own adaptor.
176176
3. [Implement your adaptor](https://github.com/OpenFn/adaptors/wiki/Adaptor-Writing-Best-Practice-&-Common-Patterns)
177177
in `packages/<adaptor-name>/src/Adaptor.js`
178178

179-
4. Test your adaptor:
179+
4. Update `packages/<adaptor-name>/configuration-schema.json` and validate it
180+
against
181+
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes):
182+
183+
```bash
184+
pnpm validate:schemas
185+
```
186+
187+
5. Test your adaptor:
180188
[See unit test guideline](https://github.com/OpenFn/adaptors/wiki/Unit-Testing-Guide)
181189

182190
```bash
183191
pnpm test
184192
```
185193

186-
5. Create a test job in `tmp/job.js` and initial state in `tmp/state.json` then
194+
6. Create a test job in `tmp/job.js` and initial state in `tmp/state.json` then
187195
run:
188196

189197
```bash
@@ -193,6 +201,7 @@ to help you build your own adaptor.
193201
### Best Practices
194202

195203
- Update the adaptor's README
204+
- Keep `configuration-schema.json` valid against JSON Schema draft-07
196205
- Include comprehensive [JSDoc](https://jsdoc.app/) comments for all functions
197206
- [Write unit tests for your adaptor functions](https://github.com/OpenFn/adaptors/wiki/Unit-Testing-Guide)
198207
- [Follow the existing code style and patterns](https://github.com/OpenFn/adaptors/wiki/Adaptor-Writing-Best-Practice-&-Common-Patterns)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test:imports": "cd tools/import-tests && pnpm test",
2424
"test": "pnpm lint && pnpm --filter \"./packages/**\" test && pnpm test:imports",
2525
"test:git": "pnpm exec scripts/status-check.sh",
26+
"validate:schemas": "node scripts/validate-configuration-schemas.mjs",
2627
"version": "pnpm changeset version && pnpm run changelog"
2728
},
2829
"author": "Open Function Group",
@@ -36,6 +37,7 @@
3637
"@openfn/parse-jsdoc": "workspace:^1.0.0",
3738
"@types/chai": "^5.2.2",
3839
"@types/mocha": "^10.0.10",
40+
"ajv": "^8.18.0",
3941
"chokidar-cli": "^3.0.0",
4042
"eslint": "10.2.0",
4143
"globals": "^17.2.0",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
#!/usr/bin/env node
2+
import fs from 'node:fs/promises';
3+
import { createRequire } from 'node:module';
4+
import path from 'node:path';
5+
import process from 'node:process';
6+
import { fileURLToPath } from 'node:url';
7+
8+
import Ajv from 'ajv';
9+
10+
const require = createRequire(import.meta.url);
11+
const draft07MetaSchema = require('ajv/dist/refs/json-schema-draft-07.json');
12+
const rootDir = path.resolve(
13+
path.dirname(fileURLToPath(import.meta.url)),
14+
'..'
15+
);
16+
const packagesDir = path.join(rootDir, 'packages');
17+
const args = new Set(process.argv.slice(2));
18+
const markdown = args.has('--markdown');
19+
const help = args.has('--help') || args.has('-h');
20+
21+
const pointerFor = ({ instancePath }) =>
22+
instancePath ? `#${instancePath}` : '#';
23+
24+
const relativePath = file =>
25+
path.relative(rootDir, file).split(path.sep).join('/');
26+
27+
const escapeMarkdown = value =>
28+
String(value)
29+
.replace(/\\/g, '\\\\')
30+
.replace(/\|/g, '\\|')
31+
.replace(/\n/g, ' ');
32+
33+
const formatReason = error => {
34+
if (!error) return 'Schema validation failed.';
35+
36+
const details = [];
37+
if (error.keyword === 'additionalProperties') {
38+
details.push(`additional property "${error.params.additionalProperty}"`);
39+
}
40+
if (error.keyword === 'enum' && error.params?.allowedValues) {
41+
details.push(`allowed values: ${error.params.allowedValues.join(', ')}`);
42+
}
43+
44+
const message = error.message ?? 'Schema validation failed.';
45+
return details.length ? `${message} (${details.join('; ')})` : message;
46+
};
47+
48+
const getUsefulErrors = errors =>
49+
errors.filter(error => {
50+
const samePointerErrors = errors.filter(
51+
other => other.instancePath === error.instancePath
52+
);
53+
54+
if (
55+
['anyOf', 'oneOf', 'allOf'].includes(error.keyword) &&
56+
samePointerErrors.length > 1
57+
) {
58+
return false;
59+
}
60+
61+
if (
62+
error.keyword === 'type' &&
63+
samePointerErrors.some(other => other.keyword === 'enum')
64+
) {
65+
return false;
66+
}
67+
68+
return true;
69+
});
70+
71+
const findSchemaFiles = async () => {
72+
const adaptors = await fs.readdir(packagesDir, { withFileTypes: true });
73+
const schemaFiles = [];
74+
75+
for (const adaptor of adaptors) {
76+
if (!adaptor.isDirectory()) continue;
77+
78+
const adaptorDir = path.join(packagesDir, adaptor.name);
79+
const files = await fs.readdir(adaptorDir, { withFileTypes: true });
80+
for (const file of files) {
81+
if (file.isFile() && file.name.endsWith('configuration-schema.json')) {
82+
schemaFiles.push(path.join(adaptorDir, file.name));
83+
}
84+
}
85+
}
86+
87+
return schemaFiles.sort((a, b) =>
88+
relativePath(a).localeCompare(relativePath(b))
89+
);
90+
};
91+
92+
const validateSchemaFile = async (file, ajv) => {
93+
const adaptor = path.basename(path.dirname(file));
94+
const filePath = relativePath(file);
95+
let schema;
96+
97+
try {
98+
schema = JSON.parse(await fs.readFile(file, 'utf8'));
99+
} catch (error) {
100+
return [
101+
{
102+
adaptor,
103+
filePath,
104+
pointer: '#',
105+
reason: `Invalid JSON: ${error.message}`,
106+
},
107+
];
108+
}
109+
110+
try {
111+
if (ajv.validateSchema(schema)) return [];
112+
} catch (error) {
113+
return [
114+
{
115+
adaptor,
116+
filePath,
117+
pointer: '#',
118+
reason: error.message,
119+
},
120+
];
121+
}
122+
123+
return getUsefulErrors(ajv.errors ?? []).map(error => ({
124+
adaptor,
125+
filePath,
126+
pointer: pointerFor(error),
127+
reason: formatReason(error),
128+
}));
129+
};
130+
131+
const printMarkdown = (failures, checked) => {
132+
console.log('| Adaptor | File | JSON pointer | Failure reason |');
133+
console.log('| --- | --- | --- | --- |');
134+
135+
if (!failures.length) {
136+
console.log(
137+
`| - | - | - | No schema validation failures (${checked} files checked). |`
138+
);
139+
return;
140+
}
141+
142+
for (const failure of failures) {
143+
console.log(
144+
`| ${escapeMarkdown(failure.adaptor)} | ${escapeMarkdown(
145+
failure.filePath
146+
)} | \`${escapeMarkdown(failure.pointer)}\` | ${escapeMarkdown(
147+
failure.reason
148+
)} |`
149+
);
150+
}
151+
};
152+
153+
const printText = (failures, checked) => {
154+
if (!failures.length) {
155+
console.log(`Validated ${checked} configuration schema files.`);
156+
return;
157+
}
158+
159+
console.error('Configuration schema validation failed:');
160+
console.error();
161+
162+
for (const failure of failures) {
163+
console.error(`- ${failure.filePath}`);
164+
console.error(` adaptor: ${failure.adaptor}`);
165+
console.error(` pointer: ${failure.pointer}`);
166+
console.error(` reason: ${failure.reason}`);
167+
}
168+
};
169+
170+
const main = async () => {
171+
if (help) {
172+
console.log(`Usage: pnpm validate:schemas [-- --markdown]
173+
174+
Validates every packages/*/*configuration-schema.json file against the JSON
175+
Schema draft-07 meta-schema. Use --markdown to print the one-shot audit table.`);
176+
return;
177+
}
178+
179+
const ajv = new Ajv({
180+
allErrors: true,
181+
strict: false,
182+
validateSchema: true,
183+
});
184+
ajv.addMetaSchema(
185+
draft07MetaSchema,
186+
'https://json-schema.org/draft-07/schema#'
187+
);
188+
189+
const schemaFiles = await findSchemaFiles();
190+
191+
if (!schemaFiles.length) {
192+
console.error('No configuration schema files found.');
193+
process.exitCode = 1;
194+
return;
195+
}
196+
197+
const failures = (
198+
await Promise.all(schemaFiles.map(file => validateSchemaFile(file, ajv)))
199+
).flat();
200+
201+
if (markdown) {
202+
printMarkdown(failures, schemaFiles.length);
203+
} else {
204+
printText(failures, schemaFiles.length);
205+
}
206+
207+
if (failures.length) {
208+
process.exitCode = 1;
209+
}
210+
};
211+
212+
main().catch(error => {
213+
console.error(error);
214+
process.exitCode = 1;
215+
});

tools/generate-fhir/template/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ View the
1616
[configuration-schema](https://docs.openfn.org/adaptors/packages/{{NAME}}-configuration-schema/)
1717
for required and optional `configuration` properties.
1818

19+
The configuration schema uses
20+
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes).
21+
Run `pnpm validate:schemas` from the adaptors repo root after editing it.
22+
1923
## Development
2024

2125
Clone the [adaptors monorepo](https://github.com/OpenFn/adaptors). Follow the

tools/generate/template/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ View the
1515
[configuration-schema](https://docs.openfn.org/adaptors/packages/{{TEMPLATE}}-configuration-schema/)
1616
for required and optional `configuration` properties.
1717

18+
The configuration schema uses
19+
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes).
20+
Run `pnpm validate:schemas` from the adaptors repo root after editing it.
21+
1822
## Development
1923

2024
Clone the [adaptors monorepo](https://github.com/OpenFn/adaptors). Follow the

wiki/build-a-new-adaptor.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ pnpm generate <adaptor-name>
3131
adhere to the size specifications mentioned in the requirements section.
3232
- Ensure the images have a transparent background. Navigate to
3333
configuration-schema.json, and change any configs that do not align with the
34-
adaptor
34+
adaptor. This file must be valid
35+
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes);
36+
run `pnpm validate:schemas` from the repo root after editing it.
3537
- Go to `/src/Adaptor.js` and create the adaptor’s Operations - the functions
3638
used in job code. You may want to set up `POST, GET,` to fit the current
3739
adaptor’s requirements

0 commit comments

Comments
 (0)