import { ConditionFunctionExpr, FilterIteratorConfig, FindIteratorConfig, IteratorConfig, SomeIteratorConfig, EveryIteratorConfig, CountIteratorConfig, CollectionPredicateExpr, NullishExpr, IterateExpr, ResolvableValue, MapIteratorConfig, PipelineExpr, PredicateTestExpr, ReferenceExpr, TransformerFunctionExpr, } from '../types/expressions.type' import { ExpressionType, IteratorType, PredicateType } from '../types/enums' import { ExpressionBuilder } from './ExpressionBuilder' import { IterableBuilder } from './IterableBuilder' import { captureCallsite, stampCallsite } from './utils/captureCallsite' import { splitKey } from './utils/splitKey' /** * Immutable builder for ReferenceExpr with path navigation support. * * Extends the base expression builder pattern with: * - .path(key) for nested property access * * @example * Data('user').path('address.city') // path: ['data', 'user', 'address', 'city'] * Answer('email').pipe(Transformer.String.Trim).match(Condition.IsRequired()) * * @internal Exposed to authors via the ChainableRef interface. */ export class ReferenceBuilder { readonly nodeKind = 'forge-builder' as const private readonly reference: ReferenceExpr private readonly negated: boolean private constructor(ref: ReferenceExpr, negate: boolean) { this.reference = ref this.negated = negate } /** * Create a builder from path segments. */ static create(path: string[]): ReferenceBuilder { return new ReferenceBuilder( { type: ExpressionType.REFERENCE, path, }, false, ) } /** * Get the underlying reference expression. */ get expr(): ReferenceExpr { return this.reference } /** * Build the final reference expression. * Called automatically by finaliseBuilders(). */ build(): ReferenceExpr { return this.reference } /** * Navigate to a nested property. * Supports dot notation: .path('user.address.city') * * @example * Data('response').path('data.items') * Answer('user').path('address').path('postcode') */ path(key: string): ReferenceBuilder { const newRef: ReferenceExpr = { type: ExpressionType.REFERENCE, path: [...this.reference.path, ...splitKey(key)], } return new ReferenceBuilder(newRef, false) } /** * Transform through a pipeline of transformers. * Returns an ExpressionBuilder wrapping the PipelineExpr. * * @example * Answer('email').pipe(Transformer.String.Trim, Transformer.String.ToLowerCase) */ pipe(...steps: TransformerFunctionExpr[]): ExpressionBuilder { return ExpressionBuilder.pipeline(this.reference, steps) } /** Uses the fallback only when the value is null or undefined, like `??`. */ nullish(fallback: ResolvableValue | undefined): ExpressionBuilder { const expression: NullishExpr = { type: ExpressionType.NULLISH, input: this.reference, ...(fallback !== undefined && { fallback }), } const builder = ExpressionBuilder.from(expression) stampCallsite(builder, captureCallsite(this.nullish)) return builder } /** * Enter per-item iteration mode with a Find iterator. * Returns an ExpressionBuilder since Find returns a single item, not an array. * * @example * Data('users').each(Iterator.Find(Item().path('id').match(Condition.Equals(Params('userId'))))) * .path('name') // Navigate into the found item */ each(iterator: FindIteratorConfig): ExpressionBuilder /** Test the collection directly as a predicate. */ each(iterator: SomeIteratorConfig | EveryIteratorConfig): CollectionPredicateExpr /** Count matching items and continue with the resulting number. */ each(iterator: CountIteratorConfig): ExpressionBuilder /** * Enter per-item iteration mode with a Map or Filter iterator. * Returns an IterableBuilder that can chain more .each() calls or exit via .pipe(). * * @example * Data('items').each(Iterator.Map({ label: Item().path('name') })) * Data('items').each(Iterator.Filter(...)).each(Iterator.Map(...)) */ each(iterator: MapIteratorConfig | FilterIteratorConfig): IterableBuilder /** * Enter per-item iteration mode with an iterator. */ each( iterator: IteratorConfig, ): IterableBuilder | ExpressionBuilder | ExpressionBuilder | CollectionPredicateExpr { if (iterator.type === IteratorType.COUNT) { const expression: IterateExpr = { type: ExpressionType.ITERATE, input: this.reference, iterator } const builder = ExpressionBuilder.from(expression) stampCallsite(builder, captureCallsite(this.each)) return builder } if (iterator.type === IteratorType.SOME || iterator.type === IteratorType.EVERY) { const predicate: CollectionPredicateExpr = { type: ExpressionType.ITERATE, input: this.reference, iterator } stampCallsite(predicate, captureCallsite(this.each)) return predicate } if (iterator.type === IteratorType.FIND) { // Find returns a single item - wrap in a reference with empty path // so .path() works naturally const iterateExpr = { type: ExpressionType.ITERATE as const, input: this.reference, iterator, } const referenceExpr: ReferenceExpr = { type: ExpressionType.REFERENCE, base: iterateExpr, path: [], } return ExpressionBuilder.from(referenceExpr) } return IterableBuilder.create(this.reference, iterator) } /** * Test against a condition. * Terminal operation - returns a plain PredicateTestExpr. * * @example * Answer('email').match(Condition.Email.IsValid()) * Self().not.match(Condition.IsRequired()) */ match(condition: ConditionFunctionExpr): PredicateTestExpr { const predicate: PredicateTestExpr = { type: PredicateType.TEST, subject: this.reference, negate: this.negated, condition, } stampCallsite(predicate, captureCallsite(this.match)) return predicate } /** * Negate the next condition test. * Returns a new builder with toggled negation. * * @example * Self().not.match(Condition.IsRequired()) // negate: true * Answer('x').not.not.match(...) // negate: false (double negation) */ get not(): ReferenceBuilder { return new ReferenceBuilder(this.reference, !this.negated) } }