import { Expectation, ExpectationMet } from '@serenity-js/assertions'; import { AnswersQuestions, Question, UsesAbilities } from '@serenity-js/core'; import { formatted } from '@serenity-js/core/lib/io'; import { RelativeQuestion } from './RelativeQuestion'; export interface Collection { filter(fn: (item: T, index?: number) => boolean | Promise): Collection; map(fn: (item: T, index?: number) => O): PromiseLike; first(): T; last(): T; get(index: number): T; count(): PromiseLike | number; } /** * @experimental */ export class Pick = Collection> { static from = Collection>(collection: Question | CT) { return new Pick(collection); } constructor( private readonly collection: Question | Collection_Type, private readonly filters: Filters = new Filters(), ) { } count(): Question> { return new NumberOfMatchingItems(this.collection, this.filters); } all(): Question { return new AllMatchingItems(this.collection, this.filters); } first(): Question { return new FirstMatchingItem(this.collection, this.filters); } last(): Question { return new LastMatchingItem(this.collection, this.filters); } get(index: number): Question { return new NthMatchingItem(this.collection, this.filters, index); } where( question: RelativeQuestion | Property_Type>, expectation: Expectation | Property_Type>, ): Pick { return new Pick( this.collection, this.filters.append(new Filter(question, expectation)), ); } } /** * @package */ class Filters> implements Question<(ct: Collection_Type) => Collection_Type> { constructor(private readonly filters: Array> = []) { } append(filter: Filter) { return new Filters(this.filters.concat(filter)); } answeredBy(actor: AnswersQuestions & UsesAbilities): (ct: Collection_Type) => Collection_Type { return (collection: Collection_Type) => this.filters.reduce((filteredCollection, filter) => filter.answeredBy(actor)(filteredCollection), collection, ); } toString() { const fullDescription = this.filters .reduce((description, filter) => description.concat(filter.toString()), [ ]) .join(' and '); return fullDescription.length > 0 ? `where ${ fullDescription }` : ''; } } /** * @package */ class Filter, Property_Type> implements Question<(ct: Collection_Type) => Collection_Type> { constructor( private readonly question: RelativeQuestion | Property_Type>, private readonly expectation: Expectation | Property_Type>, ) { } answeredBy(actor: AnswersQuestions & UsesAbilities): (ct: Collection_Type) => Collection_Type { return (collection: Collection_Type) => collection.filter((item: Item_Type) => { const expectation = this.expectation.answeredBy(actor); return Promise.resolve(this.question.of(item).answeredBy(actor)) .then(answer => expectation(answer)) .then(outcome => outcome instanceof ExpectationMet); }) as Collection_Type; } toString() { return formatted `${ this.question } does ${ this.expectation }`; } } /** * @package */ abstract class QuestionAboutCollectionItems, Answer_Type> implements Question { constructor( protected readonly collection: Question | CT, private readonly filters: Filters, private readonly description: string, ) { } abstract answeredBy(actor: AnswersQuestions & UsesAbilities): Answer_Type; toString() { return `${ this.description } ${ formatted `${ this.collection }`} ${ this.filters.toString() }`.trim(); } protected collectionFilteredBy(actor: AnswersQuestions & UsesAbilities): CT { const collection = this.isAQuestion(this.collection) ? this.collection.answeredBy(actor) : this.collection; return this.filters.answeredBy(actor)(collection); } private isAQuestion(h: any): h is Question { return !! (h as any).answeredBy; } } /** * @package */ class NumberOfMatchingItems> extends QuestionAboutCollectionItems> { constructor(collection: Question | CT, filters: Filters) { super(collection, filters, 'the number of'); } answeredBy(actor: AnswersQuestions & UsesAbilities): Promise { return Promise.resolve(this.collectionFilteredBy(actor).count()); } } /** * @package */ class AllMatchingItems> extends QuestionAboutCollectionItems { constructor(collection: Question | CT, filters: Filters) { super(collection, filters, ''); } answeredBy(actor: AnswersQuestions & UsesAbilities): CT { return this.collectionFilteredBy(actor); } } /** * @package */ class FirstMatchingItem> extends QuestionAboutCollectionItems { constructor(collection: Question | CT, filters: Filters) { super(collection, filters, 'the first of'); } answeredBy(actor: AnswersQuestions & UsesAbilities): IT { return this.collectionFilteredBy(actor).first(); } } /** * @package */ class LastMatchingItem> extends QuestionAboutCollectionItems { constructor(collection: Question | CT, filters: Filters) { super(collection, filters, 'the last of'); } answeredBy(actor: AnswersQuestions & UsesAbilities): IT { return this.collectionFilteredBy(actor).last(); } } /** * @package */ class NthMatchingItem> extends QuestionAboutCollectionItems { private static ordinalSuffixOf(index: number) { const j = index % 10, k = index % 100; switch (true) { case (j === 1 && k !== 11): return index + 'st'; case (j === 2 && k !== 12): return index + 'nd'; case (j === 3 && k !== 13): return index + 'rd'; default: return index + 'th'; } } constructor( collection: Question | CT, filters: Filters, private readonly index: number, ) { super(collection, filters, `the ${ NthMatchingItem.ordinalSuffixOf(index + 1) } of`); } answeredBy(actor: AnswersQuestions & UsesAbilities): IT { return this.collectionFilteredBy(actor).get(this.index); } }