///|
/// Errors introduced by typed argument combinators.
pub(all) suberror ArgsDecodeError {
Validation(name~ : String, message~ : String)
} derive(Debug)
///|
/// One typed command option, combining its registration definition and reader.
pub struct Arg[A] {
priv definition : @model.CommandOption
priv read : (CommandOptions) -> A raise
priv suggest : SuggestHandler?
}
///|
/// A typed collection of command options used for registration and decoding.
pub struct Args[A] {
priv definitions_ : Array[@model.CommandOption]
priv decode_ : (CommandOptions) -> A raise
priv suggests_ : Array[SuggestHandler]
}
///|
fn[A] required_arg(
definition : @model.CommandOption,
read : (CommandOptions) -> A raise,
suggest? : SuggestHandler,
) -> Arg[A] {
{ definition, read, suggest, }
}
///|
fn[A] typed_suggest(
name : String,
decode : (SuggestInput) -> A raise,
suggest : (async (SuggestCtx, A) -> Array[@model.CommandOptionChoice])?,
) -> SuggestHandler? {
match suggest {
Some(run) =>
Some({ name_: name, run_: (ctx, input) => run(ctx, decode(input)), })
None => None
}
}
///|
fn collect_suggests(suggests : Array[SuggestHandler?]) -> Array[SuggestHandler] {
suggests.filter_map(value => value)
}
///|
fn[A] Arg::read_value(self : Arg[A], options : CommandOptions) -> A raise {
(self.read)(options)
}
///|
fn[A] Args::decode_value(self : Args[A], options : CommandOptions) -> A raise {
(self.decode_)(options)
}
///|
/// Define a required string option.
pub fn arg_string(
name~ : String,
description~ : String,
choices? : Array[@model.CommandOptionChoice],
min_length? : Int,
max_length? : Int,
suggest? : async (SuggestCtx, String) -> Array[@model.CommandOptionChoice],
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[String] {
let suggest_handler = typed_suggest(
name,
input => json_string(input.value_, name, "string"),
suggest,
)
required_arg(
string_option(
name,
description,
required=true,
choices?,
min_length?,
max_length?,
autocomplete?=suggest_handler.map(_ => true),
name_localizations?,
description_localizations?,
),
options => options.string(name),
suggest?=suggest_handler,
)
}
///|
/// Define a required integer option.
pub fn arg_int(
name~ : String,
description~ : String,
min? : Int64,
max? : Int64,
choices? : Array[@model.CommandOptionChoice],
suggest? : async (SuggestCtx, Int64) -> Array[@model.CommandOptionChoice],
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[Int64] {
let suggest_handler = typed_suggest(
name,
input => json_int64(input.value_, name, "integer"),
suggest,
)
required_arg(
integer_option(
name,
description,
required=true,
choices?,
min_value?=min,
max_value?=max,
autocomplete?=suggest_handler.map(_ => true),
name_localizations?,
description_localizations?,
),
options => options.int(name),
suggest?=suggest_handler,
)
}
///|
/// Define a required floating-point number option.
pub fn arg_number(
name~ : String,
description~ : String,
min? : Double,
max? : Double,
choices? : Array[@model.CommandOptionChoice],
suggest? : async (SuggestCtx, Double) -> Array[@model.CommandOptionChoice],
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[Double] {
let suggest_handler = typed_suggest(
name,
input => json_double(input.value_, name, "number"),
suggest,
)
required_arg(
number_option(
name,
description,
required=true,
choices?,
min_value?=min,
max_value?=max,
autocomplete?=suggest_handler.map(_ => true),
name_localizations?,
description_localizations?,
),
options => options.number(name),
suggest?=suggest_handler,
)
}
///|
/// Define a required boolean option.
pub fn arg_bool(
name~ : String,
description~ : String,
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[Bool] {
required_arg(
boolean_option(
name,
description,
required=true,
name_localizations?,
description_localizations?,
),
options => options.bool(name),
)
}
///|
/// Define a required user option.
pub fn arg_user(
name~ : String,
description~ : String,
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[@model.UserId] {
required_arg(
user_option(
name,
description,
required=true,
name_localizations?,
description_localizations?,
),
options => options.user(name),
)
}
///|
/// Define a required channel option.
pub fn arg_channel(
name~ : String,
description~ : String,
channel_types? : Array[@model.ChannelType],
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[@model.ChannelId] {
required_arg(
channel_option(
name,
description,
required=true,
channel_types?,
name_localizations?,
description_localizations?,
),
options => options.channel(name),
)
}
///|
/// Define a required role option.
pub fn arg_role(
name~ : String,
description~ : String,
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[@model.RoleId] {
required_arg(
role_option(
name,
description,
required=true,
name_localizations?,
description_localizations?,
),
options => options.role(name),
)
}
///|
/// Define a required mentionable option.
pub fn arg_mentionable(
name~ : String,
description~ : String,
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[@model.GenericId] {
required_arg(
mentionable_option(
name,
description,
required=true,
name_localizations?,
description_localizations?,
),
options => options.mentionable(name),
)
}
///|
/// Define a required attachment option resolved to its attachment object.
pub fn arg_attachment(
name~ : String,
description~ : String,
name_localizations? : Map[String, String],
description_localizations? : Map[String, String],
) -> Arg[@model.Attachment] {
required_arg(
attachment_option(
name,
description,
required=true,
name_localizations?,
description_localizations?,
),
options => options.attachment(name),
)
}
///|
/// Make an argument optional. A missing submitted option decodes to `None`.
///
/// ```mbt check
/// test "make a command option optional" {
/// let args = @interaction.Args::of(
/// @interaction.arg_string(name="note", description="Optional note").optional(),
/// )
/// json_inspect(args.definitions(), content=[
/// {
/// "type": 3,
/// "name": "note",
/// "description": "Optional note",
/// "required": false,
/// },
/// ])
/// }
/// ```
pub fn[A] Arg::optional(self : Arg[A]) -> Arg[A?] {
let definition = { ..self.definition, required: Some(false), }
let name = definition.name
{
definition,
suggest: self.suggest,
read: options => {
if options.has(name) {
Some(self.read_value(options))
} else {
None
}
},
}
}
///|
/// Make an argument optional in the registration payload and use a default
/// value when it is absent from a submitted command.
pub fn[A] Arg::with_default(self : Arg[A], value : A) -> Arg[A] {
let definition = { ..self.definition, required: Some(false), }
let name = definition.name
{
definition,
suggest: self.suggest,
read: options => {
if options.has(name) {
self.read_value(options)
} else {
value
}
},
}
}
///|
/// Validate a decoded argument. Returning `Some(message)` rejects the value.
pub fn[A] Arg::validate(self : Arg[A], check : (A) -> String?) -> Arg[A] {
let name = self.definition.name
{
definition: self.definition,
suggest: self.suggest,
read: options => {
let value = self.read_value(options)
match check(value) {
Some(message) => raise ArgsDecodeError::Validation(name~, message~)
None => value
}
},
}
}
///|
/// Transform one decoded argument without changing its registration definition.
pub fn[A, B] Arg::map(self : Arg[A], f : (A) -> B) -> Arg[B] {
{
definition: self.definition,
read: options => f(self.read_value(options)),
suggest: self.suggest,
}
}
///|
/// Return the option definitions in registration order.
pub fn[A] Args::definitions(self : Args[A]) -> Array[@model.CommandOption] {
self.definitions_.copy()
}
///|
/// Return the type-erased autocomplete handlers carried by these arguments.
pub fn[A] Args::suggestions(self : Args[A]) -> Array[SuggestHandler] {
self.suggests_.copy()
}
///|
/// Decode submitted command options into the composed value.
pub fn[A] Args::decode(self : Args[A], input : CommandOptions) -> A raise {
self.decode_value(input)
}
///|
/// Lift one argument into a composable argument collection.
///
/// ```mbt check
/// test "compose defaulting and validation into an argument spec" {
/// let args = @interaction.Args::of(
/// @interaction.arg_int(
/// name="times",
/// description="Repeat count",
/// min=1L,
/// max=5L,
/// )
/// .with_default(1L)
/// .validate(value => if value <= 5L { None } else { Some("too large") }),
/// )
/// json_inspect(args.definitions(), content=[
/// {
/// "type": 4,
/// "name": "times",
/// "description": "Repeat count",
/// "required": false,
/// "min_value": 1,
/// "max_value": 5,
/// },
/// ])
/// }
/// ```
pub fn[A] Args::of(arg : Arg[A]) -> Args[A] {
{
definitions_: [arg.definition],
decode_: arg.read,
suggests_: collect_suggests([arg.suggest]),
}
}
///|
/// Transform the result of a composed argument collection.
pub fn[A, B] Args::map(self : Args[A], f : (A) -> B) -> Args[B] {
{
definitions_: self.definitions_,
decode_: options => f(self.decode_value(options)),
suggests_: self.suggests_,
}
}
///|
/// Combine two argument collections, preserving left-to-right definition and
/// decoding order.
pub fn[A, B] Args::zip(self : Args[A], other : Args[B]) -> Args[(A, B)] {
let definitions = self.definitions_.copy()
definitions.push_iter(other.definitions_.iter())
let suggests = self.suggests_.copy()
suggests.push_iter(other.suggests_.iter())
{
definitions_: definitions,
decode_: options => {
let left = self.decode_value(options)
let right = other.decode_value(options)
(left, right)
},
suggests_: suggests,
}
}
///|
/// Define a command with no options.
pub fn Args::unit() -> Args[Unit] {
{ definitions_: [], decode_: _ => (), suggests_: [], }
}
///|
/// Build an argument collection from custom definitions and a custom decoder.
pub fn[A] Args::custom(
definitions~ : Array[@model.CommandOption],
decode~ : (CommandOptions) -> A raise,
) -> Args[A] {
{ definitions_: definitions.copy(), decode_: decode, suggests_: [], }
}
///|
/// Build a command's argument decoder from one option. The option is
/// registered from `a`'s definition and `f` maps its decoded value
/// into the handler's argument type.
pub fn[A, Z] Args::map1(a : Arg[A], f : (A) -> Z) -> Args[Z] {
Args::of(a).map(f)
}
///|
/// Build a command's argument decoder from two options, combining
/// their decoded values with the final function argument.
pub fn[A, B, Z] Args::map2(a : Arg[A], b : Arg[B], f : (A, B) -> Z) -> Args[Z] {
{
definitions_: [a.definition, b.definition],
suggests_: collect_suggests([a.suggest, b.suggest]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
f(av, bv)
},
}
}
///|
/// Build a command's argument decoder from three options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, Z] Args::map3(
a : Arg[A],
b : Arg[B],
c : Arg[C],
f : (A, B, C) -> Z,
) -> Args[Z] {
{
definitions_: [a.definition, b.definition, c.definition],
suggests_: collect_suggests([a.suggest, b.suggest, c.suggest]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
f(av, bv, cv)
},
}
}
///|
/// Build a command's argument decoder from four options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, D, Z] Args::map4(
a : Arg[A],
b : Arg[B],
c : Arg[C],
d : Arg[D],
f : (A, B, C, D) -> Z,
) -> Args[Z] {
{
definitions_: [a.definition, b.definition, c.definition, d.definition],
suggests_: collect_suggests([a.suggest, b.suggest, c.suggest, d.suggest]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
let dv = d.read_value(options)
f(av, bv, cv, dv)
},
}
}
///|
/// Build a command's argument decoder from five options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, D, E, Z] Args::map5(
a : Arg[A],
b : Arg[B],
c : Arg[C],
d : Arg[D],
e : Arg[E],
f : (A, B, C, D, E) -> Z,
) -> Args[Z] {
{
definitions_: [
a.definition,
b.definition,
c.definition,
d.definition,
e.definition,
],
suggests_: collect_suggests([
a.suggest,
b.suggest,
c.suggest,
d.suggest,
e.suggest,
]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
let dv = d.read_value(options)
let ev = e.read_value(options)
f(av, bv, cv, dv, ev)
},
}
}
///|
/// Build a command's argument decoder from six options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, D, E, F, Z] Args::map6(
a : Arg[A],
b : Arg[B],
c : Arg[C],
d : Arg[D],
e : Arg[E],
f : Arg[F],
combine : (A, B, C, D, E, F) -> Z,
) -> Args[Z] {
{
definitions_: [
a.definition,
b.definition,
c.definition,
d.definition,
e.definition,
f.definition,
],
suggests_: collect_suggests([
a.suggest,
b.suggest,
c.suggest,
d.suggest,
e.suggest,
f.suggest,
]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
let dv = d.read_value(options)
let ev = e.read_value(options)
let fv = f.read_value(options)
combine(av, bv, cv, dv, ev, fv)
},
}
}
///|
/// Build a command's argument decoder from seven options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, D, E, F, G, Z] Args::map7(
a : Arg[A],
b : Arg[B],
c : Arg[C],
d : Arg[D],
e : Arg[E],
f : Arg[F],
g : Arg[G],
combine : (A, B, C, D, E, F, G) -> Z,
) -> Args[Z] {
{
definitions_: [
a.definition,
b.definition,
c.definition,
d.definition,
e.definition,
f.definition,
g.definition,
],
suggests_: collect_suggests([
a.suggest,
b.suggest,
c.suggest,
d.suggest,
e.suggest,
f.suggest,
g.suggest,
]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
let dv = d.read_value(options)
let ev = e.read_value(options)
let fv = f.read_value(options)
let gv = g.read_value(options)
combine(av, bv, cv, dv, ev, fv, gv)
},
}
}
///|
/// Build a command's argument decoder from eight options, combining
/// their decoded values with the final function argument.
pub fn[A, B, C, D, E, F, G, H, Z] Args::map8(
a : Arg[A],
b : Arg[B],
c : Arg[C],
d : Arg[D],
e : Arg[E],
f : Arg[F],
g : Arg[G],
h : Arg[H],
combine : (A, B, C, D, E, F, G, H) -> Z,
) -> Args[Z] {
{
definitions_: [
a.definition,
b.definition,
c.definition,
d.definition,
e.definition,
f.definition,
g.definition,
h.definition,
],
suggests_: collect_suggests([
a.suggest,
b.suggest,
c.suggest,
d.suggest,
e.suggest,
f.suggest,
g.suggest,
h.suggest,
]),
decode_: options => {
let av = a.read_value(options)
let bv = b.read_value(options)
let cv = c.read_value(options)
let dv = d.read_value(options)
let ev = e.read_value(options)
let fv = f.read_value(options)
let gv = g.read_value(options)
let hv = h.read_value(options)
combine(av, bv, cv, dv, ev, fv, gv, hv)
},
}
}