feat: file types for builders

This commit is contained in:
Jiralite
2026-07-20 11:39:16 +01:00
parent cffb0b588f
commit 8047fe7c55
6 changed files with 184 additions and 6 deletions
@@ -1,4 +1,4 @@
import type { APIFileUploadComponent } from 'discord-api-types/v10';
import type { APIFileUploadComponent, FileUploadType } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { FileUploadBuilder } from '../../src/components/fileUpload/FileUpload.js';
@@ -13,6 +13,16 @@ describe('File Upload Components', () => {
).not.toThrowError();
expect(() => new FileUploadBuilder().setCustomId('file_upload').setRequired(false).toJSON()).not.toThrowError();
expect(() =>
new FileUploadBuilder().setCustomId('file_upload').setFileTypes('audio', 'image', 'video', '.pdf').toJSON(),
).not.toThrowError();
});
test('File types can be added.', () => {
expect(
new FileUploadBuilder().setCustomId('file_upload').addFileTypes('image').addFileTypes(['.pdf']).toJSON()
.file_types,
).toEqual(['image', '.pdf']);
});
test('Invalid builder does throw.', () => {
@@ -26,6 +36,29 @@ describe('File Upload Components', () => {
).toThrowError();
expect(() => new FileUploadBuilder().setRequired(false).toJSON()).toThrowError();
expect(() =>
new FileUploadBuilder()
.setCustomId('file_upload')
.setFileTypes(Array.from({ length: 11 }, () => '.txt' as const))
.toJSON(),
).toThrowError();
for (const invalidFileType of ['document', 'pdf', '.']) {
expect(() =>
new FileUploadBuilder()
.setCustomId('file_upload')
.setFileTypes(invalidFileType as FileUploadType)
.toJSON(),
).toThrowError();
}
expect(() =>
new FileUploadBuilder({
type: ComponentType.FileUpload,
custom_id: 'file_upload',
file_types: ['document' as FileUploadType],
}).toJSON(),
).toThrowError();
});
test('API data equals toJSON().', () => {
@@ -34,13 +67,22 @@ describe('File Upload Components', () => {
custom_id: 'file_upload',
min_values: 4,
max_values: 9,
file_types: ['image', '.pdf'],
required: false,
} satisfies APIFileUploadComponent;
expect(new FileUploadBuilder(fileUploadData).toJSON()).toEqual(fileUploadData);
expect(
new FileUploadBuilder().setCustomId('file_upload').setMinValues(4).setMaxValues(9).setRequired(false).toJSON(),
new FileUploadBuilder()
.setCustomId('file_upload')
.setMinValues(4)
.setMaxValues(9)
.setFileTypes(fileUploadData.file_types)
.setRequired(false)
.toJSON(),
).toEqual(fileUploadData);
expect(new FileUploadBuilder(fileUploadData).clearFileTypes().toJSON().file_types).toBeUndefined();
});
});
@@ -10,6 +10,7 @@ import {
type APIApplicationCommandRoleOption,
type APIApplicationCommandStringOption,
type APIApplicationCommandUserOption,
type FileUploadType,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
@@ -212,11 +213,37 @@ describe('Application Command toJSON() results', () => {
});
test('GIVEN an attachment option THEN calling toJSON should return a valid JSON', () => {
expect(getAttachmentOption().toJSON()).toEqual<APIApplicationCommandAttachmentOption>({
expect(
getAttachmentOption().setFileTypes(['image', '.pdf']).toJSON(),
).toEqual<APIApplicationCommandAttachmentOption>({
name: 'attachment',
description: 'attachment',
type: ApplicationCommandOptionType.Attachment,
required: true,
file_types: ['image', '.pdf'],
});
});
test('GIVEN attachment option file types THEN they can be added', () => {
expect(
Reflect.get(getAttachmentOption().addFileTypes('image').addFileTypes(['.pdf']).toJSON(), 'file_types'),
).toEqual(['image', '.pdf']);
});
test('GIVEN attachment option file types THEN they can be cleared', () => {
expect(
Reflect.get(getAttachmentOption().setFileTypes('audio', '.ogg').clearFileTypes().toJSON(), 'file_types'),
).toBeUndefined();
});
test('GIVEN too many attachment option file types THEN the builder should throw', () => {
expect(() => getAttachmentOption().setFileTypes(Array.from({ length: 11 }, () => '.txt' as const))).toThrowError();
});
test.each(['document', 'pdf', '.'])(
'GIVEN invalid attachment file type %s THEN the builder should throw',
(fileType) => {
expect(() => getAttachmentOption().setFileTypes(fileType as FileUploadType)).toThrowError();
},
);
});
@@ -1,5 +1,6 @@
import { s } from '@sapphire/shapeshift';
import { ComponentType } from 'discord-api-types/v10';
import { fileUploadTypesValidator } from '../../util/fileUpload.js';
import { customIdValidator, idValidator } from '../Assertions.js';
export const fileUploadPredicate = s.object({
@@ -8,5 +9,6 @@ export const fileUploadPredicate = s.object({
custom_id: customIdValidator,
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(10).optional(),
max_values: s.number().greaterThanOrEqual(1).lessThanOrEqual(10).optional(),
file_types: fileUploadTypesValidator.optional(),
required: s.boolean().optional(),
});
@@ -1,4 +1,5 @@
import { type APIFileUploadComponent, ComponentType } from 'discord-api-types/v10';
import { type APIFileUploadComponent, ComponentType, type FileUploadType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { ComponentBuilder } from '../Component.js';
import { fileUploadPredicate } from './Assertions.js';
@@ -17,6 +18,7 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
* custom_id: "file_upload",
* min_values: 2,
* max_values: 5,
* file_types: ["image", ".pdf"],
* });
* ```
* @example
@@ -26,7 +28,9 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
* custom_id: "file_upload",
* min_values: 2,
* max_values: 5,
* }).setRequired();
* })
* .setFileTypes("image", ".pdf")
* .setRequired();
* ```
*/
public constructor(data: Partial<APIFileUploadComponent> = {}) {
@@ -79,6 +83,41 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
return this;
}
/**
* Adds file types allowed in this file upload.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public addFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types ??= [];
this.data.file_types.push(...normalizeArray(fileTypes));
return this;
}
/**
* Sets the file types allowed in this file upload.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public setFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types = normalizeArray(fileTypes);
return this;
}
/**
* Clears the file types allowed in this file upload.
*/
public clearFileTypes() {
this.data.file_types = undefined;
return this;
}
/**
* Sets whether this file upload is required.
*
@@ -1,4 +1,10 @@
import { ApplicationCommandOptionType, type APIApplicationCommandAttachmentOption } from 'discord-api-types/v10';
import {
ApplicationCommandOptionType,
type APIApplicationCommandAttachmentOption,
type FileUploadType,
} from 'discord-api-types/v10';
import { fileUploadTypesValidator } from '../../../util/fileUpload.js';
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
@@ -10,6 +16,54 @@ export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase {
*/
public override readonly type = ApplicationCommandOptionType.Attachment as const;
/**
* The file types allowed for this attachment option.
*/
public readonly file_types?: FileUploadType[];
/**
* Adds file types allowed for this attachment option.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public addFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
const normalizedFileTypes = normalizeArray(fileTypes);
fileUploadTypesValidator.parse([...(this.file_types ?? []), ...normalizedFileTypes]);
if (this.file_types === undefined) {
Reflect.set(this, 'file_types', []);
}
this.file_types!.push(...normalizedFileTypes);
return this;
}
/**
* Sets the file types allowed for this attachment option.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public setFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
const normalizedFileTypes = normalizeArray(fileTypes);
fileUploadTypesValidator.parse(normalizedFileTypes);
Reflect.set(this, 'file_types', normalizedFileTypes);
return this;
}
/**
* Clears the file types allowed for this attachment option.
*/
public clearFileTypes() {
Reflect.set(this, 'file_types', undefined);
return this;
}
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
+14
View File
@@ -0,0 +1,14 @@
import { s } from '@sapphire/shapeshift';
import { isValidationEnabled } from './validation.js';
export const fileUploadTypesValidator = s
.array(
s.union([
s.literal('audio'),
s.literal('image'),
s.literal('video'),
s.string().lengthGreaterThanOrEqual(2).regex(/^\./),
]),
)
.lengthLessThanOrEqual(10)
.setValidationEnabled(isValidationEnabled);