@loutrejs/loutre 0.4.0 → 0.5.0

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.
@@ -1,4 +1,5 @@
1
1
  import { argumentsProvider, asModuleInstance, childPipelineOf, contextKeyName, defineModule, isArgsClass, isEnvClass, layerDefinitionOf, normalizeProvider, queueRuntimeToken, tokenName, } from '../core/index.js';
2
+ import { contractOfBinding, contractRootOf, resolveContractProcedureIdentity, } from '../core/contract-internal.js';
2
3
  import { Container, Logger } from '../runtime/index.js';
3
4
  export class StaticValidationError extends Error {
4
5
  diagnostics;
@@ -38,6 +39,7 @@ export function compileApplication(input) {
38
39
  validateNamedDescriptors(queues, 'LUTRE_QUEUE_DUPLICATE', diagnostics);
39
40
  validateCronTriggers(triggers, diagnostics);
40
41
  diagnostics.push(...validateQueueDrivers(input.modules, queueTriggers));
42
+ const applicationContract = input.contract === undefined ? undefined : contractOfBinding(input.contract);
41
43
  const contractIds = new Map();
42
44
  const idContract = (contract) => {
43
45
  const current = contractIds.get(contract);
@@ -56,29 +58,36 @@ export function compileApplication(input) {
56
58
  implementationIds.set(implementation, id);
57
59
  return id;
58
60
  };
61
+ if (applicationContract)
62
+ idContract(applicationContract);
59
63
  const descriptors = modules.flatMap((module) => module.definition.implementations ?? []);
60
64
  for (const implementation of new Set(descriptors)) {
61
- idContract(implementation.contract);
65
+ const firstProcedure = implementation.procedures[0];
66
+ const identity = firstProcedure === undefined
67
+ ? { contract: contractRootOf(implementation.contract) }
68
+ : resolveContractProcedureIdentity(implementation.contract, firstProcedure, applicationContract);
69
+ idContract(identity.contract);
62
70
  idImplementation(implementation);
63
71
  }
64
72
  const targets = [];
65
73
  for (const implementation of descriptors) {
66
74
  for (const procedureName of implementation.procedures) {
75
+ const identity = resolveContractProcedureIdentity(implementation.contract, procedureName, applicationContract);
67
76
  const procedure = implementation.contract.procedures[procedureName];
68
77
  const protocol = procedure?.protocols[implementation.protocol];
69
78
  if (!procedure || !protocol) {
70
79
  diagnostics.push({
71
80
  code: 'LUTRE_IMPL_003',
72
81
  message: `${procedureName} is not declared for protocol ${implementation.protocol}`,
73
- path: `${idContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
82
+ path: `${idContract(identity.contract)}.${identity.procedure}.${implementation.protocol}`,
74
83
  });
75
84
  continue;
76
85
  }
77
86
  targets.push({
78
87
  implementation,
79
- contractId: idContract(implementation.contract),
88
+ contractId: idContract(identity.contract),
80
89
  implementationId: idImplementation(implementation),
81
- procedure: procedureName,
90
+ procedure: identity.procedure,
82
91
  protocol: implementation.protocol,
83
92
  dispatchKey: protocol.dispatchKey,
84
93
  pipeline: protocol.pipeline ?? protocol.definition?.pipeline ?? [],
@@ -90,7 +99,7 @@ export function compileApplication(input) {
90
99
  }
91
100
  }
92
101
  validateDispatchKeys(targets, diagnostics);
93
- validateCoverage(descriptors, contractIds, diagnostics);
102
+ validateCoverage(descriptors, targets, contractIds, diagnostics, applicationContract);
94
103
  validateDuplicateProviders(modules, diagnostics);
95
104
  const tokensById = collectCustomTokens(providers, targets, diagnostics);
96
105
  const contextKeysByName = collectContextKeys(targets, diagnostics);
@@ -104,13 +113,17 @@ export function compileApplication(input) {
104
113
  layers: target.pipeline.map(toLayerIR),
105
114
  });
106
115
  }
107
- const implementations = [...new Set(descriptors)].map((implementation) => ({
108
- id: idImplementation(implementation),
109
- name: implementation.name,
110
- contract: idContract(implementation.contract),
111
- protocol: implementation.protocol,
112
- procedures: implementation.procedures,
113
- }));
116
+ const implementations = [...new Set(descriptors)].map((implementation) => {
117
+ const implementationTargets = targets.filter((target) => target.implementation === implementation);
118
+ return {
119
+ id: idImplementation(implementation),
120
+ name: implementation.name,
121
+ contract: implementationTargets[0]?.contractId ??
122
+ idContract(contractRootOf(implementation.contract)),
123
+ protocol: implementation.protocol,
124
+ procedures: implementationTargets.map((target) => target.procedure),
125
+ };
126
+ });
114
127
  const dependencyGraph = buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics);
115
128
  const probedTokenIds = dependencyGraph.nodes
116
129
  .filter((node) => node.kind === 'token')
@@ -272,7 +285,7 @@ function validateDispatchKeys(targets, diagnostics) {
272
285
  const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
273
286
  const existing = targetsByKey.get(target.dispatchKey);
274
287
  if (existing) {
275
- if (existing.implementation.contract === target.implementation.contract &&
288
+ if (existing.contractId === target.contractId &&
276
289
  existing.procedure === target.procedure &&
277
290
  existing.protocol === target.protocol) {
278
291
  continue;
@@ -914,18 +927,20 @@ function collectModules(roots) {
914
927
  visit(root);
915
928
  return modules;
916
929
  }
917
- function validateCoverage(implementations, contractIds, diagnostics) {
918
- const contracts = new Set(implementations.map((implementation) => implementation.contract));
930
+ function validateCoverage(implementations, targets, contractIds, diagnostics, applicationContract) {
931
+ const contracts = applicationContract
932
+ ? [applicationContract]
933
+ : [
934
+ ...new Set(implementations.map((implementation) => contractRootOf(implementation.contract))),
935
+ ];
919
936
  for (const contract of contracts) {
920
- const protocols = new Set(Object.values(contract.procedures).flatMap((procedure) => Object.keys(procedure.protocols)));
921
- for (const protocol of protocols) {
922
- for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
923
- if (!(protocol in procedure.protocols))
924
- continue;
925
- const covering = implementations.filter((implementation) => implementation.contract === contract &&
926
- implementation.protocol === protocol &&
927
- implementation.procedures.includes(procedureName));
928
- const path = `${contractIds.get(contract) ?? 'contract:unknown'}.${procedureName}.${protocol}`;
937
+ const contractId = contractIds.get(contract) ?? 'contract:unknown';
938
+ for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
939
+ for (const protocol of Object.keys(procedure.protocols)) {
940
+ const covering = targets.filter((target) => target.contractId === contractId &&
941
+ target.protocol === protocol &&
942
+ target.procedure === procedureName);
943
+ const path = `${contractId}.${procedureName}.${protocol}`;
929
944
  if (covering.length === 0) {
930
945
  diagnostics.push({
931
946
  code: 'LUTRE_IMPL_001',
@@ -363,7 +363,16 @@ async function finalizeResponse(definition, result, signal) {
363
363
  });
364
364
  }
365
365
  const body = await validateSchema(response.body, result.body);
366
- return jsonResponse(response.status, body, mergeResponseHeaders(response.staticHeaders, responseHeaders));
366
+ const headers = mergeResponseHeaders(response.staticHeaders, responseHeaders);
367
+ if (response.status === 204 ||
368
+ response.status === 205 ||
369
+ response.status === 304) {
370
+ if (body !== undefined) {
371
+ throw new Error(`HTTP ${response.status} response body must be undefined`);
372
+ }
373
+ return new Response(undefined, { status: response.status, headers });
374
+ }
375
+ return jsonResponse(response.status, body, headers);
367
376
  }
368
377
  async function validateResponseHeaders(schema, headers) {
369
378
  if (!schema) {
@@ -1,4 +1,4 @@
1
- import { type ContractDefinition, type ProcedureDefinition, type SchemaInput, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
1
+ import { type ContractBinding, type ContractDefinition, type ContractOfBinding, type ProcedureDefinition, type SchemaInput, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
2
2
  import type { HttpHeaders, HttpParamsSchemas, HttpProtocol, HttpProtocolDefinition, HttpRequestBodyDefinition, HttpResponseDefinition } from './definitions.js';
3
3
  import { type PathParamNames, type RawPathParams } from './path.js';
4
4
  export interface HttpClientTransportRequest {
@@ -52,8 +52,8 @@ export type HttpClientResponse<TDefinition extends HttpProtocolDefinition> = {
52
52
  } : never;
53
53
  }[keyof TDefinition['responses'] & string];
54
54
  type HttpClientMethod<TDefinition extends HttpProtocolDefinition> = keyof HttpClientRequest<TDefinition> extends never ? () => Promise<HttpClientResponse<TDefinition>> : (request: HttpClientRequest<TDefinition>) => Promise<HttpClientResponse<TDefinition>>;
55
- export type HttpClient<TContract extends ContractDefinition> = {
56
- readonly [TProcedure in HttpProcedureNames<TContract>]: HttpClientMethod<HttpDefinitionOf<TContract, TProcedure>>;
55
+ export type HttpClient<TBinding extends ContractBinding> = {
56
+ readonly [TProcedure in HttpProcedureNames<ContractOfBinding<TBinding>>]: HttpClientMethod<HttpDefinitionOf<ContractOfBinding<TBinding>, TProcedure>>;
57
57
  };
58
58
  export declare class HttpClientResponseError extends Error {
59
59
  readonly status: number;
@@ -62,7 +62,7 @@ export declare class HttpClientResponseError extends Error {
62
62
  readonly procedure: string;
63
63
  constructor(status: number, method: string, path: string, procedure: string, message: string, options?: ErrorOptions);
64
64
  }
65
- export declare function createHttpClient<const TContract extends ContractDefinition>(contract: TContract, transport: HttpClientTransport): HttpClient<TContract>;
65
+ export declare function createHttpClient<const TBinding extends ContractBinding>(binding: TBinding, transport: HttpClientTransport): HttpClient<TBinding>;
66
66
  export interface FetchHttpTransportOptions {
67
67
  readonly baseUrl: string | URL;
68
68
  readonly fetch?: typeof globalThis.fetch;
@@ -1,4 +1,5 @@
1
1
  import { SchemaValidationError, validateSchema, } from '../core/index.js';
2
+ import { contractOfBinding } from '../core/contract-internal.js';
2
3
  import { parseHttpPath, } from './path.js';
3
4
  export class HttpClientResponseError extends Error {
4
5
  status;
@@ -14,7 +15,8 @@ export class HttpClientResponseError extends Error {
14
15
  this.name = 'HttpClientResponseError';
15
16
  }
16
17
  }
17
- export function createHttpClient(contract, transport) {
18
+ export function createHttpClient(binding, transport) {
19
+ const contract = contractOfBinding(binding);
18
20
  const client = {};
19
21
  for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
20
22
  const definition = httpDefinitionOf(procedure);
@@ -189,8 +191,11 @@ function encodeBody(body, contentType) {
189
191
  return body;
190
192
  }
191
193
  async function decodeFetchBody(response) {
192
- if (response.status === 204 || response.status === 205)
194
+ if (response.status === 204 ||
195
+ response.status === 205 ||
196
+ response.status === 304) {
193
197
  return undefined;
198
+ }
194
199
  const mediaType = normalizeMediaType(response.headers.get('content-type'));
195
200
  if (mediaType === 'text/event-stream') {
196
201
  if (!response.body) {
@@ -1,6 +1,8 @@
1
- import type { ContractDefinition, ContextProvidedBeforeTerminal, HasValidationBeforeTerminal, IsValidProtocolPipeline, PipelineItem, ProtocolDescriptor, ProtocolFactory, ProtocolGroup, SchemaInput, SchemaOutput, ShortCircuitDeclarationsOf, ShortCircuitResultOf, StandardSchemaV1, TerminalLayerDescriptor, ValidationLayerDescriptor } from '../core/index.js';
1
+ import type { ContractBinding, ContractDefinition, ContractOfBinding, ResolvedContractNode, ContextProvidedBeforeTerminal, HasValidationBeforeTerminal, IsValidProtocolPipeline, PipelineItem, ProcedureDefinition, ProtocolDescriptor, ProtocolFactory, ProtocolGroup, SchemaInput, SchemaOutput, ShortCircuitDeclarationsOf, ShortCircuitResultOf, StandardSchemaV1, TerminalLayerDescriptor, ValidationLayerDescriptor } from '../core/index.js';
2
+ import { protocolNamespaceType } from '../core/contract-internal.js';
3
+ import { httpNodeMetadata } from './internal.js';
2
4
  import type { Logger } from '../runtime/index.js';
3
- import { type HttpDispatchKey, type IsValidHttpPath, type PathParamNames, type RawPathParams } from './path.js';
5
+ import { type HttpDispatchKey, type IsValidHttpMethod, type IsValidHttpPath, type PathParamNames, type RawPathParams } from './path.js';
4
6
  export type HttpParamsSchemas = Readonly<Record<string, StandardSchemaV1>>;
5
7
  export interface HttpRequestBodyDefinition<TSchema extends StandardSchemaV1 = StandardSchemaV1> {
6
8
  readonly contentType: string;
@@ -49,8 +51,110 @@ export interface HttpProtocolDefinition {
49
51
  readonly pipeline: readonly PipelineItem[];
50
52
  readonly interaction?: 'unary' | 'server-stream';
51
53
  }
52
- export interface HttpProtocol<TDefinition extends HttpProtocolDefinition = HttpProtocolDefinition> extends ProtocolDescriptor<'http', HttpControllerContextDefinition<TDefinition>, DeclaredHttpResults<TDefinition['responses']>, HttpDispatchKey<TDefinition['method'], TDefinition['path']>> {
54
+ export interface HttpBranchDefinition {
55
+ readonly path?: string;
56
+ readonly pipeline?: readonly PipelineItem[];
57
+ readonly responses?: Readonly<Record<string, HttpResponseDefinition>>;
58
+ readonly routes: HttpRouteTree;
59
+ }
60
+ export type HttpRouteTree = Readonly<Record<string, HttpProtocolDefinition | HttpBranchDefinition | HttpResolvedNode>>;
61
+ interface HttpResolvedNodeMetadata<TSource extends HttpProtocolDefinition | HttpBranchDefinition = HttpProtocolDefinition | HttpBranchDefinition, THandlerName extends string | never = string | never> {
62
+ readonly kind: 'leaf' | 'branch';
63
+ readonly source: TSource;
64
+ readonly handlerName: THandlerName;
65
+ }
66
+ export type HttpResolvedLeaf<THandlerName extends string = string, TSource extends HttpProtocolDefinition = HttpProtocolDefinition, TEffective extends HttpProtocolDefinition = HttpProtocolDefinition> = ResolvedContractNode<ContractDefinition<{
67
+ readonly [K in THandlerName]: {
68
+ readonly kind: 'procedure';
69
+ readonly protocols: {
70
+ readonly http: HttpProtocol<TEffective>;
71
+ };
72
+ };
73
+ }>, 'leaf'> & {
74
+ readonly [httpNodeMetadata]: HttpResolvedNodeMetadata<TSource, THandlerName> & {
75
+ readonly kind: 'leaf';
76
+ };
77
+ };
78
+ type ResolvedContractOf<TNode> = TNode extends ResolvedContractNode<infer TContract> ? TContract : never;
79
+ type HttpProtocolOfResolvedLeaf<TNode> = ResolvedContractOf<TNode> extends ContractDefinition<infer TProcedures> ? TProcedures[keyof TProcedures & string]['protocols'] extends {
80
+ readonly http: infer TProtocol extends ProtocolDescriptor<'http'>;
81
+ } ? TProtocol : never : never;
82
+ type FlatResolvedHttpProcedures<TChildren extends Readonly<Record<string, unknown>>, TPrefix extends string = ''> = UnionToIntersection<{
83
+ [K in keyof TChildren & string]: TChildren[K] extends HttpResolvedLeaf<any, any, any> ? {
84
+ readonly [P in JoinProcedurePath<TPrefix, K>]: ProcedureDefinition<{
85
+ readonly http: HttpProtocolOfResolvedLeaf<TChildren[K]>;
86
+ }>;
87
+ } : TChildren[K] extends HttpResolvedBranch<any, infer TGrandchildren> ? FlatResolvedHttpProcedures<TGrandchildren, JoinProcedurePath<TPrefix, K>> : never;
88
+ }[keyof TChildren & string]> extends infer TProcedures extends Record<string, ProcedureDefinition> ? TProcedures : never;
89
+ export type HttpResolvedBranch<TSource extends HttpBranchDefinition = HttpBranchDefinition, TChildren extends Readonly<Record<string, HttpResolvedNode>> = Readonly<Record<string, HttpResolvedNode>>> = ResolvedContractNode<ContractDefinition<FlatResolvedHttpProcedures<TChildren>>, 'branch'> & TChildren & {
90
+ readonly [httpNodeMetadata]: HttpResolvedNodeMetadata<TSource, never> & {
91
+ readonly kind: 'branch';
92
+ };
93
+ };
94
+ export type HttpResolvedNode = ResolvedContractNode<ContractDefinition, 'leaf' | 'branch'> & {
95
+ readonly [httpNodeMetadata]: HttpResolvedNodeMetadata;
96
+ };
97
+ type SourceOfHttpNode<TNode> = TNode extends {
98
+ readonly [httpNodeMetadata]: HttpResolvedNodeMetadata<infer TSource, any>;
99
+ } ? TSource : TNode;
100
+ type HandlerNameOfHttpNode<TName extends string, TNode> = TNode extends {
101
+ readonly [httpNodeMetadata]: HttpResolvedNodeMetadata<any, infer THandlerName>;
102
+ } ? [THandlerName] extends [never] ? TName : THandlerName & string : TName;
103
+ type BranchPathOf<TBranch extends HttpBranchDefinition> = TBranch extends {
104
+ readonly path: infer TPath extends string;
105
+ } ? TPath : '';
106
+ type BranchPipelineOf<TBranch extends HttpBranchDefinition> = TBranch extends {
107
+ readonly pipeline: infer TPipeline extends readonly PipelineItem[];
108
+ } ? TPipeline : readonly [];
109
+ type BranchResponsesOf<TBranch extends HttpBranchDefinition> = TBranch extends {
110
+ readonly responses: infer TResponses extends Readonly<Record<string, HttpResponseDefinition>>;
111
+ } ? TResponses : {};
112
+ type JoinHttpPath<TParent extends string, TChild extends string> = TParent extends '' ? TChild : TChild extends '/' ? TParent : `${TParent}${TChild}`;
113
+ type MergeHttpResponses<TParent extends Readonly<Record<string, HttpResponseDefinition>>, TLocal extends Readonly<Record<string, HttpResponseDefinition>>> = Extract<keyof TParent, keyof TLocal> extends never ? TParent & TLocal : never;
114
+ type EffectiveHttpDefinition<TDefinition extends HttpProtocolDefinition, TParentPath extends string, TParentPipeline extends readonly PipelineItem[], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>>> = {
115
+ readonly method: TDefinition['method'];
116
+ readonly path: JoinHttpPath<TParentPath, TDefinition['path']>;
117
+ readonly responses: MergeHttpResponses<TParentResponses, TDefinition['responses']>;
118
+ readonly pipeline: readonly [...TParentPipeline, ...TDefinition['pipeline']];
119
+ } & (TDefinition extends {
120
+ readonly summary: infer TValue extends string;
121
+ } ? {
122
+ readonly summary: TValue;
123
+ } : {}) & (TDefinition extends {
124
+ readonly description: infer TValue extends string;
125
+ } ? {
126
+ readonly description: TValue;
127
+ } : {}) & (TDefinition extends {
128
+ readonly tags: infer TValue extends readonly string[];
129
+ } ? {
130
+ readonly tags: TValue;
131
+ } : {}) & (TDefinition extends {
132
+ readonly deprecated: infer TValue extends boolean;
133
+ } ? {
134
+ readonly deprecated: TValue;
135
+ } : {}) & (TDefinition extends {
136
+ readonly request: infer TValue extends HttpRequestDefinition;
137
+ } ? {
138
+ readonly request: TValue;
139
+ } : {}) & (TDefinition extends {
140
+ readonly interaction: infer TValue extends 'unary' | 'server-stream';
141
+ } ? {
142
+ readonly interaction: TValue;
143
+ } : {});
144
+ type ResolvedHttpTree<TTree extends Readonly<Record<string, unknown>>, TParentPath extends string = '', TParentPipeline extends readonly PipelineItem[] = readonly [], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>> = {}> = {
145
+ readonly [K in keyof TTree]: ResolveHttpNode<K & string, TTree[K], TParentPath, TParentPipeline, TParentResponses>;
146
+ };
147
+ type ResolveHttpNode<TName extends string, TNode, TParentPath extends string, TParentPipeline extends readonly PipelineItem[], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>>> = SourceOfHttpNode<TNode> extends infer TSource ? TSource extends HttpProtocolDefinition ? HttpResolvedLeaf<HandlerNameOfHttpNode<TName, TNode>, TSource, EffectiveHttpDefinition<TSource, TParentPath, TParentPipeline, TParentResponses>> : TSource extends HttpBranchDefinition ? HttpResolvedBranch<TSource, ResolvedHttpTree<TSource['routes'], JoinHttpPath<TParentPath, BranchPathOf<TSource>>, readonly [...TParentPipeline, ...BranchPipelineOf<TSource>], MergeHttpResponses<TParentResponses, BranchResponsesOf<TSource>>>> : never : never;
148
+ type JoinProcedurePath<TPrefix extends string, TName extends string> = TPrefix extends '' ? TName : `${TPrefix}.${TName}`;
149
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
150
+ type FlatHttpProtocols<TTree extends Readonly<Record<string, unknown>>, TParentPath extends string = '', TParentPipeline extends readonly PipelineItem[] = readonly [], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>> = {}, TProcedurePrefix extends string = '', TRootTree extends Readonly<Record<string, unknown>> = TTree> = UnionToIntersection<{
151
+ [K in keyof TTree & string]: SourceOfHttpNode<TTree[K]> extends infer TSource ? TSource extends HttpProtocolDefinition ? {
152
+ readonly [P in JoinProcedurePath<TProcedurePrefix, K>]: HttpProtocol<EffectiveHttpDefinition<TSource, TParentPath, TParentPipeline, TParentResponses>, ResolvedHttpTree<TRootTree>>;
153
+ } : TSource extends HttpBranchDefinition ? FlatHttpProtocols<TSource['routes'], JoinHttpPath<TParentPath, BranchPathOf<TSource>>, readonly [...TParentPipeline, ...BranchPipelineOf<TSource>], MergeHttpResponses<TParentResponses, BranchResponsesOf<TSource>>, JoinProcedurePath<TProcedurePrefix, K>, TRootTree> : never : never;
154
+ }[keyof TTree & string]>;
155
+ export interface HttpProtocol<TDefinition extends HttpProtocolDefinition = HttpProtocolDefinition, TNamespace = unknown> extends ProtocolDescriptor<'http', HttpControllerContextDefinition<TDefinition>, DeclaredHttpResults<TDefinition['responses']>, HttpDispatchKey<TDefinition['method'], TDefinition['path']>> {
53
156
  readonly definition: TDefinition;
157
+ readonly [protocolNamespaceType]: TNamespace;
54
158
  readonly interaction: TDefinition extends {
55
159
  readonly interaction: infer TInteraction;
56
160
  } ? TInteraction & ('unary' | 'server-stream') : 'unary';
@@ -88,6 +192,14 @@ type HttpPipelineConstraint<TDefinition extends HttpProtocolDefinition> = IsVali
88
192
  } : {
89
193
  readonly pipeline: never;
90
194
  };
195
+ type HttpStatusDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
196
+ type HttpStatusHundreds = '2' | '3' | '4' | '5';
197
+ type IsValidHttpResponseStatus<TStatus extends number> = number extends TStatus ? false : `${TStatus}` extends `${HttpStatusHundreds}${HttpStatusDigit}${HttpStatusDigit}` ? true : false;
198
+ type IsBodylessHttpStatus<TStatus extends number> = TStatus extends 204 | 205 | 304 ? true : false;
199
+ type IsResponseStatusCompatible<TResponse> = TResponse extends HttpResponseDefinition ? IsValidHttpResponseStatus<TResponse['status']> extends true ? IsBodylessHttpStatus<TResponse['status']> extends true ? [SchemaOutput<TResponse['body']>] extends [undefined] ? true : false : true : false : false;
200
+ type AreResponseStatusesCompatible<TResponses extends HttpProtocolDefinition['responses']> = false extends {
201
+ [TVariant in keyof TResponses]: IsResponseStatusCompatible<TResponses[TVariant]>;
202
+ }[keyof TResponses] ? false : true;
91
203
  type IsResponseHeadersSchemaCompatible<TResponse> = TResponse extends HttpResponseDefinition ? TResponse extends {
92
204
  readonly headers: infer THeaders extends StandardSchemaV1;
93
205
  } ? SchemaOutput<THeaders> extends HttpHeaders | undefined ? true : false : true : false;
@@ -96,7 +208,9 @@ type AreResponseHeadersSchemasCompatible<TResponses extends HttpProtocolDefiniti
96
208
  }[keyof TResponses] ? false : true;
97
209
  type HttpResponseConstraint<TDefinition extends HttpProtocolDefinition> = string extends keyof TDefinition['responses'] ? {
98
210
  readonly responses: never;
99
- } : AreResponseHeadersSchemasCompatible<TDefinition['responses']> extends true ? AreErrorMappingsCompatible<TDefinition['responses']> extends true ? unknown : {
211
+ } : AreResponseStatusesCompatible<TDefinition['responses']> extends true ? AreResponseHeadersSchemasCompatible<TDefinition['responses']> extends true ? AreErrorMappingsCompatible<TDefinition['responses']> extends true ? unknown : {
212
+ readonly responses: never;
213
+ } : {
100
214
  readonly responses: never;
101
215
  } : {
102
216
  readonly responses: never;
@@ -110,6 +224,8 @@ type IsUnion<TValue, TCandidate = TValue> = TValue extends unknown ? [TCandidate
110
224
  type IsSingleStringLiteral<TValue extends string> = string extends TValue ? false : true extends IsUnion<TValue> ? false : true;
111
225
  type HttpPathConstraint<TDefinition extends HttpProtocolDefinition> = IsSingleStringLiteral<TDefinition['method']> extends false ? {
112
226
  readonly method: never;
227
+ } : IsValidHttpMethod<TDefinition['method']> extends false ? {
228
+ readonly method: never;
113
229
  } : IsSingleStringLiteral<TDefinition['path']> extends false ? {
114
230
  readonly path: never;
115
231
  } : IsValidHttpPath<TDefinition['path']> extends true ? TDefinition['request'] extends {
@@ -132,15 +248,39 @@ type IsErrorMappingCompatible<TResponse> = TResponse extends HttpResponseDefinit
132
248
  type AreErrorMappingsCompatible<TResponses extends HttpProtocolDefinition['responses']> = false extends {
133
249
  [TVariant in keyof TResponses]: IsErrorMappingCompatible<TResponses[TVariant]>;
134
250
  }[keyof TResponses] ? false : true;
135
- declare function defineHttp<const TDefinition extends HttpProtocolDefinition>(definition: TDefinition & HttpPipelineConstraint<TDefinition> & HttpResponseConstraint<TDefinition> & HttpPathConstraint<TDefinition>): HttpProtocol<TDefinition>;
136
- type HttpProtocolGroup<TDefinitions extends Record<string, HttpProtocolDefinition>> = ProtocolGroup<'http', {
137
- [K in keyof TDefinitions]: HttpProtocol<TDefinitions[K]>;
138
- }>;
139
- type HttpDefinitionsConstraint<TDefinitions extends Record<string, HttpProtocolDefinition>> = {
140
- [K in keyof TDefinitions]: TDefinitions[K] & HttpPipelineConstraint<TDefinitions[K]> & HttpResponseConstraint<TDefinitions[K]> & HttpPathConstraint<TDefinitions[K]>;
251
+ type IsConstraintSatisfied<TConstraint> = keyof TConstraint extends never ? true : false;
252
+ type IsValidHttpRouteName<TName extends string> = TName extends '' ? false : TName extends '__proto__' ? false : TName extends `${string}.${string}` ? false : TName extends `${number}` ? false : true;
253
+ type HasOnlyHttpNodeProperties<TNode, TAllowed extends PropertyKey> = Exclude<keyof TNode, TAllowed> extends never ? true : false;
254
+ type IsExactHttpLeaf<TNode> = HasOnlyHttpNodeProperties<TNode, keyof HttpProtocolDefinition>;
255
+ type IsExactHttpBranch<TNode> = HasOnlyHttpNodeProperties<TNode, keyof HttpBranchDefinition>;
256
+ type IsBranchPipelineTerminalFree<TPipeline extends readonly PipelineItem[]> = number extends TPipeline['length'] ? false : TPipeline extends readonly [infer THead, ...infer TTail] ? THead extends TerminalLayerDescriptor ? false : THead extends {
257
+ readonly kind: 'layer';
258
+ readonly pipeline: infer TChild extends readonly PipelineItem[];
259
+ } ? IsBranchPipelineTerminalFree<TChild> extends true ? IsBranchPipelineTerminalFree<Extract<TTail, readonly PipelineItem[]>> : false : IsBranchPipelineTerminalFree<Extract<TTail, readonly PipelineItem[]>> : true;
260
+ type IsHttpBranchPathValid<TBranch extends HttpBranchDefinition> = TBranch extends {
261
+ readonly path: infer TPath extends string;
262
+ } ? string extends TPath ? false : TPath extends '/' ? true : IsValidHttpPath<TPath> : true;
263
+ type IsHttpBranchPipelineValid<TBranch extends HttpBranchDefinition> = TBranch extends {
264
+ readonly pipeline: infer TPipeline extends readonly PipelineItem[];
265
+ } ? IsBranchPipelineTerminalFree<TPipeline> : true;
266
+ type IsHttpBranchResponsesValid<TBranch extends HttpBranchDefinition> = TBranch extends {
267
+ readonly responses: infer TResponses extends Readonly<Record<string, HttpResponseDefinition>>;
268
+ } ? string extends keyof TResponses ? false : AreResponseHeadersSchemasCompatible<TResponses> extends true ? AreErrorMappingsCompatible<TResponses> : false : true;
269
+ type HasHttpResponseCollision<TParent extends Readonly<Record<string, HttpResponseDefinition>>, TLocal extends Readonly<Record<string, HttpResponseDefinition>>> = Extract<keyof TParent, keyof TLocal> extends never ? false : true;
270
+ type IsHttpResolvedLeafValid<TDefinition extends HttpProtocolDefinition, TParentPath extends string, TParentPipeline extends readonly PipelineItem[], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>>, TEffective extends HttpProtocolDefinition = EffectiveHttpDefinition<TDefinition, TParentPath, TParentPipeline, TParentResponses>> = IsConstraintSatisfied<HttpPipelineConstraint<TEffective> & HttpResponseConstraint<TEffective> & HttpPathConstraint<TEffective>>;
271
+ type IsHttpTreeValid<TTree extends Readonly<Record<string, unknown>>, TParentPath extends string = '', TParentPipeline extends readonly PipelineItem[] = readonly [], TParentResponses extends Readonly<Record<string, HttpResponseDefinition>> = {}> = Extract<keyof TTree, number> extends never ? false extends {
272
+ [K in keyof TTree & string]: IsValidHttpRouteName<K> extends true ? SourceOfHttpNode<TTree[K]> extends infer TSource ? TSource extends HttpProtocolDefinition ? IsExactHttpLeaf<TSource> extends true ? IsHttpResolvedLeafValid<TSource, TParentPath, TParentPipeline, TParentResponses> : false : TSource extends HttpBranchDefinition ? IsExactHttpBranch<TSource> extends true ? IsHttpBranchPathValid<TSource> extends true ? IsHttpBranchPipelineValid<TSource> extends true ? IsHttpBranchResponsesValid<TSource> extends true ? HasHttpResponseCollision<TParentResponses, BranchResponsesOf<TSource>> extends false ? IsHttpTreeValid<TSource['routes'], JoinHttpPath<TParentPath, BranchPathOf<TSource>>, readonly [
273
+ ...TParentPipeline,
274
+ ...BranchPipelineOf<TSource>
275
+ ], MergeHttpResponses<TParentResponses, BranchResponsesOf<TSource>>> : false : false : false : false : false : false : false : false;
276
+ }[keyof TTree & string] ? false : true : false;
277
+ type HttpTreeConstraint<TDefinitions extends HttpRouteTree> = IsHttpTreeValid<TDefinitions> extends true ? unknown : {
278
+ readonly __invalidHttpRouteTree__: never;
141
279
  };
280
+ declare function defineHttp<const TDefinition extends HttpProtocolDefinition>(definition: TDefinition & HttpPipelineConstraint<TDefinition> & HttpResponseConstraint<TDefinition> & HttpPathConstraint<TDefinition>): HttpProtocol<TDefinition>;
281
+ type HttpProtocolGroup<TDefinitions extends HttpRouteTree> = ProtocolGroup<'http', FlatHttpProtocols<TDefinitions> extends infer TProtocols extends Record<string, ProtocolDescriptor<'http'>> ? TProtocols : never>;
142
282
  export interface HttpProtocolFactory extends ProtocolFactory<'http'> {
143
- <const TDefinitions extends Record<string, HttpProtocolDefinition>>(definitions: TDefinitions & HttpDefinitionsConstraint<TDefinitions>): HttpProtocolGroup<TDefinitions>;
283
+ <const TDefinitions extends HttpRouteTree>(definitions: TDefinitions & HttpTreeConstraint<TDefinitions>): HttpProtocolGroup<TDefinitions>;
144
284
  readonly route: typeof defineHttp;
145
285
  readonly controller: TerminalLayerDescriptor<'http'>;
146
286
  readonly error: typeof httpError;
@@ -164,12 +304,10 @@ export declare const validate: Readonly<{
164
304
  readonly name: "validate.body";
165
305
  };
166
306
  }>;
167
- type ProceduresForHttp<TContract extends ContractDefinition> = {
307
+ type ProceduresForHttp<TBinding extends ContractBinding, TContract extends ContractDefinition = ContractOfBinding<TBinding>> = {
168
308
  [K in keyof TContract['procedures']]: 'http' extends keyof TContract['procedures'][K]['protocols'] ? K : never;
169
309
  }[keyof TContract['procedures']] & string;
170
- type HttpProtocolAt<TContract extends ContractDefinition, TProcedure extends keyof TContract['procedures']> = TContract['procedures'][TProcedure]['protocols']['http' & keyof TContract['procedures'][TProcedure]['protocols']] extends infer TProtocol ? TProtocol extends {
171
- readonly definition: infer TDefinition;
172
- } ? TDefinition extends HttpProtocolDefinition ? HttpProtocol<TDefinition> : never : never : never;
310
+ type HttpProtocolAt<TBinding extends ContractBinding, TProcedure extends keyof ContractOfBinding<TBinding>['procedures'], TContract extends ContractDefinition = ContractOfBinding<TBinding>> = TContract['procedures'][TProcedure]['protocols']['http' & keyof TContract['procedures'][TProcedure]['protocols']] extends infer TProtocol ? TProtocol extends HttpProtocol<any, any> ? TProtocol : never : never;
173
311
  type RequestBodySchema<TBody> = TBody extends HttpRequestBodyDefinition<infer TSchema> ? TSchema : never;
174
312
  type PartOutput<TDefinition extends HttpProtocolDefinition, TPart extends keyof HttpRequestDefinition> = TPart extends 'params' ? HasValidationBeforeTerminal<TDefinition['pipeline'], 'params'> extends true ? TDefinition['request'] extends {
175
313
  readonly params: infer TSchemas extends HttpParamsSchemas;
@@ -218,8 +356,8 @@ type HttpControllerContextDefinition<TDefinition extends HttpProtocolDefinition>
218
356
  readonly signal: AbortSignal;
219
357
  } & ContextProvidedBeforeTerminal<TDefinition['pipeline']>;
220
358
  export type HttpControllerContext<TProtocol extends HttpProtocol<any>> = HttpControllerContextDefinition<TProtocol['definition']>;
221
- export type ControllerOf<TContract extends ContractDefinition, TProtocol extends 'http', TProcedures extends ProceduresForHttp<TContract> = ProceduresForHttp<TContract>> = TProtocol extends 'http' ? {
222
- [K in TProcedures]: (context: HttpControllerContext<HttpProtocolAt<TContract, K>>) => DeclaredHttpResults<HttpProtocolAt<TContract, K>['definition']['responses']> | Promise<DeclaredHttpResults<HttpProtocolAt<TContract, K>['definition']['responses']>>;
359
+ export type ControllerOf<TBinding extends ContractBinding, TProtocol extends 'http', TProcedures extends ProceduresForHttp<TBinding> = ProceduresForHttp<TBinding>> = TProtocol extends 'http' ? {
360
+ [K in TProcedures]: (context: HttpControllerContext<HttpProtocolAt<TBinding, K>>) => DeclaredHttpResults<HttpProtocolAt<TBinding, K>['definition']['responses']> | Promise<DeclaredHttpResults<HttpProtocolAt<TBinding, K>['definition']['responses']>>;
223
361
  } : never;
224
362
  export type ContextOf<TController, TProcedure extends keyof TController> = TController[TProcedure] extends (context: infer TContext, ...args: any[]) => any ? TContext : never;
225
363
  export {};