util/builder/commandOptionBuilder.js

import {
  APPLICATION_COMMAND_TYPES,
  COMMAND_NAME_REGEX,
  LIMITS,
  TO_JSON_TYPES_ENUM,
} from "../../constants.js";

/**
 * Helps to create a choice for a command.
 * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure}
 */
class CommandOption {
  /**
   * Creates an option for a command.
   */
  constructor() {
    this.choices = [];

    this.options = [];

    this.defaultLocale = "en-US";
  }

  /**
   * Sets the name of the option.
   * @param {String | Object} name Sets the name of the option, or an object of names for localisation.
   * @returns {CommandOption}
   */
  setName(name) {
    if (!name)
      throw new TypeError("GLUON: Command option name must be provided.");

    if (typeof name != "string" && typeof name != "object")
      throw new TypeError(
        "GLUON: Command option name must be a string or an object.",
      );

    if (typeof name == "object") {
      if (name[this.defaultLocale].length > LIMITS.MAX_COMMAND_OPTION_NAME)
        throw new RangeError(
          `GLUON: Command option name must be less than ${LIMITS.MAX_COMMAND_OPTION_NAME} characters.`,
        );

      this.name = name[this.defaultLocale];

      delete name[this.defaultLocale];

      this.name_localizations = name;
    } else {
      if (name.length > LIMITS.MAX_COMMAND_OPTION_NAME)
        throw new RangeError(
          `GLUON: Command option name must be less than ${LIMITS.MAX_COMMAND_OPTION_NAME} characters.`,
        );

      this.name = name;
    }

    return this;
  }

  /**
   * Sets the option type.
   * @param {Number} type The option type.
   * @returns {Command}
   * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type}
   */
  setType(type) {
    if (typeof type != "number")
      throw new TypeError("GLUON: Command option type must be a number.");

    this.type = type;

    return this;
  }

  /**
   * Sets the description of the command choice.
   * @param {String | Object} description The description of the command choice, or an object of descriptions for localisation.
   * @returns {Command}
   * @see {@link https://discord.com/developers/docs/interactions/application-commands#localization}
   */
  setDescription(description) {
    if (!description)
      throw new TypeError(
        "GLUON: Command option description must be provided.",
      );

    if (typeof description != "string" && typeof description != "object")
      throw new TypeError(
        "GLUON: Command option description must be a string or an object.",
      );

    if (typeof description == "object") {
      if (
        description[this.defaultLocale].length >
        LIMITS.MAX_COMMAND_OPTION_DESCRIPTION
      )
        throw new RangeError(
          `GLUON: Command option description must be less than ${LIMITS.MAX_COMMAND_OPTION_DESCRIPTION} characters.`,
        );

      this.description = description[this.defaultLocale];

      delete description[this.defaultLocale];

      this.description_localizations = description;
    } else {
      if (description.length > LIMITS.MAX_COMMAND_OPTION_DESCRIPTION)
        throw new RangeError(
          `GLUON: Command option description must be less than ${LIMITS.MAX_COMMAND_OPTION_DESCRIPTION} characters.`,
        );

      this.description = description;
    }

    return this;
  }

  /**
   * Sets whether the option is required.
   * @param {Boolean} isRequired Whether the option is required.
   * @returns {CommandOption}
   */
  setRequired(isRequired) {
    if (typeof isRequired != "boolean")
      throw new TypeError(
        "GLUON: Command option required status must be a boolean.",
      );

    this.required = isRequired;

    return this;
  }

  /**
   * Adds a choice to the option.
   * @param {CommandChoice} choice Adds a choice to the option.
   * @returns {CommandOption}
   */
  addChoice(choice) {
    if (this.choices.length >= LIMITS.MAX_COMMAND_OPTION_CHOICES)
      throw new RangeError(
        `GLUON: Command option choices must be less than ${LIMITS.MAX_COMMAND_OPTION_CHOICES}.`,
      );

    if (!choice)
      throw new TypeError("GLUON: Command option choice must be provided.");

    this.choices.push(choice);

    return this;
  }

  /**
   * Adds an option to this option.
   * @param {CommandOption} option Adds an option to this option.
   * @returns {CommandOption}
   */
  addOption(option) {
    if (this.options.length >= LIMITS.MAX_COMMAND_OPTIONS)
      throw new RangeError(
        `GLUON: Command option options must be less than ${LIMITS.MAX_COMMAND_OPTIONS}.`,
      );

    if (!option) throw new TypeError("GLUON: Command option must be provided.");

    this.options.push(option);

    return this;
  }

  /**
   * Sets which channel types are selectable by the user.
   * @param {Array<Number>} channelTypes An array of channel types to offer as a choice.
   * @returns {CommandOption}
   * @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-types}
   */
  setChannelTypes(channelTypes) {
    if (!channelTypes)
      throw new TypeError(
        "GLUON: Command option channel types must be provided.",
      );

    if (!Array.isArray(channelTypes))
      throw new TypeError(
        "GLUON: Command option channel types must be an array.",
      );

    this.channel_types = channelTypes;

    return this;
  }

  /**
   * Sets the minimum value the user may enter.
   * @param {Number} value The minimum number value that the user may enter.
   * @returns {CommandOption}
   */
  setMinValue(value) {
    if (typeof value != "number")
      throw new TypeError("GLUON: Command option min value must be a number.");

    this.min_value = value;

    return this;
  }

  /**
   * Sets the maximum value the user may enter.
   * @param {Number} value The maximum number value that the user may enter.
   * @returns {CommandOption}
   */
  setMaxValue(value) {
    if (typeof value != "number")
      throw new TypeError("GLUON: Command option max value must be a number.");

    this.max_value = value;

    return this;
  }

  /**
   * Sets the minimum length the user may enter.
   * @param {Number} length The minimum length that the user may enter.
   * @returns {CommandOption}
   */
  setMinLength(length) {
    if (typeof length != "number")
      throw new TypeError("GLUON: Command option min length must be a number.");

    this.min_length = length;

    return this;
  }

  /**
   * Sets the maximum length the user may enter.
   * @param {Number} length The maximum length that the user may enter.
   * @returns {CommandOption}
   */
  setMaxLength(length) {
    if (typeof length != "number")
      throw new TypeError("GLUON: Command option max length must be a number.");

    this.max_length = length;

    return this;
  }

  /**
   * Sets whether autocomplete is enabled for this option.
   * @param {Boolean} autocomplete Whether autocomplete is enabled for this option.
   * @returns {CommandOption}
   */
  setAutocomplete(autocomplete) {
    if (typeof autocomplete != "boolean")
      throw new TypeError(
        "GLUON: Command option autocomplete must be a boolean.",
      );

    this.autocomplete = autocomplete;

    return this;
  }

  /**
   * Sets the default locale for localisation.
   * @param {String?} locale Sets the default locale for localisation.
   * @returns {Command}
   * @see {@link https://discord.com/developers/docs/reference#locales}
   */
  setDefaultLocale(locale) {
    if (!locale) throw new TypeError("GLUON: Default locale must be provided.");

    this.defaultLocale = locale;

    return this;
  }

  /**
   * Returns the correct Discord format for a command option.
   * @returns {Object}
   */
  toJSON(
    format,
    { suppressValidation = false } = { suppressValidation: false },
  ) {
    if (suppressValidation !== true) {
      if (!this.name)
        throw new TypeError("GLUON: Command option name must be provided.");
      if (typeof this.name !== "string")
        throw new TypeError("GLUON: Command option name must be a string.");
      if (
        this.name.length > LIMITS.MAX_COMMAND_OPTION_NAME ||
        this.name.length < LIMITS.MIN_COMMAND_OPTION_NAME
      )
        throw new RangeError(
          `GLUON: Command option name must be between ${LIMITS.MIN_COMMAND_OPTION_NAME} and ${LIMITS.MAX_COMMAND_OPTION_NAME} characters.`,
        );
      if (!this.type)
        throw new TypeError("GLUON: Command option type must be provided.");
      if (typeof this.type !== "number")
        throw new TypeError("GLUON: Command option type must be a number.");
      if (
        (this.type === APPLICATION_COMMAND_TYPES.CHAT_INPUT ||
          typeof this.type === "undefined") &&
        !COMMAND_NAME_REGEX.test(this.name)
      )
        throw new TypeError("GLUON: Command option name must match the regex.");
      if (!this.description)
        throw new TypeError(
          "GLUON: Command option description must be provided.",
        );
      if (typeof this.description !== "string")
        throw new TypeError(
          "GLUON: Command option description must be a string.",
        );
      if (
        this.description.length > LIMITS.MAX_COMMAND_OPTION_DESCRIPTION ||
        this.description.length < LIMITS.MIN_COMMAND_OPTION_DESCRIPTION
      )
        throw new RangeError(
          `GLUON: Command option description must be less than ${LIMITS.MAX_COMMAND_OPTION_DESCRIPTION} characters.`,
        );
      if (
        this.name_localizations &&
        typeof this.name_localizations !== "object"
      )
        throw new TypeError(
          "GLUON: Command option name localizations must be an object.",
        );
      if (
        this.name_localizations &&
        !Object.values(this.name_localizations).every(
          (v) =>
            typeof v === "string" &&
            v.length >= LIMITS.MIN_COMMAND_OPTION_NAME &&
            v.length <= LIMITS.MAX_COMMAND_OPTION_NAME,
        )
      )
        throw new RangeError(
          `GLUON: Command option name localizations must be a string between ${LIMITS.MIN_COMMAND_OPTION_NAME} and ${LIMITS.MAX_COMMAND_OPTION_NAME} characters.`,
        );
      if (
        this.description_localizations &&
        typeof this.description_localizations !== "object"
      )
        throw new TypeError(
          "GLUON: Command option description localizations must be an object.",
        );
      if (
        this.description_localizations &&
        !Object.values(this.description_localizations).every(
          (v) =>
            typeof v === "string" &&
            v.length >= LIMITS.MIN_COMMAND_OPTION_DESCRIPTION &&
            v.length <= LIMITS.MAX_COMMAND_OPTION_DESCRIPTION,
        )
      )
        throw new RangeError(
          `GLUON: Command option description localizations must be a string between ${LIMITS.MIN_COMMAND_OPTION_DESCRIPTION} and ${LIMITS.MAX_COMMAND_OPTION_DESCRIPTION} characters.`,
        );
      if (
        typeof this.required !== "undefined" &&
        typeof this.required !== "boolean"
      )
        throw new TypeError(
          "GLUON: Command option required status must be a boolean.",
        );
      if (this.choices && !Array.isArray(this.choices))
        throw new TypeError("GLUON: Command option choices must be an array.");
      if (
        this.choices &&
        this.choices.length > LIMITS.MAX_COMMAND_OPTION_CHOICES
      )
        throw new RangeError(
          `GLUON: Command option choices must be less than ${LIMITS.MAX_COMMAND_OPTION_CHOICES}.`,
        );
      if (this.options && !Array.isArray(this.options))
        throw new TypeError("GLUON: Command option options must be an array.");
      if (this.options && this.options.length > LIMITS.MAX_COMMAND_OPTIONS)
        throw new RangeError(
          `GLUON: Command option options must be less than ${LIMITS.MAX_COMMAND_OPTIONS}.`,
        );
      if (this.channel_types && !Array.isArray(this.channel_types))
        throw new TypeError(
          "GLUON: Command option channel types must be an array.",
        );
      if (
        typeof this.min_value !== "undefined" &&
        typeof this.min_value !== "number"
      )
        throw new TypeError(
          "GLUON: Command option min value must be a number.",
        );
      if (
        typeof this.max_value !== "undefined" &&
        typeof this.max_value !== "number"
      )
        throw new TypeError(
          "GLUON: Command option max value must be a number.",
        );
      if (
        typeof this.min_length !== "undefined" &&
        typeof this.min_length !== "number"
      )
        throw new TypeError(
          "GLUON: Command option min length must be a number.",
        );
      if (
        typeof this.min_length !== "undefined" &&
        (this.min_length < LIMITS.MIN_MIN_COMMAND_OPTION_LENGTH ||
          this.min_length > LIMITS.MAX_MIN_COMMAND_OPTION_LENGTH)
      )
        throw new RangeError(
          `GLUON: Command option min length must be between ${LIMITS.MIN_MIN_COMMAND_OPTION_LENGTH} and ${LIMITS.MAX_MIN_COMMAND_OPTION_LENGTH}.`,
        );
      if (
        typeof this.max_length !== "undefined" &&
        typeof this.max_length !== "number"
      )
        throw new TypeError(
          "GLUON: Command option max length must be a number.",
        );
      if (
        typeof this.max_length !== "undefined" &&
        (this.max_length < LIMITS.MIN_MAX_COMMAND_OPTION_LENGTH ||
          this.max_length > LIMITS.MAX_MAX_COMMAND_OPTION_LENGTH)
      )
        throw new RangeError(
          `GLUON: Command option max length must be between ${LIMITS.MIN_MAX_COMMAND_OPTION_LENGTH} and ${LIMITS.MAX_MAX_COMMAND_OPTION_LENGTH}.`,
        );
      if (
        typeof this.autocomplete !== "undefined" &&
        typeof this.autocomplete !== "boolean"
      )
        throw new TypeError(
          "GLUON: Command option autocomplete must be a boolean.",
        );
    }
    switch (format) {
      case TO_JSON_TYPES_ENUM.CACHE_FORMAT:
      case TO_JSON_TYPES_ENUM.DISCORD_FORMAT:
      case TO_JSON_TYPES_ENUM.STORAGE_FORMAT:
      default: {
        return {
          name: this.name,
          name_localizations: this.name_localizations,
          type: this.type,
          description: this.description,
          description_localizations: this.description_localizations,
          required: this.required,
          choices: this.choices,
          options: this.options,
          channel_types: this.channel_types,
          min_value: this.min_value,
          max_value: this.max_value,
          min_length: this.min_length,
          max_length: this.max_length,
          autocomplete: this.autocomplete,
        };
      }
    }
  }
}

export default CommandOption;