import { ConfigMetadata } from '../core'; import { ContextMap, ContextScope, ContextScopeOptions } from '../context'; import { Rule, CompiledRule } from './rule'; import { RulePrimitive } from './primitive'; /** * What {@link RuleRegistry.validate} returns. Check `ok`: when it's * `true` ({@link RuleValidationSuccess}), the rule is fine. When it's * `false` ({@link RuleValidationFailure}), `issues` lists what's wrong. * * @beta * @group Rules */ export type RuleValidationResult = RuleValidationSuccess | RuleValidationFailure; /** * Successful outcome of {@link RuleRegistry.validate}. * * @beta * @group Rules */ export interface RuleValidationSuccess { /** * Discriminant. The rule passed validation. */ ok: true; } /** * Failure outcome of {@link RuleRegistry.validate}. * * @beta * @group Rules */ export interface RuleValidationFailure { /** * Discriminant. The rule has one or more issues. */ ok: false; /** * Structured issues, ordered by traversal of the rule tree. */ issues: RuleValidationIssue[]; } /** * One thing that's wrong with a rule. * * `path` points at the spot in the tree (a mix of property names and * array indices, `['rules', 0, 'rule']` means `root.rules[0].rule`). * `code` * is a short tag for routing on the failure type, `message` is the * explanation you'd show a person. * * @beta * @group Rules */ export interface RuleValidationIssue { /** * Where in the rule tree the issue is, property names and array * indices in order. Empty when the issue applies to the rule as a * whole. */ path: (string | number)[]; /** * Short machine-readable tag for the kind of issue. Useful when * something more than `message` is needed, for example, branching * a UI on the failure type. * * Documented values: * * - `unknown-ref`, a `ref` points at an id that is neither a * registered primitive nor a known saved rule. * - `args-mismatch`, the `args` on a `ref` don't match what the * primitive expects. Only emitted by implementations that can * introspect a primitive's args (e.g. via a separate schema * mechanism). The contract does not require args validation. * - `cycle`, saved rules reference each other in a loop. * - `missing-slot`, the primitive reads a context key the caller * didn't list in `available`. * - `args-on-saved-rule`, a `ref` to a saved rule carries `args`, * which are ignored. Flagged so the author can clean it up. * * The list is open: implementations may emit codes not listed here, * and future releases may add new ones. If a code is unfamiliar, * fall back to `message`. */ code: 'unknown-ref' | 'args-mismatch' | 'cycle' | 'missing-slot' | 'args-on-saved-rule' | (string & {}); /** * Human-readable description. */ message: string; } /** * The central place primitives and rules come together. * * This package only describes the contract; the actual behavior is * supplied by an implementation, reached through the platform service * `ruleRegistry`. * * What the API does, by area: * * - **Primitives.** {@link RuleRegistry.register | register} adds a * {@link RulePrimitive}; * {@link RuleRegistry.listPrimitives | listPrimitives} lists them. * - **Rule lifecycle.** {@link RuleRegistry.compile | compile} turns a * {@link Rule} into a callable predicate. * {@link RuleRegistry.validate | validate} runs the same checks * without throwing or producing a predicate. * - **Building a scope (convenience).** {@link RuleRegistry.scope | scope} * delegates to {@link ContextRegistry.scope | ContextRegistry.scope} * so rule code does not have to thread two services. New consumers * that don't compile rules should go to the context registry * directly. * * Context resolution (DOM-walked providers, session-wide fallbacks, * scope construction) lives on {@link ContextRegistry}. Primitives * read from a {@link ContextScope} without needing to know which * service built it. * * **Saved rules** are loaded by the host client from its own config * layer at boot and live in the same id namespace as primitives. They * are not registered through this interface. The registry resolves * references during {@link RuleRegistry.compile | compile} and * {@link RuleRegistry.validate | validate} but exposes no public API * for managing them. * * @example Compiling and evaluating a rule from a web component * ```ts * import { Element, Prop, State } from '@stencil/core'; * import { * ComponentDescriptor, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * class MyPanel { * @Element() * private host: HTMLElement; * * @Prop() * public descriptor: ComponentDescriptor; * * @State() * private visible = true; * * public connectedCallback() { * const visibility = this.descriptor.rules?.visibility; * * if (!visibility) { * return; * } * * const registry = platform.get(PlatformServiceName.RuleRegistry); * const compiled = registry.compile(visibility); * * // `host` lets the scope resolve every key in `compiled.reads` from the * // providers above this element in the DOM. * this.visible = compiled.evaluate(registry.scope({ host: this.host })); * } * * public render() { * return this.visible ? 'Visible' : ''; * } * } * ``` * * @example Evaluating against pinned values, as a test would * ```ts * import { * LimeObjectDraft, * PlatformServiceName, * Rule, * } from '@limetech/lime-web-components'; * * const registry = platform.get(PlatformServiceName.RuleRegistry); * * // The object the form is editing, with `dealstatus` already changed to `won`. * declare const draft: LimeObjectDraft; * * const rule: Rule = { * type: 'ref', * id: 'limetech.form-field-equals', * args: { field: 'dealstatus', value: 'won' }, * }; * * // `with` pins values, so the rule can be evaluated without a host element and * // without any registered providers. * const scope = registry.scope({ * with: { * context: { limetype: 'deal', id: 1234 }, * limeobjectDraft: draft, * }, * }); * * const passes = registry.compile(rule).evaluate(scope); * ``` * * @beta * @group Rules */ export interface RuleRegistry { /** * Register a {@link RulePrimitive}. * * The id must be namespaced (contain a `.`) and must not be in use * already, neither by another primitive nor by a saved rule. * * `metadata` is optional and carries the display side of things: * title, description, icon, tags, and a * {@link ConfigMetadata.configComponent | configComponent} for * editing the primitive's args. None of it is used at evaluation * time; it shows up later via * {@link RuleRegistry.listMetadata | listMetadata}. * * @param primitive - The primitive to register. * @param metadata - Optional display metadata. * @throws If the id is unnamespaced, already taken by another * primitive, or already in use by a saved rule. */ register(primitive: RulePrimitive, metadata?: ConfigMetadata): void; /** * Attach display metadata to an already-registered primitive. * * Use this when the UI binding (a * {@link ConfigMetadata.configComponent | configComponent} to * author a primitive's args) is contributed by a UI layer that * loads after the primitive itself is registered. * * If a field is already set, the existing value is kept and a * warning is logged. * * @param id - The primitive id to attach metadata to. * @param metadata - The fields to attach. * @throws If no primitive with `id` has been registered. Throwing * (rather than silently retaining orphan metadata) surfaces typos * and ordering bugs at boot. */ attachMetadata(id: string, metadata: ConfigMetadata): void; /** * List the registered primitives, the runtime-callable side only. * For titles, icons, or custom args editors, use * {@link RuleRegistry.listMetadata | listMetadata}. */ listPrimitives(): RulePrimitive[]; /** * List the display metadata supplied at registration. * * Optional, an implementation that doesn't track metadata can * leave this method off. When present, returns one entry per * registered primitive. * * @returns Metadata for each registered primitive. */ listMetadata?(): PrimitiveMetadata[]; /** * Turn a {@link Rule} into a {@link CompiledRule}: an * {@link CompiledRule.evaluate | evaluate} function paired with the * static {@link CompiledRule.reads | reads} it depends on. * * Every `ref` is resolved against the registered primitives and * saved rules. Saved rules are expanded in place and cycles are * detected. Args at each leaf are not validated by the contract; * primitives are responsible for handling their own args (see * {@link RulePrimitive}). An implementation may surface * `args-mismatch` issues if it has a way to introspect a primitive's * args, but the contract does not require it. * * The returned {@link CompiledRule} is a snapshot: it reflects * whatever was registered when `compile` ran, so anything compiled * against an older set of saved rules is stale once those rules * change. If * results are cached, recompile when saved rules change. Re-running * `compile` on a structurally-equal rule may or may not return the * same `CompiledRule` instance, so don't rely on identity equality. * * Thrown errors inside a predicate are caught and treated as * `false` for that node, the same outcome as a missing context key. * Callers do not need to wrap evaluations in `try/catch`. This * fail-closed behavior applies only to errors raised at * evaluation time. Errors raised by {@link RulePrimitive.build} * while compiling propagate out of `compile`, so a primitive that * cannot handle its args should return a constant-`false` * predicate from `build` rather than throwing. * * @param rule - The rule to compile. * @returns A {@link CompiledRule} carrying `evaluate` and `reads`. * @throws If the rule has an `unknown-ref` or `cycle` issue, or if * a {@link RulePrimitive.build} call throws. Implementations that * validate args may also throw `args-mismatch`. * `args-on-saved-rule` does not throw, the args are dropped and * {@link RuleRegistry.validate} reports it as a diagnostic. Use * {@link RuleRegistry.validate} for the non-throwing variant. */ compile(rule: Rule): CompiledRule; /** * Reactively evaluate a compiled rule against an already-built * {@link ContextScope} — including one produced by * {@link ContextScope.extend}. The push-side mirror of * {@link CompiledRule.evaluate}, which already takes a scope. * * Semantics match the options-based overload: emits the current * result immediately, then re-emits only when the boolean flips. It * re-evaluates when any key in `compiled.reads` changes, **except keys * the scope has pinned** (its `with` plus everything layered on by * `extend`) — a pinned key can't affect the result, so it is never * subscribed to. Dynamic keys are watched against the scope's `host`. * * Pinned values are captured for the life of the subscription. If a * pinned value's identity changes (e.g. a row's `limeobject` is * replaced), discard this subscription and subscribe again against a * freshly-extended scope — the same lifecycle the options overload * already implies for `with`. * * Declared before the options overload: a {@link ContextScope} also * structurally satisfies {@link ContextScopeOptions} (every member of * which is optional), so this more specific shape must come first to * be selectable. A plain options object lacks `get`/`extend` and falls * through to the overload below. * * @param compiled - A rule compiled via {@link compile}. * @param scope - The scope to evaluate against, typically host-bound * and possibly extended with per-item overrides. * @param listener - Called with the current result, then on change. * @returns A teardown that stops every underlying subscription. */ subscribe(compiled: CompiledRule, scope: ContextScope, listener: (result: boolean) => void): () => void; /** * Reactively evaluate a compiled rule and call `listener` whenever * the result changes. * * Emits the current result immediately, then re-emits only when the * boolean flips — not on every context tick. Re-evaluates when any * key in `compiled.reads` changes, except keys pinned by * `options.with` (a pinned key can't affect the result). `with` * values are captured once at subscribe time; a value that needs to * change must come from a provider resolved via `options.host`. * * Composes {@link ContextRegistry.subscribe}, * {@link RuleRegistry.scope | scope}, and {@link CompiledRule.reads}; * no new context machinery. * * @param compiled - A rule compiled via {@link compile}. * @param options - Scope construction options (`host` and/or `with`), * the same shape {@link RuleRegistry.scope | scope} takes. * @param listener - Called with the current result, then on change. * @returns A teardown that stops every underlying subscription. */ subscribe(compiled: CompiledRule, options: ContextScopeOptions, listener: (result: boolean) => void): () => void; /** * Same checks as {@link RuleRegistry.compile | compile}, but * returns structured diagnostics instead of throwing, and no * predicate is produced. * * `available` is a hint from the caller, not something the * registry can verify: the registry doesn't know which context * keys will be supplied at evaluation time, only the caller * building the scope does. Pass it when known; otherwise the * runtime fallback (missing keys produce `false`) catches the gap. * * @param rule - The rule to validate. * @param available - Optional list of context keys the caller * knows will be available where the rule runs. Used to surface * `missing-slot` issues at save time. * @returns `ok: true` when the rule passes every check; otherwise * `ok: false` with a list of issues. */ validate(rule: Rule, available?: (keyof ContextMap)[]): RuleValidationResult; /** * Build a {@link ContextScope}, the same as * {@link ContextRegistry.scope | ContextRegistry.scope}. Exists on * the rule registry as a convenience so the typical "compile and * evaluate" sequence stays a single-service interaction. * * New non-rule consumers should reach for * {@link ContextRegistry.scope | ContextRegistry.scope} directly. * * @param options - Optional construction options. * @returns A new {@link ContextScope}. */ scope(options?: ContextScopeOptions): ContextScope; } /** * Display metadata for a registered {@link RulePrimitive}. * * The {@link ConfigMetadata} the caller supplied at registration, * paired with the primitive's `id` and `reads` so consumers can find * what they need without a second lookup. * * Returned by {@link RuleRegistry.listMetadata}. * * @beta * @group Rules */ export type PrimitiveMetadata = ConfigMetadata & { /** * The primitive's id. */ id: string; /** * Context keys the primitive reads. Lets a caller filter the list * to just the primitives that work with the keys it can provide. */ reads: readonly (keyof ContextMap)[]; };