Class: ESM::Command::Argument

Inherits:
Object
  • Object
show all
Defined in:
lib/esm/command/argument.rb

Constant Summary collapse

DEFAULT_TEMPLATE =

Global default values for all argument You may overwrite these in your argument definition

{
  checked_against: :present?,
  checked_against_if: lambda do |argument, content|
    argument.required? || content.present?
  end
}.freeze
TEMPLATES =

Global templates for any argument to use These can be used by defining an argument with the same name, or by providing the :template option during argument definition

{
  # Required: Majority of the time, this is needed.
  target: {
    checked_against: ESM::Regex::TARGET,
    description_extra: "commands.arguments.target.description_extra",
    description: "commands.arguments.target.description",
    required: true
  },

  # Required: Majority of the time, this is needed.
  command: {
    checked_against: ->(content) { ESM::Command.include?(content) },
    description: "commands.arguments.command.description",
    required: true
  },

  # Required: No functionality to "guess" this
  territory_id: {
    checked_against: ESM::Regex::TERRITORY_ID,
    description_extra: "commands.arguments.territory_id.description_extra",
    description: "commands.arguments.territory_id.description",
    required: true,
    placeholder: "territory"
  },

  # Optional in Discord: UserDefault/CommunityDefault can be used. It will be validated so it is "semi-required"
  # Required: In the bot
  community_id: {
    required: {discord: false, bot: true},
    checked_against: ESM::Regex::COMMUNITY_ID,
    description_extra: "commands.arguments.community_id.description_extra",
    description: "commands.arguments.community_id.description",
    optional_text: "commands.arguments.community_id.optional_text",
    placeholder: "community",
    modifier: lambda do |content|
      if content.present?
        # User alias
        if (id_alias = current_user.id_aliases.find_community_alias(content))
          content = id_alias.community.community_id
        end

        return content
      end

      # content == nil, attempt to find and use a default
      user_defaults = current_user.id_defaults
      return user_defaults.community.community_id if user_defaults.community_id

      # Community autofill
      return current_community.community_id if current_channel.text?

      nil
    end
  },

  # Optional in Discord: UserDefault/CommunityDefault can be used. It will be validated so it is "semi-required"
  # Required: In the bot
  server_id: {
    required: {discord: false, bot: true},
    checked_against: ESM::Regex::SERVER_ID,
    description_extra: "commands.arguments.server_id.description_extra",
    description: "commands.arguments.server_id.description",
    optional_text: "commands.arguments.server_id.optional_text",
    placeholder: "server",
    modifier: lambda do |content|
      if content.present?
        # User provided - Starts with a community ID
        return content if content.match("#{ESM::Regex::COMMUNITY_ID_OPTIONAL.source}_")

        # User alias
        if (id_alias = current_user.id_aliases.find_server_alias(content))
          return id_alias.server.server_id
        end

        # Community autofill
        if current_channel.text? && current_community.servers.by_server_id_fuzzy(content).any?
          return "#{current_community.community_id}_#{content}"
        end

        return content
      end

      # content == nil, attempt to find and use a default
      if current_channel.text?
        channel_default = current_community.id_defaults.for_channel(current_channel)
        return channel_default.server.server_id if channel_default&.server_id

        global_default = current_community.id_defaults.global
        return global_default.server.server_id if global_default&.server_id
      end

      if current_user.id_defaults.server_id
        user_defaults = current_user.id_defaults
        return user_defaults.server.server_id if user_defaults.server_id
      end

      nil
    end
  }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, type = nil, opts = {}) ⇒ Argument

A configurable representation of a command argument

@option opts [TrueClass, FalseClass, Hash] :required
Controls if the argument should be required by Discord and by the Bot
Fine grain control can be achieved by providing a Hash.
For example, not require an argument on Discord but require it on the bot side
  required: {discord: false, bot: true}
Optional. Default: false

@option opts [Symbol, String, nil] :template
The name of a default entry in which `opts` are merged into.
Useful for having an argument that acts like another argument, but may have different configuration

@option opts [String] :description
This argument's description, in less than 100 characters.
  This description is used in Discord when viewing the argument.
  Note: Providing this option is optional, however, all arguments MUST have a non-blank description
This value defaults to the value located at the locale path:
    commands.<command_name>.arguments.<argument_name>.description

@option opts [String] :description_extra
Any extra information to be included that wouldn't fit in the 100 character limit
  Note: Providing this option is optional, however, this argument MUST have a non-blank description
  This description is used in the help documentation with the help command and on the website
This value defaults to the value located at the locale path:
    commands.<command_name>.arguments.<argument_name>.description_extra

@option opts [String] :optional_text
Allows for overriding the "this argument is optional" text in the help documentation.
  This opt is ignored if `required: true`
Optional.
This value defaults to the value located at the locale path:
    commands.<command_name>.arguments.<argument_name>.optional_text

@option opts [Symbol, String] :display_name
Changes how the argument is displayed to the user, but not in the code
Optional.

@option opts [Object] :default
The default value if this argument. This value is ignored if `required: true`
Optional.
Default: nil

@option opts [Boolean] :preserve_case
Controls if this argument's value should be converted to lowercase or not.
Optional.
Default: false

@option opts [Proc] :modifier
A block of code used to modify this argument's value before validation
Optional.

@option opts [Hash] :choices
The key: display_value of choices the user can pick from
Optional.

@option opts [Integer] :min_value
If type is integer/number, this is the minimum value that can be selected

@option opts [Integer] :max_value
If type is integer/number, this is the maximum value that can be selected

@option opts [Regex, String, Proc, Array] :checked_against
Used to perform validation against the content provided to the argument
Regex/String  - Content must `match?`
Proc          - Content is passed in and can return a truthy value to consider the content as valid
Array         - Content must be one of the values

@option opts [Proc] :checked_against_if
Used to determine if the argument should be validated against :checked_against.
Can return a truthy value to continue to validation. Falsey values will cause validation to be skipped

@option opts [String, Symbol] :placeholder
Used when the command usage is displayed and use_placeholders are true. Defaults to the display name

Parameters:

  • name (Symbol, String)

    The argument's name

  • type (Symbol, String) (defaults to: nil)

    The argument's type (directly linked to Discord). Optional. Default: :string

  • opts (Hash) (defaults to: {})

    Options to configure the argument



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/esm/command/argument.rb', line 215

def initialize(name, type = nil, opts = {})
  template_name = (opts[:template] || name).to_sym

  # Precedence:
  #   opts -> template -> default template
  opts = DEFAULT_TEMPLATE.merge(TEMPLATES[template_name] || {}).merge(opts)

  @name = name
  @type = type ? type.to_sym : :string
  @discord_type = Discordrb::Interactions::OptionBuilder::TYPES[@type]
  @display_name = (opts[:display_name] || name).to_sym
  @command_class = opts[:command_class]
  @command_name = command_class.command_name.to_sym

  @default_value = opts[:default]
  @preserve_case = !!opts[:preserve_case]
  @modifier = opts[:modifier]
  @checked_against = opts[:checked_against]
  @checked_against_if = opts[:checked_against_if]
  @placeholder = opts[:placeholder].presence || name

  if opts[:required].is_a?(Hash)
    @required_by_discord = !!opts.dig(:required, :discord)
    @required_by_bot = !!opts.dig(:required, :bot)
  else
    required = !!opts[:required]
    @required_by_discord = required
    @required_by_bot = required
  end

  @options = {required: @required_by_discord}
  @options[:min_value] = opts[:min_value] if opts[:min_value]
  @options[:max_value] = opts[:max_value] if opts[:max_value]

  # I prefer {value: "Display Name"}, Discord/rb wants it to be {"Display Name": "value"}
  if opts[:choices]
    @options[:choices] = opts[:choices].map { |k, v| [v.to_s, k.to_s] }.to_h
  end

  @description = load_locale_or_provided(opts[:description], "description")
  @description_extra = load_locale_or_provided(opts[:description_extra], "description_extra").presence
  @optional_text = load_optional_text(opts[:optional_text])

  check_for_valid_configuration!
end

Instance Attribute Details

#checked_againstObject (readonly)

Returns the value of attribute checked_against.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def checked_against
  @checked_against
end

#checked_against_ifObject (readonly)

Returns the value of attribute checked_against_if.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def checked_against_if
  @checked_against_if
end

#command_classObject (readonly)

Returns the value of attribute command_class.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def command_class
  @command_class
end

#command_nameObject (readonly)

Returns the value of attribute command_name.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def command_name
  @command_name
end

#default_valueObject (readonly)

Returns the value of attribute default_value.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def default_value
  @default_value
end

#descriptionObject (readonly)

Returns the value of attribute description.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def description
  @description
end

#description_extraObject (readonly)

Returns the value of attribute description_extra.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def description_extra
  @description_extra
end

#discord_typeObject (readonly)

Returns the value of attribute discord_type.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def discord_type
  @discord_type
end

#display_nameObject (readonly)

Returns the value of attribute display_name.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def display_name
  @display_name
end

#modifierObject (readonly)

Returns the value of attribute modifier.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def modifier
  @modifier
end

#nameObject (readonly)

Returns the value of attribute name.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def name
  @name
end

#optional_textObject (readonly)

Returns the value of attribute optional_text.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def optional_text
  @optional_text
end

#optionsObject (readonly)

Returns the value of attribute options.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def options
  @options
end

#placeholderObject (readonly)

Returns the value of attribute placeholder.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def placeholder
  @placeholder
end

#typeObject (readonly)

Returns the value of attribute type.



123
124
125
# File 'lib/esm/command/argument.rb', line 123

def type
  @type
end

Instance Method Details

#default_value?Boolean

Returns:

  • (Boolean)


307
308
309
# File 'lib/esm/command/argument.rb', line 307

def default_value?
  !!@default_value
end

#help_documentationObject



339
340
341
342
343
344
# File 'lib/esm/command/argument.rb', line 339

def help_documentation
  output = ["**`#{self}:`**", description]
  output << "#{description_extra}." if description_extra.presence
  output << "**Note:** #{optional_text}" if optional_text?
  output.join("\n")
end

#modifier?Boolean

Returns:

  • (Boolean)


303
304
305
# File 'lib/esm/command/argument.rb', line 303

def modifier?
  !!modifier&.respond_to?(:call)
end

#optional?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'lib/esm/command/argument.rb', line 323

def optional?
  !required_by_bot? && !required_by_discord?
end

#optional_text?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/esm/command/argument.rb', line 327

def optional_text?
  optional_text.present?
end

#preserve_case?Boolean

Returns:

  • (Boolean)


299
300
301
# File 'lib/esm/command/argument.rb', line 299

def preserve_case?
  @preserve_case
end

#required?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'lib/esm/command/argument.rb', line 319

def required?
  required_by_bot? || required_by_discord?
end

#required_by_bot?Boolean

Returns:

  • (Boolean)


311
312
313
# File 'lib/esm/command/argument.rb', line 311

def required_by_bot?
  @required_by_bot
end

#required_by_discord?Boolean

Returns:

  • (Boolean)


315
316
317
# File 'lib/esm/command/argument.rb', line 315

def required_by_discord?
  @required_by_discord
end

#to_hObject



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/esm/command/argument.rb', line 346

def to_h
  {
    name: name,
    command_name: command_name,
    display_name: display_name,
    description: description,
    description_extra: description_extra,
    optional_text: optional_text,
    default_value: default_value,
    modifier: modifier,
    checked_against: checked_against,
    preserve_case: preserve_case?,
    discord: @options,
    bot: {
      required: @required_by_bot
    }
  }
end

#to_sObject



331
332
333
334
335
336
337
# File 'lib/esm/command/argument.rb', line 331

def to_s
  if display_name.present?
    display_name.to_s
  else
    name.to_s
  end
end

#transform_and_validate!(input, command) ⇒ Object

Raises:

  • (ArgumentError)


261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/esm/command/argument.rb', line 261

def transform_and_validate!(input, command)
  raise ArgumentError, "Invalid command argument" unless command.is_a?(ApplicationCommand)

  input_present = input.present?
  input = default_value if !input_present && default_value?

  sanitized_content =
    if input.is_a?(String) && input_present
      preserve_case? ? input.strip : input.downcase.strip
    else
      input
    end

  content =
    if modifier?
      command.instance_exec(sanitized_content, &modifier)
    else
      sanitized_content
    end

  debug!(
    argument: to_h.except(:description, :description_extra, :optional_text),
    input: input,
    before: {
      type: sanitized_content.class.name,
      content: sanitized_content
    },
    after: {
      type: content.class.name,
      content: content
    }
  )

  check_for_valid_content!(command, content)

  content
end