@loutrejs/loutre 0.4.1 → 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 { supportsJsonSchema, } from '../core/index.js';
2
+ import { resolveContractProcedureIdentity } from '../core/contract-internal.js';
2
3
  import { assertValidCompilation, compileApplication } from '../graph/index.js';
3
4
  import { httpRequestBodyContentType, httpRequestBodySchema, } from '../http/index.js';
4
5
  const FIXED_METHODS = new Set([
@@ -35,7 +36,7 @@ export function generateOpenApi(application, options) {
35
36
  const typed = protocol;
36
37
  const target = {
37
38
  definition: typed.definition,
38
- procedure,
39
+ procedure: resolveContractProcedureIdentity(implementation.contract, procedure).procedure,
39
40
  };
40
41
  const operation = createOperation(target, registry, operationIds, options.operationId);
41
42
  attachOperation(paths, typed.definition, operation);
@@ -8,7 +8,9 @@ export type ContextKeyValue<TKey> = TKey extends ContextKey<string, infer TValue
8
8
  export type ContextProperties<TKeys extends readonly ContextKey[]> = {
9
9
  readonly [TKey in TKeys[number] as TKey['name']]: ContextKeyValue<TKey>;
10
10
  };
11
- export declare function contextKey<const TName extends string>(name: TName): {
11
+ type ContextKeyNameConstraint<TName extends string> = TName extends '' | '__proto__' ? never : unknown;
12
+ export declare function contextKey<const TName extends string>(name: TName & ContextKeyNameConstraint<TName>): {
12
13
  of<TValue>(): ContextKey<TName, TValue>;
13
14
  };
14
15
  export declare function contextKeyName(key: ContextKey): string;
16
+ export {};
@@ -1,4 +1,7 @@
1
1
  export function contextKey(name) {
2
+ if (name.length === 0 || name === '__proto__') {
3
+ throw new Error(`Invalid Context Key name: ${JSON.stringify(name)}`);
4
+ }
2
5
  return {
3
6
  of() {
4
7
  return Object.freeze({
@@ -0,0 +1,22 @@
1
+ import type { ContractDefinition } from './contract.js';
2
+ export declare const protocolNamespaceBuilder: unique symbol;
3
+ export declare const protocolNamespaceType: unique symbol;
4
+ export declare const contractNodeBinding: unique symbol;
5
+ export declare const contractNodeKindType: unique symbol;
6
+ export declare const contractNodeMetadata: unique symbol;
7
+ export interface ContractNodeMetadata {
8
+ readonly kind: 'leaf' | 'branch';
9
+ readonly root: ContractDefinition;
10
+ readonly path: readonly string[];
11
+ readonly procedures: Readonly<Record<string, string>>;
12
+ }
13
+ export declare function contractNodeMetadataOf(contract: ContractDefinition): ContractNodeMetadata | undefined;
14
+ export declare function contractRootOf(contract: ContractDefinition): ContractDefinition;
15
+ export declare function contractProcedurePathOf(contract: ContractDefinition, procedure: string): string;
16
+ export interface ContractProcedureIdentity {
17
+ readonly contract: ContractDefinition;
18
+ readonly procedure: string;
19
+ }
20
+ export declare function resolveContractProcedureIdentity(contract: ContractDefinition, procedure: string, applicationContract?: ContractDefinition): ContractProcedureIdentity;
21
+ export declare function contractNodePathOf(contract: ContractDefinition): readonly string[] | undefined;
22
+ export declare function contractOfBinding(value: unknown): ContractDefinition;
@@ -0,0 +1,61 @@
1
+ export const protocolNamespaceBuilder = Symbol('loutre.protocol-namespace-builder');
2
+ export const protocolNamespaceType = Symbol('loutre.protocol-namespace-type');
3
+ export const contractNodeBinding = Symbol.for('loutre.contract-node-binding');
4
+ export const contractNodeKindType = Symbol('loutre.contract-node-kind-type');
5
+ export const contractNodeMetadata = Symbol.for('loutre.contract-node-metadata');
6
+ export function contractNodeMetadataOf(contract) {
7
+ return contractNodeMetadata in contract
8
+ ? contract[contractNodeMetadata]
9
+ : undefined;
10
+ }
11
+ export function contractRootOf(contract) {
12
+ return contractNodeMetadata in contract
13
+ ? contract[contractNodeMetadata].root
14
+ : contract;
15
+ }
16
+ export function contractProcedurePathOf(contract, procedure) {
17
+ if (!(contractNodeMetadata in contract))
18
+ return procedure;
19
+ return (contract[contractNodeMetadata].procedures[procedure] ?? procedure);
20
+ }
21
+ export function resolveContractProcedureIdentity(contract, procedure, applicationContract) {
22
+ if (applicationContract && contract === applicationContract) {
23
+ return { contract: applicationContract, procedure };
24
+ }
25
+ const canonicalProcedure = contractProcedurePathOf(contract, procedure);
26
+ if (applicationContract) {
27
+ const applicationNode = contractNodeMetadataOf(applicationContract);
28
+ const implementationNode = contractNodeMetadataOf(contract);
29
+ if (applicationNode &&
30
+ implementationNode &&
31
+ applicationNode.root === implementationNode.root &&
32
+ isContractNodePathPrefix(applicationNode.path, implementationNode.path)) {
33
+ const prefix = applicationNode.path.slice(1).join('.');
34
+ const relativeProcedure = canonicalProcedure.startsWith(`${prefix}.`)
35
+ ? canonicalProcedure.slice(prefix.length + 1)
36
+ : canonicalProcedure;
37
+ return { contract: applicationContract, procedure: relativeProcedure };
38
+ }
39
+ }
40
+ return {
41
+ contract: contractRootOf(contract),
42
+ procedure: canonicalProcedure,
43
+ };
44
+ }
45
+ function isContractNodePathPrefix(prefix, candidate) {
46
+ return (prefix.length <= candidate.length &&
47
+ prefix.every((segment, index) => candidate[index] === segment));
48
+ }
49
+ export function contractNodePathOf(contract) {
50
+ return contractNodeMetadata in contract
51
+ ? contract[contractNodeMetadata].path
52
+ : undefined;
53
+ }
54
+ export function contractOfBinding(value) {
55
+ if (typeof value === 'object' &&
56
+ value !== null &&
57
+ contractNodeBinding in value) {
58
+ return value[contractNodeBinding];
59
+ }
60
+ return value;
61
+ }
@@ -1,3 +1,4 @@
1
+ import { contractNodeBinding, contractNodeKindType, protocolNamespaceType } from './contract-internal.js';
1
2
  export interface ProtocolDescriptor<TName extends string = string, TContext = unknown, TResult = unknown, TDispatchKey extends string | null = string | null, TCapabilities extends readonly string[] = readonly string[]> {
2
3
  readonly kind: 'protocol';
3
4
  readonly protocol: TName;
@@ -22,6 +23,12 @@ export interface ContractDefinition<TProcedures extends Record<string, Procedure
22
23
  readonly kind: 'contract';
23
24
  readonly procedures: TProcedures;
24
25
  }
26
+ export interface ResolvedContractNode<TContract extends ContractDefinition = ContractDefinition, TKind extends 'leaf' | 'branch' = 'leaf' | 'branch'> {
27
+ readonly [contractNodeBinding]: TContract;
28
+ readonly [contractNodeKindType]: TKind;
29
+ }
30
+ export type ContractBinding = ContractDefinition | ResolvedContractNode;
31
+ export type ContractOfBinding<TBinding extends ContractBinding> = TBinding extends ResolvedContractNode<infer TContract> ? TContract : TBinding extends ContractDefinition ? TBinding : never;
25
32
  type ProcedureNamesOfGroup<TGroup> = TGroup extends ProtocolGroup<string, infer TProcedures> ? keyof TProcedures & string : never;
26
33
  type ProcedureNamesOfGroups<TGroups extends readonly ProtocolGroup[]> = ProcedureNamesOfGroup<TGroups[number]>;
27
34
  type ProtocolOfGroupProcedure<TGroup, TProcedure extends string> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? TProcedure extends keyof TProcedures ? {
@@ -30,39 +37,39 @@ type ProtocolOfGroupProcedure<TGroup, TProcedure extends string> = TGroup extend
30
37
  type ProceduresFromGroups<TGroups extends readonly ProtocolGroup[]> = {
31
38
  [TProcedure in ProcedureNamesOfGroups<TGroups>]: ProcedureDefinition<UnionToIntersection<ProtocolOfGroupProcedure<TGroups[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
32
39
  };
33
- type NonEmptyArrayConstraint<TValues extends readonly unknown[]> = TValues extends readonly [] ? {
34
- readonly __requiresAtLeastOneEntry__: never;
35
- } : unknown;
36
40
  type ProcedureProtocolKeysOfGroup<TGroup> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? `${keyof TProcedures & string}.${TProtocol}` : never;
37
41
  type DuplicateGroupProcedureProtocolKeys<TGroups extends readonly ProtocolGroup[], TSeen extends string = never> = number extends TGroups['length'] ? never : TGroups extends readonly [
38
42
  infer THead extends ProtocolGroup,
39
43
  ...infer TTail extends readonly ProtocolGroup[]
40
44
  ] ? Extract<ProcedureProtocolKeysOfGroup<THead>, TSeen> | DuplicateGroupProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfGroup<THead>> : never;
41
- type GroupProcedureProtocolUniquenessConstraint<TGroups extends readonly ProtocolGroup[]> = [DuplicateGroupProcedureProtocolKeys<TGroups>] extends [never] ? unknown : {
42
- readonly __duplicateProcedureProtocol__: DuplicateGroupProcedureProtocolKeys<TGroups>;
43
- };
44
- declare function defineContract<const TGroups extends readonly ProtocolGroup[]>(groups: TGroups & NonEmptyArrayConstraint<TGroups> & GroupProcedureProtocolUniquenessConstraint<TGroups> & DispatchKeyUniquenessConstraint<ProceduresFromGroups<TGroups>>): ContractDefinition<ProceduresFromGroups<TGroups>>;
45
- type ProcedureNamesOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? keyof TProcedures & string : never;
46
- type ProcedureNamesOfContracts<TContracts extends readonly ContractDefinition[]> = ProcedureNamesOfContract<TContracts[number]>;
47
- type ProtocolsOfContractProcedure<TContract, TProcedure extends string> = TContract extends ContractDefinition<infer TProcedures> ? TProcedure extends keyof TProcedures ? TProcedures[TProcedure]['protocols'] : never : never;
45
+ type ProtocolNamespaceOfGroup<TGroup> = TGroup extends {
46
+ readonly protocol: infer TProtocol extends string;
47
+ readonly procedures: infer TProcedures;
48
+ } ? TProcedures extends object ? TProcedures[keyof TProcedures] extends {
49
+ readonly [protocolNamespaceType]: infer TNamespace;
50
+ } ? [TNamespace] extends [never] ? never : {
51
+ readonly [K in TProtocol]: TNamespace;
52
+ } : never : never : never;
53
+ type ProtocolNamespaceNodeKeysOfGroup<TGroup> = ProtocolNamespaceOfGroup<TGroup> extends infer TNamespaces ? TNamespaces extends Readonly<Record<string, unknown>> ? {
54
+ [TProtocol in keyof TNamespaces & string]: TNamespaces[TProtocol] extends object ? `${TProtocol}.${keyof TNamespaces[TProtocol] & string}` : never;
55
+ }[keyof TNamespaces & string] : never : never;
56
+ type DuplicateGroupProtocolNamespaceNodeKeys<TGroups extends readonly ProtocolGroup[], TSeen extends string = never> = number extends TGroups['length'] ? never : TGroups extends readonly [
57
+ infer THead extends ProtocolGroup,
58
+ ...infer TTail extends readonly ProtocolGroup[]
59
+ ] ? Extract<ProtocolNamespaceNodeKeysOfGroup<THead>, TSeen> | DuplicateGroupProtocolNamespaceNodeKeys<TTail, TSeen | ProtocolNamespaceNodeKeysOfGroup<THead>> : never;
60
+ type ProtocolNamespacesOfGroups<TGroups extends readonly ProtocolGroup[]> = [
61
+ ProtocolNamespaceOfGroup<TGroups[number]>
62
+ ] extends [never] ? unknown : UnionToIntersection<ProtocolNamespaceOfGroup<TGroups[number]>>;
63
+ type ContractFromGroups<TGroups extends readonly ProtocolGroup[]> = ContractDefinition<ProceduresFromGroups<TGroups>> & ProtocolNamespacesOfGroups<TGroups>;
64
+ type ContractGroupsOf<TGroups extends readonly unknown[]> = TGroups extends readonly ProtocolGroup[] ? TGroups : never;
65
+ type ContractArgumentValidation<TGroups extends readonly unknown[]> = TGroups extends readonly [] ? readonly [error: 'contract requires at least one protocol group'] : TGroups extends readonly ProtocolGroup[] ? [DuplicateGroupProcedureProtocolKeys<TGroups>] extends [never] ? [DuplicateGroupProtocolNamespaceNodeKeys<TGroups>] extends [never] ? [
66
+ DuplicateDispatchKeys<ProtocolDispatchEntries<ProceduresFromGroups<TGroups>>>
67
+ ] extends [never] ? readonly [] : readonly [error: 'contract contains duplicate dispatch keys'] : readonly [
68
+ error: 'contract contains duplicate protocol namespace nodes'
69
+ ] : readonly [error: 'contract contains duplicate procedure protocols'] : readonly [error: 'contract entries must be protocol groups'];
70
+ declare function defineContract<const TGroups extends readonly unknown[]>(groups: TGroups, ...validation: ContractArgumentValidation<TGroups>): ContractFromGroups<ContractGroupsOf<TGroups>>;
48
71
  type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
49
- type MergedProcedures<TContracts extends readonly ContractDefinition[]> = {
50
- [TProcedure in ProcedureNamesOfContracts<TContracts>]: ProcedureDefinition<UnionToIntersection<ProtocolsOfContractProcedure<TContracts[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
51
- };
52
- type ProcedureProtocolKeysOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? {
53
- [TProcedure in keyof TProcedures & string]: `${TProcedure}.${keyof TProcedures[TProcedure]['protocols'] & string}`;
54
- }[keyof TProcedures & string] : never;
55
- type DuplicateContractProcedureProtocolKeys<TContracts extends readonly ContractDefinition[], TSeen extends string = never> = number extends TContracts['length'] ? never : TContracts extends readonly [
56
- infer THead extends ContractDefinition,
57
- ...infer TTail extends readonly ContractDefinition[]
58
- ] ? Extract<ProcedureProtocolKeysOfContract<THead>, TSeen> | DuplicateContractProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfContract<THead>> : never;
59
- type ContractProcedureProtocolUniquenessConstraint<TContracts extends readonly ContractDefinition[]> = [DuplicateContractProcedureProtocolKeys<TContracts>] extends [never] ? unknown : {
60
- readonly __duplicateProcedureProtocol__: DuplicateContractProcedureProtocolKeys<TContracts>;
61
- };
62
- declare function mergeContracts<const TContracts extends readonly ContractDefinition[]>(contracts: TContracts & NonEmptyArrayConstraint<TContracts> & ContractProcedureProtocolUniquenessConstraint<TContracts> & DispatchKeyUniquenessConstraint<MergedProcedures<TContracts>>): ContractDefinition<MergedProcedures<TContracts>>;
63
- export declare const contract: typeof defineContract & {
64
- merge: typeof mergeContracts;
65
- };
72
+ export declare const contract: typeof defineContract;
66
73
  type ProtocolDispatchEntries<TProcedures extends Record<string, ProcedureDefinition>> = {
67
74
  [TProcedure in keyof TProcedures & string]: {
68
75
  [TProtocol in keyof TProcedures[TProcedure]['protocols'] & string]: TProcedures[TProcedure]['protocols'][TProtocol] extends {
@@ -81,11 +88,6 @@ type DuplicateDispatchKeys<TEntries, TAllEntries = TEntries> = TEntries extends
81
88
  }> extends {
82
89
  readonly path: infer TPath;
83
90
  } ? TPath : never> extends true ? TKey : never : never;
84
- type DispatchKeyUniquenessConstraint<TProcedures extends Record<string, ProcedureDefinition>> = [DuplicateDispatchKeys<ProtocolDispatchEntries<TProcedures>>] extends [
85
- never
86
- ] ? unknown : {
87
- readonly __duplicateProtocolDispatchKey__: DuplicateDispatchKeys<ProtocolDispatchEntries<TProcedures>>;
88
- };
89
91
  export type ContractProcedures<TContract extends ContractDefinition> = TContract['procedures'];
90
92
  export interface ProtocolFactory<TName extends string, TCapabilities extends readonly string[] = readonly string[]> {
91
93
  readonly protocol: TName;
@@ -1,3 +1,4 @@
1
+ import { contractNodeBinding, contractNodeKindType, protocolNamespaceBuilder, protocolNamespaceType, } from './contract-internal.js';
1
2
  export function protocolGroup(protocol, procedures) {
2
3
  return Object.freeze({
3
4
  kind: 'protocol-group',
@@ -5,7 +6,7 @@ export function protocolGroup(protocol, procedures) {
5
6
  procedures: Object.freeze({ ...procedures }),
6
7
  });
7
8
  }
8
- function defineContract(groups) {
9
+ function defineContract(groups, ..._validation) {
9
10
  if (groups.length === 0) {
10
11
  throw new Error('contract([]) requires at least one protocol group');
11
12
  }
@@ -26,38 +27,46 @@ function defineContract(groups) {
26
27
  }
27
28
  }
28
29
  assertUniqueDispatchKeys(procedures);
29
- return Object.freeze({
30
+ const resolved = {
30
31
  kind: 'contract',
31
32
  procedures: Object.freeze(procedures),
32
- });
33
- }
34
- function mergeContracts(contracts) {
35
- if (contracts.length === 0) {
36
- throw new Error('contract.merge([]) requires at least one Contract');
33
+ };
34
+ const namespaceBuilders = [];
35
+ for (const group of groups) {
36
+ const build = group[protocolNamespaceBuilder];
37
+ if (typeof build !== 'function')
38
+ continue;
39
+ namespaceBuilders.push({
40
+ protocol: group.protocol,
41
+ build: build,
42
+ });
37
43
  }
38
- const procedures = {};
39
- for (const source of contracts) {
40
- for (const [procedureName, procedureDefinition] of Object.entries(source.procedures)) {
41
- const current = procedures[procedureName];
42
- const protocols = { ...current?.protocols };
43
- for (const [protocolName, descriptor] of Object.entries(procedureDefinition.protocols)) {
44
- if (Object.hasOwn(protocols, protocolName)) {
45
- throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${protocolName}`);
46
- }
47
- protocols[protocolName] = descriptor;
44
+ applyProtocolNamespaces(resolved, namespaceBuilders);
45
+ return Object.freeze(resolved);
46
+ }
47
+ function applyProtocolNamespaces(resolved, builders) {
48
+ for (const { protocol, build } of builders) {
49
+ const namespace = build(resolved);
50
+ const current = resolved[protocol];
51
+ if (current === undefined) {
52
+ resolved[protocol] = namespace;
53
+ continue;
54
+ }
55
+ if (!isRecord(current) || !isRecord(namespace)) {
56
+ throw new Error(`Duplicate contract protocol namespace: ${protocol}`);
57
+ }
58
+ for (const name of Object.keys(namespace)) {
59
+ if (Object.hasOwn(current, name)) {
60
+ throw new Error(`Duplicate contract protocol namespace node: ${protocol}.${name}`);
48
61
  }
49
- procedures[procedureName] = { kind: 'procedure', protocols };
50
62
  }
63
+ resolved[protocol] = Object.freeze({ ...current, ...namespace });
51
64
  }
52
- assertUniqueDispatchKeys(procedures);
53
- return Object.freeze({
54
- kind: 'contract',
55
- procedures: Object.freeze(procedures),
56
- });
57
65
  }
58
- export const contract = Object.assign(defineContract, {
59
- merge: mergeContracts,
60
- });
66
+ function isRecord(value) {
67
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
68
+ }
69
+ export const contract = defineContract;
61
70
  function assertUniqueDispatchKeys(procedures) {
62
71
  const paths = new Map();
63
72
  for (const [procedureName, procedureDefinition] of Object.entries(procedures)) {
@@ -1,8 +1,9 @@
1
- import type { ContractDefinition, ContractProcedures, ProtocolDescriptor, ProtocolFactory } from './contract.js';
2
- type ProcedureNamesForProtocol<TContract extends ContractDefinition, TProtocol extends string> = {
1
+ import type { ContractDefinition, ContractOfBinding, ResolvedContractNode, ContractProcedures, ProtocolDescriptor, ProtocolFactory } from './contract.js';
2
+ type ImplementationBinding = ContractDefinition | ResolvedContractNode<ContractDefinition, 'leaf'>;
3
+ type ProcedureNamesForProtocol<TBinding extends ImplementationBinding, TProtocol extends string, TContract extends ContractDefinition = ContractOfBinding<TBinding>> = {
3
4
  [K in keyof ContractProcedures<TContract>]: TProtocol extends keyof ContractProcedures<TContract>[K]['protocols'] ? K : never;
4
5
  }[keyof ContractProcedures<TContract>] & string;
5
- type ImplementationRuntimeShape<TContract extends ContractDefinition, TProtocol extends string, TProcedures extends string> = {
6
+ type ImplementationRuntimeShape<TBinding extends ImplementationBinding, TProtocol extends string, TProcedures extends string, TContract extends ContractDefinition = ContractOfBinding<TBinding>> = {
6
7
  [K in TProcedures]: ContractProcedures<TContract>[K]['protocols'][TProtocol & keyof ContractProcedures<TContract>[K]['protocols']] extends ProtocolDescriptor<TProtocol, infer TContext, infer TResult, any, any> ? (context: TContext) => TResult | Promise<TResult> : never;
7
8
  };
8
9
  export interface ImplementationDescriptor<TContract extends ContractDefinition = ContractDefinition, TProtocol extends string = string, TProcedures extends readonly string[] = readonly string[], TRuntime extends object = object, TCapabilities extends readonly string[] = readonly string[]> {
@@ -14,21 +15,30 @@ export interface ImplementationDescriptor<TContract extends ContractDefinition =
14
15
  readonly procedures: TProcedures;
15
16
  readonly factory: () => TRuntime;
16
17
  }
17
- type AvailableProtocolConstraint<TContract extends ContractDefinition, TProtocol extends string, TCapabilities extends readonly string[]> = [ProcedureNamesForProtocol<TContract, TProtocol>] extends [never] ? never : ProtocolFactory<TProtocol, TCapabilities>;
18
- type FullImplementationDeclaration<TContract extends ContractDefinition, TProtocol extends string, TCapabilities extends readonly string[]> = {
18
+ type AvailableProtocolConstraint<TBinding extends ImplementationBinding, TProtocol extends string, TCapabilities extends readonly string[]> = [ProcedureNamesForProtocol<TBinding, TProtocol>] extends [never] ? never : ProtocolFactory<TProtocol, TCapabilities>;
19
+ type FullImplementationDeclaration<TBinding extends ImplementationBinding, TProtocol extends string, TCapabilities extends readonly string[]> = {
19
20
  readonly name: string;
20
- readonly contract: TContract;
21
- readonly protocol: AvailableProtocolConstraint<TContract, TProtocol, TCapabilities>;
21
+ readonly contract: TBinding;
22
+ readonly protocol: AvailableProtocolConstraint<TBinding, TProtocol, TCapabilities>;
22
23
  readonly procedures?: never;
23
- readonly factory: () => ImplementationRuntimeShape<TContract, TProtocol, ProcedureNamesForProtocol<TContract, TProtocol>>;
24
+ readonly factory: () => ImplementationRuntimeShape<TBinding, TProtocol, ProcedureNamesForProtocol<TBinding, TProtocol>>;
25
+ };
26
+ type DuplicateProcedureNames<TProcedures extends readonly string[], TSeen extends string = never> = number extends TProcedures['length'] ? never : TProcedures extends readonly [
27
+ infer THead extends string,
28
+ ...infer TTail extends readonly string[]
29
+ ] ? THead extends TSeen ? THead | DuplicateProcedureNames<TTail, TSeen> : DuplicateProcedureNames<TTail, TSeen | THead> : never;
30
+ type UniqueProcedureSelectionConstraint<TProcedures extends readonly string[]> = [
31
+ DuplicateProcedureNames<TProcedures>
32
+ ] extends [never] ? unknown : {
33
+ readonly __duplicateImplementationProcedure__: never;
24
34
  };
25
- type PartialImplementationDeclaration<TContract extends ContractDefinition, TProtocol extends string, TProcedures extends readonly ProcedureNamesForProtocol<TContract, TProtocol>[], TCapabilities extends readonly string[]> = {
35
+ type PartialImplementationDeclaration<TBinding extends ImplementationBinding, TProtocol extends string, TProcedures extends readonly ProcedureNamesForProtocol<TBinding, TProtocol>[], TCapabilities extends readonly string[]> = {
26
36
  readonly name: string;
27
- readonly contract: TContract;
28
- readonly protocol: AvailableProtocolConstraint<TContract, TProtocol, TCapabilities>;
29
- readonly procedures: TProcedures;
30
- readonly factory: () => ImplementationRuntimeShape<TContract, TProtocol, TProcedures[number]>;
37
+ readonly contract: TBinding;
38
+ readonly protocol: AvailableProtocolConstraint<TBinding, TProtocol, TCapabilities>;
39
+ readonly procedures: TProcedures & UniqueProcedureSelectionConstraint<TProcedures>;
40
+ readonly factory: () => ImplementationRuntimeShape<TBinding, TProtocol, TProcedures[number]>;
31
41
  };
32
- export declare function implementation<const TContract extends ContractDefinition, const TProtocol extends string, const TCapabilities extends readonly string[]>(declaration: FullImplementationDeclaration<TContract, TProtocol, TCapabilities>): ImplementationDescriptor<TContract, TProtocol, readonly ProcedureNamesForProtocol<TContract, TProtocol>[], ImplementationRuntimeShape<TContract, TProtocol, ProcedureNamesForProtocol<TContract, TProtocol>>, TCapabilities>;
33
- export declare function implementation<const TContract extends ContractDefinition, const TProtocol extends string, const TProcedures extends readonly ProcedureNamesForProtocol<TContract, TProtocol>[], const TCapabilities extends readonly string[]>(declaration: PartialImplementationDeclaration<TContract, TProtocol, TProcedures, TCapabilities>): ImplementationDescriptor<TContract, TProtocol, TProcedures, ImplementationRuntimeShape<TContract, TProtocol, TProcedures[number]>, TCapabilities>;
42
+ export declare function implementation<const TBinding extends ImplementationBinding, const TProtocol extends string, const TCapabilities extends readonly string[]>(declaration: FullImplementationDeclaration<TBinding, TProtocol, TCapabilities>): ImplementationDescriptor<ContractOfBinding<TBinding>, TProtocol, readonly ProcedureNamesForProtocol<TBinding, TProtocol>[], ImplementationRuntimeShape<TBinding, TProtocol, ProcedureNamesForProtocol<TBinding, TProtocol>>, TCapabilities>;
43
+ export declare function implementation<const TBinding extends ImplementationBinding, const TProtocol extends string, const TProcedures extends readonly ProcedureNamesForProtocol<TBinding, TProtocol>[], const TCapabilities extends readonly string[]>(declaration: PartialImplementationDeclaration<TBinding, TProtocol, TProcedures, TCapabilities>): ImplementationDescriptor<ContractOfBinding<TBinding>, TProtocol, TProcedures, ImplementationRuntimeShape<TBinding, TProtocol, TProcedures[number]>, TCapabilities>;
34
44
  export {};
@@ -1,6 +1,11 @@
1
+ import { contractNodeMetadataOf, contractOfBinding, } from './contract-internal.js';
1
2
  export function implementation(declaration) {
2
3
  const protocol = declaration.protocol.protocol;
3
- const available = Object.entries(declaration.contract.procedures)
4
+ const contract = contractOfBinding(declaration.contract);
5
+ if (contractNodeMetadataOf(contract)?.kind === 'branch') {
6
+ throw new Error('LUTRE_IMPL_003: Implementation must bind to a resolved leaf Contract node.');
7
+ }
8
+ const available = Object.entries(contract.procedures)
4
9
  .filter(([, procedure]) => protocol in procedure.protocols)
5
10
  .map(([name]) => name);
6
11
  if (available.length === 0) {
@@ -13,7 +18,7 @@ export function implementation(declaration) {
13
18
  throw new Error(`LUTRE_IMPL_003: Implementation ${declaration.name} selects procedure ${procedure} more than once.`);
14
19
  }
15
20
  selected.add(procedure);
16
- const definition = declaration.contract.procedures[procedure];
21
+ const definition = contract.procedures[procedure];
17
22
  if (!definition || !(protocol in definition.protocols)) {
18
23
  throw new Error(`LUTRE_IMPL_003: ${procedure} is not declared for protocol ${protocol}.`);
19
24
  }
@@ -21,7 +26,7 @@ export function implementation(declaration) {
21
26
  return Object.freeze({
22
27
  kind: 'implementation',
23
28
  name: declaration.name,
24
- contract: declaration.contract,
29
+ contract,
25
30
  protocol,
26
31
  capabilities: declaration.protocol.capabilities ?? [],
27
32
  procedures: Object.freeze(procedures),
@@ -79,7 +79,26 @@ interface PipelineTerminalState<THasTerminal extends boolean, TValid extends boo
79
79
  type InvalidPipelineTerminalState = PipelineTerminalState<boolean, false>;
80
80
  type FoldPipelineTerminals<TPipeline extends readonly PipelineItem[], TProtocol extends string, TState extends PipelineTerminalState<boolean, boolean>> = TState['valid'] extends false ? TState : number extends TPipeline['length'] ? InvalidPipelineTerminalState : TPipeline extends readonly [infer THead, ...infer TTail] ? TState['hasTerminal'] extends true ? InvalidPipelineTerminalState : FoldPipelineTerminals<Extract<TTail, readonly PipelineItem[]>, TProtocol, FoldPipelineItemTerminals<THead, TProtocol, TState>> : TState;
81
81
  type FoldPipelineItemTerminals<TItem, TProtocol extends string, TState extends PipelineTerminalState<boolean, boolean>> = TItem extends TerminalLayerDescriptor<infer TItemProtocol> ? [TItemProtocol, TProtocol] extends [TProtocol, TItemProtocol] ? PipelineTerminalState<true, true> : InvalidPipelineTerminalState : TItem extends LayerOccurrenceDescriptor<any, any, any, any, any, any, any, any, infer TPipeline> ? FoldPipelineTerminals<TPipeline, TProtocol, TState> : TState;
82
- export type IsValidProtocolPipeline<TPipeline extends readonly PipelineItem[], TProtocol extends string> = FoldPipelineTerminals<TPipeline, TProtocol, PipelineTerminalState<false, true>> extends PipelineTerminalState<true, true> ? true : false;
82
+ type ContextKeyNameOf<TKey> = TKey extends ContextKey<infer TName, any> ? TName : never;
83
+ type ContextKeyNamesOf<TKeys extends readonly ContextKey[]> = ContextKeyNameOf<TKeys[number]>;
84
+ type DuplicateContextKeyNames<TKeys extends readonly ContextKey[], TSeen extends string = never> = number extends TKeys['length'] ? never : TKeys extends readonly [
85
+ infer THead extends ContextKey,
86
+ ...infer TTail extends readonly ContextKey[]
87
+ ] ? ContextKeyNameOf<THead> extends infer TName extends string ? TName extends TSeen ? TName | DuplicateContextKeyNames<TTail, TSeen> : DuplicateContextKeyNames<TTail, TSeen | TName> : never : never;
88
+ type AreRequiredContextKeysAvailable<TRequires extends readonly ContextKey[], TAvailable extends ContextKey> = [TRequires[number]] extends [never] ? true : false extends (TRequires[number] extends infer TRequired extends ContextKey ? TRequired extends TAvailable ? true : false : never) ? false : true;
89
+ type AreRequiredValidatedPartsAvailable<TRequires extends readonly ValidatedInputPart[], TValidated extends ValidatedInputPart> = Exclude<TRequires[number], TValidated> extends never ? true : false;
90
+ type HasProvidedContextCollision<TProvides extends readonly ContextKey[], TAvailable extends ContextKey> = DuplicateContextKeyNames<TProvides> | Extract<ContextKeyNamesOf<TProvides>, ContextKeyNameOf<TAvailable>> extends never ? false : true;
91
+ interface PipelineRequirementState<TAvailable extends ContextKey, TValidated extends ValidatedInputPart, TValid extends boolean> {
92
+ readonly available: TAvailable;
93
+ readonly validated: TValidated;
94
+ readonly valid: TValid;
95
+ }
96
+ type InvalidPipelineRequirementState = PipelineRequirementState<ContextKey, ValidatedInputPart, false>;
97
+ type FoldLayerRequirements<TRequires extends readonly ContextKey[], TProvides extends readonly ContextKey[], TRequiresValidated extends readonly ValidatedInputPart[], TState extends PipelineRequirementState<ContextKey, ValidatedInputPart, boolean>> = TState['valid'] extends false ? TState : AreRequiredContextKeysAvailable<TRequires, TState['available']> extends true ? AreRequiredValidatedPartsAvailable<TRequiresValidated, TState['validated']> extends true ? HasProvidedContextCollision<TProvides, TState['available']> extends false ? PipelineRequirementState<TState['available'] | TProvides[number], TState['validated'], true> : InvalidPipelineRequirementState : InvalidPipelineRequirementState : InvalidPipelineRequirementState;
98
+ type FoldPipelineRequirements<TPipeline extends readonly PipelineItem[], TState extends PipelineRequirementState<ContextKey, ValidatedInputPart, boolean>> = TState['valid'] extends false ? TState : number extends TPipeline['length'] ? TState : TPipeline extends readonly [infer THead, ...infer TTail] ? FoldPipelineRequirements<Extract<TTail, readonly PipelineItem[]>, FoldPipelineItemRequirements<THead, TState>> : TState;
99
+ type FoldPipelineItemRequirements<TItem, TState extends PipelineRequirementState<ContextKey, ValidatedInputPart, boolean>> = TItem extends ValidationLayerDescriptor ? PipelineRequirementState<TState['available'], TState['validated'] | TItem['part'], TState['valid']> : TItem extends LayerOccurrenceDescriptor<infer TRequires, infer TProvides, any, any, any, any, infer TRequiresValidated, any, infer TPipeline> ? FoldPipelineRequirements<TPipeline, FoldLayerRequirements<TRequires, TProvides, TRequiresValidated, TState>> : TItem extends LayerDescriptor<infer TRequires, infer TProvides, any, any, any, any, infer TRequiresValidated, any> ? FoldLayerRequirements<TRequires, TProvides, TRequiresValidated, TState> : TState;
100
+ type IsPipelineRequirementsValid<TPipeline extends readonly PipelineItem[]> = FoldPipelineRequirements<TPipeline, PipelineRequirementState<never, never, true>>['valid'] extends true ? true : false;
101
+ export type IsValidProtocolPipeline<TPipeline extends readonly PipelineItem[], TProtocol extends string> = FoldPipelineTerminals<TPipeline, TProtocol, PipelineTerminalState<false, true>> extends PipelineTerminalState<true, true> ? IsPipelineRequirementsValid<TPipeline> : false;
83
102
  export type ShortCircuitResultOf<TItem> = TItem extends LayerOccurrenceDescriptor<any, any, any, any, any, any, any, any, infer TPipeline> ? ShortCircuitResultOfPipeline<TPipeline> | ShortCircuitResultOfLayer<TItem> : ShortCircuitResultOfLayer<TItem>;
84
103
  type ShortCircuitResultOfLayer<TItem> = TItem extends {
85
104
  readonly [shortCircuitResultType]?: infer TResult;
@@ -105,5 +124,14 @@ type FoldPipeline<TPipeline extends readonly PipelineItem[], TState extends Pipe
105
124
  type FoldPipelineItem<TItem, TState extends PipelineTypeState<object, ValidatedInputPart, boolean>> = TItem extends TerminalLayerDescriptor ? PipelineTypeState<TState['context'], TState['validated'], true> : TItem extends ValidationLayerDescriptor ? PipelineTypeState<TState['context'], TState['validated'] | TItem['part'], false> : TItem extends LayerOccurrenceDescriptor<any, infer TProvides, any, any, any, any, any, any, infer TPipeline> ? FoldPipeline<TPipeline, PipelineTypeState<TState['context'] & ContextProperties<TProvides>, TState['validated'], false>> : TItem extends LayerDescriptor<any, infer TProvides, any, any, any, any, any, any> ? PipelineTypeState<TState['context'] & ContextProperties<TProvides>, TState['validated'], false> : TState;
106
125
  export type ContextProvidedBeforeTerminal<TPipeline extends readonly PipelineItem[], TContext extends object = {}> = number extends TPipeline['length'] ? TContext : FoldPipeline<TPipeline, PipelineTypeState<TContext, never, false>>['context'];
107
126
  export type HasValidationBeforeTerminal<TPipeline extends readonly PipelineItem[], TPart extends ValidatedInputPart> = number extends TPipeline['length'] ? false : TPart extends FoldPipeline<TPipeline, PipelineTypeState<{}, never, false>>['validated'] ? true : false;
108
- export declare function layer<const TRequires extends readonly ContextKey[] = readonly [], const TProvides extends readonly ContextKey[] = readonly [], TContext extends object = {}, TShortCircuitResult = never, const TShortCircuits extends readonly ShortCircuitDeclaration[] = readonly [], const TName extends string = string, const TRole extends Exclude<LayerRole, 'terminal' | 'validation'> = 'generic', const TRequiresValidated extends readonly ValidatedInputPart[] = readonly []>(definition: LayerDefinition<TRequires, TProvides, TContext, TShortCircuitResult, TShortCircuits, TName, TRole, TRequiresValidated>): LayerDescriptor<TRequires, TProvides, TShortCircuitResult, TShortCircuits, TName, TRole, TRequiresValidated, TContext>;
127
+ type DuplicateValidatedInputParts<TParts extends readonly ValidatedInputPart[], TSeen extends ValidatedInputPart = never> = number extends TParts['length'] ? never : TParts extends readonly [
128
+ infer THead extends ValidatedInputPart,
129
+ ...infer TTail extends readonly ValidatedInputPart[]
130
+ ] ? THead extends TSeen ? THead | DuplicateValidatedInputParts<TTail, TSeen> : DuplicateValidatedInputParts<TTail, TSeen | THead> : never;
131
+ type LayerDefinitionConstraint<TRequires extends readonly ContextKey[], TProvides extends readonly ContextKey[], TRequiresValidated extends readonly ValidatedInputPart[]> = [
132
+ DuplicateContextKeyNames<TRequires> | DuplicateContextKeyNames<TProvides> | Extract<ContextKeyNamesOf<TRequires>, ContextKeyNamesOf<TProvides>> | DuplicateValidatedInputParts<TRequiresValidated>
133
+ ] extends [never] ? unknown : {
134
+ readonly __invalidLayerDefinition__: never;
135
+ };
136
+ export declare function layer<const TRequires extends readonly ContextKey[] = readonly [], const TProvides extends readonly ContextKey[] = readonly [], TContext extends object = {}, TShortCircuitResult = never, const TShortCircuits extends readonly ShortCircuitDeclaration[] = readonly [], const TName extends string = string, const TRole extends Exclude<LayerRole, 'terminal' | 'validation'> = 'generic', const TRequiresValidated extends readonly ValidatedInputPart[] = readonly []>(definition: LayerDefinition<TRequires, TProvides, TContext, TShortCircuitResult, TShortCircuits, TName, TRole, TRequiresValidated> & LayerDefinitionConstraint<TRequires, TProvides, TRequiresValidated>): LayerDescriptor<TRequires, TProvides, TShortCircuitResult, TShortCircuits, TName, TRole, TRequiresValidated, TContext>;
109
137
  export {};
@@ -21,7 +21,36 @@ export function layerDefinitionOf(item) {
21
21
  export function childPipelineOf(item) {
22
22
  return isLayerOccurrence(item) ? item.pipeline : undefined;
23
23
  }
24
+ function assertUniqueContextKeyNames(keys, label) {
25
+ const names = new Set();
26
+ for (const key of keys) {
27
+ if (names.has(key.name)) {
28
+ throw new Error(`Layer ${label} contains duplicate Context property ${key.name}`);
29
+ }
30
+ names.add(key.name);
31
+ }
32
+ }
33
+ function assertLayerDefinition(definition) {
34
+ const requires = definition.requires ?? [];
35
+ const provides = definition.provides ?? [];
36
+ assertUniqueContextKeyNames(requires, 'requires');
37
+ assertUniqueContextKeyNames(provides, 'provides');
38
+ const requiredNames = new Set(requires.map((key) => key.name));
39
+ for (const key of provides) {
40
+ if (requiredNames.has(key.name)) {
41
+ throw new Error(`Layer cannot require and provide the same Context property ${key.name}`);
42
+ }
43
+ }
44
+ const validated = new Set();
45
+ for (const part of definition.requiresValidated ?? []) {
46
+ if (validated.has(part)) {
47
+ throw new Error(`Layer requiresValidated contains duplicate part ${part}`);
48
+ }
49
+ validated.add(part);
50
+ }
51
+ }
24
52
  export function layer(definition) {
53
+ assertLayerDefinition(definition);
25
54
  let descriptor;
26
55
  const callable = (pipeline) => Object.freeze({
27
56
  kind: 'layer',
@@ -1 +1 @@
1
- export declare const LOUTRE_VERSION = "0.4.1";
1
+ export declare const LOUTRE_VERSION = "0.5.0";
@@ -1,2 +1,2 @@
1
1
  // package manifestとのずれを避けるため、scripts/generate-version.mjsがbuild前に更新する。
2
- export const LOUTRE_VERSION = '0.4.1';
2
+ export const LOUTRE_VERSION = '0.5.0';
@@ -1,4 +1,4 @@
1
- import { type ArgsClass, type ModuleInstance, type ModuleTemplate, type TaskDescriptor, type TriggerDescriptor } from '../core/index.js';
1
+ import { type ArgsClass, type ContractBinding, type ModuleInstance, type ModuleTemplate, type TaskDescriptor, type TriggerDescriptor } from '../core/index.js';
2
2
  import type { ApplicationGraphIR, CompilationResult, Diagnostic } from './ir.js';
3
3
  export declare class StaticValidationError extends Error {
4
4
  readonly diagnostics: readonly Diagnostic[];
@@ -6,6 +6,7 @@ export declare class StaticValidationError extends Error {
6
6
  constructor(diagnostics: readonly Diagnostic[], graph?: ApplicationGraphIR | undefined);
7
7
  }
8
8
  export interface ApplicationCompilationInput {
9
+ readonly contract?: ContractBinding;
9
10
  readonly modules: readonly (ModuleInstance | ModuleTemplate<void>)[];
10
11
  readonly arguments?: ArgsClass | undefined;
11
12
  readonly tasks?: readonly TaskDescriptor<any, any>[];