feat: define getters and setters

This commit is contained in:
Mohamed Marrouchi 2025-03-24 12:11:54 +01:00
parent 8bef5e521b
commit 172a167296
5 changed files with 55 additions and 36 deletions

View File

@ -60,12 +60,12 @@ The envelope helpers introduce two key components to streamline outgoing message
```typescript ```typescript
// Build a simple text envelope: // Build a simple text envelope:
const env1 = EnvelopeBuilder(OutgoingMessageFormat.text) const env1 = EnvelopeBuilder(OutgoingMessageFormat.text)
.text('Hello') .setText('Hello')
.build(); .build();
// Append multiple quick replies: // Append multiple quick replies:
const env2 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies) const env2 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies)
.text('Are you interested?') .setText('Are you interested?')
.appendToQuickReplies({ .appendToQuickReplies({
content_type: QuickReplyType.text, content_type: QuickReplyType.text,
title: 'Yes', title: 'Yes',
@ -91,8 +91,8 @@ const textEnvelope = envelopeFactory.buildTextEnvelope('Hello, {context.user.nam
const quickRepliesEnvelope = envelopeFactory.buildQuickRepliesEnvelope( const quickRepliesEnvelope = envelopeFactory.buildQuickRepliesEnvelope(
'Do you want to proceed?', 'Do you want to proceed?',
[ [
{ content_type: 'text', title: 'Yes', payload: 'yes' }, { content_type: QuickReplyType.text, title: 'Yes', payload: 'yes' },
{ content_type: 'text', title: 'No', payload: 'no' } { content_type: QuickReplyType.text, title: 'No', payload: 'no' }
] ]
); );
``` ```

View File

@ -28,7 +28,7 @@ describe('EnvelopeBuilder', () => {
stdOutgoingTextEnvelopeSchema, stdOutgoingTextEnvelopeSchema,
); );
builder.text('Hello'); builder.setText('Hello');
const result = builder.build(); const result = builder.build();
expect(result).toEqual({ expect(result).toEqual({
@ -46,9 +46,9 @@ describe('EnvelopeBuilder', () => {
stdOutgoingTextEnvelopeSchema, stdOutgoingTextEnvelopeSchema,
); );
builder.text('Hello world'); builder.setText('Hello world');
// Retrieve current value with no argument // Retrieve current value with no argument
expect(builder.text()).toBe('Hello world'); expect(builder.getText()).toBe('Hello world');
}); });
it('should append items to array fields with appendToX methods', () => { it('should append items to array fields with appendToX methods', () => {
@ -58,7 +58,7 @@ describe('EnvelopeBuilder', () => {
stdOutgoingQuickRepliesEnvelopeSchema, stdOutgoingQuickRepliesEnvelopeSchema,
); );
builder.text('Choose an option'); builder.setText('Choose an option');
builder.appendToQuickReplies({ builder.appendToQuickReplies({
content_type: QuickReplyType.text, content_type: QuickReplyType.text,
title: 'Yes', title: 'Yes',
@ -76,8 +76,8 @@ describe('EnvelopeBuilder', () => {
message: { message: {
text: 'Choose an option', text: 'Choose an option',
quickReplies: [ quickReplies: [
{ content_type: 'text', title: 'Yes', payload: 'yes' }, { content_type: QuickReplyType.text, title: 'Yes', payload: 'yes' },
{ content_type: 'text', title: 'No', payload: 'no' }, { content_type: QuickReplyType.text, title: 'No', payload: 'no' },
], ],
}, },
}); });
@ -97,7 +97,7 @@ describe('EnvelopeBuilder', () => {
describe('getEnvelopeBuilder', () => { describe('getEnvelopeBuilder', () => {
it('should return a builder for text format that passes validation with required field', () => { it('should return a builder for text format that passes validation with required field', () => {
const builder = getEnvelopeBuilder(OutgoingMessageFormat.text); const builder = getEnvelopeBuilder(OutgoingMessageFormat.text);
builder.text('Hello from text envelope!'); builder.setText('Hello from text envelope!');
const envelope = builder.build(); const envelope = builder.build();
expect(envelope.format).toBe(OutgoingMessageFormat.text); expect(envelope.format).toBe(OutgoingMessageFormat.text);
@ -106,7 +106,7 @@ describe('getEnvelopeBuilder', () => {
it('should return a builder for quickReplies format that can append items', () => { it('should return a builder for quickReplies format that can append items', () => {
const builder = getEnvelopeBuilder(OutgoingMessageFormat.quickReplies); const builder = getEnvelopeBuilder(OutgoingMessageFormat.quickReplies);
builder.text('Pick an option'); builder.setText('Pick an option');
builder.appendToQuickReplies({ builder.appendToQuickReplies({
content_type: QuickReplyType.text, content_type: QuickReplyType.text,
title: 'Option A', title: 'Option A',

View File

@ -31,8 +31,11 @@ type ArrayKeys<T> = {
}[keyof T]; }[keyof T];
export type IEnvelopeBuilder<T extends StdOutgoingEnvelope> = { export type IEnvelopeBuilder<T extends StdOutgoingEnvelope> = {
[k in keyof T['message']]-?: ((arg: T['message'][k]) => IEnvelopeBuilder<T>) & [K in keyof T['message'] as `set${Capitalize<string & K>}`]-?: (
(() => T['message'][k]); arg: T['message'][K],
) => IEnvelopeBuilder<T>;
} & {
[K in keyof T['message'] as `get${Capitalize<string & K>}`]-?: () => T['message'][K];
} & { } & {
[K in ArrayKeys<T['message']> as `appendTo${Capitalize<string & K>}`]: ( [K in ArrayKeys<T['message']> as `appendTo${Capitalize<string & K>}`]: (
item: NonNullable<T['message'][K]> extends (infer U)[] ? U : never, item: NonNullable<T['message'][K]> extends (infer U)[] ? U : never,
@ -41,6 +44,21 @@ export type IEnvelopeBuilder<T extends StdOutgoingEnvelope> = {
build(): T; build(): T;
}; };
/**
* Extracts and transforms a property name into a standardized attribute name.
*
* @param prop - The property name from which to derive the attribute name.
* @param prefix - A regular expression that matches the prefix to remove from the property.
* @returns The transformed attribute name with its first character in lowercase.
*/
function getAttributeNameFromProp(prop: string, prefix: RegExp) {
// e.g. "appendToButtons" => "Buttons"
const rawKey = prop.toString().replace(prefix, '');
// e.g. "Buttons" -> "buttons"
const messageKey = rawKey.charAt(0).toLowerCase() + rawKey.slice(1);
return messageKey;
}
/** /**
* Builds an envelope object (containing a `format` and a `message` property) * Builds an envelope object (containing a `format` and a `message` property)
* and returns a proxy-based builder interface with chainable setter methods. * and returns a proxy-based builder interface with chainable setter methods.
@ -60,20 +78,20 @@ export type IEnvelopeBuilder<T extends StdOutgoingEnvelope> = {
* @example * @example
* // Build a simple text envelope: * // Build a simple text envelope:
* const env1 = EnvelopeBuilder(OutgoingMessageFormat.text) * const env1 = EnvelopeBuilder(OutgoingMessageFormat.text)
* .text('Hello') * .setText('Hello')
* .build(); * .build();
* *
* @example * @example
* // Build a text envelope with quick replies: * // Build a text envelope with quick replies:
* const env2 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies) * const env2 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies)
* .text('Hello') * .setText('Hello')
* .quickReplies([]) * .setQuickReplies([])
* .build(); * .build();
* *
* @example * @example
* // Append multiple quickReplies items: * // Append multiple quickReplies items:
* const env3 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies) * const env3 = EnvelopeBuilder(OutgoingMessageFormat.quickReplies)
* .text('Are you interested?') * .setText('Are you interested?')
* .appendToQuickReplies({ * .appendToQuickReplies({
* content_type: QuickReplyType.text, * content_type: QuickReplyType.text,
* title: 'Yes', * title: 'Yes',
@ -89,7 +107,7 @@ export type IEnvelopeBuilder<T extends StdOutgoingEnvelope> = {
* @example * @example
* // Build a system envelope with an outcome: * // Build a system envelope with an outcome:
* const env4 = EnvelopeBuilder(OutgoingMessageFormat.system) * const env4 = EnvelopeBuilder(OutgoingMessageFormat.system)
* .outcome('success') * .setOutcome('success')
* .build(); * .build();
*/ */
export function EnvelopeBuilder<T extends StdOutgoingEnvelope>( export function EnvelopeBuilder<T extends StdOutgoingEnvelope>(
@ -119,10 +137,7 @@ export function EnvelopeBuilder<T extends StdOutgoingEnvelope>(
} }
if (typeof prop === 'string' && prop.startsWith('appendTo')) { if (typeof prop === 'string' && prop.startsWith('appendTo')) {
// e.g. "appendToButtons" => "Buttons" const messageKey = getAttributeNameFromProp(prop, /^appendTo/);
const rawKey = prop.replace(/^appendTo/, '');
// e.g. "Buttons" -> "buttons"
const messageKey = rawKey.charAt(0).toLowerCase() + rawKey.slice(1);
return (item: unknown) => { return (item: unknown) => {
// Initialize the array if needed // Initialize the array if needed
@ -137,12 +152,16 @@ export function EnvelopeBuilder<T extends StdOutgoingEnvelope>(
return (...args: unknown[]): unknown => { return (...args: unknown[]): unknown => {
// If no arguments passed return current value. // If no arguments passed return current value.
if (0 === args.length) { if (0 === args.length) {
return built.message[prop.toString()]; const messageKey = getAttributeNameFromProp(
prop.toString(),
/^get/,
);
return built.message[messageKey];
} }
const value = args[0]; const value = args[0];
const messageKey = getAttributeNameFromProp(prop.toString(), /^set/);
built.message[prop.toString()] = value; built.message[messageKey] = value;
return builder; return builder;
}; };
}, },

View File

@ -15,7 +15,7 @@ import {
OutgoingMessageFormat, OutgoingMessageFormat,
} from '../schemas/types/message'; } from '../schemas/types/message';
import { ContentOptions } from '../schemas/types/options'; import { ContentOptions } from '../schemas/types/options';
import { StdQuickReply } from '../schemas/types/quick-reply'; import { QuickReplyType, StdQuickReply } from '../schemas/types/quick-reply';
import { EnvelopeFactory } from './envelope-factory'; import { EnvelopeFactory } from './envelope-factory';
@ -245,7 +245,7 @@ describe('EnvelopeFactory', () => {
} as AttachmentPayload; } as AttachmentPayload;
const quickReplies = [ const quickReplies = [
{ {
content_type: 'text', content_type: QuickReplyType.text,
title: 'Yes {contact.company_name}', title: 'Yes {contact.company_name}',
payload: 'do_{context.user.id}', payload: 'do_{context.user.id}',
}, },

View File

@ -130,7 +130,7 @@ export class EnvelopeFactory {
buildTextEnvelope(text: string | string[]): StdOutgoingTextEnvelope { buildTextEnvelope(text: string | string[]): StdOutgoingTextEnvelope {
const builder = this.getBuilder(OutgoingMessageFormat.text); const builder = this.getBuilder(OutgoingMessageFormat.text);
const processedText = this.processText(text); const processedText = this.processText(text);
return builder.text(processedText).build(); return builder.setText(processedText).build();
} }
/** /**
@ -149,7 +149,7 @@ export class EnvelopeFactory {
): StdOutgoingQuickRepliesEnvelope { ): StdOutgoingQuickRepliesEnvelope {
const builder = this.getBuilder(OutgoingMessageFormat.quickReplies); const builder = this.getBuilder(OutgoingMessageFormat.quickReplies);
const processedText = this.processText(text); const processedText = this.processText(text);
const envelope = builder.text(processedText); const envelope = builder.setText(processedText);
quickReplies.forEach((qr) => { quickReplies.forEach((qr) => {
envelope.appendToQuickReplies({ envelope.appendToQuickReplies({
@ -179,7 +179,7 @@ export class EnvelopeFactory {
): StdOutgoingButtonsEnvelope { ): StdOutgoingButtonsEnvelope {
const builder = this.getBuilder(OutgoingMessageFormat.buttons); const builder = this.getBuilder(OutgoingMessageFormat.buttons);
const processedText = this.processText(text); const processedText = this.processText(text);
const envelope = builder.text(processedText); const envelope = builder.setText(processedText);
buttons.forEach((btn) => { buttons.forEach((btn) => {
if (btn.type === ButtonType.postback) { if (btn.type === ButtonType.postback) {
@ -214,7 +214,7 @@ export class EnvelopeFactory {
quickReplies: StdQuickReply[] = [], quickReplies: StdQuickReply[] = [],
): StdOutgoingAttachmentEnvelope { ): StdOutgoingAttachmentEnvelope {
const builder = this.getBuilder(OutgoingMessageFormat.attachment); const builder = this.getBuilder(OutgoingMessageFormat.attachment);
const envelope = builder.attachment(attachment); const envelope = builder.setAttachment(attachment);
quickReplies.forEach((qr) => { quickReplies.forEach((qr) => {
envelope.appendToQuickReplies({ envelope.appendToQuickReplies({
@ -247,9 +247,9 @@ export class EnvelopeFactory {
): StdOutgoingListEnvelope { ): StdOutgoingListEnvelope {
const builder = this.getBuilder(format); const builder = this.getBuilder(format);
return builder return builder
.options(options) .setOptions(options)
.elements(elements) .setElements(elements)
.pagination(pagination) .setPagination(pagination)
.build(); .build();
} }
@ -268,6 +268,6 @@ export class EnvelopeFactory {
data?: unknown, data?: unknown,
): StdOutgoingSystemEnvelope { ): StdOutgoingSystemEnvelope {
const builder = this.getBuilder(OutgoingMessageFormat.system); const builder = this.getBuilder(OutgoingMessageFormat.system);
return builder.outcome(outcome).data(data).build(); return builder.setOutcome(outcome).setData(data).build();
} }
} }