Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | 155x 83x 155x 37x 155x 20x 155x 1x 155x 11x 155x 9x 155x 5x 5x 5x 5x 5x 5x 5x 12x 12x 1x 11x 11x 5x 155x 11x 155x 34x 155x 33x 29x 10x 1x 32x 32x 28x 28x 8x 29x 29x 32x 28x 24x 29x 155x 15x 15x 15x 24x 24x 15x 15x 155x | import { M } from '@endo/patterns';
import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns';
import type { JsonSchema, MethodSchema } from './schema.ts';
/**
* A described value: an `@endo/patterns` {@link Pattern} (the enforced shape) paired
* with the {@link JsonSchema} that hangs descriptive text on that shape.
*
* The pattern is the source of truth for the invocable shape; the schema is a
* semantic-hint projection. Authoring both from one leaf is what makes their
* conformance a construction invariant rather than an after-the-fact check.
*/
export type Described = {
pattern: Pattern;
schema: JsonSchema;
};
/**
* Like {@link Described}, but the schema may be absent — used for a method's
* return position, where a `void` return has a pattern ({@link M.undefined}) but
* no JSON Schema counterpart (JSON Schema cannot express `void`/`undefined`).
*/
export type DescribedReturn = {
pattern: Pattern;
schema: JsonSchema | undefined;
};
/**
* A named positional method parameter: a {@link Described} value plus the name
* under which it is hung in the method's {@link MethodSchema.args} and, after
* discovery, the key by which a caller supplies it.
*/
export type NamedArg = {
name: string;
described: Described;
optional: boolean;
};
/**
* A described method: the {@link MethodGuard} that enforces its call shape and
* the {@link MethodSchema} that describes it. Both are projected from the same
* authored leaves, so they cannot drift.
*/
export type DescribedMethod = {
guard: MethodGuard;
schema: MethodSchema;
};
/**
* A described interface: the {@link InterfaceGuard} that the exo membrane
* enforces, and the per-method {@link MethodSchema} map to pass as the
* `__getDescription__` payload. Splat both into `makeDiscoverableExo`.
*/
export type DescribedInterface = {
interfaceGuard: InterfaceGuard;
schemas: Record<string, MethodSchema>;
};
const withDescription = (
schema: JsonSchema,
description: string | undefined,
): JsonSchema =>
description === undefined ? schema : { ...schema, description };
/**
* A string leaf: matches a string; describes `{ type: 'string' }`.
*
* @param description - Optional human/LLM-facing description.
* @returns The described string.
*/
const string = (description?: string): Described =>
harden({
pattern: M.string(),
schema: withDescription({ type: 'string' }, description),
});
/**
* A number leaf: matches a number; describes `{ type: 'number' }`.
*
* @param description - Optional human/LLM-facing description.
* @returns The described number.
*/
const number = (description?: string): Described =>
harden({
pattern: M.number(),
schema: withDescription({ type: 'number' }, description),
});
/**
* A boolean leaf: matches a boolean; describes `{ type: 'boolean' }`.
*
* @param description - Optional human/LLM-facing description.
* @returns The described boolean.
*/
const boolean = (description?: string): Described =>
harden({
pattern: M.boolean(),
schema: withDescription({ type: 'boolean' }, description),
});
/**
* An array leaf: matches an array whose elements match `items`; describes
* `{ type: 'array', items }`.
*
* @param items - The described element type.
* @param description - Optional human/LLM-facing description.
* @returns The described array.
*/
const arrayOf = (items: Described, description?: string): Described =>
harden({
pattern: M.arrayOf(items.pattern),
schema: withDescription(
{ type: 'array', items: items.schema },
description,
),
});
/**
* An open object leaf: matches any record (extra keys allowed); describes
* `{ type: 'object', properties: {}, additionalProperties: true }`.
*
* Use when the shape is genuinely open (e.g. free-form attachments).
*
* @param description - Optional human/LLM-facing description.
* @returns The described open object.
*/
const record = (description?: string): Described =>
harden({
pattern: M.record(),
schema: withDescription(
{ type: 'object', properties: {}, additionalProperties: true },
description,
),
});
/**
* A closed/shaped object leaf: matches a record with exactly the given
* properties (extra keys are rejected), where keys not listed in `optional` are
* required. Describes `{ type: 'object', properties, required,
* additionalProperties: false }`.
*
* @param properties - The described properties, keyed by name.
* @param options - Options bag.
* @param options.optional - Property names that may be omitted.
* @param options.description - Optional human/LLM-facing description.
* @returns The described object.
*/
const object = (
properties: Record<string, Described>,
options: { optional?: string[]; description?: string } = {},
): Described => {
const { optional = [], description } = options;
const optionalSet = new Set(optional);
const requiredPatterns: Record<string, Pattern> = {};
const optionalPatterns: Record<string, Pattern> = {};
const schemaProperties: Record<string, JsonSchema> = {};
const required: string[] = [];
for (const [key, described] of Object.entries(properties)) {
schemaProperties[key] = described.schema;
if (optionalSet.has(key)) {
optionalPatterns[key] = described.pattern;
} else {
requiredPatterns[key] = described.pattern;
required.push(key);
}
}
return harden({
// The empty-record rest pattern closes the record: keys beyond those listed
// are rejected, matching the schema's `additionalProperties: false`.
pattern: M.splitRecord(requiredPatterns, optionalPatterns, {}),
schema: withDescription(
{
type: 'object',
properties: schemaProperties,
required,
additionalProperties: false,
},
description,
),
});
};
/**
* The void return leaf: matches `undefined` (an async method that resolves to
* nothing); has no JSON Schema counterpart.
*
* @returns The described void return.
*/
const nothing = (): DescribedReturn =>
harden({ pattern: M.undefined(), schema: undefined });
/**
* Name a positional method parameter.
*
* @param name - The argument name (its key in {@link MethodSchema.args}).
* @param described - The described value at this position.
* @param options - Options bag.
* @param options.optional - Whether the argument may be omitted. Optional
* arguments must be trailing (enforced by {@link describedMethod}).
* @returns The named argument.
*/
const arg = (
name: string,
described: Described,
options: { optional?: boolean } = {},
): NamedArg => harden({ name, described, optional: options.optional ?? false });
/**
* Describe a method: build the {@link MethodGuard} (async, via `M.callWhen`,
* since discoverable-exo methods are invoked across an eventual-send boundary)
* and the matching {@link MethodSchema} from the same arguments.
*
* @param description - The method's description.
* @param args - The positional, named arguments. Optional arguments must all be
* trailing — `M.call(...).optional(...)` is positional, so an optional argument
* before a required one cannot be expressed.
* @param returns - The described return value (use {@link nothing} for `void`).
* @returns The described method.
*/
const describedMethod = (
description: string,
args: NamedArg[],
returns: DescribedReturn,
): DescribedMethod => {
const firstOptional = args.findIndex((each) => each.optional);
if (
firstOptional !== -1 &&
args.slice(firstOptional).some((each) => !each.optional)
) {
throw new Error(
'describedMethod: optional arguments must be trailing (a required argument cannot follow an optional one).',
);
}
const required = args.filter((each) => !each.optional);
const optional = args.filter((each) => each.optional);
const base = M.callWhen(...required.map((each) => each.described.pattern));
const guard =
optional.length > 0
? base
.optional(...optional.map((each) => each.described.pattern))
.returns(returns.pattern)
: base.returns(returns.pattern);
const schemaArgs: Record<string, JsonSchema> = {};
for (const each of args) {
schemaArgs[each.name] = each.described.schema;
}
const schema: MethodSchema = {
description,
args: schemaArgs,
required: required.map((each) => each.name),
...(returns.schema === undefined ? {} : { returns: returns.schema }),
};
return harden({ guard, schema });
};
/**
* Describe an interface: collect method guards into an {@link InterfaceGuard}
* and method schemas into the `__getDescription__` payload.
*
* The guard uses `defaultGuards: 'passable'` so the `__getDescription__` method
* that `makeDiscoverableExo` injects (and which is not listed here) is allowed.
*
* @param name - The interface name.
* @param methods - The described methods, keyed by method name.
* @returns The interface guard and the per-method schema map.
*/
const describedInterface = (
name: string,
methods: Record<string, DescribedMethod>,
): DescribedInterface => {
const methodGuards: Record<string, MethodGuard> = {};
const schemas: Record<string, MethodSchema> = {};
for (const [methodName, method] of Object.entries(methods)) {
methodGuards[methodName] = method.guard;
schemas[methodName] = method.schema;
}
const interfaceGuard = M.interface(name, methodGuards, {
defaultGuards: 'passable',
});
return harden({ interfaceGuard, schemas });
};
/**
* Combinators for authoring an `@endo/patterns` guard and a {@link MethodSchema}
* description from a single source, so the two cannot drift.
*
* Leaves (`string`, `number`, `boolean`, `arrayOf`, `record`, `object`,
* `nothing`) each yield a `{ pattern, schema }` pair; `arg` names a positional
* parameter; `method` and `interface` assemble them.
*/
// eslint-disable-next-line id-length -- `S` is the intended terse public namespace, mirroring `@endo/patterns`'s `M`.
export const S = harden({
string,
number,
boolean,
arrayOf,
record,
object,
nothing,
arg,
method: describedMethod,
interface: describedInterface,
});
|