@beignet/core 0.0.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 +5 -0
- package/README.md +288 -0
- package/dist/application/index.d.ts +260 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +324 -0
- package/dist/application/index.js.map +1 -0
- package/dist/client/client.d.ts +241 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +531 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +139 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +216 -0
- package/dist/config/index.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +121 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -0
- package/dist/contracts/contract-builder.js +346 -0
- package/dist/contracts/contract-builder.js.map +1 -0
- package/dist/contracts/contract-group.d.ts +106 -0
- package/dist/contracts/contract-group.d.ts.map +1 -0
- package/dist/contracts/contract-group.js +240 -0
- package/dist/contracts/contract-group.js.map +1 -0
- package/dist/contracts/contract-like.d.ts +21 -0
- package/dist/contracts/contract-like.d.ts.map +1 -0
- package/dist/contracts/contract-like.js +9 -0
- package/dist/contracts/contract-like.js.map +1 -0
- package/dist/contracts/index.d.ts +15 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +11 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/openapi-meta.d.ts +23 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -0
- package/dist/contracts/openapi-meta.js +2 -0
- package/dist/contracts/openapi-meta.js.map +1 -0
- package/dist/contracts/path-template.d.ts +17 -0
- package/dist/contracts/path-template.d.ts.map +1 -0
- package/dist/contracts/path-template.js +50 -0
- package/dist/contracts/path-template.js.map +1 -0
- package/dist/contracts/rate-limit.d.ts +50 -0
- package/dist/contracts/rate-limit.d.ts.map +1 -0
- package/dist/contracts/rate-limit.js +2 -0
- package/dist/contracts/rate-limit.js.map +1 -0
- package/dist/contracts/types.d.ts +97 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +54 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/contracts/utils.d.ts +3 -0
- package/dist/contracts/utils.d.ts.map +1 -0
- package/dist/contracts/utils.js +44 -0
- package/dist/contracts/utils.js.map +1 -0
- package/dist/domain/entity.d.ts +87 -0
- package/dist/domain/entity.d.ts.map +1 -0
- package/dist/domain/entity.js +155 -0
- package/dist/domain/entity.js.map +1 -0
- package/dist/domain/events.d.ts +41 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +21 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +14 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/value-object.d.ts +60 -0
- package/dist/domain/value-object.d.ts.map +1 -0
- package/dist/domain/value-object.js +87 -0
- package/dist/domain/value-object.js.map +1 -0
- package/dist/errors/catalog.d.ts +71 -0
- package/dist/errors/catalog.d.ts.map +1 -0
- package/dist/errors/catalog.js +71 -0
- package/dist/errors/catalog.js.map +1 -0
- package/dist/errors/http.d.ts +77 -0
- package/dist/errors/http.d.ts.map +1 -0
- package/dist/errors/http.js +74 -0
- package/dist/errors/http.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +14 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/response.d.ts +26 -0
- package/dist/errors/response.d.ts.map +1 -0
- package/dist/errors/response.js +34 -0
- package/dist/errors/response.js.map +1 -0
- package/dist/errors/validation.d.ts +18 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +21 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +102 -0
- package/dist/events/index.js.map +1 -0
- package/dist/jobs/index.d.ts +56 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +89 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/mail/index.d.ts +75 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +84 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/openapi/index.d.ts +207 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +449 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/schema-introspector.d.ts +38 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -0
- package/dist/openapi/schema-introspector.js +67 -0
- package/dist/openapi/schema-introspector.js.map +1 -0
- package/dist/ports/audit.d.ts +58 -0
- package/dist/ports/audit.d.ts.map +1 -0
- package/dist/ports/audit.js +74 -0
- package/dist/ports/audit.js.map +1 -0
- package/dist/ports/auth.d.ts +23 -0
- package/dist/ports/auth.d.ts.map +1 -0
- package/dist/ports/auth.js +31 -0
- package/dist/ports/auth.js.map +1 -0
- package/dist/ports/builder.d.ts +61 -0
- package/dist/ports/builder.d.ts.map +1 -0
- package/dist/ports/builder.js +48 -0
- package/dist/ports/builder.js.map +1 -0
- package/dist/ports/cache.d.ts +15 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +57 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/clock.d.ts +10 -0
- package/dist/ports/clock.d.ts.map +1 -0
- package/dist/ports/clock.js +21 -0
- package/dist/ports/clock.js.map +1 -0
- package/dist/ports/events.d.ts +71 -0
- package/dist/ports/events.d.ts.map +1 -0
- package/dist/ports/events.js +2 -0
- package/dist/ports/events.js.map +1 -0
- package/dist/ports/id-generator.d.ts +12 -0
- package/dist/ports/id-generator.d.ts.map +1 -0
- package/dist/ports/id-generator.js +22 -0
- package/dist/ports/id-generator.js.map +1 -0
- package/dist/ports/index.d.ts +98 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +67 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/logger.d.ts +22 -0
- package/dist/ports/logger.d.ts.map +1 -0
- package/dist/ports/logger.js +34 -0
- package/dist/ports/logger.js.map +1 -0
- package/dist/ports/mailer.d.ts +6 -0
- package/dist/ports/mailer.d.ts.map +1 -0
- package/dist/ports/mailer.js +2 -0
- package/dist/ports/mailer.js.map +1 -0
- package/dist/ports/policy.d.ts +53 -0
- package/dist/ports/policy.d.ts.map +1 -0
- package/dist/ports/policy.js +81 -0
- package/dist/ports/policy.js.map +1 -0
- package/dist/ports/rate-limit.d.ts +41 -0
- package/dist/ports/rate-limit.d.ts.map +1 -0
- package/dist/ports/rate-limit.js +37 -0
- package/dist/ports/rate-limit.js.map +1 -0
- package/dist/ports/redaction.d.ts +26 -0
- package/dist/ports/redaction.d.ts.map +1 -0
- package/dist/ports/redaction.js +126 -0
- package/dist/ports/redaction.js.map +1 -0
- package/dist/ports/schedules.d.ts +9 -0
- package/dist/ports/schedules.d.ts.map +1 -0
- package/dist/ports/schedules.js +2 -0
- package/dist/ports/schedules.js.map +1 -0
- package/dist/ports/storage.d.ts +47 -0
- package/dist/ports/storage.d.ts.map +1 -0
- package/dist/ports/storage.js +185 -0
- package/dist/ports/storage.js.map +1 -0
- package/dist/ports/testing.d.ts +73 -0
- package/dist/ports/testing.d.ts.map +1 -0
- package/dist/ports/testing.js +105 -0
- package/dist/ports/testing.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +56 -0
- package/dist/ports/unit-of-work.d.ts.map +1 -0
- package/dist/ports/unit-of-work.js +64 -0
- package/dist/ports/unit-of-work.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/instrumentation.d.ts +91 -0
- package/dist/providers/instrumentation.d.ts.map +1 -0
- package/dist/providers/instrumentation.js +93 -0
- package/dist/providers/instrumentation.js.map +1 -0
- package/dist/providers/provider.d.ts +146 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +31 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/schedules/index.d.ts +105 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +178 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/server/contract-like.d.ts +5 -0
- package/dist/server/contract-like.d.ts.map +1 -0
- package/dist/server/contract-like.js +5 -0
- package/dist/server/contract-like.js.map +1 -0
- package/dist/server/health.d.ts +41 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +46 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/hooks/auth.d.ts +42 -0
- package/dist/server/hooks/auth.d.ts.map +1 -0
- package/dist/server/hooks/auth.js +61 -0
- package/dist/server/hooks/auth.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +13 -0
- package/dist/server/hooks/cors.d.ts.map +1 -0
- package/dist/server/hooks/cors.js +70 -0
- package/dist/server/hooks/cors.js.map +1 -0
- package/dist/server/hooks/errors.d.ts +66 -0
- package/dist/server/hooks/errors.d.ts.map +1 -0
- package/dist/server/hooks/errors.js +83 -0
- package/dist/server/hooks/errors.js.map +1 -0
- package/dist/server/hooks/index.d.ts +12 -0
- package/dist/server/hooks/index.d.ts.map +1 -0
- package/dist/server/hooks/index.js +12 -0
- package/dist/server/hooks/index.js.map +1 -0
- package/dist/server/hooks/logging.d.ts +33 -0
- package/dist/server/hooks/logging.d.ts.map +1 -0
- package/dist/server/hooks/logging.js +90 -0
- package/dist/server/hooks/logging.js.map +1 -0
- package/dist/server/hooks/rate-limit.d.ts +29 -0
- package/dist/server/hooks/rate-limit.d.ts.map +1 -0
- package/dist/server/hooks/rate-limit.js +93 -0
- package/dist/server/hooks/rate-limit.js.map +1 -0
- package/dist/server/hooks/utils.d.ts +9 -0
- package/dist/server/hooks/utils.d.ts.map +1 -0
- package/dist/server/hooks/utils.js +16 -0
- package/dist/server/hooks/utils.js.map +1 -0
- package/dist/server/hooks.d.ts +2 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +2 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/http.d.ts +124 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +2 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/openapi.d.ts +32 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/openapi.js +43 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/providers/index.d.ts +4 -0
- package/dist/server/providers/index.d.ts.map +1 -0
- package/dist/server/providers/index.js +4 -0
- package/dist/server/providers/index.js.map +1 -0
- package/dist/server/providers/loadProviderConfig.d.ts +7 -0
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
- package/dist/server/providers/loadProviderConfig.js +42 -0
- package/dist/server/providers/loadProviderConfig.js.map +1 -0
- package/dist/server/server.d.ts +86 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1031 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +129 -0
- package/src/application/index.ts +747 -0
- package/src/client/client.ts +1105 -0
- package/src/client/index.ts +45 -0
- package/src/client/types.ts +305 -0
- package/src/config/index.ts +497 -0
- package/src/contracts/contract-builder.ts +583 -0
- package/src/contracts/contract-group.ts +502 -0
- package/src/contracts/contract-like.ts +29 -0
- package/src/contracts/index.ts +53 -0
- package/src/contracts/openapi-meta.ts +22 -0
- package/src/contracts/path-template.ts +91 -0
- package/src/contracts/rate-limit.ts +50 -0
- package/src/contracts/types.ts +207 -0
- package/src/contracts/utils.ts +56 -0
- package/src/domain/entity.ts +256 -0
- package/src/domain/events.ts +52 -0
- package/src/domain/index.ts +18 -0
- package/src/domain/value-object.ts +135 -0
- package/src/errors/catalog.ts +149 -0
- package/src/errors/http.ts +80 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/response.ts +54 -0
- package/src/errors/validation.ts +35 -0
- package/src/events/index.ts +246 -0
- package/src/jobs/index.ts +211 -0
- package/src/mail/index.ts +177 -0
- package/src/openapi/index.ts +865 -0
- package/src/openapi/schema-introspector.ts +107 -0
- package/src/ports/audit.ts +176 -0
- package/src/ports/auth.ts +76 -0
- package/src/ports/builder.ts +97 -0
- package/src/ports/cache.ts +94 -0
- package/src/ports/clock.ts +34 -0
- package/src/ports/events.ts +100 -0
- package/src/ports/id-generator.ts +36 -0
- package/src/ports/index.ts +221 -0
- package/src/ports/logger.ts +67 -0
- package/src/ports/policy.ts +242 -0
- package/src/ports/rate-limit.ts +91 -0
- package/src/ports/redaction.ts +199 -0
- package/src/ports/storage.ts +282 -0
- package/src/ports/testing.ts +234 -0
- package/src/ports/unit-of-work.ts +134 -0
- package/src/providers/index.ts +40 -0
- package/src/providers/instrumentation.ts +248 -0
- package/src/providers/provider.ts +191 -0
- package/src/schedules/index.ts +442 -0
- package/src/server/contract-like.ts +8 -0
- package/src/server/health.ts +82 -0
- package/src/server/hooks/auth.ts +147 -0
- package/src/server/hooks/cors.ts +87 -0
- package/src/server/hooks/errors.ts +126 -0
- package/src/server/hooks/index.ts +43 -0
- package/src/server/hooks/logging.ts +121 -0
- package/src/server/hooks/rate-limit.ts +171 -0
- package/src/server/hooks/utils.ts +16 -0
- package/src/server/hooks.ts +1 -0
- package/src/server/http.ts +189 -0
- package/src/server/index.ts +35 -0
- package/src/server/openapi.ts +72 -0
- package/src/server/providers/index.ts +3 -0
- package/src/server/providers/loadProviderConfig.ts +72 -0
- package/src/server/server.ts +1521 -0
- package/src/server/types.ts +2 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { ContractBuilder } from "./contract-builder";
|
|
2
|
+
import { parsePathTemplate } from "./path-template";
|
|
3
|
+
import type {
|
|
4
|
+
ContractErrorResponses,
|
|
5
|
+
ContractHeaderSchemas,
|
|
6
|
+
ContractMeta,
|
|
7
|
+
ContractResponses,
|
|
8
|
+
HttpMethod,
|
|
9
|
+
ResponsesFromErrorDefinitions,
|
|
10
|
+
StandardSchema,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { STANDARD_ERROR_RESPONSE_SCHEMA } from "./types";
|
|
13
|
+
import { generateContractName } from "./utils";
|
|
14
|
+
|
|
15
|
+
type TrimLeadingSlash<T extends string> = T extends `/${infer Rest}`
|
|
16
|
+
? TrimLeadingSlash<Rest>
|
|
17
|
+
: T;
|
|
18
|
+
|
|
19
|
+
type TrimTrailingSlash<T extends string> = T extends "/"
|
|
20
|
+
? ""
|
|
21
|
+
: T extends `${infer Rest}/`
|
|
22
|
+
? TrimTrailingSlash<Rest>
|
|
23
|
+
: T;
|
|
24
|
+
|
|
25
|
+
type NormalizedPrefix<T extends string> = T extends "" | "/"
|
|
26
|
+
? ""
|
|
27
|
+
: TrimTrailingSlash<T>;
|
|
28
|
+
|
|
29
|
+
type NormalizedChildPath<T extends string> = TrimTrailingSlash<
|
|
30
|
+
TrimLeadingSlash<T>
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
type JoinPaths<TPrefix extends string, TPath extends string> = string extends
|
|
34
|
+
| TPrefix
|
|
35
|
+
| TPath
|
|
36
|
+
? string
|
|
37
|
+
: NormalizedPrefix<TPrefix> extends ""
|
|
38
|
+
? TPath
|
|
39
|
+
: NormalizedChildPath<TPath> extends ""
|
|
40
|
+
? NormalizedPrefix<TPrefix>
|
|
41
|
+
: `${NormalizedPrefix<TPrefix>}/${NormalizedChildPath<TPath>}`;
|
|
42
|
+
|
|
43
|
+
function joinPathPrefix(prefix: string, path: string): string {
|
|
44
|
+
const segments = [
|
|
45
|
+
...prefix.split("/").filter(Boolean),
|
|
46
|
+
...path.split("/").filter(Boolean),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return `/${segments.join("/")}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function responsesFromErrors(
|
|
53
|
+
errors: ContractErrorResponses,
|
|
54
|
+
): ContractResponses {
|
|
55
|
+
const responses: ContractResponses = {};
|
|
56
|
+
for (const error of Object.values(errors)) {
|
|
57
|
+
responses[error.status] = STANDARD_ERROR_RESPONSE_SCHEMA;
|
|
58
|
+
}
|
|
59
|
+
return responses;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertNoResponseStatusConflicts(
|
|
63
|
+
current: ContractResponses,
|
|
64
|
+
next: ContractResponses,
|
|
65
|
+
): void {
|
|
66
|
+
const currentStatuses = new Set(Object.keys(current));
|
|
67
|
+
const conflicts = Object.keys(next).filter((status) =>
|
|
68
|
+
currentStatuses.has(status),
|
|
69
|
+
);
|
|
70
|
+
if (conflicts.length) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`.errors() cannot declare status ${conflicts.join(", ")} because a response schema for that status already exists. Use .responses() without .errors() when you need a custom error response body.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertNoCatalogErrorStatusConflicts(
|
|
78
|
+
metadata: ContractMeta,
|
|
79
|
+
next: ContractResponses,
|
|
80
|
+
): void {
|
|
81
|
+
const errors = metadata.errors;
|
|
82
|
+
if (typeof errors !== "object" || errors === null) return;
|
|
83
|
+
|
|
84
|
+
const errorStatuses = new Set(
|
|
85
|
+
Object.values(errors)
|
|
86
|
+
.map((error) =>
|
|
87
|
+
typeof error === "object" &&
|
|
88
|
+
error !== null &&
|
|
89
|
+
typeof (error as { status?: unknown }).status === "number"
|
|
90
|
+
? String((error as { status: number }).status)
|
|
91
|
+
: undefined,
|
|
92
|
+
)
|
|
93
|
+
.filter((status): status is string => status !== undefined),
|
|
94
|
+
);
|
|
95
|
+
const conflicts = Object.keys(next).filter((status) =>
|
|
96
|
+
errorStatuses.has(status),
|
|
97
|
+
);
|
|
98
|
+
if (conflicts.length) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`.responses() cannot declare status ${conflicts.join(", ")} because that status is already declared by .errors(). Use .responses() without .errors() when you need a custom error response body.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* ContractGroup is a feature-scoped factory for creating related contracts
|
|
107
|
+
* with shared configuration. Immutable — each method returns a new instance.
|
|
108
|
+
*/
|
|
109
|
+
export class ContractGroup<
|
|
110
|
+
TSharedResponses extends ContractResponses = Record<never, never>,
|
|
111
|
+
TSharedMeta extends ContractMeta = ContractMeta,
|
|
112
|
+
TSharedHeaders extends ContractHeaderSchemas = null,
|
|
113
|
+
TPathPrefix extends string = "",
|
|
114
|
+
> {
|
|
115
|
+
private readonly _namespace: string;
|
|
116
|
+
private readonly _meta: TSharedMeta;
|
|
117
|
+
private readonly _responses: TSharedResponses;
|
|
118
|
+
private readonly _headers: TSharedHeaders;
|
|
119
|
+
private readonly _pathPrefix: TPathPrefix;
|
|
120
|
+
|
|
121
|
+
constructor(
|
|
122
|
+
state: {
|
|
123
|
+
namespace?: string;
|
|
124
|
+
meta?: TSharedMeta;
|
|
125
|
+
responses?: TSharedResponses;
|
|
126
|
+
headers?: TSharedHeaders;
|
|
127
|
+
pathPrefix?: TPathPrefix;
|
|
128
|
+
} = {} as {
|
|
129
|
+
namespace?: string;
|
|
130
|
+
meta?: TSharedMeta;
|
|
131
|
+
responses?: TSharedResponses;
|
|
132
|
+
headers?: TSharedHeaders;
|
|
133
|
+
pathPrefix?: TPathPrefix;
|
|
134
|
+
},
|
|
135
|
+
) {
|
|
136
|
+
this._namespace = state.namespace ?? "";
|
|
137
|
+
this._meta = state.meta ?? ({} as TSharedMeta);
|
|
138
|
+
this._responses = state.responses ?? ({} as TSharedResponses);
|
|
139
|
+
this._headers = state.headers ?? (null as TSharedHeaders);
|
|
140
|
+
this._pathPrefix = state.pathPrefix ?? ("" as TPathPrefix);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set the namespace for all contracts created from this group
|
|
145
|
+
*/
|
|
146
|
+
namespace(
|
|
147
|
+
ns: string,
|
|
148
|
+
): ContractGroup<TSharedResponses, TSharedMeta, TSharedHeaders, TPathPrefix> {
|
|
149
|
+
return new ContractGroup({
|
|
150
|
+
namespace: ns,
|
|
151
|
+
meta: this._meta,
|
|
152
|
+
responses: this._responses,
|
|
153
|
+
headers: this._headers,
|
|
154
|
+
pathPrefix: this._pathPrefix,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Add a path prefix to all contracts created from this group.
|
|
160
|
+
*
|
|
161
|
+
* Prefixes compose immutably, so `prefix("/api").prefix("/v1")`
|
|
162
|
+
* produces paths under `/api/v1`.
|
|
163
|
+
*/
|
|
164
|
+
prefix<const TNewPrefix extends string>(
|
|
165
|
+
pathPrefix: TNewPrefix,
|
|
166
|
+
): ContractGroup<
|
|
167
|
+
TSharedResponses,
|
|
168
|
+
TSharedMeta,
|
|
169
|
+
TSharedHeaders,
|
|
170
|
+
JoinPaths<TPathPrefix, TNewPrefix>
|
|
171
|
+
> {
|
|
172
|
+
parsePathTemplate(pathPrefix);
|
|
173
|
+
const nextPrefix = joinPathPrefix(this._pathPrefix, pathPrefix);
|
|
174
|
+
const normalizedPrefix = nextPrefix === "/" ? "" : nextPrefix;
|
|
175
|
+
|
|
176
|
+
return new ContractGroup({
|
|
177
|
+
namespace: this._namespace,
|
|
178
|
+
meta: this._meta,
|
|
179
|
+
responses: this._responses,
|
|
180
|
+
headers: this._headers,
|
|
181
|
+
pathPrefix: normalizedPrefix as JoinPaths<TPathPrefix, TNewPrefix>,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Set shared metadata for all contracts created from this group
|
|
187
|
+
*/
|
|
188
|
+
meta<TNewMeta extends ContractMeta>(
|
|
189
|
+
meta: TNewMeta,
|
|
190
|
+
): ContractGroup<
|
|
191
|
+
TSharedResponses,
|
|
192
|
+
Omit<TSharedMeta, keyof TNewMeta> & TNewMeta,
|
|
193
|
+
TSharedHeaders,
|
|
194
|
+
TPathPrefix
|
|
195
|
+
> {
|
|
196
|
+
return new ContractGroup({
|
|
197
|
+
namespace: this._namespace,
|
|
198
|
+
meta: { ...this._meta, ...meta } as unknown as Omit<
|
|
199
|
+
TSharedMeta,
|
|
200
|
+
keyof TNewMeta
|
|
201
|
+
> &
|
|
202
|
+
TNewMeta,
|
|
203
|
+
responses: this._responses,
|
|
204
|
+
headers: this._headers,
|
|
205
|
+
pathPrefix: this._pathPrefix,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set shared response schemas for all contracts created from this group.
|
|
211
|
+
* Typically used for shared error responses (e.g. 401, 403, 500).
|
|
212
|
+
*/
|
|
213
|
+
responses<TNewResponses extends ContractResponses>(
|
|
214
|
+
responseSchemas: TNewResponses,
|
|
215
|
+
): ContractGroup<
|
|
216
|
+
Omit<TSharedResponses, keyof TNewResponses> & TNewResponses,
|
|
217
|
+
TSharedMeta,
|
|
218
|
+
TSharedHeaders,
|
|
219
|
+
TPathPrefix
|
|
220
|
+
> {
|
|
221
|
+
assertNoCatalogErrorStatusConflicts(this._meta, responseSchemas);
|
|
222
|
+
return new ContractGroup({
|
|
223
|
+
namespace: this._namespace,
|
|
224
|
+
meta: this._meta,
|
|
225
|
+
responses: { ...this._responses, ...responseSchemas } as unknown as Omit<
|
|
226
|
+
TSharedResponses,
|
|
227
|
+
keyof TNewResponses
|
|
228
|
+
> &
|
|
229
|
+
TNewResponses,
|
|
230
|
+
headers: this._headers,
|
|
231
|
+
pathPrefix: this._pathPrefix,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set shared route-owned application errors for all contracts in the group.
|
|
237
|
+
*/
|
|
238
|
+
errors<TErrorDefs extends ContractErrorResponses>(
|
|
239
|
+
errorDefs: TErrorDefs,
|
|
240
|
+
): ContractGroup<
|
|
241
|
+
Omit<TSharedResponses, keyof ResponsesFromErrorDefinitions<TErrorDefs>> &
|
|
242
|
+
ResponsesFromErrorDefinitions<TErrorDefs>,
|
|
243
|
+
Omit<TSharedMeta, "errors"> & { errors: TErrorDefs },
|
|
244
|
+
TSharedHeaders,
|
|
245
|
+
TPathPrefix
|
|
246
|
+
> {
|
|
247
|
+
const errorResponses = responsesFromErrors(errorDefs);
|
|
248
|
+
assertNoResponseStatusConflicts(this._responses, errorResponses);
|
|
249
|
+
return new ContractGroup({
|
|
250
|
+
namespace: this._namespace,
|
|
251
|
+
meta: {
|
|
252
|
+
...this._meta,
|
|
253
|
+
errors: errorDefs,
|
|
254
|
+
} as Omit<TSharedMeta, "errors"> & { errors: TErrorDefs },
|
|
255
|
+
responses: { ...this._responses, ...errorResponses } as unknown as Omit<
|
|
256
|
+
TSharedResponses,
|
|
257
|
+
keyof ResponsesFromErrorDefinitions<TErrorDefs>
|
|
258
|
+
> &
|
|
259
|
+
ResponsesFromErrorDefinitions<TErrorDefs>,
|
|
260
|
+
headers: this._headers,
|
|
261
|
+
pathPrefix: this._pathPrefix,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Add shared request header schema for all contracts created from this group.
|
|
267
|
+
*/
|
|
268
|
+
headers<TNewHeaders extends StandardSchema>(
|
|
269
|
+
schema: TNewHeaders,
|
|
270
|
+
): ContractGroup<
|
|
271
|
+
TSharedResponses,
|
|
272
|
+
TSharedMeta,
|
|
273
|
+
TSharedHeaders extends readonly StandardSchema[]
|
|
274
|
+
? readonly [...TSharedHeaders, TNewHeaders]
|
|
275
|
+
: TSharedHeaders extends StandardSchema
|
|
276
|
+
? readonly [TSharedHeaders, TNewHeaders]
|
|
277
|
+
: readonly [TNewHeaders],
|
|
278
|
+
TPathPrefix
|
|
279
|
+
> {
|
|
280
|
+
const existingHeaders = this._headers
|
|
281
|
+
? Array.isArray(this._headers)
|
|
282
|
+
? this._headers
|
|
283
|
+
: [this._headers]
|
|
284
|
+
: [];
|
|
285
|
+
|
|
286
|
+
return new ContractGroup({
|
|
287
|
+
namespace: this._namespace,
|
|
288
|
+
meta: this._meta,
|
|
289
|
+
responses: this._responses,
|
|
290
|
+
headers: [
|
|
291
|
+
...existingHeaders,
|
|
292
|
+
schema,
|
|
293
|
+
] as unknown as TSharedHeaders extends readonly StandardSchema[]
|
|
294
|
+
? readonly [...TSharedHeaders, TNewHeaders]
|
|
295
|
+
: TSharedHeaders extends StandardSchema
|
|
296
|
+
? readonly [TSharedHeaders, TNewHeaders]
|
|
297
|
+
: readonly [TNewHeaders],
|
|
298
|
+
pathPrefix: this._pathPrefix,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a GET contract
|
|
304
|
+
*/
|
|
305
|
+
get<const TPath extends string>(
|
|
306
|
+
path: TPath,
|
|
307
|
+
name?: string,
|
|
308
|
+
): ContractBuilder<
|
|
309
|
+
"GET",
|
|
310
|
+
null,
|
|
311
|
+
null,
|
|
312
|
+
null,
|
|
313
|
+
TSharedHeaders,
|
|
314
|
+
TSharedResponses,
|
|
315
|
+
TSharedMeta,
|
|
316
|
+
JoinPaths<TPathPrefix, TPath>
|
|
317
|
+
> {
|
|
318
|
+
return this.createBuilder("GET", path, name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Create a POST contract
|
|
323
|
+
*/
|
|
324
|
+
post<const TPath extends string>(
|
|
325
|
+
path: TPath,
|
|
326
|
+
name?: string,
|
|
327
|
+
): ContractBuilder<
|
|
328
|
+
"POST",
|
|
329
|
+
null,
|
|
330
|
+
null,
|
|
331
|
+
null,
|
|
332
|
+
TSharedHeaders,
|
|
333
|
+
TSharedResponses,
|
|
334
|
+
TSharedMeta,
|
|
335
|
+
JoinPaths<TPathPrefix, TPath>
|
|
336
|
+
> {
|
|
337
|
+
return this.createBuilder("POST", path, name);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a PUT contract
|
|
342
|
+
*/
|
|
343
|
+
put<const TPath extends string>(
|
|
344
|
+
path: TPath,
|
|
345
|
+
name?: string,
|
|
346
|
+
): ContractBuilder<
|
|
347
|
+
"PUT",
|
|
348
|
+
null,
|
|
349
|
+
null,
|
|
350
|
+
null,
|
|
351
|
+
TSharedHeaders,
|
|
352
|
+
TSharedResponses,
|
|
353
|
+
TSharedMeta,
|
|
354
|
+
JoinPaths<TPathPrefix, TPath>
|
|
355
|
+
> {
|
|
356
|
+
return this.createBuilder("PUT", path, name);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Create a PATCH contract
|
|
361
|
+
*/
|
|
362
|
+
patch<const TPath extends string>(
|
|
363
|
+
path: TPath,
|
|
364
|
+
name?: string,
|
|
365
|
+
): ContractBuilder<
|
|
366
|
+
"PATCH",
|
|
367
|
+
null,
|
|
368
|
+
null,
|
|
369
|
+
null,
|
|
370
|
+
TSharedHeaders,
|
|
371
|
+
TSharedResponses,
|
|
372
|
+
TSharedMeta,
|
|
373
|
+
JoinPaths<TPathPrefix, TPath>
|
|
374
|
+
> {
|
|
375
|
+
return this.createBuilder("PATCH", path, name);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create a DELETE contract
|
|
380
|
+
*/
|
|
381
|
+
delete<const TPath extends string>(
|
|
382
|
+
path: TPath,
|
|
383
|
+
name?: string,
|
|
384
|
+
): ContractBuilder<
|
|
385
|
+
"DELETE",
|
|
386
|
+
null,
|
|
387
|
+
null,
|
|
388
|
+
null,
|
|
389
|
+
TSharedHeaders,
|
|
390
|
+
TSharedResponses,
|
|
391
|
+
TSharedMeta,
|
|
392
|
+
JoinPaths<TPathPrefix, TPath>
|
|
393
|
+
> {
|
|
394
|
+
return this.createBuilder("DELETE", path, name);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a HEAD contract
|
|
399
|
+
*/
|
|
400
|
+
head<const TPath extends string>(
|
|
401
|
+
path: TPath,
|
|
402
|
+
name?: string,
|
|
403
|
+
): ContractBuilder<
|
|
404
|
+
"HEAD",
|
|
405
|
+
null,
|
|
406
|
+
null,
|
|
407
|
+
null,
|
|
408
|
+
TSharedHeaders,
|
|
409
|
+
TSharedResponses,
|
|
410
|
+
TSharedMeta,
|
|
411
|
+
JoinPaths<TPathPrefix, TPath>
|
|
412
|
+
> {
|
|
413
|
+
return this.createBuilder("HEAD", path, name);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Create a OPTIONS contract
|
|
418
|
+
*/
|
|
419
|
+
options<const TPath extends string>(
|
|
420
|
+
path: TPath,
|
|
421
|
+
name?: string,
|
|
422
|
+
): ContractBuilder<
|
|
423
|
+
"OPTIONS",
|
|
424
|
+
null,
|
|
425
|
+
null,
|
|
426
|
+
null,
|
|
427
|
+
TSharedHeaders,
|
|
428
|
+
TSharedResponses,
|
|
429
|
+
TSharedMeta,
|
|
430
|
+
JoinPaths<TPathPrefix, TPath>
|
|
431
|
+
> {
|
|
432
|
+
return this.createBuilder("OPTIONS", path, name);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Internal helper to create a contract builder with shared config
|
|
437
|
+
*/
|
|
438
|
+
private createBuilder<TMethod extends HttpMethod, const TPath extends string>(
|
|
439
|
+
method: TMethod,
|
|
440
|
+
path: TPath,
|
|
441
|
+
name?: string,
|
|
442
|
+
): ContractBuilder<
|
|
443
|
+
TMethod,
|
|
444
|
+
null,
|
|
445
|
+
null,
|
|
446
|
+
null,
|
|
447
|
+
TSharedHeaders,
|
|
448
|
+
TSharedResponses,
|
|
449
|
+
TSharedMeta,
|
|
450
|
+
JoinPaths<TPathPrefix, TPath>
|
|
451
|
+
> {
|
|
452
|
+
parsePathTemplate(path);
|
|
453
|
+
const fullPath = (
|
|
454
|
+
this._pathPrefix ? joinPathPrefix(this._pathPrefix, path) : path
|
|
455
|
+
) as JoinPaths<TPathPrefix, TPath>;
|
|
456
|
+
parsePathTemplate(fullPath);
|
|
457
|
+
const contractName = name || generateContractName(method, fullPath);
|
|
458
|
+
const fullName = this._namespace
|
|
459
|
+
? `${this._namespace}.${contractName}`
|
|
460
|
+
: contractName;
|
|
461
|
+
|
|
462
|
+
return new ContractBuilder({
|
|
463
|
+
kind: "http",
|
|
464
|
+
name: fullName,
|
|
465
|
+
namespace: this._namespace || undefined,
|
|
466
|
+
localName: contractName,
|
|
467
|
+
method,
|
|
468
|
+
path: fullPath,
|
|
469
|
+
pathParams: null,
|
|
470
|
+
query: null,
|
|
471
|
+
headers: this._headers,
|
|
472
|
+
body: null,
|
|
473
|
+
responses: { ...this._responses } as unknown as TSharedResponses,
|
|
474
|
+
metadata: { ...this._meta } as unknown as TSharedMeta,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a new ContractGroup for grouping related contracts with shared configuration.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```ts
|
|
484
|
+
* const todos = createContractGroup()
|
|
485
|
+
* .namespace("todos")
|
|
486
|
+
* .prefix("/api/todos")
|
|
487
|
+
* .meta({ auth: "required" })
|
|
488
|
+
* .responses({
|
|
489
|
+
* 401: z.object({ message: z.literal("Unauthorized") }),
|
|
490
|
+
* });
|
|
491
|
+
*
|
|
492
|
+
* const getTodo = todos.get("/:id")...
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export function createContractGroup(): ContractGroup<
|
|
496
|
+
Record<never, never>,
|
|
497
|
+
ContractMeta,
|
|
498
|
+
null,
|
|
499
|
+
""
|
|
500
|
+
> {
|
|
501
|
+
return new ContractGroup();
|
|
502
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HttpContractConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type that represents either a raw HttpContractConfig
|
|
5
|
+
* or a contract-like object with a .config property (e.g., ContractBuilder)
|
|
6
|
+
*/
|
|
7
|
+
export type ContractLike = HttpContractConfig | { config: HttpContractConfig };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a ContractLike to its underlying HttpContractConfig
|
|
11
|
+
*/
|
|
12
|
+
export type ResolveContract<T> = T extends { config: infer C }
|
|
13
|
+
? C extends HttpContractConfig
|
|
14
|
+
? C
|
|
15
|
+
: never
|
|
16
|
+
: T extends HttpContractConfig
|
|
17
|
+
? T
|
|
18
|
+
: never;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper function to resolve a ContractLike to its underlying HttpContractConfig
|
|
22
|
+
* @param c - The contract-like object to resolve
|
|
23
|
+
* @returns The underlying HttpContractConfig
|
|
24
|
+
*/
|
|
25
|
+
export function resolveContract<T extends ContractLike>(
|
|
26
|
+
c: T,
|
|
27
|
+
): ResolveContract<T> {
|
|
28
|
+
return ("config" in c ? c.config : c) as ResolveContract<T>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/core/contracts
|
|
3
|
+
*
|
|
4
|
+
* HTTP contract definitions and builders for Beignet.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
ContractBuilder,
|
|
11
|
+
type CreateContractOptions,
|
|
12
|
+
createContract,
|
|
13
|
+
} from "./contract-builder";
|
|
14
|
+
export { ContractGroup, createContractGroup } from "./contract-group";
|
|
15
|
+
export {
|
|
16
|
+
type ContractLike,
|
|
17
|
+
type ResolveContract,
|
|
18
|
+
resolveContract,
|
|
19
|
+
} from "./contract-like";
|
|
20
|
+
export type { OpenAPIOperationMeta } from "./openapi-meta";
|
|
21
|
+
export {
|
|
22
|
+
type ParsedPathTemplate,
|
|
23
|
+
type PathTemplateSegment,
|
|
24
|
+
parsePathTemplate,
|
|
25
|
+
} from "./path-template";
|
|
26
|
+
export type { RateLimitMeta, RateLimitScope } from "./rate-limit";
|
|
27
|
+
export type {
|
|
28
|
+
AnyContract,
|
|
29
|
+
BodyHttpMethod,
|
|
30
|
+
ContractErrorDefinition,
|
|
31
|
+
ContractErrorResponses,
|
|
32
|
+
ContractHeaderSchemas,
|
|
33
|
+
ContractMeta,
|
|
34
|
+
ContractResponseSchema,
|
|
35
|
+
ContractResponses,
|
|
36
|
+
HttpContractConfig,
|
|
37
|
+
HttpMethod,
|
|
38
|
+
InferHeaderSchemaInput,
|
|
39
|
+
InferHeaderSchemaOutput,
|
|
40
|
+
InferInput,
|
|
41
|
+
InferOutput,
|
|
42
|
+
ResponsesFromErrorDefinitions,
|
|
43
|
+
StandardErrorResponseBody,
|
|
44
|
+
StandardErrorResponseSchema,
|
|
45
|
+
StandardSchema,
|
|
46
|
+
} from "./types";
|
|
47
|
+
export {
|
|
48
|
+
BEIGNET_ERROR_OWNER_HEADER,
|
|
49
|
+
BODY_HTTP_METHODS,
|
|
50
|
+
getContractHeaderSchemas,
|
|
51
|
+
methodSupportsRequestBody,
|
|
52
|
+
STANDARD_ERROR_RESPONSE_SCHEMA,
|
|
53
|
+
} from "./types";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI operation-level metadata for HTTP contracts
|
|
3
|
+
*/
|
|
4
|
+
export type OpenAPIOperationMeta = {
|
|
5
|
+
/** A brief summary of the operation */
|
|
6
|
+
summary?: string;
|
|
7
|
+
/** A detailed description of the operation */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Tags for grouping operations */
|
|
10
|
+
tags?: string[];
|
|
11
|
+
/** Marks the operation as deprecated */
|
|
12
|
+
deprecated?: boolean;
|
|
13
|
+
/** External documentation reference */
|
|
14
|
+
externalDocs?: {
|
|
15
|
+
description?: string;
|
|
16
|
+
url: string;
|
|
17
|
+
};
|
|
18
|
+
/** Override for operationId (default is contract.name) */
|
|
19
|
+
operationId?: string;
|
|
20
|
+
/** Per-operation security requirements */
|
|
21
|
+
security?: Array<Record<string, string[]>>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type PathTemplateSegment =
|
|
2
|
+
| { kind: "static"; value: string }
|
|
3
|
+
| { kind: "dynamic"; name: string; raw: string };
|
|
4
|
+
|
|
5
|
+
export interface ParsedPathTemplate {
|
|
6
|
+
keys: string[];
|
|
7
|
+
segments: PathTemplateSegment[];
|
|
8
|
+
normalizedPath: string;
|
|
9
|
+
shapeKey: string;
|
|
10
|
+
openApiPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PARAM_NAME = "[A-Za-z0-9_-]+";
|
|
14
|
+
const COLON_PARAM = new RegExp(`^:(${PARAM_NAME})$`);
|
|
15
|
+
const BRACKET_PARAM = new RegExp(`^\\[(${PARAM_NAME})\\]$`);
|
|
16
|
+
|
|
17
|
+
function createPathTemplateError(path: string, segment: string): Error {
|
|
18
|
+
return new Error(
|
|
19
|
+
`Unsupported path template segment "${segment}" in "${path}". ` +
|
|
20
|
+
"Use single-segment params like :id or [id]. Catch-all and partial-segment params are not supported.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parsePathSegment(path: string, segment: string): PathTemplateSegment {
|
|
25
|
+
const colonMatch = segment.match(COLON_PARAM);
|
|
26
|
+
if (colonMatch) {
|
|
27
|
+
return { kind: "dynamic", name: colonMatch[1], raw: segment };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bracketMatch = segment.match(BRACKET_PARAM);
|
|
31
|
+
if (bracketMatch) {
|
|
32
|
+
return { kind: "dynamic", name: bracketMatch[1], raw: segment };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
segment.startsWith(":") ||
|
|
37
|
+
segment.startsWith("[") ||
|
|
38
|
+
segment.endsWith("]")
|
|
39
|
+
) {
|
|
40
|
+
throw createPathTemplateError(path, segment);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { kind: "static", value: segment };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parsePathTemplate(path: string): ParsedPathTemplate {
|
|
47
|
+
if (!path.startsWith("/")) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid path template "${path}". Paths must start with "/".`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const segments = path
|
|
54
|
+
.split("/")
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((segment) => parsePathSegment(path, segment));
|
|
57
|
+
|
|
58
|
+
const keys = segments
|
|
59
|
+
.filter(
|
|
60
|
+
(segment): segment is Extract<PathTemplateSegment, { kind: "dynamic" }> =>
|
|
61
|
+
segment.kind === "dynamic",
|
|
62
|
+
)
|
|
63
|
+
.map((segment) => segment.name);
|
|
64
|
+
|
|
65
|
+
const duplicateKeys = keys.filter(
|
|
66
|
+
(key, index) => keys.indexOf(key) !== index,
|
|
67
|
+
);
|
|
68
|
+
if (duplicateKeys.length) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid path template "${path}". Path parameter names must be unique; duplicate parameter "${duplicateKeys[0]}" was found.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalizedPath = `/${segments
|
|
75
|
+
.map((segment) =>
|
|
76
|
+
segment.kind === "static" ? segment.value : `:${segment.name}`,
|
|
77
|
+
)
|
|
78
|
+
.join("/")}`;
|
|
79
|
+
|
|
80
|
+
const shapeKey = `/${segments
|
|
81
|
+
.map((segment) => (segment.kind === "static" ? segment.value : ":"))
|
|
82
|
+
.join("/")}`;
|
|
83
|
+
|
|
84
|
+
const openApiPath = `/${segments
|
|
85
|
+
.map((segment) =>
|
|
86
|
+
segment.kind === "static" ? segment.value : `{${segment.name}}`,
|
|
87
|
+
)
|
|
88
|
+
.join("/")}`;
|
|
89
|
+
|
|
90
|
+
return { keys, segments, normalizedPath, shapeKey, openApiPath };
|
|
91
|
+
}
|