Skip to content

Commit 36826e3

Browse files
committed
feat(validator-speclynx): simplify plugins signature
Along with that, provide more of convinient deps via toolbox.
1 parent 530e583 commit 36826e3

File tree

6 files changed

+178
-141
lines changed

6 files changed

+178
-141
lines changed

packages/jentic-openapi-validator-speclynx/README.md

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -196,38 +196,45 @@ if "dict" in validator.accepts():
196196

197197
## Custom Plugins
198198

199-
Create custom validation plugins as ES modules (`.mjs` files). Plugins use the ApiDOM visitor pattern:
199+
Create custom validation plugins as ES modules (`.mjs` files). Plugins use the ApiDOM visitor pattern and receive
200+
a toolbox with dependencies and diagnostics array:
200201

201202
```javascript
202203
// custom-plugin.mjs
203-
import {toValue} from '@speclynx/apidom-core';
204-
import {DiagnosticSeverity} from 'vscode-languageserver-types';
205-
206-
export default ({diagnostics}) => () => ({
207-
pre() {
208-
},
209-
visitor: {
210-
InfoElement(path) {
211-
const info = path.node;
212-
const version = info.get('version');
213-
214-
if (version && typeof toValue(version) !== 'string') {
215-
diagnostics.push({
216-
severity: DiagnosticSeverity.Error,
217-
message: 'info.version must be a string',
218-
code: 'invalid-info-version-type',
219-
range: {
220-
start: {line: 0, character: 0},
221-
end: {line: 0, character: 0}
222-
},
223-
data: {path: ['info', 'version']}
224-
});
204+
205+
export default (toolbox) => {
206+
const {diagnostics, deps} = toolbox;
207+
const {DiagnosticSeverity} = deps['vscode-languageserver-types'];
208+
const {toValue} = deps['@speclynx/apidom-core'];
209+
210+
return {
211+
pre() {
212+
// Called before traversal starts
213+
},
214+
visitor: {
215+
InfoElement(path) {
216+
const info = path.node;
217+
const version = info.get('version');
218+
219+
if (version && typeof toValue(version) !== 'string') {
220+
diagnostics.push({
221+
severity: DiagnosticSeverity.Error,
222+
message: 'info.version must be a string',
223+
code: 'invalid-info-version-type',
224+
range: {
225+
start: {line: 0, character: 0},
226+
end: {line: 0, character: 0}
227+
},
228+
data: {path: path.getPathKeys()}
229+
});
230+
}
225231
}
226-
}
227-
},
228-
post() {
229-
},
230-
});
232+
},
233+
post() {
234+
// Called after traversal completes
235+
},
236+
};
237+
};
231238
```
232239

233240
Use it with the validator:

packages/jentic-openapi-validator-speclynx/src/jentic/apitools/openapi/validator/backends/speclynx/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def __init__(
5959
Plugins are loaded automatically and used to validate the OpenAPI document.
6060
When specified, custom plugins are merged with the built-in plugins (both are loaded).
6161
If None (default), only the built-in plugins directory is used (which is empty by default).
62+
Plugins must export a function that receives a toolbox object with:
63+
- deps: External dependencies (vscode-languageserver-types, @speclynx/apidom-reference)
64+
- diagnostics: Array to collect validation diagnostics
6265
See resources/plugins/example-plugin.mjs.sample for plugin format.
6366
"""
6467
self.speclynx_path = speclynx_path

packages/jentic-openapi-validator-speclynx/src/jentic/apitools/openapi/validator/backends/speclynx/resources/plugins/example-plugin.mjs.sample

Lines changed: 80 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,101 @@
55
* using the ApiDOM visitor pattern.
66
*
77
* Plugin structure:
8-
* - Must export a default function that accepts a context object with:
8+
* - Must export a default function that accepts a toolbox object with:
99
* - diagnostics: Array to collect validation diagnostics
10+
* - deps: External dependencies (vscode-languageserver-types, @speclynx/apidom-reference)
1011
* - Returns an object with pre/post hooks and a visitor property
1112
* - The visitor contains methods named after ApiDOM element types
12-
* - Each visitor method receives a path object with information about the current element
13+
* - Each visitor method receives the element being visited
1314
*
14-
* @param {Object} context - Plugin context
15-
* @param {Array} context.diagnostics - Array to collect validation diagnostics
15+
* @param {Object} toolbox - Plugin toolbox
16+
* @param {Array} toolbox.diagnostics - Array to collect validation diagnostics
17+
* @param {Object} toolbox.deps - External dependencies
1618
*/
1719

18-
import { toValue } from '@speclynx/apidom-core';
19-
import { DiagnosticSeverity } from 'vscode-languageserver-types';
20+
export default (toolbox) => {
21+
const {diagnostics, deps} = toolbox;
22+
const {DiagnosticSeverity} = deps['vscode-languageserver-types'];
23+
const {toValue} = deps['@speclynx/apidom-core'];
2024

21-
export default ({diagnostics}) => () => ({
22-
pre() {
23-
console.log('Pre-validation hook');
24-
},
25-
post() {
26-
console.log('Post-validation hook');
27-
},
28-
visitor: {
29-
/**
30-
* Visit all InfoElement nodes in the OpenAPI document
31-
* @param {Object} path - Contains information about the current element
32-
*/
33-
InfoElement(path) {
34-
const info = path.node;
25+
return {
26+
pre() {
27+
console.log('Pre-validation hook');
28+
},
29+
post() {
30+
console.log('Post-validation hook');
31+
},
32+
visitor: {
33+
/**
34+
* Visit all InfoElement nodes in the OpenAPI document
35+
* @param {Object} path - Contains information about the current element
36+
*/
37+
InfoElement(path) {
38+
const info = path.node;
3539

36-
// Example validation: Check if info.title exists
37-
if (!info.get('title')) {
38-
diagnostics.push({
39-
severity: DiagnosticSeverity.Error,
40-
message: 'OpenAPI document is missing info.title',
41-
code: 'missing-info-title',
42-
range: getRange(info),
43-
data: {path: path.getPathKeys()}
44-
});
45-
}
40+
// Example validation: Check if info.title exists
41+
if (!info.get('title')) {
42+
diagnostics.push({
43+
severity: DiagnosticSeverity.Error,
44+
message: 'OpenAPI document is missing info.title',
45+
code: 'missing-info-title',
46+
range: getRange(info),
47+
data: {path: path.getPathKeys()}
48+
});
49+
}
4650

47-
// Example validation: Check if info.version exists
48-
if (!info.get('version')) {
49-
diagnostics.push({
50-
severity: DiagnosticSeverity.Warning,
51-
message: 'OpenAPI document is missing info.version',
52-
code: 'missing-info-version',
53-
range: getRange(info),
54-
data: {path: path.getPathKeys()}
55-
});
56-
}
57-
},
51+
// Example validation: Check if info.version exists
52+
if (!info.get('version')) {
53+
diagnostics.push({
54+
severity: DiagnosticSeverity.Warning,
55+
message: 'OpenAPI document is missing info.version',
56+
code: 'missing-info-version',
57+
range: getRange(info),
58+
data: {path: path.getPathKeys()}
59+
});
60+
}
61+
},
5862

59-
/**
60-
* Visit all OperationElement nodes (HTTP operations like GET, POST, etc.)
61-
* @param {Object} path - Contains information about the current element
62-
*/
63-
OperationElement(path) {
64-
const operation = path.node;
63+
/**
64+
* Visit all OperationElement nodes (HTTP operations like GET, POST, etc.)
65+
* @param {Object} path - Contains information about the current element
66+
*/
67+
OperationElement(path) {
68+
const operation = path.node;
6569

66-
// Example validation: Check if operation has a summary
67-
if (!operation.get('summary')) {
68-
diagnostics.push({
69-
severity: DiagnosticSeverity.Warning,
70-
message: 'Operation is missing a summary',
71-
code: 'missing-operation-summary',
72-
range: getRange(operation),
73-
data: {path: path.getPathKeys()}
74-
});
75-
}
76-
},
70+
// Example validation: Check if operation has a summary
71+
if (!operation.get('summary')) {
72+
diagnostics.push({
73+
severity: DiagnosticSeverity.Warning,
74+
message: 'Operation is missing a summary',
75+
code: 'missing-operation-summary',
76+
range: getRange(operation),
77+
data: {path: path.getPathKeys()}
78+
});
79+
}
80+
},
7781

78-
/**
79-
* Visit all SchemaElement nodes (JSON Schema definitions)
80-
* @param {Object} path - Contains information about the current element
81-
*/
82-
SchemaElement(path) {
83-
const schema = path.node;
82+
/**
83+
* Visit all SchemaElement nodes (JSON Schema definitions)
84+
* @param {Object} path - Contains information about the current element
85+
*/
86+
SchemaElement(path) {
87+
const schema = path.node;
8488

85-
// Example validation: Check for schemas without descriptions
86-
if (!schema.get('description') && schema.get('type')) {
87-
diagnostics.push({
88-
severity: DiagnosticSeverity.Information,
89-
message: `Schema of type "${toValue(schema.get('type'))}" has no description`,
90-
code: 'schema-missing-description',
91-
range: getRange(schema),
92-
data: {path: path.getPathKeys()}
93-
});
89+
// Example validation: Check for schemas without descriptions
90+
if (!schema.get('description') && schema.get('type')) {
91+
diagnostics.push({
92+
severity: DiagnosticSeverity.Information,
93+
message: `Schema of type "${toValue(schema.get('type'))}" has no description`,
94+
code: 'schema-missing-description',
95+
range: getRange(schema),
96+
data: {path: path.getPathKeys()}
97+
});
98+
}
9499
}
95100
}
96-
}
97-
});
101+
};
102+
};
98103

99104
/**
100105
* Extract LSP range from ApiDOM element

packages/jentic-openapi-validator-speclynx/src/jentic/apitools/openapi/validator/backends/speclynx/resources/plugins/openapi-document.mjs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,32 @@
77
* Note: This visitor is only called when traversing parseResult (invalid documents).
88
* When traversing parseResult.api (valid documents), ParseResultElement is not visited.
99
*
10-
* @param {Object} context - Plugin context
11-
* @param {Array} context.diagnostics - Array to collect validation diagnostics
10+
* Plugin receives toolbox with:
11+
* - deps: External dependencies (vscode-languageserver-types, @speclynx/apidom-reference)
12+
* - diagnostics: Array to collect validation diagnostics
1213
*/
1314

14-
import {DiagnosticSeverity, Diagnostic, Range} from 'vscode-languageserver-types';
15+
export default (toolbox) => {
16+
const {diagnostics, deps} = toolbox;
17+
const {DiagnosticSeverity, Diagnostic, Range} = deps['vscode-languageserver-types'];
1518

16-
export default ({diagnostics}) => () => ({
17-
visitor: {
18-
ParseResultElement(path) {
19-
const parseResult = path.node;
19+
return {
20+
visitor: {
21+
ParseResultElement(path) {
22+
const parseResult = path.node;
2023

21-
if (!parseResult.api) {
22-
const diagnostic = Diagnostic.create(
23-
Range.create(0, 0, 0, 0),
24-
'Document is not recognized as a valid OpenAPI 3.x document',
25-
DiagnosticSeverity.Error,
26-
'invalid-openapi-document',
27-
'speclynx-validator'
28-
);
29-
diagnostic.data = {path: path.getPathKeys()};
30-
diagnostics.push(diagnostic);
24+
if (!parseResult.api) {
25+
const diagnostic = Diagnostic.create(
26+
Range.create(0, 0, 0, 0),
27+
'Document is not recognized as a valid OpenAPI 3.x document',
28+
DiagnosticSeverity.Error,
29+
'invalid-openapi-document',
30+
'speclynx-validator'
31+
);
32+
diagnostic.data = {path: path.getPathKeys()};
33+
diagnostics.push(diagnostic);
34+
}
3135
}
3236
}
33-
}
34-
});
37+
};
38+
};

packages/jentic-openapi-validator-speclynx/src/jentic/apitools/openapi/validator/backends/speclynx/resources/speclynx.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {pathToFileURL, fileURLToPath} from 'node:url';
66
import {Command} from 'commander';
77
import * as vscodeLanguageServerTypes from 'vscode-languageserver-types';
88
import {Diagnostic, DiagnosticSeverity, Range} from 'vscode-languageserver-types';
9+
import * as apidomCore from '@speclynx/apidom-core';
910
import {dispatchRefractorPlugins, createToolbox as createToolboxBase} from '@speclynx/apidom-core';
11+
import * as apidomDatamodel from '@speclynx/apidom-datamodel';
12+
import * as apidomJsonPath from '@speclynx/apidom-json-path';
13+
import * as apidomJsonPointer from '@speclynx/apidom-json-pointer';
14+
import * as apidomTraverse from '@speclynx/apidom-traverse';
1015
import * as apidomReference from '@speclynx/apidom-reference';
1116
import {parse, options} from '@speclynx/apidom-reference';
1217
import FileResolver from '@speclynx/apidom-reference/resolve/resolvers/file';
@@ -135,12 +140,18 @@ async function validate(document, cliOptions) {
135140
return {valid: false, diagnostics};
136141
}
137142

138-
// Toolbox creation
143+
// Toolbox creation - provides deps and diagnostics to plugins
139144
const createToolbox = () => ({
140145
deps: {
141146
'vscode-languageserver-types': vscodeLanguageServerTypes,
147+
'@speclynx/apidom-core': apidomCore,
148+
'@speclynx/apidom-datamodel': apidomDatamodel,
149+
'@speclynx/apidom-json-path': apidomJsonPath,
150+
'@speclynx/apidom-json-pointer': apidomJsonPointer,
151+
'@speclynx/apidom-traverse': apidomTraverse,
142152
'@speclynx/apidom-reference': apidomReference,
143153
},
154+
diagnostics,
144155
...createToolboxBase()
145156
});
146157

@@ -149,7 +160,7 @@ async function validate(document, cliOptions) {
149160
// When it doesn't exist (e.g., Swagger 2.0), traverse parseResult so ParseResultElement visitor runs
150161
const elementToTraverse = parseResult.api ?? parseResult;
151162
const dispatchRefractorPluginsAsync = promisify(dispatchRefractorPlugins);
152-
await dispatchRefractorPluginsAsync(elementToTraverse, plugins.map(plugin => plugin({diagnostics})), {
163+
await dispatchRefractorPluginsAsync(elementToTraverse, plugins, {
153164
toolboxCreator: createToolbox,
154165
});
155166

0 commit comments

Comments
 (0)