候选封套、数量与浅层限制
策略的具体实现如下。
ts
export function defineCandidateSelection(definition: CandidateSelectionDefinition) {
const { field, error: configuredError, maxCandidates: max } = definition;
const error =
typeof configuredError === 'function' ? configuredError.bind(definition) : configuredError;
const min = definition.minCandidates ?? 1;
const tags = [...(definition.tags ?? [])];
breakWhile([
{ occur: () => !field.trim(), error: 'Missing candidate field' },
{
occur: [
() => !Number.isSafeInteger(min) || !Number.isSafeInteger(max),
() => min < 1 || max < min,
],
error: 'Invalid candidate bounds',
},
]);
/**
* Unpack the candidates from one parsed reply value.
*
* @param request - The raw value plus whether proposals are still allowed at this point.
* @returns The candidate list, or `[value]` when no envelope is present.
* @throws TypeError When `allowed` is not boolean, or when the envelope is disallowed,
* malformed, out of bounds or contains nested candidate envelopes.
*/
function parse({ value, allowed }: { value: unknown; allowed: boolean }): unknown[] {
if (typeof allowed !== 'boolean') throw new TypeError('Candidate availability must be boolean');
if (!value || typeof value !== 'object' || !Object.hasOwn(value, field)) return [value];
const candidates = (value as Record<string, unknown>)[field] as unknown[];
breakWhile(
[
{ occur: () => !allowed, error, tags: ['availability'] },
{
occur: () => Array.isArray(value) || Object.keys(value).length !== 1,
error,
tags: ['envelope'],
},
{
occur: [
() => !Array.isArray(candidates),
() => candidates.length < min || candidates.length > max,
],
error,
tags: ['selection'],
},
{
occur: () => candidates.some((candidate) => invalidCandidate(candidate, field)),
error,
tags: ['members'],
},
],
tags,
);
return candidates;
}
return Object.freeze({ field, minCandidates: min, maxCandidates: max, parse });
}