import { ContextMap, ContextScope } from '../context'; import { JsonValue } from '../json'; /** * A named, parameterised check that a {@link Rule} can refer to by id. * * Each primitive declares which context keys it reads (`reads`) and * takes some args. Calling `build(args)` returns the actual check, a * function that reads from a {@link ContextScope} and returns `true` * or `false`. Primitives are registered once at bootstrap; rules * reference them by id from then on. * * Display fields (title, description, icon, a custom args editor) live * in {@link ConfigMetadata}, passed alongside the primitive when * registering it. Same pattern as * {@link WebComponentRegistry | WebComponentRegistry} and * {@link CommandBus | CommandBus}. * * @typeParam TArgs - The shape of the args bound by `build`. Must * extend {@link JsonValue} because rules round-trip through JSON * persistence. Defaults to {@link JsonValue}. Use * {@link definePrimitive} so TypeScript can infer this from `build`'s * parameter. * @typeParam TReads - Union of context keys the predicate is allowed * to read. Inferred from `reads` by {@link definePrimitive}. Defaults * to every key in {@link ContextMap}. * * @example A primitive that reads one context key * ```ts * import { definePrimitive } from '@limetech/lime-web-components'; * * type OlderThanArgs = { days: number }; * * const MS_PER_DAY = 24 * 60 * 60 * 1000; * * const objectOlderThan = definePrimitive({ * id: 'limetech.object-older-than', * reads: ['limeobject'], * build: * ({ days }: OlderThanArgs) => * (scope) => { * const object = scope.get('limeobject'); * const created = object?.getValue('createdtime'); * * if (typeof created !== 'string') { * return false; * } * * return Date.now() - new Date(created).getTime() > days * MS_PER_DAY; * }, * }); * ``` * * @remarks * An id must contain a `.` (e.g. `limetech.is-admin`, * `pluginx.has-feature`) so two unrelated authors can't accidentally * claim the same one, and because the same id space is shared with * saved rules, the prefix keeps those from colliding too. * * There is no separate dependency-injection mechanism: the code that * registers a primitive already has access to whatever it needs * (translator, http, repositories, etc.) and the `build` callback * closes over them. * * If a context key from `reads` has no provider, * {@link ContextScope.get} returns `undefined` and the predicate * should return `false`. Primitive authors should also fail closed on * unexpected args or downstream errors. The fail-closed behavior when * a predicate does throw is part of * {@link RuleRegistry.compile | compile}'s contract. See there for * details. * * `reads` is a `readonly` tuple, so the predicate's `scope.get` * parameter is restricted to keys declared here. The trade-off is that * callers should not mutate the array after construction; the registry * implementation already treats it as read-only. * * @see {@link definePrimitive} for the inference helper. * @see {@link RuleRegistry.register} for the registration API and how * to attach metadata. * * @beta * @group Rules */ export interface RulePrimitive { /** * Unique, namespaced identifier (e.g. `limetech.is-admin`). */ id: string; /** * Context keys the predicate will read at evaluation time. Used by * {@link RuleRegistry.validate | validate} to flag rules that * reference keys a caller can't provide, and by the type system to * narrow {@link ContextScope.get} to just those keys. */ reads: readonly TReads[]; /** * Bind args to a predicate. {@link RuleRegistry.compile | compile} * calls this once per `ref` leaf in the rule tree. * * @param args - The args from the `ref` leaf. * @returns A function that reads from a {@link ContextScope} and * returns `true` or `false`. */ build(args: TArgs): (scope: ContextScope) => boolean; } /** * Identity helper for declaring a primitive. Returns its argument * unchanged. * * The helper exists so TypeScript can infer `TArgs` from the * annotation on `build`'s parameter, and `TReads` from the `reads` * tuple. Without it, the args shape must either be repeated as a * variable type annotation, or `TArgs` falls back to its `unknown` * default, discarding the args type at every downstream call site. * * Inferring `TReads` also narrows the predicate's * {@link ContextScope.get | scope.get}, so reading a key that is not * listed in `reads` is a compile error. * * Does not validate the `id` namespace, that check happens at runtime * inside {@link RuleRegistry.register}. * * @param primitive - The primitive definition. * @returns The same `primitive`, unchanged. * * @example Inferring the args shape and the readable keys * ```ts * import { definePrimitive } from '@limetech/lime-web-components'; * * type OlderThanArgs = { days: number }; * * const olderThan = definePrimitive({ * id: 'limetech.older-than', * reads: ['limeobject'], * build: * ({ days }: OlderThanArgs) => * (scope) => { * // @ts-expect-error 'user' is not listed in reads * scope.get('user'); * * return scope.get('limeobject') !== undefined && days > 0; * }, * }); * ``` * * @beta * @group Rules */ export declare function definePrimitive(primitive: RulePrimitive): RulePrimitive;