@opsydyn/elysia-spectral 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +42 -3
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.mjs +4 -1
- package/dist/{index-CyJXdIRT.d.mts → index-DzJWrqPA.d.mts} +7 -5
- package/dist/index.d.mts +8 -3
- package/dist/index.mjs +19 -4
- package/dist/lint-openapi-D76sC7S5.mjs +122 -0
- package/dist/load-ruleset-CiikrzWx.mjs +301 -0
- package/dist/presets-CCfU_diN.mjs +132 -0
- package/dist/recommended-DgrTqq-3.mjs +40 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/ruleset-load-error-CogUOC7W.mjs +10 -0
- package/dist/{core-BLJeXQ15.mjs → runtime-4LlfDIZv.mjs} +13 -570
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { t as RulesetLoadError } from "./ruleset-load-error-CogUOC7W.mjs";
|
|
3
|
+
import { t as recommended } from "./recommended-DgrTqq-3.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { access, readFile } from "node:fs/promises";
|
|
6
|
+
import YAML from "yaml";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/rulesets/default-ruleset.ts
|
|
9
|
+
const defaultRuleset = recommended;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/core/stoplight-runtime.ts
|
|
12
|
+
let runtimeBindingsPromise = null;
|
|
13
|
+
const loadStoplightRuntimeBindings = async () => {
|
|
14
|
+
const [spectralCore, { default: spectralFunctions }, { default: spectralRulesets }] = await Promise.all([
|
|
15
|
+
import("@stoplight/spectral-core"),
|
|
16
|
+
import("@stoplight/spectral-functions"),
|
|
17
|
+
import("@stoplight/spectral-rulesets")
|
|
18
|
+
]);
|
|
19
|
+
const { alphabetical, casing, defined, enumeration, falsy, length, or, pattern, schema, truthy, undefined: undefinedFunction, unreferencedReusableObject, xor } = spectralFunctions;
|
|
20
|
+
const { oas } = spectralRulesets;
|
|
21
|
+
return {
|
|
22
|
+
Spectral: spectralCore.Spectral,
|
|
23
|
+
extendsMap: { "spectral:oas": oas },
|
|
24
|
+
functionMap: {
|
|
25
|
+
alphabetical,
|
|
26
|
+
casing,
|
|
27
|
+
defined,
|
|
28
|
+
enumeration,
|
|
29
|
+
falsy,
|
|
30
|
+
length,
|
|
31
|
+
or,
|
|
32
|
+
pattern,
|
|
33
|
+
schema,
|
|
34
|
+
truthy,
|
|
35
|
+
undefined: undefinedFunction,
|
|
36
|
+
unreferencedReusableObject,
|
|
37
|
+
xor
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const getStoplightRuntimeBindings = async () => {
|
|
42
|
+
if (runtimeBindingsPromise === null) runtimeBindingsPromise = loadStoplightRuntimeBindings();
|
|
43
|
+
return await runtimeBindingsPromise;
|
|
44
|
+
};
|
|
45
|
+
const getBuiltInFunctionMap = async () => (await getStoplightRuntimeBindings()).functionMap;
|
|
46
|
+
const getExtendsMap = async () => (await getStoplightRuntimeBindings()).extendsMap;
|
|
47
|
+
const getSpectralConstructor = async () => (await getStoplightRuntimeBindings()).Spectral;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/core/load-ruleset.ts
|
|
50
|
+
var load_ruleset_exports = /* @__PURE__ */ __exportAll({
|
|
51
|
+
defaultRulesetResolvers: () => defaultRulesetResolvers,
|
|
52
|
+
loadResolvedRuleset: () => loadResolvedRuleset,
|
|
53
|
+
loadRuleset: () => loadRuleset
|
|
54
|
+
});
|
|
55
|
+
const autodiscoverRulesetFilenames = [
|
|
56
|
+
"spectral.yaml",
|
|
57
|
+
"spectral.yml",
|
|
58
|
+
"spectral.ts",
|
|
59
|
+
"spectral.mts",
|
|
60
|
+
"spectral.cts",
|
|
61
|
+
"spectral.js",
|
|
62
|
+
"spectral.mjs",
|
|
63
|
+
"spectral.cjs",
|
|
64
|
+
"spectral.config.yaml",
|
|
65
|
+
"spectral.config.yml",
|
|
66
|
+
"spectral.config.ts",
|
|
67
|
+
"spectral.config.mts",
|
|
68
|
+
"spectral.config.cts",
|
|
69
|
+
"spectral.config.js",
|
|
70
|
+
"spectral.config.mjs",
|
|
71
|
+
"spectral.config.cjs"
|
|
72
|
+
];
|
|
73
|
+
const loadRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
74
|
+
return (await loadResolvedRuleset(input, baseDirOrOptions)).ruleset;
|
|
75
|
+
};
|
|
76
|
+
const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
77
|
+
const options = normalizeLoadResolvedRulesetOptions(baseDirOrOptions);
|
|
78
|
+
const context = {
|
|
79
|
+
baseDir: options.baseDir,
|
|
80
|
+
defaultRuleset: options.defaultRuleset,
|
|
81
|
+
mergeAutodiscoveredWithDefault: options.mergeAutodiscoveredWithDefault
|
|
82
|
+
};
|
|
83
|
+
for (const resolver of options.resolvers) {
|
|
84
|
+
const loaded = await resolver(input, context);
|
|
85
|
+
if (loaded) {
|
|
86
|
+
const normalized = { ruleset: await normalizeRulesetDefinition(loaded.ruleset) };
|
|
87
|
+
if (loaded.source) normalized.source = loaded.source;
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (input === void 0) return { ruleset: await normalizeRulesetDefinition(options.defaultRuleset) };
|
|
92
|
+
throw new RulesetLoadError("Ruleset input could not be resolved.");
|
|
93
|
+
};
|
|
94
|
+
const normalizeLoadResolvedRulesetOptions = (value) => {
|
|
95
|
+
if (typeof value === "string") return {
|
|
96
|
+
baseDir: value,
|
|
97
|
+
resolvers: defaultRulesetResolvers,
|
|
98
|
+
mergeAutodiscoveredWithDefault: true,
|
|
99
|
+
defaultRuleset
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
baseDir: value.baseDir ?? process.cwd(),
|
|
103
|
+
resolvers: value.resolvers ?? defaultRulesetResolvers,
|
|
104
|
+
mergeAutodiscoveredWithDefault: value.mergeAutodiscoveredWithDefault ?? true,
|
|
105
|
+
defaultRuleset: value.defaultRuleset ?? defaultRuleset
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const resolveAutodiscoveredRuleset = async (input, context) => {
|
|
109
|
+
if (input !== void 0) return;
|
|
110
|
+
const autodiscoveredPath = await findAutodiscoveredRulesetPath(context.baseDir);
|
|
111
|
+
if (!autodiscoveredPath) return;
|
|
112
|
+
const loaded = await loadResolvedPathRuleset(autodiscoveredPath, context);
|
|
113
|
+
if (!context.mergeAutodiscoveredWithDefault) return {
|
|
114
|
+
...loaded,
|
|
115
|
+
source: {
|
|
116
|
+
path: autodiscoveredPath,
|
|
117
|
+
autodiscovered: true,
|
|
118
|
+
mergedWithDefault: false
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
ruleset: mergeRulesets(context.defaultRuleset, loaded.ruleset),
|
|
123
|
+
source: {
|
|
124
|
+
path: autodiscoveredPath,
|
|
125
|
+
autodiscovered: true,
|
|
126
|
+
mergedWithDefault: true
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const resolvePathRuleset = async (input, context) => {
|
|
131
|
+
if (typeof input !== "string") return;
|
|
132
|
+
return await loadResolvedPathRuleset(input, context);
|
|
133
|
+
};
|
|
134
|
+
const resolveInlineRuleset = async (input) => {
|
|
135
|
+
if (input === void 0 || typeof input === "string") return;
|
|
136
|
+
return { ruleset: input };
|
|
137
|
+
};
|
|
138
|
+
const defaultRulesetResolvers = [
|
|
139
|
+
resolveAutodiscoveredRuleset,
|
|
140
|
+
resolvePathRuleset,
|
|
141
|
+
resolveInlineRuleset
|
|
142
|
+
];
|
|
143
|
+
const loadResolvedPathRuleset = async (inputPath, context) => {
|
|
144
|
+
const resolvedPath = path.resolve(context.baseDir, inputPath);
|
|
145
|
+
if (isYamlRulesetPath(resolvedPath)) return {
|
|
146
|
+
ruleset: await loadYamlRuleset(resolvedPath),
|
|
147
|
+
source: {
|
|
148
|
+
path: inputPath,
|
|
149
|
+
autodiscovered: false,
|
|
150
|
+
mergedWithDefault: false
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
if (!isModuleRulesetPath(resolvedPath)) throw new RulesetLoadError(`Unsupported ruleset path: ${inputPath}. Supported local rulesets are .yaml, .yml, .js, .mjs, .cjs, .ts, .mts, and .cts.`);
|
|
154
|
+
return {
|
|
155
|
+
ruleset: await loadModuleRuleset(resolvedPath),
|
|
156
|
+
source: {
|
|
157
|
+
path: inputPath,
|
|
158
|
+
autodiscovered: false,
|
|
159
|
+
mergedWithDefault: false
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const findAutodiscoveredRulesetPath = async (baseDir) => {
|
|
164
|
+
for (const filename of autodiscoverRulesetFilenames) {
|
|
165
|
+
const candidatePath = path.resolve(baseDir, filename);
|
|
166
|
+
try {
|
|
167
|
+
await access(candidatePath);
|
|
168
|
+
return `./${filename}`;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error.code !== "ENOENT") throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const loadYamlRuleset = async (resolvedPath) => {
|
|
175
|
+
let fileContents;
|
|
176
|
+
try {
|
|
177
|
+
fileContents = await readFile(resolvedPath, "utf8");
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new RulesetLoadError(`Unable to read ruleset at ${resolvedPath}.`, { cause: error });
|
|
180
|
+
}
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = YAML.parse(fileContents);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new RulesetLoadError(`Unable to parse YAML ruleset at ${resolvedPath}.`, { cause: error });
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
};
|
|
189
|
+
const loadModuleRuleset = async (resolvedPath) => {
|
|
190
|
+
let imported;
|
|
191
|
+
try {
|
|
192
|
+
imported = await import(pathToFileURL(resolvedPath).href);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new RulesetLoadError(`Unable to import module ruleset at ${resolvedPath}.`, { cause: error });
|
|
195
|
+
}
|
|
196
|
+
const resolvedRuleset = resolveModuleRulesetValue(imported);
|
|
197
|
+
if (resolvedRuleset === void 0) throw new RulesetLoadError(`Module ruleset at ${resolvedPath} must export a ruleset as the default export or a named "ruleset" export.`);
|
|
198
|
+
return await normalizeRulesetDefinition(resolvedRuleset, {
|
|
199
|
+
...await getBuiltInFunctionMap(),
|
|
200
|
+
...resolveModuleFunctions(imported)
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const resolveModuleRulesetValue = (imported) => {
|
|
204
|
+
if (!isRecord(imported)) return;
|
|
205
|
+
if ("default" in imported) return imported.default;
|
|
206
|
+
if ("ruleset" in imported) return imported.ruleset;
|
|
207
|
+
};
|
|
208
|
+
const resolveModuleFunctions = (imported) => {
|
|
209
|
+
if (!isRecord(imported) || !("functions" in imported)) return {};
|
|
210
|
+
const { functions } = imported;
|
|
211
|
+
if (!isRecord(functions)) throw new RulesetLoadError("Module ruleset \"functions\" export must be an object map of function names to Spectral functions.");
|
|
212
|
+
const entries = Object.entries(functions).filter(([, value]) => typeof value === "function");
|
|
213
|
+
return Object.fromEntries(entries);
|
|
214
|
+
};
|
|
215
|
+
const isYamlRulesetPath = (value) => value.endsWith(".yaml") || value.endsWith(".yml");
|
|
216
|
+
const isModuleRulesetPath = (value) => value.endsWith(".js") || value.endsWith(".mjs") || value.endsWith(".cjs") || value.endsWith(".ts") || value.endsWith(".mts") || value.endsWith(".cts");
|
|
217
|
+
const normalizeRulesetDefinition = async (input, availableFunctions) => {
|
|
218
|
+
if (!isRecord(input)) throw new RulesetLoadError("Ruleset must be an object.");
|
|
219
|
+
const resolvedAvailableFunctions = availableFunctions ?? await getBuiltInFunctionMap();
|
|
220
|
+
const normalized = { ...input };
|
|
221
|
+
if ("extends" in normalized) normalized.extends = await normalizeExtends(normalized.extends);
|
|
222
|
+
if ("rules" in normalized) normalized.rules = await normalizeRules(normalized.rules, resolvedAvailableFunctions);
|
|
223
|
+
return normalized;
|
|
224
|
+
};
|
|
225
|
+
const mergeRuleEntry = (base, override) => {
|
|
226
|
+
if (!isRecord(override)) return override;
|
|
227
|
+
if ("given" in override || "then" in override) return override;
|
|
228
|
+
if (isRecord(base) && ("given" in base || "then" in base)) return {
|
|
229
|
+
...base,
|
|
230
|
+
...override
|
|
231
|
+
};
|
|
232
|
+
const keys = Object.keys(override);
|
|
233
|
+
if (keys.length === 1 && keys[0] === "severity") return override.severity;
|
|
234
|
+
return override;
|
|
235
|
+
};
|
|
236
|
+
const mergeRulesets = (baseRuleset, overrideRuleset) => {
|
|
237
|
+
const mergedBase = baseRuleset;
|
|
238
|
+
const mergedOverride = overrideRuleset;
|
|
239
|
+
const baseRules = isRecord(mergedBase.rules) ? mergedBase.rules : {};
|
|
240
|
+
const overrideRules = isRecord(mergedOverride.rules) ? mergedOverride.rules : {};
|
|
241
|
+
const mergedRules = { ...baseRules };
|
|
242
|
+
for (const [name, overrideRule] of Object.entries(overrideRules)) mergedRules[name] = mergeRuleEntry(baseRules[name], overrideRule);
|
|
243
|
+
const baseExtends = toExtendsArray(mergedBase.extends);
|
|
244
|
+
const overrideExtends = toExtendsArray(mergedOverride.extends);
|
|
245
|
+
const mergedExtends = [...baseExtends, ...overrideExtends];
|
|
246
|
+
const merged = {
|
|
247
|
+
...mergedBase,
|
|
248
|
+
...mergedOverride
|
|
249
|
+
};
|
|
250
|
+
delete merged.extends;
|
|
251
|
+
delete merged.rules;
|
|
252
|
+
if (mergedExtends.length > 0) merged.extends = mergedExtends;
|
|
253
|
+
if (Object.keys(mergedRules).length > 0) merged.rules = mergedRules;
|
|
254
|
+
return merged;
|
|
255
|
+
};
|
|
256
|
+
const toExtendsArray = (value) => {
|
|
257
|
+
if (value === void 0) return [];
|
|
258
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
259
|
+
};
|
|
260
|
+
const normalizeExtends = async (value) => {
|
|
261
|
+
if (typeof value === "string") return await resolveExtendsEntry(value);
|
|
262
|
+
if (!Array.isArray(value)) return value;
|
|
263
|
+
return await Promise.all(value.map(async (entry) => {
|
|
264
|
+
if (typeof entry === "string") return await resolveExtendsEntry(entry);
|
|
265
|
+
if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") return [await resolveExtendsEntry(entry[0]), entry[1]];
|
|
266
|
+
return entry;
|
|
267
|
+
}));
|
|
268
|
+
};
|
|
269
|
+
const resolveExtendsEntry = async (value) => {
|
|
270
|
+
const resolved = (await getExtendsMap())[value];
|
|
271
|
+
if (!resolved) throw new RulesetLoadError(`Unsupported ruleset extend target: "${value}". Supported extend targets: spectral:oas.`);
|
|
272
|
+
return resolved;
|
|
273
|
+
};
|
|
274
|
+
const normalizeRules = async (value, availableFunctions) => {
|
|
275
|
+
if (!isRecord(value)) return value;
|
|
276
|
+
const entries = await Promise.all(Object.entries(value).map(async ([ruleName, ruleValue]) => [ruleName, await normalizeRule(ruleValue, availableFunctions)]));
|
|
277
|
+
return Object.fromEntries(entries);
|
|
278
|
+
};
|
|
279
|
+
const normalizeRule = async (value, availableFunctions) => {
|
|
280
|
+
if (!isRecord(value)) return value;
|
|
281
|
+
const normalized = { ...value };
|
|
282
|
+
if ("then" in normalized) normalized.then = await normalizeThen(normalized.then, availableFunctions);
|
|
283
|
+
return normalized;
|
|
284
|
+
};
|
|
285
|
+
const normalizeThen = async (value, availableFunctions) => {
|
|
286
|
+
if (Array.isArray(value)) return await Promise.all(value.map((entry) => normalizeThenEntry(entry, availableFunctions)));
|
|
287
|
+
return await normalizeThenEntry(value, availableFunctions);
|
|
288
|
+
};
|
|
289
|
+
const normalizeThenEntry = async (value, availableFunctions) => {
|
|
290
|
+
if (!isRecord(value)) return value;
|
|
291
|
+
const normalized = { ...value };
|
|
292
|
+
if (typeof normalized.function === "string") {
|
|
293
|
+
const resolved = availableFunctions[normalized.function];
|
|
294
|
+
if (!resolved) throw new RulesetLoadError(`Unsupported Spectral function: ${String(normalized.function)}.`);
|
|
295
|
+
normalized.function = resolved;
|
|
296
|
+
}
|
|
297
|
+
return normalized;
|
|
298
|
+
};
|
|
299
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
300
|
+
//#endregion
|
|
301
|
+
export { getSpectralConstructor as a, load_ruleset_exports as i, loadResolvedRuleset as n, loadRuleset as r, defaultRulesetResolvers as t };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { t as recommended } from "./recommended-DgrTqq-3.mjs";
|
|
3
|
+
//#region src/presets/server.ts
|
|
4
|
+
const operationSelector$1 = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
5
|
+
/**
|
|
6
|
+
* Production API quality preset. Suitable as a CI gate for teams shipping
|
|
7
|
+
* public or internal APIs where contract quality matters.
|
|
8
|
+
*
|
|
9
|
+
* Tightens recommended:
|
|
10
|
+
* - elysia-operation-summary and elysia-operation-tags escalated to error
|
|
11
|
+
* - operation-description, operation-operationId, operation-success-response at warn
|
|
12
|
+
* - oas3-api-servers at warn (servers should be declared in production specs)
|
|
13
|
+
*/
|
|
14
|
+
const server = {
|
|
15
|
+
extends: [["spectral:oas", "recommended"]],
|
|
16
|
+
rules: {
|
|
17
|
+
"oas3-api-servers": "warn",
|
|
18
|
+
"info-contact": "off",
|
|
19
|
+
"elysia-operation-summary": {
|
|
20
|
+
description: "Operations should define a summary for generated docs and clients.",
|
|
21
|
+
severity: "error",
|
|
22
|
+
given: operationSelector$1,
|
|
23
|
+
then: {
|
|
24
|
+
field: "summary",
|
|
25
|
+
function: "truthy"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"elysia-operation-tags": {
|
|
29
|
+
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
30
|
+
severity: "error",
|
|
31
|
+
given: operationSelector$1,
|
|
32
|
+
then: {
|
|
33
|
+
field: "tags",
|
|
34
|
+
function: "schema",
|
|
35
|
+
functionOptions: { schema: {
|
|
36
|
+
type: "array",
|
|
37
|
+
minItems: 1
|
|
38
|
+
} }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"operation-description": "warn",
|
|
42
|
+
"operation-operationId": "warn",
|
|
43
|
+
"operation-success-response": "warn"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/presets/strict.ts
|
|
48
|
+
const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
49
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
50
|
+
/**
|
|
51
|
+
* Checks that all 4xx/5xx responses declare application/problem+json as
|
|
52
|
+
* their content type, conforming to RFC 9457 Problem Details for HTTP APIs.
|
|
53
|
+
*/
|
|
54
|
+
const checkProblemDetails = (operation) => {
|
|
55
|
+
if (!isRecord(operation) || !isRecord(operation.responses)) return;
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
58
|
+
const code = Number(statusCode);
|
|
59
|
+
if (!Number.isFinite(code) || code < 400) continue;
|
|
60
|
+
if (!isRecord(response)) continue;
|
|
61
|
+
const content = response.content;
|
|
62
|
+
if (!isRecord(content) || !("application/problem+json" in content)) results.push({
|
|
63
|
+
message: `${statusCode} error response should use "application/problem+json" content type (RFC 9457 Problem Details).`,
|
|
64
|
+
path: ["responses", statusCode]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return results.length > 0 ? results : void 0;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Full API governance preset. Suitable for teams with formal API governance
|
|
71
|
+
* requirements, public API programs, or downstream client generation pipelines.
|
|
72
|
+
*
|
|
73
|
+
* Tightens server:
|
|
74
|
+
* - All elysia rules and operation metadata rules escalated to error
|
|
75
|
+
* - info-contact at warn (API ownership should be declared)
|
|
76
|
+
* - oas3-api-servers at error (server declaration is required)
|
|
77
|
+
* - rfc9457-problem-details at warn (error responses should use Problem Details)
|
|
78
|
+
*/
|
|
79
|
+
const strict = {
|
|
80
|
+
extends: [["spectral:oas", "recommended"]],
|
|
81
|
+
rules: {
|
|
82
|
+
"oas3-api-servers": "error",
|
|
83
|
+
"info-contact": "warn",
|
|
84
|
+
"elysia-operation-summary": {
|
|
85
|
+
description: "Operations should define a summary for generated docs and clients.",
|
|
86
|
+
severity: "error",
|
|
87
|
+
given: operationSelector,
|
|
88
|
+
then: {
|
|
89
|
+
field: "summary",
|
|
90
|
+
function: "truthy"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"elysia-operation-tags": {
|
|
94
|
+
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
95
|
+
severity: "error",
|
|
96
|
+
given: operationSelector,
|
|
97
|
+
then: {
|
|
98
|
+
field: "tags",
|
|
99
|
+
function: "schema",
|
|
100
|
+
functionOptions: { schema: {
|
|
101
|
+
type: "array",
|
|
102
|
+
minItems: 1
|
|
103
|
+
} }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"operation-description": "error",
|
|
107
|
+
"operation-operationId": "error",
|
|
108
|
+
"operation-success-response": "error",
|
|
109
|
+
"rfc9457-problem-details": {
|
|
110
|
+
description: "Error responses (4xx, 5xx) should use RFC 9457 Problem Details (application/problem+json).",
|
|
111
|
+
message: "{{error}}",
|
|
112
|
+
severity: "warn",
|
|
113
|
+
given: operationSelector,
|
|
114
|
+
then: { function: checkProblemDetails }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/presets/index.ts
|
|
120
|
+
var presets_exports = /* @__PURE__ */ __exportAll({
|
|
121
|
+
presets: () => presets,
|
|
122
|
+
recommended: () => recommended,
|
|
123
|
+
server: () => server,
|
|
124
|
+
strict: () => strict
|
|
125
|
+
});
|
|
126
|
+
const presets = {
|
|
127
|
+
recommended,
|
|
128
|
+
server,
|
|
129
|
+
strict
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
export { server as i, presets_exports as n, strict as r, presets as t };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/presets/recommended.ts
|
|
2
|
+
const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
3
|
+
/**
|
|
4
|
+
* Baseline quality preset. Equivalent to the package default ruleset.
|
|
5
|
+
*
|
|
6
|
+
* - Extends spectral:oas/recommended
|
|
7
|
+
* - elysia-operation-summary and elysia-operation-tags at warn
|
|
8
|
+
* - oas3-api-servers and info-contact disabled (local-dev friendly)
|
|
9
|
+
*/
|
|
10
|
+
const recommended = {
|
|
11
|
+
extends: [["spectral:oas", "recommended"]],
|
|
12
|
+
rules: {
|
|
13
|
+
"oas3-api-servers": "off",
|
|
14
|
+
"info-contact": "off",
|
|
15
|
+
"elysia-operation-summary": {
|
|
16
|
+
description: "Operations should define a summary for generated docs and clients.",
|
|
17
|
+
severity: "warn",
|
|
18
|
+
given: operationSelector,
|
|
19
|
+
then: {
|
|
20
|
+
field: "summary",
|
|
21
|
+
function: "truthy"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"elysia-operation-tags": {
|
|
25
|
+
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
26
|
+
severity: "warn",
|
|
27
|
+
given: operationSelector,
|
|
28
|
+
then: {
|
|
29
|
+
field: "tags",
|
|
30
|
+
function: "schema",
|
|
31
|
+
functionOptions: { schema: {
|
|
32
|
+
type: "array",
|
|
33
|
+
minItems: 1
|
|
34
|
+
} }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { recommended as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/core/ruleset-load-error.ts
|
|
2
|
+
var RulesetLoadError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "RulesetLoadError";
|
|
6
|
+
if (options?.cause !== void 0) this.cause = options.cause;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { RulesetLoadError as t };
|