diff --git a/src/lib/utils/application-commands/compute-differences/option.ts b/src/lib/utils/application-commands/compute-differences/option.ts deleted file mode 100644 index 65d734332..000000000 --- a/src/lib/utils/application-commands/compute-differences/option.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { - ApplicationCommandOptionType, - type APIApplicationCommandBasicOption, - type APIApplicationCommandChannelOption, - type APIApplicationCommandOption -} from 'discord-api-types/v10'; -import { - hasChannelTypesSupport, - hasChoicesAndAutocompleteSupport, - hasMinMaxLengthSupport, - hasMinMaxValueSupport, - optionTypeToPrettyName, - subcommandTypes, - type APIApplicationCommandChoosableAndAutocompletableTypes, - type APIApplicationCommandMinAndMaxValueTypes, - type APIApplicationCommandMinMaxLengthTypes, - type APIApplicationCommandSubcommandTypes, - type CommandDifference -} from './_shared'; -import { checkDescription } from './description'; -import { checkLocalizations } from './localizations'; -import { checkName } from './name'; -import { handleAutocomplete } from './option/autocomplete'; -import { checkChannelTypes } from './option/channelTypes'; -import { handleMinMaxLengthOptions } from './option/minMaxLength'; -import { handleMinMaxValueOptions } from './option/minMaxValue'; -import { checkOptionRequired } from './option/required'; -import { checkOptionType } from './option/type'; - -export function* reportOptionDifferences({ - option, - existingOption, - currentIndex, - keyPath = (index: number) => `options[${index}]` -}: { - option: APIApplicationCommandOption; - currentIndex: number; - existingOption?: APIApplicationCommandOption; - keyPath?: (index: number) => string; -}): Generator { - // If current option doesn't exist, report and return - if (!existingOption) { - const expectedType = optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; - - yield { - key: keyPath(currentIndex), - expected: `${expectedType} with name ${option.name}`, - original: 'no option present' - }; - - return; - } - - // Check type - yield* checkOptionType({ - key: `${keyPath(currentIndex)}.type`, - originalType: existingOption.type, - expectedType: option.type - }); - - // Check name - yield* checkName({ - key: `${keyPath(currentIndex)}.name`, - oldName: existingOption.name, - newName: option.name - }); - - // Check localized names - const originalLocalizedNames = existingOption.name_localizations; - const expectedLocalizedNames = option.name_localizations; - - yield* checkLocalizations({ - localeMapName: `${keyPath(currentIndex)}.nameLocalizations`, - localePresentMessage: 'localized names', - localeMissingMessage: 'no localized names', - originalLocalizedDescriptions: originalLocalizedNames, - expectedLocalizedDescriptions: expectedLocalizedNames - }); - - // Check description - yield* checkDescription({ - key: `${keyPath(currentIndex)}.description`, - oldDescription: existingOption.description, - newDescription: option.description - }); - - // Check localized descriptions - const originalLocalizedDescriptions = existingOption.description_localizations; - const expectedLocalizedDescriptions = option.description_localizations; - - yield* checkLocalizations({ - localeMapName: `${keyPath(currentIndex)}.descriptionLocalizations`, - localePresentMessage: 'localized descriptions', - localeMissingMessage: 'no localized descriptions', - originalLocalizedDescriptions, - expectedLocalizedDescriptions - }); - - // Check required - yield* checkOptionRequired({ - key: `${keyPath(currentIndex)}.required`, - oldRequired: existingOption.required, - newRequired: option.required - }); - - // Check for subcommands - if (subcommandTypes.includes(existingOption.type) && subcommandTypes.includes(option.type)) { - const castedExisting = existingOption as APIApplicationCommandSubcommandTypes; - const castedExpected = option as APIApplicationCommandSubcommandTypes; - - if ( - castedExisting.type === ApplicationCommandOptionType.SubcommandGroup && - castedExpected.type === ApplicationCommandOptionType.SubcommandGroup - ) { - // We know we have options in this case, because they are both groups - for (const [subcommandIndex, subcommandOption] of castedExpected.options!.entries()) { - yield* reportOptionDifferences({ - currentIndex: subcommandIndex, - option: subcommandOption, - existingOption: castedExisting.options?.[subcommandIndex], - keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` - }); - } - } else if ( - castedExisting.type === ApplicationCommandOptionType.Subcommand && - castedExpected.type === ApplicationCommandOptionType.Subcommand - ) { - yield* handleSubcommandOptions({ - expectedOptions: castedExpected.options, - existingOptions: castedExisting.options, - currentIndex, - keyPath - }); - } - } - - if (hasMinMaxValueSupport(option)) { - // Check min and max_value - const existingCasted = existingOption as APIApplicationCommandMinAndMaxValueTypes; - - yield* handleMinMaxValueOptions({ - currentIndex, - existingOption: existingCasted, - expectedOption: option, - keyPath - }); - } - - if (hasChoicesAndAutocompleteSupport(option)) { - const existingCasted = existingOption as APIApplicationCommandChoosableAndAutocompletableTypes; - - yield* handleAutocomplete({ - expectedOption: option, - existingOption: existingCasted, - currentIndex, - keyPath - }); - } - - if (hasMinMaxLengthSupport(option)) { - // Check min and max_value - const existingCasted = existingOption as APIApplicationCommandMinMaxLengthTypes; - - yield* handleMinMaxLengthOptions({ - currentIndex, - existingOption: existingCasted, - expectedOption: option, - keyPath - }); - } - - if (hasChannelTypesSupport(option)) { - // Check channel_types - const existingCasted = existingOption as APIApplicationCommandChannelOption; - - yield* checkChannelTypes({ - currentIndex, - existingChannelTypes: existingCasted.channel_types, - keyPath, - newChannelTypes: option.channel_types - }); - } -} - -function* handleSubcommandOptions({ - expectedOptions, - existingOptions, - currentIndex, - keyPath -}: { - expectedOptions?: APIApplicationCommandBasicOption[]; - existingOptions?: APIApplicationCommandBasicOption[]; - currentIndex: number; - keyPath: (index: number) => string; -}): Generator { - // 0. No existing options and now we have options - if (!existingOptions?.length && expectedOptions?.length) { - yield { - key: `${keyPath(currentIndex)}.options`, - expected: 'options present', - original: 'no options present' - }; - } - - // 1. Existing options and now we have no options - else if (existingOptions?.length && !expectedOptions?.length) { - yield { - key: `${keyPath(currentIndex)}.options`, - expected: 'no options present', - original: 'options present' - }; - } - - // 2. Iterate over each option if we have any and see what's different - else if (expectedOptions?.length) { - let processedIndex = 0; - for (const subcommandOption of expectedOptions) { - const currentSubCommandOptionIndex = processedIndex++; - const existingSubcommandOption = existingOptions![currentSubCommandOptionIndex]; - - yield* reportOptionDifferences({ - currentIndex: currentSubCommandOptionIndex, - option: subcommandOption, - existingOption: existingSubcommandOption, - keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` - }); - } - - // If we went through less options than we previously had, report that - if (processedIndex < existingOptions!.length) { - let option: APIApplicationCommandOption; - while ((option = existingOptions![processedIndex]) !== undefined) { - const expectedType = - optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; - - yield { - key: `existing command option at path ${keyPath(currentIndex)}.options[${processedIndex}]`, - expected: 'no option present', - original: `${expectedType} with name ${option.name}` - }; - - processedIndex++; - } - } - } -} diff --git a/src/lib/utils/application-commands/compute-differences/options.ts b/src/lib/utils/application-commands/compute-differences/options.ts index 0d8d7304d..3293532ae 100644 --- a/src/lib/utils/application-commands/compute-differences/options.ts +++ b/src/lib/utils/application-commands/compute-differences/options.ts @@ -1,6 +1,31 @@ -import type { APIApplicationCommandOption } from 'discord-api-types/v10'; -import { optionTypeToPrettyName, type CommandDifference } from './_shared'; -import { reportOptionDifferences } from './option'; +import { + ApplicationCommandOptionType, + type APIApplicationCommandBasicOption, + type APIApplicationCommandChannelOption, + type APIApplicationCommandOption +} from 'discord-api-types/v10'; +import { + hasChannelTypesSupport, + hasChoicesAndAutocompleteSupport, + hasMinMaxLengthSupport, + hasMinMaxValueSupport, + optionTypeToPrettyName, + subcommandTypes, + type APIApplicationCommandChoosableAndAutocompletableTypes, + type APIApplicationCommandMinAndMaxValueTypes, + type APIApplicationCommandMinMaxLengthTypes, + type APIApplicationCommandSubcommandTypes, + type CommandDifference +} from './_shared'; +import { checkDescription } from './description'; +import { checkLocalizations } from './localizations'; +import { checkName } from './name'; +import { handleAutocomplete } from './option/autocomplete'; +import { checkChannelTypes } from './option/channelTypes'; +import { handleMinMaxLengthOptions } from './option/minMaxLength'; +import { handleMinMaxValueOptions } from './option/minMaxValue'; +import { checkOptionRequired } from './option/required'; +import { checkOptionType } from './option/type'; export function* checkOptions( existingOptions?: APIApplicationCommandOption[], @@ -49,3 +74,221 @@ export function* checkOptions( } } } + +function* reportOptionDifferences({ + option, + existingOption, + currentIndex, + keyPath = (index: number) => `options[${index}]` +}: { + option: APIApplicationCommandOption; + currentIndex: number; + existingOption?: APIApplicationCommandOption; + keyPath?: (index: number) => string; +}): Generator { + // If current option doesn't exist, report and return + if (!existingOption) { + const expectedType = optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; + + yield { + key: keyPath(currentIndex), + expected: `${expectedType} with name ${option.name}`, + original: 'no option present' + }; + + return; + } + + // Check type + yield* checkOptionType({ + key: `${keyPath(currentIndex)}.type`, + originalType: existingOption.type, + expectedType: option.type + }); + + // Check name + yield* checkName({ + key: `${keyPath(currentIndex)}.name`, + oldName: existingOption.name, + newName: option.name + }); + + // Check localized names + const originalLocalizedNames = existingOption.name_localizations; + const expectedLocalizedNames = option.name_localizations; + + yield* checkLocalizations({ + localeMapName: `${keyPath(currentIndex)}.nameLocalizations`, + localePresentMessage: 'localized names', + localeMissingMessage: 'no localized names', + originalLocalizedDescriptions: originalLocalizedNames, + expectedLocalizedDescriptions: expectedLocalizedNames + }); + + // Check description + yield* checkDescription({ + key: `${keyPath(currentIndex)}.description`, + oldDescription: existingOption.description, + newDescription: option.description + }); + + // Check localized descriptions + const originalLocalizedDescriptions = existingOption.description_localizations; + const expectedLocalizedDescriptions = option.description_localizations; + + yield* checkLocalizations({ + localeMapName: `${keyPath(currentIndex)}.descriptionLocalizations`, + localePresentMessage: 'localized descriptions', + localeMissingMessage: 'no localized descriptions', + originalLocalizedDescriptions, + expectedLocalizedDescriptions + }); + + // Check required + yield* checkOptionRequired({ + key: `${keyPath(currentIndex)}.required`, + oldRequired: existingOption.required, + newRequired: option.required + }); + + // Check for subcommands + if (subcommandTypes.includes(existingOption.type) && subcommandTypes.includes(option.type)) { + const castedExisting = existingOption as APIApplicationCommandSubcommandTypes; + const castedExpected = option as APIApplicationCommandSubcommandTypes; + + if ( + castedExisting.type === ApplicationCommandOptionType.SubcommandGroup && + castedExpected.type === ApplicationCommandOptionType.SubcommandGroup + ) { + // We know we have options in this case, because they are both groups + for (const [subcommandIndex, subcommandOption] of castedExpected.options!.entries()) { + yield* reportOptionDifferences({ + currentIndex: subcommandIndex, + option: subcommandOption, + existingOption: castedExisting.options?.[subcommandIndex], + keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` + }); + } + } else if ( + castedExisting.type === ApplicationCommandOptionType.Subcommand && + castedExpected.type === ApplicationCommandOptionType.Subcommand + ) { + yield* handleSubcommandOptions({ + expectedOptions: castedExpected.options, + existingOptions: castedExisting.options, + currentIndex, + keyPath + }); + } + } + + if (hasMinMaxValueSupport(option)) { + // Check min and max_value + const existingCasted = existingOption as APIApplicationCommandMinAndMaxValueTypes; + + yield* handleMinMaxValueOptions({ + currentIndex, + existingOption: existingCasted, + expectedOption: option, + keyPath + }); + } + + if (hasChoicesAndAutocompleteSupport(option)) { + const existingCasted = existingOption as APIApplicationCommandChoosableAndAutocompletableTypes; + + yield* handleAutocomplete({ + expectedOption: option, + existingOption: existingCasted, + currentIndex, + keyPath + }); + } + + if (hasMinMaxLengthSupport(option)) { + // Check min and max_value + const existingCasted = existingOption as APIApplicationCommandMinMaxLengthTypes; + + yield* handleMinMaxLengthOptions({ + currentIndex, + existingOption: existingCasted, + expectedOption: option, + keyPath + }); + } + + if (hasChannelTypesSupport(option)) { + // Check channel_types + const existingCasted = existingOption as APIApplicationCommandChannelOption; + + yield* checkChannelTypes({ + currentIndex, + existingChannelTypes: existingCasted.channel_types, + keyPath, + newChannelTypes: option.channel_types + }); + } +} + +function* handleSubcommandOptions({ + expectedOptions, + existingOptions, + currentIndex, + keyPath +}: { + expectedOptions?: APIApplicationCommandBasicOption[]; + existingOptions?: APIApplicationCommandBasicOption[]; + currentIndex: number; + keyPath: (index: number) => string; +}): Generator { + // 0. No existing options and now we have options + if (!existingOptions?.length && expectedOptions?.length) { + yield { + key: `${keyPath(currentIndex)}.options`, + expected: 'options present', + original: 'no options present' + }; + } + + // 1. Existing options and now we have no options + else if (existingOptions?.length && !expectedOptions?.length) { + yield { + key: `${keyPath(currentIndex)}.options`, + expected: 'no options present', + original: 'options present' + }; + } + + // 2. Iterate over each option if we have any and see what's different + else if (expectedOptions?.length) { + let processedIndex = 0; + for (const subcommandOption of expectedOptions) { + const currentSubCommandOptionIndex = processedIndex++; + const existingSubcommandOption = existingOptions![currentSubCommandOptionIndex]; + + yield* reportOptionDifferences({ + currentIndex: currentSubCommandOptionIndex, + option: subcommandOption, + existingOption: existingSubcommandOption, + keyPath: (index) => `${keyPath(currentIndex)}.options[${index}]` + }); + } + + // If we went through less options than we previously had, report that + if (processedIndex < existingOptions!.length) { + let option: APIApplicationCommandOption; + while ((option = existingOptions![processedIndex]) !== undefined) { + const expectedType = + optionTypeToPrettyName.get(option.type) ?? `unknown (${option.type}); please contact Sapphire developers about this!`; + + yield { + key: `existing command option at path ${keyPath(currentIndex)}.options[${processedIndex}]`, + expected: 'no option present', + original: `${expectedType} with name ${option.name}` + }; + + processedIndex++; + } + } + } +} diff --git a/yarn.lock b/yarn.lock index 8aa94b4cd..663a69d3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1191,15 +1191,6 @@ __metadata: languageName: node linkType: hard -"@sapphire/discord-utilities@npm:^3.4.0": - version: 3.4.0 - resolution: "@sapphire/discord-utilities@npm:3.4.0" - dependencies: - discord-api-types: "npm:^0.37.97" - checksum: 10/007368a5332d82faa2e4d6045343a4079b358dea616692c696a204e962a1fc59982dd411e5b26c1b4936d759f77d1ed74517417948f02a8558d3049253419f14 - languageName: node - linkType: hard - "@sapphire/discord.js-utilities@npm:^7.3.0": version: 7.3.0 resolution: "@sapphire/discord.js-utilities@npm:7.3.0" @@ -2616,13 +2607,6 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.37.97": - version: 0.37.98 - resolution: "discord-api-types@npm:0.37.98" - checksum: 10/972bdcc6d15e60ea12d35b6da784dd434254d58b036d4218ccd6fcde5be78c63586c1c85e26bcedaacd97ece057efc6130fdef0d3d8279e3d6bc67e66a7b61bd - languageName: node - linkType: hard - "discord.js@npm:^14.15.3": version: 14.15.3 resolution: "discord.js@npm:14.15.3"