@ovencord/builders 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +92 -0
  3. package/package.json +70 -0
  4. package/src/Assertions.ts +15 -0
  5. package/src/components/ActionRow.ts +346 -0
  6. package/src/components/Assertions.ts +190 -0
  7. package/src/components/Component.ts +47 -0
  8. package/src/components/Components.ts +275 -0
  9. package/src/components/button/Button.ts +34 -0
  10. package/src/components/button/CustomIdButton.ts +74 -0
  11. package/src/components/button/LinkButton.ts +39 -0
  12. package/src/components/button/PremiumButton.ts +26 -0
  13. package/src/components/button/mixins/EmojiOrLabelButtonMixin.ts +52 -0
  14. package/src/components/fileUpload/Assertions.ts +12 -0
  15. package/src/components/fileUpload/FileUpload.ts +109 -0
  16. package/src/components/label/Assertions.ts +28 -0
  17. package/src/components/label/Label.ts +215 -0
  18. package/src/components/selectMenu/BaseSelectMenu.ts +89 -0
  19. package/src/components/selectMenu/ChannelSelectMenu.ts +115 -0
  20. package/src/components/selectMenu/MentionableSelectMenu.ts +126 -0
  21. package/src/components/selectMenu/RoleSelectMenu.ts +89 -0
  22. package/src/components/selectMenu/StringSelectMenu.ts +165 -0
  23. package/src/components/selectMenu/StringSelectMenuOption.ts +113 -0
  24. package/src/components/selectMenu/UserSelectMenu.ts +89 -0
  25. package/src/components/textInput/Assertions.ts +15 -0
  26. package/src/components/textInput/TextInput.ts +154 -0
  27. package/src/components/v2/Assertions.ts +82 -0
  28. package/src/components/v2/Container.ts +254 -0
  29. package/src/components/v2/File.ts +81 -0
  30. package/src/components/v2/MediaGallery.ts +128 -0
  31. package/src/components/v2/MediaGalleryItem.ts +85 -0
  32. package/src/components/v2/Section.ts +266 -0
  33. package/src/components/v2/Separator.ts +82 -0
  34. package/src/components/v2/TextDisplay.ts +63 -0
  35. package/src/components/v2/Thumbnail.ts +100 -0
  36. package/src/index.ts +109 -0
  37. package/src/interactions/commands/Command.ts +87 -0
  38. package/src/interactions/commands/SharedName.ts +68 -0
  39. package/src/interactions/commands/SharedNameAndDescription.ts +69 -0
  40. package/src/interactions/commands/chatInput/Assertions.ts +180 -0
  41. package/src/interactions/commands/chatInput/ChatInputCommand.ts +40 -0
  42. package/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts +117 -0
  43. package/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts +52 -0
  44. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts +57 -0
  45. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts +32 -0
  46. package/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts +43 -0
  47. package/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts +204 -0
  48. package/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts +61 -0
  49. package/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts +66 -0
  50. package/src/interactions/commands/chatInput/options/attachment.ts +20 -0
  51. package/src/interactions/commands/chatInput/options/boolean.ts +20 -0
  52. package/src/interactions/commands/chatInput/options/channel.ts +28 -0
  53. package/src/interactions/commands/chatInput/options/integer.ts +34 -0
  54. package/src/interactions/commands/chatInput/options/mentionable.ts +20 -0
  55. package/src/interactions/commands/chatInput/options/number.ts +34 -0
  56. package/src/interactions/commands/chatInput/options/role.ts +20 -0
  57. package/src/interactions/commands/chatInput/options/string.ts +71 -0
  58. package/src/interactions/commands/chatInput/options/user.ts +20 -0
  59. package/src/interactions/commands/contextMenu/Assertions.ts +31 -0
  60. package/src/interactions/commands/contextMenu/ContextMenuCommand.ts +42 -0
  61. package/src/interactions/commands/contextMenu/MessageCommand.ts +19 -0
  62. package/src/interactions/commands/contextMenu/UserCommand.ts +19 -0
  63. package/src/interactions/modals/Assertions.ts +27 -0
  64. package/src/interactions/modals/Modal.ts +158 -0
  65. package/src/messages/AllowedMentions.ts +193 -0
  66. package/src/messages/Assertions.ts +148 -0
  67. package/src/messages/Attachment.ts +209 -0
  68. package/src/messages/Message.ts +692 -0
  69. package/src/messages/MessageReference.ts +111 -0
  70. package/src/messages/embed/Assertions.ts +53 -0
  71. package/src/messages/embed/Embed.ts +352 -0
  72. package/src/messages/embed/EmbedAuthor.ts +83 -0
  73. package/src/messages/embed/EmbedField.ts +67 -0
  74. package/src/messages/embed/EmbedFooter.ts +65 -0
  75. package/src/messages/poll/Assertions.ts +20 -0
  76. package/src/messages/poll/Poll.ts +243 -0
  77. package/src/messages/poll/PollAnswer.ts +77 -0
  78. package/src/messages/poll/PollAnswerMedia.ts +38 -0
  79. package/src/messages/poll/PollMedia.ts +41 -0
  80. package/src/messages/poll/PollQuestion.ts +20 -0
  81. package/src/util/ValidationError.ts +21 -0
  82. package/src/util/normalizeArray.ts +19 -0
  83. package/src/util/resolveBuilder.ts +40 -0
  84. package/src/util/validation.ts +58 -0
@@ -0,0 +1,65 @@
1
+ import type { JSONEncodable } from '@ovencord/util';
2
+ import type { APIEmbedFooter } from 'discord-api-types/v10';
3
+ import { validate } from '../../util/validation.js';
4
+ import { embedFooterPredicate } from './Assertions.js';
5
+
6
+ /**
7
+ * A builder that creates API-compatible JSON data for the embed footer.
8
+ */
9
+ export class EmbedFooterBuilder implements JSONEncodable<APIEmbedFooter> {
10
+ /**
11
+ * The API data associated with this embed footer.
12
+ */
13
+ private readonly data: Partial<APIEmbedFooter>;
14
+
15
+ /**
16
+ * Creates a new embed footer.
17
+ *
18
+ * @param data - The API data to create this embed footer with
19
+ */
20
+ public constructor(data: Partial<APIEmbedFooter> = {}) {
21
+ this.data = structuredClone(data);
22
+ }
23
+
24
+ /**
25
+ * Sets the text for this embed footer.
26
+ *
27
+ * @param text - The text to use
28
+ */
29
+ public setText(text: string): this {
30
+ this.data.text = text;
31
+ return this;
32
+ }
33
+
34
+ /**
35
+ * Sets the url for this embed footer.
36
+ *
37
+ * @param url - The url to use
38
+ */
39
+ public setIconURL(url: string): this {
40
+ this.data.icon_url = url;
41
+ return this;
42
+ }
43
+
44
+ /**
45
+ * Clears the icon URL for this embed footer.
46
+ */
47
+ public clearIconURL(): this {
48
+ this.data.icon_url = undefined;
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Serializes this builder to API-compatible JSON data.
54
+ *
55
+ * Note that by disabling validation, there is no guarantee that the resulting object will be valid.
56
+ *
57
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
58
+ */
59
+ public toJSON(validationOverride?: boolean): APIEmbedFooter {
60
+ const clone = structuredClone(this.data);
61
+ validate(embedFooterPredicate, clone, validationOverride);
62
+
63
+ return clone as APIEmbedFooter;
64
+ }
65
+ }
@@ -0,0 +1,20 @@
1
+ import { PollLayoutType } from 'discord-api-types/v10';
2
+ import { z } from 'zod';
3
+ import { emojiPredicate } from '../../components/Assertions';
4
+
5
+ export const pollQuestionPredicate = z.object({ text: z.string().min(1).max(300) });
6
+
7
+ export const pollAnswerMediaPredicate = z.object({
8
+ text: z.string().min(1).max(55),
9
+ emoji: emojiPredicate.optional(),
10
+ });
11
+
12
+ export const pollAnswerPredicate = z.object({ poll_media: pollAnswerMediaPredicate });
13
+
14
+ export const pollPredicate = z.object({
15
+ question: pollQuestionPredicate,
16
+ answers: z.array(pollAnswerPredicate).min(1).max(10),
17
+ duration: z.number().min(1).max(768).optional(),
18
+ allow_multiselect: z.boolean().optional(),
19
+ layout_type: z.nativeEnum(PollLayoutType).optional(),
20
+ });
@@ -0,0 +1,243 @@
1
+ import type { JSONEncodable } from '@ovencord/util';
2
+ import type { RESTAPIPoll, APIPollMedia, PollLayoutType, APIPollAnswer } from 'discord-api-types/v10';
3
+ import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
4
+ import { resolveBuilder } from '../../util/resolveBuilder.js';
5
+ import { validate } from '../../util/validation.js';
6
+ import { pollPredicate } from './Assertions';
7
+ import { PollAnswerBuilder } from './PollAnswer.js';
8
+ import { PollQuestionBuilder } from './PollQuestion.js';
9
+
10
+ export interface PollData extends Omit<RESTAPIPoll, 'answers' | 'question'> {
11
+ answers: PollAnswerBuilder[];
12
+ question: PollQuestionBuilder;
13
+ }
14
+
15
+ /**
16
+ * A builder that creates API-compatible JSON data for polls.
17
+ */
18
+ export class PollBuilder implements JSONEncodable<RESTAPIPoll> {
19
+ /**
20
+ * The API data associated with this poll.
21
+ */
22
+ private readonly data: PollData;
23
+
24
+ /**
25
+ * Gets the answers of this poll.
26
+ */
27
+ public get answers(): readonly PollAnswerBuilder[] {
28
+ return this.data.answers;
29
+ }
30
+
31
+ /**
32
+ * Creates a new poll.
33
+ *
34
+ * @param data - The API data to create this poll with
35
+ */
36
+ public constructor(data: Partial<RESTAPIPoll> = {}) {
37
+ const { question, answers = [], ...rest } = data;
38
+
39
+ this.data = {
40
+ ...structuredClone(rest),
41
+ question: new PollQuestionBuilder(question),
42
+ answers: answers.map((answer) => new PollAnswerBuilder(answer)),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Appends answers to the poll.
48
+ *
49
+ * @remarks
50
+ * This method accepts either an array of answers or a variable number of answer parameters.
51
+ * The maximum amount of answers that can be added is 10.
52
+ * @example
53
+ * Using an array:
54
+ * ```ts
55
+ * const answers: APIPollMedia[] = ...;
56
+ * const poll = new PollBuilder()
57
+ * .addAnswers(answers);
58
+ * ```
59
+ * @example
60
+ * Using rest parameters (variadic):
61
+ * ```ts
62
+ * const poll = new PollBuilder()
63
+ * .addAnswers(
64
+ * { text: 'Answer 1' },
65
+ * { text: 'Answer 2' },
66
+ * );
67
+ * ```
68
+ * @param answers - The answers to add
69
+ */
70
+ public addAnswers(
71
+ ...answers: RestOrArray<
72
+ Omit<APIPollAnswer, 'answer_id'> | PollAnswerBuilder | ((builder: PollAnswerBuilder) => PollAnswerBuilder)
73
+ >
74
+ ): this {
75
+ const normalizedAnswers = normalizeArray(answers);
76
+ const resolved = normalizedAnswers.map((answer) => resolveBuilder(answer, PollAnswerBuilder));
77
+
78
+ this.data.answers.push(...resolved);
79
+ return this;
80
+ }
81
+
82
+ /**
83
+ * Removes, replaces, or inserts answers for this poll.
84
+ *
85
+ * @remarks
86
+ * This method behaves similarly
87
+ * to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
88
+ * The maximum amount of answers that can be added is 10.
89
+ *
90
+ * It's useful for modifying and adjusting order of the already-existing answers of a poll.
91
+ * @example
92
+ * Remove the first answer:
93
+ * ```ts
94
+ * poll.spliceAnswers(0, 1);
95
+ * ```
96
+ * @example
97
+ * Remove the first n answers:
98
+ * ```ts
99
+ * const n = 4;
100
+ * poll.spliceAnswers(0, n);
101
+ * ```
102
+ * @example
103
+ * Remove the last answer:
104
+ * ```ts
105
+ * poll.spliceAnswers(-1, 1);
106
+ * ```
107
+ * @param index - The index to start at
108
+ * @param deleteCount - The number of answers to remove
109
+ * @param answers - The replacing answer objects
110
+ */
111
+ public spliceAnswers(
112
+ index: number,
113
+ deleteCount: number,
114
+ ...answers: (
115
+ | Omit<APIPollAnswer, 'answer_id'>
116
+ | PollAnswerBuilder
117
+ | ((builder: PollAnswerBuilder) => PollAnswerBuilder)
118
+ )[]
119
+ ): this {
120
+ const normalizedAnswers = normalizeArray(answers);
121
+ const resolved = normalizedAnswers.map((answer) => resolveBuilder(answer, PollAnswerBuilder));
122
+
123
+ this.data.answers.splice(index, deleteCount, ...resolved);
124
+ return this;
125
+ }
126
+
127
+ /**
128
+ * Sets the answers for this poll.
129
+ *
130
+ * @remarks
131
+ * This method is an alias for {@link PollBuilder.spliceAnswers}. More specifically,
132
+ * it splices the entire array of answers, replacing them with the provided answers.
133
+ *
134
+ * You can set a maximum of 10 answers.
135
+ * @param answers - The answers to set
136
+ */
137
+ public setAnswers(
138
+ ...answers: RestOrArray<
139
+ Omit<APIPollAnswer, 'answer_id'> | PollAnswerBuilder | ((builder: PollAnswerBuilder) => PollAnswerBuilder)
140
+ >
141
+ ): this {
142
+ return this.spliceAnswers(0, this.data.answers.length, ...normalizeArray(answers));
143
+ }
144
+
145
+ /**
146
+ * Sets the question for this poll.
147
+ *
148
+ * @param options - The data to use for this poll's question
149
+ */
150
+ public setQuestion(
151
+ options:
152
+ | Omit<APIPollMedia, 'emoji'>
153
+ | PollQuestionBuilder
154
+ | ((builder: PollQuestionBuilder) => PollQuestionBuilder),
155
+ ): this {
156
+ this.data.question = resolveBuilder(options, PollQuestionBuilder);
157
+ return this;
158
+ }
159
+
160
+ /**
161
+ * Updates the question of this poll.
162
+ *
163
+ * @param updater - The function to update the question with
164
+ */
165
+ public updateQuestion(updater: (builder: PollQuestionBuilder) => void): this {
166
+ updater(this.data.question);
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Sets the layout type for this poll.
172
+ *
173
+ * @remarks
174
+ * This method is redundant while only one type of poll layout exists (`PollLayoutType.Default`)
175
+ * with Discord using that as the layout type if none is specified.
176
+ * @param type - The type of poll layout to use
177
+ */
178
+ public setLayoutType(type: PollLayoutType): this {
179
+ this.data.layout_type = type;
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Clears the layout type for this poll.
185
+ */
186
+ public clearLayoutType(): this {
187
+ this.data.layout_type = undefined;
188
+ return this;
189
+ }
190
+
191
+ /**
192
+ * Sets whether multi-select is enabled for this poll.
193
+ *
194
+ * @param multiSelect - Whether to allow multi-select
195
+ */
196
+ public setMultiSelect(multiSelect = true): this {
197
+ this.data.allow_multiselect = multiSelect;
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Sets how long this poll will be open for in hours.
203
+ *
204
+ * @remarks
205
+ * Minimum duration is `1`, with maximum duration being `768` (32 days).
206
+ * Default if none specified is `24` (one day).
207
+ * @param duration - The amount of hours this poll will be open for
208
+ */
209
+ public setDuration(duration: number): this {
210
+ this.data.duration = duration;
211
+ return this;
212
+ }
213
+
214
+ /**
215
+ * Clears the duration for this poll.
216
+ */
217
+ public clearDuration(): this {
218
+ this.data.duration = undefined;
219
+ return this;
220
+ }
221
+
222
+ /**
223
+ * Serializes this builder to API-compatible JSON data.
224
+ *
225
+ * Note that by disabling validation, there is no guarantee that the resulting object will be valid.
226
+ *
227
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
228
+ */
229
+ public toJSON(validationOverride?: boolean): RESTAPIPoll {
230
+ const { answers, question, ...rest } = this.data;
231
+
232
+ const data = {
233
+ ...structuredClone(rest),
234
+ // Disable validation because the pollPredicate below will validate those as well
235
+ answers: answers.map((answer) => answer.toJSON(false)),
236
+ question: question.toJSON(false),
237
+ };
238
+
239
+ validate(pollPredicate, data, validationOverride);
240
+
241
+ return data;
242
+ }
243
+ }
@@ -0,0 +1,77 @@
1
+ import type { JSONEncodable } from '@ovencord/util';
2
+ import type { APIPollAnswer, APIPollMedia } from 'discord-api-types/v10';
3
+ import { resolveBuilder } from '../../util/resolveBuilder';
4
+ import { validate } from '../../util/validation';
5
+ import { pollAnswerPredicate } from './Assertions';
6
+ import { PollAnswerMediaBuilder } from './PollAnswerMedia';
7
+
8
+ export interface PollAnswerData extends Omit<APIPollAnswer, 'answer_id' | 'poll_media'> {
9
+ poll_media: PollAnswerMediaBuilder;
10
+ }
11
+
12
+ /**
13
+ * A builder that creates API-compatible JSON data for poll answers.
14
+ */
15
+ export class PollAnswerBuilder implements JSONEncodable<Omit<APIPollAnswer, 'answer_id'>> {
16
+ /**
17
+ * The API data associated with this poll answer.
18
+ */
19
+ private readonly data: PollAnswerData;
20
+
21
+ /**
22
+ * Creates a new poll answer.
23
+ *
24
+ * @param data - The API data to create this poll answer with
25
+ */
26
+ public constructor(data: Partial<Omit<APIPollAnswer, 'answer_id'>> = {}) {
27
+ const { poll_media, ...rest } = data;
28
+
29
+ this.data = {
30
+ ...structuredClone(rest),
31
+ poll_media: new PollAnswerMediaBuilder(poll_media),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Sets the media for this poll answer.
37
+ *
38
+ * @param options - The data to use for this poll answer's media
39
+ */
40
+ public setMedia(
41
+ options: APIPollMedia | PollAnswerMediaBuilder | ((builder: PollAnswerMediaBuilder) => PollAnswerMediaBuilder),
42
+ ): this {
43
+ this.data.poll_media = resolveBuilder(options, PollAnswerMediaBuilder);
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Updates the media of this poll answer.
49
+ *
50
+ * @param updater - The function to update the media with
51
+ */
52
+ public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void): this {
53
+ updater(this.data.poll_media);
54
+ return this;
55
+ }
56
+
57
+ /**
58
+ * Serializes this builder to API-compatible JSON data.
59
+ *
60
+ * Note that by disabling validation, there is no guarantee that the resulting object will be valid.
61
+ *
62
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
63
+ */
64
+ public toJSON(validationOverride?: boolean): Omit<APIPollAnswer, 'answer_id'> {
65
+ const { poll_media, ...rest } = this.data;
66
+
67
+ const data = {
68
+ ...structuredClone(rest),
69
+ // Disable validation because the pollAnswerPredicate below will validate this as well
70
+ poll_media: poll_media.toJSON(false),
71
+ };
72
+
73
+ validate(pollAnswerPredicate, data, validationOverride);
74
+
75
+ return data;
76
+ }
77
+ }
@@ -0,0 +1,38 @@
1
+ import type { APIPartialEmoji, APIPollMedia } from 'discord-api-types/v10';
2
+ import { validate } from '../../util/validation.js';
3
+ import { pollAnswerMediaPredicate } from './Assertions.js';
4
+ import { PollMediaBuilder } from './PollMedia.js';
5
+
6
+ /**
7
+ * A builder that creates API-compatible JSON data for the media of a poll answer.
8
+ */
9
+ export class PollAnswerMediaBuilder extends PollMediaBuilder {
10
+ /**
11
+ * Sets the emoji for this poll answer media.
12
+ *
13
+ * @param emoji - The emoji to use
14
+ */
15
+ public setEmoji(emoji: APIPartialEmoji): this {
16
+ this.data.emoji = emoji;
17
+ return this;
18
+ }
19
+
20
+ /**
21
+ * Clears the emoji for this poll answer media.
22
+ */
23
+ public clearEmoji(): this {
24
+ this.data.emoji = undefined;
25
+ return this;
26
+ }
27
+
28
+ /**
29
+ * {@inheritDoc PollMediaBuilder.toJSON}
30
+ */
31
+ public override toJSON(validationOverride?: boolean): APIPollMedia {
32
+ const clone = structuredClone(this.data);
33
+
34
+ validate(pollAnswerMediaPredicate, clone, validationOverride);
35
+
36
+ return clone;
37
+ }
38
+ }
@@ -0,0 +1,41 @@
1
+ import type { APIPollMedia } from 'discord-api-types/v10';
2
+
3
+ /**
4
+ * The base poll media builder that contains common symbols for poll media builders.
5
+ */
6
+ export abstract class PollMediaBuilder {
7
+ /**
8
+ * The API data associated with this poll media.
9
+ *
10
+ * @internal
11
+ */
12
+ protected readonly data: Partial<APIPollMedia>;
13
+
14
+ /**
15
+ * Creates new poll media.
16
+ *
17
+ * @param data - The API data to create this poll media with
18
+ */
19
+ public constructor(data: Partial<APIPollMedia> = {}) {
20
+ this.data = structuredClone(data);
21
+ }
22
+
23
+ /**
24
+ * Sets the text for this poll media.
25
+ *
26
+ * @param text - The text to use
27
+ */
28
+ public setText(text: string): this {
29
+ this.data.text = text;
30
+ return this;
31
+ }
32
+
33
+ /**
34
+ * Serializes this builder to API-compatible JSON data.
35
+ *
36
+ * Note that by disabling validation, there is no guarantee that the resulting object will be valid.
37
+ *
38
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
39
+ */
40
+ public abstract toJSON(validationOverride?: boolean): APIPollMedia;
41
+ }
@@ -0,0 +1,20 @@
1
+ import type { APIPollMedia } from 'discord-api-types/v10';
2
+ import { validate } from '../../util/validation.js';
3
+ import { pollQuestionPredicate } from './Assertions.js';
4
+ import { PollMediaBuilder } from './PollMedia.js';
5
+
6
+ /**
7
+ * A builder that creates API-compatible JSON data for a poll question.
8
+ */
9
+ export class PollQuestionBuilder extends PollMediaBuilder {
10
+ /**
11
+ * {@inheritDoc PollMediaBuilder.toJSON}
12
+ */
13
+ public override toJSON(validationOverride?: boolean): Omit<APIPollMedia, 'emoji'> {
14
+ const clone = structuredClone(this.data);
15
+
16
+ validate(pollQuestionPredicate, clone, validationOverride);
17
+
18
+ return clone;
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ import type { z } from 'zod';
2
+
3
+ /**
4
+ * An error that is thrown when validation fails.
5
+ */
6
+ export class ValidationError extends Error {
7
+ /**
8
+ * The underlying cause of the validation error.
9
+ */
10
+ public override readonly cause: z.ZodError;
11
+
12
+ /**
13
+ * @internal
14
+ */
15
+ public constructor(error: z.ZodError) {
16
+ super(error.message);
17
+
18
+ this.name = 'ValidationError';
19
+ this.cause = error;
20
+ }
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Normalizes data that is a rest parameter or an array into an array with a depth of 1.
3
+ *
4
+ * @typeParam ItemType - The data that must satisfy {@link RestOrArray}.
5
+ * @param arr - The (possibly variadic) data to normalize
6
+ */
7
+ export function normalizeArray<ItemType>(arr: RestOrArray<ItemType>): ItemType[] {
8
+ if (Array.isArray(arr[0])) return [...arr[0]];
9
+ return arr as ItemType[];
10
+ }
11
+
12
+ /**
13
+ * Represents data that may be an array or came from a rest parameter.
14
+ *
15
+ * @remarks
16
+ * This type is used throughout builders to ensure both an array and variadic arguments
17
+ * may be used. It is normalized with {@link normalizeArray}.
18
+ */
19
+ export type RestOrArray<Type> = Type[] | [Type[]];
@@ -0,0 +1,40 @@
1
+ import type { JSONEncodable } from '@ovencord/util';
2
+
3
+ /**
4
+ * @privateRemarks
5
+ * This is a type-guard util, because if you were to in-line `builder instanceof Constructor` in the `resolveBuilder`
6
+ * function, TS doesn't narrow out the type `Builder`, causing a type error on the last return statement.
7
+ * @internal
8
+ */
9
+ function isBuilder<Builder extends JSONEncodable<any>>(
10
+ builder: unknown,
11
+ Constructor: new () => Builder,
12
+ ): builder is Builder {
13
+ return builder instanceof Constructor;
14
+ }
15
+
16
+ /**
17
+ * "Resolves" a builder from the 3 ways it can be input:
18
+ * 1. A clean instance
19
+ * 2. A data object that can be used to construct the builder
20
+ * 3. A function that takes a builder and returns a builder e.g. `builder => builder.setFoo('bar')`
21
+ *
22
+ * @typeParam Builder - The builder type
23
+ * @typeParam BuilderData - The data object that can be used to construct the builder
24
+ * @param builder - The user input, as described in the function description
25
+ * @param Constructor - The constructor of the builder
26
+ */
27
+ export function resolveBuilder<Builder extends JSONEncodable<any>, BuilderData extends Record<PropertyKey, any>>(
28
+ builder: Builder | BuilderData | ((builder: Builder) => Builder),
29
+ Constructor: new (data?: BuilderData) => Builder,
30
+ ): Builder {
31
+ if (isBuilder(builder, Constructor)) {
32
+ return builder;
33
+ }
34
+
35
+ if (typeof builder === 'function') {
36
+ return builder(new Constructor());
37
+ }
38
+
39
+ return new Constructor(builder);
40
+ }
@@ -0,0 +1,58 @@
1
+ import type { z } from 'zod';
2
+ import { ValidationError } from './ValidationError.js';
3
+
4
+ let validationEnabled = true;
5
+
6
+ /**
7
+ * Enables validators.
8
+ *
9
+ * @returns Whether validation is occurring.
10
+ */
11
+ export function enableValidators() {
12
+ return (validationEnabled = true);
13
+ }
14
+
15
+ /**
16
+ * Disables validators.
17
+ *
18
+ * @returns Whether validation is occurring.
19
+ */
20
+ export function disableValidators() {
21
+ return (validationEnabled = false);
22
+ }
23
+
24
+ /**
25
+ * Checks whether validation is occurring.
26
+ */
27
+ export function isValidationEnabled() {
28
+ return validationEnabled;
29
+ }
30
+
31
+ /**
32
+ * Parses a value with a given validator, accounting for whether validation is enabled.
33
+ *
34
+ * @param validator - The zod validator to use
35
+ * @param value - The value to parse
36
+ * @param validationOverride - Force validation to run/not run regardless of your global preference
37
+ * @returns The result from parsing
38
+ * @throws {@link ValidationError}
39
+ * Throws if the value does not pass validation, if enabled.
40
+ * @internal
41
+ */
42
+ export function validate<Validator extends z.ZodType>(
43
+ validator: Validator,
44
+ value: unknown,
45
+ validationOverride?: boolean,
46
+ ): z.output<Validator> {
47
+ if (validationOverride === false || (validationOverride === undefined && !isValidationEnabled())) {
48
+ return value as z.output<Validator>;
49
+ }
50
+
51
+ const result = validator.safeParse(value);
52
+
53
+ if (!result.success) {
54
+ throw new ValidationError(result.error);
55
+ }
56
+
57
+ return result.data;
58
+ }