ts-procedures 6.0.0 → 6.0.2
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/agent_config/claude-code/agents/ts-procedures-architect.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +2 -2
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +15 -27
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +11 -4
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +2 -2
- package/agent_config/copilot/copilot-instructions.md +3 -2
- package/agent_config/cursor/cursorrules +3 -2
- package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +24 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.js +33 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js +19 -0
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.js +43 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +11 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +73 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -0
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +11 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +35 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +52 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +4 -0
- package/build/codegen/targets/kotlin/format-kotlin.js +20 -0
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.js +24 -0
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
- package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/integration.test.js +34 -0
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
- package/build/implementations/http/doc-registry.d.ts +14 -19
- package/build/implementations/http/doc-registry.js +41 -46
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +141 -10
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +11 -2
- package/build/implementations/http/error-taxonomy.js +24 -2
- package/build/implementations/http/error-taxonomy.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +5 -6
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +13 -1
- package/docs/http-integrations.md +39 -5
- package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
- package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
- package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
- package/package.json +1 -1
- package/src/implementations/http/README.md +4 -3
- package/src/implementations/http/doc-registry.test.ts +154 -10
- package/src/implementations/http/doc-registry.ts +46 -53
- package/src/implementations/http/error-taxonomy.ts +26 -2
- package/src/implementations/http/express-rpc/README.md +2 -2
- package/src/implementations/http/hono-rpc/README.md +2 -2
- package/src/implementations/http/hono-stream/README.md +15 -0
- package/src/implementations/http/route-errors.test.ts +5 -6
- package/src/implementations/types.ts +13 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { emitKotlinScope } from './emit-scope-kotlin.js';
|
|
3
|
+
import { createStubKotlinEmitter } from './ajsc-adapter.js';
|
|
4
|
+
const ok = (code, rootTypeName) => ({
|
|
5
|
+
code,
|
|
6
|
+
rootTypeName,
|
|
7
|
+
extractedTypeNames: [],
|
|
8
|
+
imports: ['kotlinx.serialization.Serializable'],
|
|
9
|
+
});
|
|
10
|
+
describe('emitKotlinScope', () => {
|
|
11
|
+
it('produces a complete kotlin source file for a single-route scope', () => {
|
|
12
|
+
const route = {
|
|
13
|
+
kind: 'api',
|
|
14
|
+
name: 'GetUser',
|
|
15
|
+
method: 'GET',
|
|
16
|
+
fullPath: '/users/:id',
|
|
17
|
+
schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
|
|
18
|
+
errors: [],
|
|
19
|
+
};
|
|
20
|
+
const group = { scopeKey: 'users', camelCase: 'users', routes: [route] };
|
|
21
|
+
const emitter = createStubKotlinEmitter({
|
|
22
|
+
PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
|
|
23
|
+
Response: ok('@Serializable data class Response(val id: String)', 'Response'),
|
|
24
|
+
});
|
|
25
|
+
const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'abc123' }, emitter, new Map());
|
|
26
|
+
expect(file.filename).toBe('Users.kt');
|
|
27
|
+
expect(file.code).toContain('package com.example.api');
|
|
28
|
+
expect(file.code).toContain('// Source hash: abc123');
|
|
29
|
+
expect(file.code).toContain('import kotlinx.serialization.Serializable');
|
|
30
|
+
expect(file.code).toContain('object Users {');
|
|
31
|
+
expect(file.code).toContain('object GetUser {');
|
|
32
|
+
expect(file.code).toContain('const val method = "GET"');
|
|
33
|
+
});
|
|
34
|
+
it('joins multiple routes inside one scope object', () => {
|
|
35
|
+
const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', schema: {}, errors: [] };
|
|
36
|
+
const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', schema: {}, errors: [] };
|
|
37
|
+
const group = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] };
|
|
38
|
+
const emitter = createStubKotlinEmitter({});
|
|
39
|
+
const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'h' }, emitter, new Map());
|
|
40
|
+
expect(file.code).toContain('object GetUser {');
|
|
41
|
+
expect(file.code).toContain('object CreateUser {');
|
|
42
|
+
// exactly one outer scope object
|
|
43
|
+
expect((file.code.match(/^object Users \{/gm) ?? []).length).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
it('uses PascalCase scope name for the filename and outer object', () => {
|
|
46
|
+
const group = { scopeKey: 'admin-users', camelCase: 'adminUsers', routes: [] };
|
|
47
|
+
const file = emitKotlinScope(group, { kotlinPackage: 'p', sourceHash: 'h' }, createStubKotlinEmitter({}), new Map());
|
|
48
|
+
expect(file.filename).toBe('AdminUsers.kt');
|
|
49
|
+
expect(file.code).toContain('object AdminUsers {');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=emit-scope-kotlin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit-scope-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function kotlinPackageDecl(pkg: string): string;
|
|
2
|
+
export declare function kotlinSourceHashHeader(hash: string): string;
|
|
3
|
+
export declare function kotlinImports(imports: string[]): string;
|
|
4
|
+
export declare function indent(text: string, level: number): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function kotlinPackageDecl(pkg) {
|
|
2
|
+
return `package ${pkg}`;
|
|
3
|
+
}
|
|
4
|
+
export function kotlinSourceHashHeader(hash) {
|
|
5
|
+
return `// Source hash: ${hash}`;
|
|
6
|
+
}
|
|
7
|
+
export function kotlinImports(imports) {
|
|
8
|
+
if (imports.length === 0)
|
|
9
|
+
return '';
|
|
10
|
+
const unique = Array.from(new Set(imports)).sort();
|
|
11
|
+
return unique.map((i) => `import ${i}`).join('\n');
|
|
12
|
+
}
|
|
13
|
+
export function indent(text, level) {
|
|
14
|
+
const prefix = ' '.repeat(level);
|
|
15
|
+
return text
|
|
16
|
+
.split('\n')
|
|
17
|
+
.map((line) => (line.length === 0 ? line : `${prefix}${line}`))
|
|
18
|
+
.join('\n');
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=format-kotlin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,WAAW,GAAG,EAAE,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAO,mBAAmB,IAAI,EAAE,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAiB;IAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAClD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACnC,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC;SAC9D,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent, } from './format-kotlin.js';
|
|
3
|
+
describe('format-kotlin', () => {
|
|
4
|
+
it('emits a package declaration', () => {
|
|
5
|
+
expect(kotlinPackageDecl('com.example.api')).toBe('package com.example.api');
|
|
6
|
+
});
|
|
7
|
+
it('emits a source-hash header line', () => {
|
|
8
|
+
expect(kotlinSourceHashHeader('abc123')).toBe('// Source hash: abc123');
|
|
9
|
+
});
|
|
10
|
+
it('dedupes and sorts imports', () => {
|
|
11
|
+
expect(kotlinImports(['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.Serializable'])).toBe('import kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable');
|
|
12
|
+
});
|
|
13
|
+
it('returns empty string when no imports', () => {
|
|
14
|
+
expect(kotlinImports([])).toBe('');
|
|
15
|
+
});
|
|
16
|
+
it('indents every line by 4 spaces per level', () => {
|
|
17
|
+
expect(indent('a\nb', 1)).toBe(' a\n b');
|
|
18
|
+
expect(indent('a', 2)).toBe(' a');
|
|
19
|
+
});
|
|
20
|
+
it('preserves blank lines without trailing whitespace when indenting', () => {
|
|
21
|
+
expect(indent('a\n\nb', 1)).toBe(' a\n\n b');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=format-kotlin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,MAAM,GACP,MAAM,oBAAoB,CAAA;AAE3B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,aAAa,CAAC,CAAC,oCAAoC,EAAE,kCAAkC,EAAE,oCAAoC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC1I,oFAAoF,CACrF,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAC9C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { runPipeline } from '../../pipeline.js';
|
|
6
|
+
import { createStubKotlinEmitter } from './ajsc-adapter.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
describe('kotlin codegen — integration', () => {
|
|
10
|
+
it('produces byte-identical output against the golden fixture', async () => {
|
|
11
|
+
const envelopePath = join(__dirname, '__fixtures__/users-envelope.json');
|
|
12
|
+
const goldenPath = join(__dirname, '__fixtures__/users-golden.kt');
|
|
13
|
+
const envelope = JSON.parse(await readFile(envelopePath, 'utf8'));
|
|
14
|
+
const goldenTemplate = await readFile(goldenPath, 'utf8');
|
|
15
|
+
const emitter = createStubKotlinEmitter({
|
|
16
|
+
PathParams: { code: '@Serializable\ndata class PathParams(val id: String)', rootTypeName: 'PathParams', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
|
|
17
|
+
Response: { code: '@Serializable\ndata class Response(val id: String, val name: String)', rootTypeName: 'Response', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
|
|
18
|
+
Body: { code: '@Serializable\ndata class Body(val name: String, val email: String)', rootTypeName: 'Body', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
|
|
19
|
+
NotFound: { code: '@Serializable\ndata class NotFound(val name: String = "NotFound", val message: String)', rootTypeName: 'NotFound', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
|
|
20
|
+
});
|
|
21
|
+
const files = await runPipeline({
|
|
22
|
+
envelope, outDir: 'out', dryRun: true,
|
|
23
|
+
target: 'kotlin', kotlinPackage: 'com.example.api',
|
|
24
|
+
kotlinEmitter: emitter,
|
|
25
|
+
});
|
|
26
|
+
expect(files).toHaveLength(1);
|
|
27
|
+
expect(files[0].path).toBe(join('out', 'Users.kt'));
|
|
28
|
+
// Hash is deterministic given envelope contents; splice into golden template.
|
|
29
|
+
const sourceHashLine = files[0].code.split('\n').find((l) => l.startsWith('// Source hash:')) ?? '';
|
|
30
|
+
const goldenWithHash = goldenTemplate.replace('// Source hash: <PLACEHOLDER>', sourceHashLine);
|
|
31
|
+
expect(files[0].code).toBe(goldenWithHash);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/integration.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,CAAA;QACxE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;QACjE,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAEzD,MAAM,OAAO,GAAG,uBAAuB,CAAC;YACtC,UAAU,EAAE,EAAE,IAAI,EAAE,sDAAsD,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACjL,QAAQ,EAAE,EAAE,IAAI,EAAE,sEAAsE,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YAC7L,IAAI,EAAE,EAAE,IAAI,EAAE,qEAAqE,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACpL,QAAQ,EAAE,EAAE,IAAI,EAAE,wFAAwF,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;SAChN,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI;YACrC,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,iBAAiB;YAClD,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QAEF,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;QAEpD,8EAA8E;QAC9E,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAA;QACpG,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,+BAA+B,EAAE,cAAc,CAAC,CAAA;QAC9F,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
import type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc } from '../types.js';
|
|
2
|
-
import { ErrorTaxonomy } from './error-taxonomy.js';
|
|
3
2
|
export type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc, HeaderDoc, } from '../types.js';
|
|
4
3
|
export declare class DocRegistry {
|
|
5
4
|
private readonly basePath;
|
|
6
5
|
private readonly headers;
|
|
7
|
-
private
|
|
6
|
+
private errors;
|
|
8
7
|
private readonly sources;
|
|
9
8
|
constructor(config?: DocRegistryConfig);
|
|
10
9
|
from(source: DocSource<AnyHttpRouteDoc>): this;
|
|
11
|
-
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
|
|
12
|
+
* outside your runtime taxonomy — middleware-level errors, infrastructure
|
|
13
|
+
* errors (502/503/504), or doc-only meta errors.
|
|
14
|
+
*
|
|
15
|
+
* Deduped by `name` — last write wins.
|
|
18
16
|
*/
|
|
19
|
-
|
|
17
|
+
documentError(...docs: ErrorDoc[]): this;
|
|
18
|
+
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* are included unless `includeDefaults: false` is passed.
|
|
20
|
+
* Framework error defaults — derived from {@link defaultErrorTaxonomy} plus
|
|
21
|
+
* `ProcedureRegistrationError` (which is thrown only at registration time
|
|
22
|
+
* and therefore lives in the catalog, not the runtime taxonomy).
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* .from(apiApp)
|
|
24
|
+
* Most consumers do not need to call this directly — the `DocRegistry`
|
|
25
|
+
* constructor auto-includes these unless `includeDefaults: false` is passed.
|
|
29
26
|
*/
|
|
30
|
-
static
|
|
31
|
-
includeDefaults?: boolean;
|
|
32
|
-
}): DocRegistry;
|
|
27
|
+
static defaultErrors(): ErrorDoc[];
|
|
33
28
|
}
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import { defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
|
|
1
|
+
import { PROCEDURE_REGISTRATION_ERROR_DOC, defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
|
|
2
|
+
function isTaxonomy(input) {
|
|
3
|
+
return !Array.isArray(input);
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
* here so consumers still see it in the error catalog.
|
|
6
|
+
* Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
|
|
7
|
+
* preserves the latest position of each name.
|
|
6
8
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
properties: {
|
|
14
|
-
name: { type: 'string', const: 'ProcedureRegistrationError' },
|
|
15
|
-
procedureName: { type: 'string' },
|
|
16
|
-
message: { type: 'string' },
|
|
17
|
-
},
|
|
18
|
-
required: ['name', 'procedureName', 'message'],
|
|
19
|
-
},
|
|
20
|
-
};
|
|
9
|
+
function dedupeByName(docs) {
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
for (const doc of docs)
|
|
12
|
+
byName.set(doc.name, doc);
|
|
13
|
+
return Array.from(byName.values());
|
|
14
|
+
}
|
|
21
15
|
export class DocRegistry {
|
|
22
16
|
basePath;
|
|
23
17
|
headers;
|
|
@@ -26,12 +20,34 @@ export class DocRegistry {
|
|
|
26
20
|
constructor(config) {
|
|
27
21
|
this.basePath = config?.basePath ?? '';
|
|
28
22
|
this.headers = config?.headers ?? [];
|
|
29
|
-
|
|
23
|
+
const includeDefaults = config?.includeDefaults ?? true;
|
|
24
|
+
const userErrors = config?.errors
|
|
25
|
+
? isTaxonomy(config.errors)
|
|
26
|
+
? taxonomyToErrorDocs(config.errors)
|
|
27
|
+
: config.errors
|
|
28
|
+
: [];
|
|
29
|
+
// Precedence: defaults come first, user errors override via dedupe
|
|
30
|
+
// (last-write-wins). Matches runtime resolution order.
|
|
31
|
+
const merged = includeDefaults
|
|
32
|
+
? [...DocRegistry.defaultErrors(), ...userErrors]
|
|
33
|
+
: userErrors;
|
|
34
|
+
this.errors = dedupeByName(merged);
|
|
30
35
|
}
|
|
31
36
|
from(source) {
|
|
32
37
|
this.sources.push(source);
|
|
33
38
|
return this;
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
|
|
42
|
+
* outside your runtime taxonomy — middleware-level errors, infrastructure
|
|
43
|
+
* errors (502/503/504), or doc-only meta errors.
|
|
44
|
+
*
|
|
45
|
+
* Deduped by `name` — last write wins.
|
|
46
|
+
*/
|
|
47
|
+
documentError(...docs) {
|
|
48
|
+
this.errors = dedupeByName([...this.errors, ...docs]);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
35
51
|
toJSON(options) {
|
|
36
52
|
let routes = this.sources.flatMap((source) => source.docs);
|
|
37
53
|
if (options?.filter) {
|
|
@@ -49,11 +65,12 @@ export class DocRegistry {
|
|
|
49
65
|
return envelope;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
|
-
* Framework error defaults
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
68
|
+
* Framework error defaults — derived from {@link defaultErrorTaxonomy} plus
|
|
69
|
+
* `ProcedureRegistrationError` (which is thrown only at registration time
|
|
70
|
+
* and therefore lives in the catalog, not the runtime taxonomy).
|
|
71
|
+
*
|
|
72
|
+
* Most consumers do not need to call this directly — the `DocRegistry`
|
|
73
|
+
* constructor auto-includes these unless `includeDefaults: false` is passed.
|
|
57
74
|
*/
|
|
58
75
|
static defaultErrors() {
|
|
59
76
|
return [
|
|
@@ -61,27 +78,5 @@ export class DocRegistry {
|
|
|
61
78
|
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
62
79
|
];
|
|
63
80
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Convenience constructor that seeds `config.errors` from a taxonomy so the
|
|
66
|
-
* DocEnvelope automatically documents every error class registered with the
|
|
67
|
-
* HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
|
|
68
|
-
* are included unless `includeDefaults: false` is passed.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
|
|
72
|
-
* .from(apiApp)
|
|
73
|
-
*/
|
|
74
|
-
static fromTaxonomy(taxonomy, config) {
|
|
75
|
-
const { includeDefaults = true, ...rest } = config ?? {};
|
|
76
|
-
const errors = [
|
|
77
|
-
...taxonomyToErrorDocs(taxonomy),
|
|
78
|
-
...(includeDefaults ? DocRegistry.defaultErrors() : []),
|
|
79
|
-
];
|
|
80
|
-
// Dedupe by name — user entries take precedence over defaults with the
|
|
81
|
-
// same key, matching runtime resolution order.
|
|
82
|
-
const seen = new Set();
|
|
83
|
-
const deduped = errors.filter((e) => seen.has(e.name) ? false : (seen.add(e.name), true));
|
|
84
|
-
return new DocRegistry({ ...rest, errors: deduped });
|
|
85
|
-
}
|
|
86
81
|
}
|
|
87
82
|
//# sourceMappingURL=doc-registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"AASA,OAAO,EACL,gCAAgC,EAChC,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,qBAAqB,CAAA;AAY5B,SAAS,UAAU,CAAC,KAAiC;IACnD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC1C,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACjD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,OAAO,WAAW;IACL,QAAQ,CAAQ;IAChB,OAAO,CAAa;IAC7B,MAAM,CAAY;IACT,OAAO,GAAiC,EAAE,CAAA;IAE3D,YAAY,MAA0B;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAA;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,EAAE,OAAO,IAAI,EAAE,CAAA;QAEpC,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,IAAI,CAAA;QACvD,MAAM,UAAU,GAAe,MAAM,EAAE,MAAM;YAC3C,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;gBACzB,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC;gBACpC,CAAC,CAAC,MAAM,CAAC,MAAM;YACjB,CAAC,CAAC,EAAE,CAAA;QAEN,mEAAmE;QACnE,uDAAuD;QACvD,MAAM,MAAM,GAAG,eAAe;YAC5B,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,aAAa,EAAE,EAAE,GAAG,UAAU,CAAC;YACjD,CAAC,CAAC,UAAU,CAAA;QAEd,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,CAAC,MAAkC;QACrC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,IAAgB;QAC/B,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;QACrD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,CAAkB,OAAqC;QAC3D,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAE1D,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxC,CAAC;QAED,MAAM,QAAQ,GAAgB;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;YACxB,MAAM;SACP,CAAA;QAED,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QACpC,CAAC;QAED,OAAO,QAAa,CAAA;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,aAAa;QAClB,OAAO;YACL,GAAG,mBAAmB,CAAC,oBAAoB,CAAC;YAC5C,gCAAgC;SACjC,CAAA;IACH,CAAC;CACF"}
|
|
@@ -3,6 +3,7 @@ import { v } from 'suretype';
|
|
|
3
3
|
import { Procedures } from '../../index.js';
|
|
4
4
|
import { HonoRPCAppBuilder } from './hono-rpc/index.js';
|
|
5
5
|
import { DocRegistry } from './doc-registry.js';
|
|
6
|
+
import { defineErrorTaxonomy } from './error-taxonomy.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Helpers — minimal doc fixtures
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -45,14 +46,14 @@ describe('DocRegistry', () => {
|
|
|
45
46
|
// --------------------------------------------------------------------------
|
|
46
47
|
describe('constructor', () => {
|
|
47
48
|
test('uses defaults when no config provided', () => {
|
|
48
|
-
const registry = new DocRegistry();
|
|
49
|
+
const registry = new DocRegistry({ includeDefaults: false });
|
|
49
50
|
const out = registry.toJSON();
|
|
50
51
|
expect(out.basePath).toBe('');
|
|
51
52
|
expect(out.headers).toEqual([]);
|
|
52
53
|
expect(out.errors).toEqual([]);
|
|
53
54
|
});
|
|
54
55
|
test('accepts partial config', () => {
|
|
55
|
-
const registry = new DocRegistry({ basePath: '/v1' });
|
|
56
|
+
const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false });
|
|
56
57
|
const out = registry.toJSON();
|
|
57
58
|
expect(out.basePath).toBe('/v1');
|
|
58
59
|
expect(out.headers).toEqual([]);
|
|
@@ -61,7 +62,7 @@ describe('DocRegistry', () => {
|
|
|
61
62
|
test('accepts full config', () => {
|
|
62
63
|
const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }];
|
|
63
64
|
const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }];
|
|
64
|
-
const registry = new DocRegistry({ basePath: '/api', headers, errors });
|
|
65
|
+
const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false });
|
|
65
66
|
const out = registry.toJSON();
|
|
66
67
|
expect(out.basePath).toBe('/api');
|
|
67
68
|
expect(out.headers).toEqual(headers);
|
|
@@ -142,7 +143,7 @@ describe('DocRegistry', () => {
|
|
|
142
143
|
test('headers and errors are copies', () => {
|
|
143
144
|
const headers = [{ name: 'X-Custom' }];
|
|
144
145
|
const errors = [{ name: 'E', statusCode: 500, description: 'd' }];
|
|
145
|
-
const registry = new DocRegistry({ headers, errors });
|
|
146
|
+
const registry = new DocRegistry({ headers, errors, includeDefaults: false });
|
|
146
147
|
const out = registry.toJSON();
|
|
147
148
|
expect(out.headers).toEqual(headers);
|
|
148
149
|
expect(out.headers).not.toBe(headers);
|
|
@@ -249,6 +250,141 @@ describe('DocRegistry', () => {
|
|
|
249
250
|
});
|
|
250
251
|
});
|
|
251
252
|
// --------------------------------------------------------------------------
|
|
253
|
+
// taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
|
|
254
|
+
// --------------------------------------------------------------------------
|
|
255
|
+
describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
|
|
256
|
+
test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
|
|
257
|
+
const taxonomy = defineErrorTaxonomy({
|
|
258
|
+
AuthError: {
|
|
259
|
+
class: class AuthError extends Error {
|
|
260
|
+
},
|
|
261
|
+
statusCode: 401,
|
|
262
|
+
description: 'unauthenticated',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
|
|
266
|
+
const auth = envelope.errors.find((e) => e.name === 'AuthError');
|
|
267
|
+
expect(auth).toBeDefined();
|
|
268
|
+
expect(auth?.statusCode).toBe(401);
|
|
269
|
+
expect(auth?.description).toBe('unauthenticated');
|
|
270
|
+
});
|
|
271
|
+
test('auto-includes framework defaults when errors is a taxonomy', () => {
|
|
272
|
+
const taxonomy = defineErrorTaxonomy({
|
|
273
|
+
AuthError: { class: class AuthError extends Error {
|
|
274
|
+
}, statusCode: 401 },
|
|
275
|
+
});
|
|
276
|
+
const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name);
|
|
277
|
+
expect(names).toContain('AuthError');
|
|
278
|
+
expect(names).toContain('ProcedureValidationError');
|
|
279
|
+
expect(names).toContain('ProcedureYieldValidationError');
|
|
280
|
+
expect(names).toContain('ProcedureError');
|
|
281
|
+
expect(names).toContain('ProcedureRegistrationError');
|
|
282
|
+
});
|
|
283
|
+
test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
|
|
284
|
+
const custom = { name: 'CustomThing', statusCode: 418, description: 'teapot' };
|
|
285
|
+
const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name);
|
|
286
|
+
expect(names).toContain('CustomThing');
|
|
287
|
+
expect(names).toContain('ProcedureValidationError');
|
|
288
|
+
});
|
|
289
|
+
test('includeDefaults: false omits framework defaults', () => {
|
|
290
|
+
const taxonomy = defineErrorTaxonomy({
|
|
291
|
+
AuthError: { class: class AuthError extends Error {
|
|
292
|
+
}, statusCode: 401 },
|
|
293
|
+
});
|
|
294
|
+
const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
|
|
295
|
+
.toJSON()
|
|
296
|
+
.errors.map((e) => e.name);
|
|
297
|
+
expect(names).toEqual(['AuthError']);
|
|
298
|
+
});
|
|
299
|
+
test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
|
|
300
|
+
const taxonomy = defineErrorTaxonomy({
|
|
301
|
+
ProcedureError: {
|
|
302
|
+
class: Error,
|
|
303
|
+
statusCode: 418,
|
|
304
|
+
description: 'custom override',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
|
|
308
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
|
|
309
|
+
expect(proc).toHaveLength(1);
|
|
310
|
+
expect(proc[0].statusCode).toBe(418);
|
|
311
|
+
expect(proc[0].description).toBe('custom override');
|
|
312
|
+
});
|
|
313
|
+
test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
|
|
314
|
+
const custom = {
|
|
315
|
+
name: 'ProcedureError',
|
|
316
|
+
statusCode: 418,
|
|
317
|
+
description: 'custom override',
|
|
318
|
+
};
|
|
319
|
+
const envelope = new DocRegistry({ errors: [custom] }).toJSON();
|
|
320
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
|
|
321
|
+
expect(proc).toHaveLength(1);
|
|
322
|
+
expect(proc[0].statusCode).toBe(418);
|
|
323
|
+
expect(proc[0].description).toBe('custom override');
|
|
324
|
+
});
|
|
325
|
+
test('empty errors config still returns framework defaults', () => {
|
|
326
|
+
const names = new DocRegistry().toJSON().errors.map((e) => e.name);
|
|
327
|
+
expect(names).toContain('ProcedureError');
|
|
328
|
+
expect(names).toContain('ProcedureValidationError');
|
|
329
|
+
expect(names).toContain('ProcedureYieldValidationError');
|
|
330
|
+
expect(names).toContain('ProcedureRegistrationError');
|
|
331
|
+
});
|
|
332
|
+
test('includeDefaults: false with no errors produces empty error list', () => {
|
|
333
|
+
const envelope = new DocRegistry({ includeDefaults: false }).toJSON();
|
|
334
|
+
expect(envelope.errors).toEqual([]);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
// --------------------------------------------------------------------------
|
|
338
|
+
// .documentError() fluent extension
|
|
339
|
+
// --------------------------------------------------------------------------
|
|
340
|
+
describe('.documentError()', () => {
|
|
341
|
+
test('adds a single ErrorDoc to the envelope', () => {
|
|
342
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({
|
|
343
|
+
name: 'RateLimitExceeded',
|
|
344
|
+
statusCode: 429,
|
|
345
|
+
description: 'too many requests',
|
|
346
|
+
});
|
|
347
|
+
const names = registry.toJSON().errors.map((e) => e.name);
|
|
348
|
+
expect(names).toEqual(['RateLimitExceeded']);
|
|
349
|
+
});
|
|
350
|
+
test('accepts multiple docs via variadic args', () => {
|
|
351
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({ name: 'A', statusCode: 400, description: 'a' }, { name: 'B', statusCode: 500, description: 'b' });
|
|
352
|
+
const names = registry.toJSON().errors.map((e) => e.name);
|
|
353
|
+
expect(names).toEqual(['A', 'B']);
|
|
354
|
+
});
|
|
355
|
+
test('returns this for chaining', () => {
|
|
356
|
+
const registry = new DocRegistry();
|
|
357
|
+
const returned = registry.documentError({
|
|
358
|
+
name: 'Foo',
|
|
359
|
+
statusCode: 500,
|
|
360
|
+
description: 'x',
|
|
361
|
+
});
|
|
362
|
+
expect(returned).toBe(registry);
|
|
363
|
+
});
|
|
364
|
+
test('composes with taxonomy input — extends docs without replacing', () => {
|
|
365
|
+
const taxonomy = defineErrorTaxonomy({
|
|
366
|
+
AuthError: { class: class AuthError extends Error {
|
|
367
|
+
}, statusCode: 401 },
|
|
368
|
+
});
|
|
369
|
+
const envelope = new DocRegistry({ errors: taxonomy })
|
|
370
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
|
|
371
|
+
.toJSON();
|
|
372
|
+
const names = envelope.errors.map((e) => e.name);
|
|
373
|
+
expect(names).toContain('AuthError');
|
|
374
|
+
expect(names).toContain('RateLimitExceeded');
|
|
375
|
+
expect(names).toContain('ProcedureValidationError');
|
|
376
|
+
});
|
|
377
|
+
test('dedupes against existing errors (last write wins)', () => {
|
|
378
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
379
|
+
.documentError({ name: 'Foo', statusCode: 400, description: 'first' })
|
|
380
|
+
.documentError({ name: 'Foo', statusCode: 500, description: 'second' });
|
|
381
|
+
const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo');
|
|
382
|
+
expect(foo).toHaveLength(1);
|
|
383
|
+
expect(foo[0].statusCode).toBe(500);
|
|
384
|
+
expect(foo[0].description).toBe('second');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// --------------------------------------------------------------------------
|
|
252
388
|
// kind discriminant
|
|
253
389
|
// --------------------------------------------------------------------------
|
|
254
390
|
describe('kind discriminant', () => {
|
|
@@ -279,7 +415,6 @@ describe('DocRegistry', () => {
|
|
|
279
415
|
const registry = new DocRegistry({
|
|
280
416
|
basePath: '/api',
|
|
281
417
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
282
|
-
errors: DocRegistry.defaultErrors(),
|
|
283
418
|
})
|
|
284
419
|
.from(makeSource([rpcDoc]))
|
|
285
420
|
.from(makeSource([apiDoc]))
|
|
@@ -291,10 +426,7 @@ describe('DocRegistry', () => {
|
|
|
291
426
|
expect(out.routes).toHaveLength(3);
|
|
292
427
|
});
|
|
293
428
|
test('filter + transform combined', () => {
|
|
294
|
-
const registry = new DocRegistry({
|
|
295
|
-
basePath: '/api',
|
|
296
|
-
errors: DocRegistry.defaultErrors(),
|
|
297
|
-
})
|
|
429
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
298
430
|
.from(makeSource([rpcDoc]))
|
|
299
431
|
.from(makeSource([apiDoc]))
|
|
300
432
|
.from(makeSource([streamDoc]));
|
|
@@ -313,7 +445,6 @@ describe('DocRegistry', () => {
|
|
|
313
445
|
const registry = new DocRegistry({
|
|
314
446
|
basePath: '/api',
|
|
315
447
|
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
316
|
-
errors: DocRegistry.defaultErrors(),
|
|
317
448
|
})
|
|
318
449
|
.from(makeSource([rpcDoc]))
|
|
319
450
|
.from(makeSource([apiDoc]));
|