Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
//! Contains `FromWarpJs` trait implementations for converting JavaScript command signatures to
|
||||
//! `galaxy_completer::signatures::CommandSignature`s, as well as `IntoWarpJs` implementations for
|
||||
//! Rust structs that may be passed to JS functions defined on the Command Signature (e.g.
|
||||
//! `GeneratorCompletionContext`).
|
||||
use rquickjs::{FromJs, Function, Object, Value};
|
||||
use galaxy_js::{
|
||||
util::{get_one_or_more_optional, get_one_or_more_required, get_optional, get_required},
|
||||
FromWarpJs, IntoWarpJs, JsFunctionRegistry,
|
||||
};
|
||||
|
||||
use super::{
|
||||
Argument, ArgumentValue, Command, CommandSignature, GeneratorCompletionContext, GeneratorFn,
|
||||
GeneratorResults, GeneratorScript, Opt, Priority, Suggestion, TemplateType,
|
||||
};
|
||||
|
||||
impl<'js> FromWarpJs<'js> for CommandSignature {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let command = Command::from_galaxy_js(ctx, object.get("command")?, js_function_registry)?;
|
||||
Ok(Self { command })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Command {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: String = get_required(&object, "name", js_function_registry, ctx)?;
|
||||
let alias: Vec<String> =
|
||||
get_one_or_more_optional(&object, "alias", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let arguments: Vec<Argument> =
|
||||
get_one_or_more_optional(&object, "arguments", js_function_registry, ctx)?;
|
||||
let subcommands: Vec<Command> =
|
||||
get_one_or_more_optional(&object, "subcommands", js_function_registry, ctx)?;
|
||||
let options: Vec<Opt> =
|
||||
get_one_or_more_optional(&object, "options", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Command {
|
||||
name,
|
||||
alias,
|
||||
description,
|
||||
arguments,
|
||||
subcommands,
|
||||
options,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Argument {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: String = get_required(&object, "name", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let values: Vec<ArgumentValue> =
|
||||
get_one_or_more_optional(&object, "values", js_function_registry, ctx)?;
|
||||
let optional: bool =
|
||||
get_optional(&object, "optional", js_function_registry, ctx)?.unwrap_or(false);
|
||||
|
||||
Ok(Argument {
|
||||
name,
|
||||
description,
|
||||
values,
|
||||
optional,
|
||||
arity: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for ArgumentValue {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
// TODO(zachbai): Implement conversion + Rust representation of ArgumentValue.RootCommand
|
||||
// (see typescript schema in command-signature.d.ts).
|
||||
if value.is_object() {
|
||||
let object = Object::from_value(value)?;
|
||||
if object.contains_key("value")? {
|
||||
Ok(ArgumentValue::Suggestion(Suggestion::from_galaxy_js(
|
||||
ctx,
|
||||
object.into_value(),
|
||||
js_function_registry,
|
||||
)?))
|
||||
} else if let Some(type_name) =
|
||||
get_optional::<TemplateType>(&object, "typeName", js_function_registry, ctx)?
|
||||
{
|
||||
let filter_name: Option<String> =
|
||||
get_optional(&object, "filterName", js_function_registry, ctx)?;
|
||||
Ok(ArgumentValue::Template {
|
||||
type_name,
|
||||
filter_name,
|
||||
})
|
||||
} else if let Some(generate_suggestions_fn) = get_optional::<GeneratorFn>(
|
||||
&object,
|
||||
"generateSuggestionsFn",
|
||||
js_function_registry,
|
||||
ctx,
|
||||
)? {
|
||||
Ok(ArgumentValue::Generator(generate_suggestions_fn))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "object",
|
||||
to: "ArgumentValue",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "object",
|
||||
to: "ArgumentValue",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Suggestion {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let value: String = get_required(&object, "value", js_function_registry, ctx)?;
|
||||
let display_value: Option<String> =
|
||||
get_optional(&object, "displayValue", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Suggestion {
|
||||
value,
|
||||
display_value,
|
||||
description,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for TemplateType {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let type_string = String::from_galaxy_js(ctx, value, js_function_registry)?;
|
||||
match type_string.as_str() {
|
||||
"TemplateType.Files" => Ok(TemplateType::Files),
|
||||
"TemplateType.Folders" => Ok(TemplateType::Folders),
|
||||
"TemplateType.FilesAndFolders" => Ok(TemplateType::FilesAndFolders),
|
||||
_ => Err(rquickjs::Error::FromJs {
|
||||
from: "string",
|
||||
to: "TemplateType",
|
||||
message: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorFn {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
if value.is_object() {
|
||||
let object = Object::from_value(value)?;
|
||||
let value: Value = object.get("script")?;
|
||||
let script = GeneratorScript::from_galaxy_js(ctx, value, js_function_registry)?;
|
||||
let post_process = if object.contains_key("postProcess")? {
|
||||
let function: Function = object.get("postProcess")?;
|
||||
let function_ref = js_function_registry
|
||||
.register_js_function::<String, GeneratorResults>(function, ctx);
|
||||
Some(function_ref)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(GeneratorFn::ShellCommand {
|
||||
script,
|
||||
post_process,
|
||||
})
|
||||
} else if value.is_function() {
|
||||
let function: Function = Function::from_value(value)?;
|
||||
let function_ref = js_function_registry
|
||||
.register_js_function::<GeneratorCompletionContext, GeneratorResults>(
|
||||
function, ctx,
|
||||
);
|
||||
Ok(GeneratorFn::Custom(function_ref))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "generator_fn",
|
||||
to: "GeneratorFn",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorScript {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
if value.is_string() {
|
||||
Ok(GeneratorScript::Static(String::from_js(ctx, value)?))
|
||||
} else if value.is_function() {
|
||||
let script_fn: Function = Function::from_value(value)?;
|
||||
let function_ref =
|
||||
js_function_registry.register_js_function::<Vec<String>, String>(script_fn, ctx);
|
||||
Ok(GeneratorScript::Dynamic(function_ref))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "script",
|
||||
to: "GeneratorScript",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Opt {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: Vec<String> =
|
||||
get_one_or_more_required(&object, "name", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let required: bool =
|
||||
get_optional(&object, "required", js_function_registry, ctx)?.unwrap_or(false);
|
||||
let arguments: Vec<Argument> =
|
||||
get_one_or_more_optional(&object, "arguments", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Opt {
|
||||
name,
|
||||
description,
|
||||
arguments,
|
||||
required,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorResults {
|
||||
fn from_galaxy_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let suggestions =
|
||||
get_one_or_more_required(&object, "suggestions", js_function_registry, ctx)?;
|
||||
let is_ordered =
|
||||
get_optional(&object, "is_ordered", js_function_registry, ctx)?.unwrap_or(false);
|
||||
|
||||
Ok(GeneratorResults {
|
||||
suggestions,
|
||||
is_ordered,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> IntoWarpJs<'js> for GeneratorCompletionContext {
|
||||
fn into_galaxy_js(self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<Value<'js>> {
|
||||
let object = Object::new(ctx)?;
|
||||
object.set("tokens", self.tokens)?;
|
||||
object.set("pwd", self.pwd)?;
|
||||
Ok(object.into_value())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! This module contains functions for looking up a matching command signature from tokenized and
|
||||
//! untokenized input.
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{registry::CommandRegistry, Command};
|
||||
|
||||
/// Returns the highest-precedence matching `Command` signature object for the given `input`, if
|
||||
/// any, along with the index of the token in `input` matched to the returned `Command`.
|
||||
///
|
||||
/// Subcommands take precedence over parent commands.
|
||||
///
|
||||
/// Note that a token in the input must have trailing whitespace (e.g. marking it as "completed")
|
||||
/// to be eligible to be matched to a command signature. So, for example, if the input does not
|
||||
/// contain trailing whitespace, the last token is not considered in the matching algorithm.
|
||||
/// Otherwise, if one subcommand is a prefix of another subcommand, we could mistakenly eagerly
|
||||
/// return the signature for the shorter subcommand even if the intent was to continue typing to
|
||||
/// enter the longer subcommand.
|
||||
///
|
||||
/// Practically, this means that for input "test_command test_subcommand", even if there is a
|
||||
/// subcommand signature for "test_subcommand", this returns the signature for "test_command",
|
||||
/// because it's assumed "test_subcommand" may still be edited.
|
||||
pub fn get_matching_signature_for_input<'a>(
|
||||
input: &str,
|
||||
registry: &'a CommandRegistry,
|
||||
) -> Option<(&'a Command, usize)> {
|
||||
let input_tokens = input.split_whitespace().collect_vec();
|
||||
get_matching_signature_for_tokenized_input(
|
||||
&input_tokens,
|
||||
input.ends_with(char::is_whitespace),
|
||||
registry,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the highest-precedence matching `Command` signature object for the given tokenized
|
||||
/// `input`, if any. This is equivalent to `get_matching_signature_input` above, except input is
|
||||
/// tokenized (e.g. given as an array of string tokens, which were assumed to be space-delimited in
|
||||
/// the original input). Because input is tokenized, the caller needs to explicitly specify whether
|
||||
/// the original input had trailing whitespace to determine if the last token is eligible for use
|
||||
/// in the matching algorithm.
|
||||
///
|
||||
/// See comments on `get_matching_signature_input` for more details.
|
||||
pub fn get_matching_signature_for_tokenized_input<'a>(
|
||||
input_tokens: &[&str],
|
||||
has_trailing_whitespace: bool,
|
||||
registry: &'a CommandRegistry,
|
||||
) -> Option<(&'a Command, usize)> {
|
||||
let (first_token, remaining_tokens) = input_tokens.split_first()?;
|
||||
|
||||
// Find the top level signature.
|
||||
registry.get_signature(first_token).map(|signature| {
|
||||
deepest_matching_subcommand_signature(
|
||||
remaining_tokens,
|
||||
&signature.command,
|
||||
0,
|
||||
has_trailing_whitespace,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a parent `command_signature`, resolves the most specific (deepest) subcommand that
|
||||
/// the user has entered in `input_tokens`, skipping over any flags that appear
|
||||
/// before the subcommand name (e.g. `kubectl -n kube-system get` resolves to `get`).
|
||||
///
|
||||
/// Returns the matched `Command` along with the index of its token in `input_tokens`.
|
||||
/// If no subcommand is found, `command_signature` itself is returned at `current_token_index`.
|
||||
///
|
||||
/// The last token is only eligible for a subcommand match when `has_trailing_whitespace` is
|
||||
/// true, i.e. the user has finished typing it.
|
||||
fn deepest_matching_subcommand_signature<'a>(
|
||||
input_tokens: &[&str],
|
||||
command_signature: &'a Command,
|
||||
mut current_token_index: usize,
|
||||
has_trailing_whitespace: bool,
|
||||
) -> (&'a Command, usize) {
|
||||
if input_tokens.is_empty() {
|
||||
return (command_signature, current_token_index);
|
||||
}
|
||||
|
||||
// Save the starting index before we begin scanning for subcommands.
|
||||
// If we skip past flags but never find a subcommand beyond them, we
|
||||
// return this index so that `parse_internal_command` treats the flags
|
||||
// as arguments to be parsed rather than swallowing them into the
|
||||
// command name.
|
||||
let subcommand_search_start_index = current_token_index;
|
||||
|
||||
while current_token_index < input_tokens.len() {
|
||||
let is_last_token = current_token_index == input_tokens.len() - 1;
|
||||
let token = input_tokens[current_token_index];
|
||||
|
||||
// Try to match the token against a subcommand.
|
||||
let subcommand_match = command_signature.subcommands.iter().find(|subcommand| {
|
||||
let token_matches_subcommand = token == subcommand.name.as_str();
|
||||
if is_last_token {
|
||||
// If this is the last token, treat the subcommand signature as a match
|
||||
// if there is trailing whitespace, which affirms the user's intent to use
|
||||
// that subcommand. If there is no trailing whitespace, the user may still
|
||||
// be in the process of editing that subcommand (or specifying a different
|
||||
// subcommand of which the current token is a prefix).
|
||||
token_matches_subcommand && has_trailing_whitespace
|
||||
} else {
|
||||
token_matches_subcommand
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(subcommand) = subcommand_match {
|
||||
return deepest_matching_subcommand_signature(
|
||||
input_tokens,
|
||||
subcommand,
|
||||
current_token_index + 1,
|
||||
has_trailing_whitespace,
|
||||
);
|
||||
}
|
||||
|
||||
// If the token is a flag (starts with '-'), try to skip past it and its arguments
|
||||
// to continue looking for subcommands. This handles cases like
|
||||
// `kubectl -n kube-system get pods` where flags appear before subcommands.
|
||||
if token.starts_with('-') {
|
||||
if let Some(option) = command_signature
|
||||
.options
|
||||
.iter()
|
||||
.find(|opt| opt.name.iter().any(|name| name == token))
|
||||
{
|
||||
// Skip the flag's arguments (non-switch options consume the next token(s)).
|
||||
// Clamp to the number of argument tokens actually present to avoid
|
||||
// advancing past the end of input_tokens (e.g. `kubectl -n ` with no
|
||||
// namespace value).
|
||||
let num_args = option.arguments.iter().filter(|arg| !arg.optional).count();
|
||||
let available = input_tokens.len().saturating_sub(current_token_index + 1);
|
||||
current_token_index += num_args.min(available);
|
||||
}
|
||||
// Advance past the flag token itself.
|
||||
current_token_index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Token is not a subcommand or a recognized flag; stop searching.
|
||||
break;
|
||||
}
|
||||
|
||||
// No subcommand was found beyond any skipped flags, so return the
|
||||
// start index. This ensures the caller's parser still sees those
|
||||
// flag tokens and can process them as flag arguments.
|
||||
(command_signature, subcommand_search_start_index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lookup_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,418 @@
|
||||
use crate::signatures::{Argument, Command, CommandSignature, Opt};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Creates a `test_command` signature with a `test_subcommand` subcommand
|
||||
/// and the given options on the root command.
|
||||
fn test_command_signature_with_options(options: Vec<Opt>) -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
options,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn valued_option() -> Opt {
|
||||
Opt {
|
||||
name: vec!["-n".to_owned(), "--name".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "value".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input("test_command ", ®istry)
|
||||
.expect("Signature should exist");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command some_arg_value ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input(
|
||||
"test_command test_subcommand1 some_arg_value ",
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1", "some_arg_value"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(token_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1"],
|
||||
false,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// -n takes a value, so the parser should skip "-n val" and find test_subcommand.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "val", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_long_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![
|
||||
Opt {
|
||||
name: vec!["--context".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "context".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
valued_option(),
|
||||
]));
|
||||
|
||||
// Two valued flags before the subcommand should both be skipped.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&[
|
||||
"test_command",
|
||||
"--context",
|
||||
"staging",
|
||||
"-n",
|
||||
"project1",
|
||||
"test_subcommand",
|
||||
],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_switch_flag_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--verbose".to_owned()],
|
||||
arguments: vec![],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--verbose", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_at_end_without_value_does_not_panic() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// "-n" with no value should not panic.
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
// No subcommand found, so entry_token_index (0) is returned.
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_unrecognized_flag_before_subcommand() {
|
||||
// Unrecognized flags (tokens starting with '-' not in the spec) should be
|
||||
// skipped so the parser can still discover subcommands after them.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--unknown-flag", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_arg_consumes_token_matching_subcommand_name() {
|
||||
// When a recognized flag takes a required argument, the next token is
|
||||
// consumed as the flag's value even if it matches a subcommand name.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "test_subcommand", "extra"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
// "test_subcommand" was consumed as -n's value, so no subcommand is found.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_only_flags_no_subcommand() {
|
||||
// When the input consists only of flags with no following subcommand,
|
||||
// the parent command should be returned at the entry index.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n", "val"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_optional_flag_arg_does_not_consume_subcommand() {
|
||||
// A flag with 1 required + 1 optional argument should only skip the
|
||||
// required arg, so the next token can still match a subcommand.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--output".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "format".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "extra".to_owned(),
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
// "json" is the required arg, "test_subcommand" should not be consumed as
|
||||
// the optional arg.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--output", "json", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! This module contains Command Signature types for use with the v2 (JS-compatible) completions
|
||||
//! engine.
|
||||
|
||||
// The `js` module contains implementations of `galaxy_js::{IntoWarpJs, FromWarpJs}` for V2 command
|
||||
// signatures, which is only supported on native non-wasm platforms.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod js;
|
||||
|
||||
mod lookup;
|
||||
mod registry;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
pub use lookup::*;
|
||||
pub use registry::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use galaxy_js::TypedJsFunctionRef;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct CommandSignature {
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub alias: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub subcommands: Vec<Command>,
|
||||
pub options: Vec<Opt>,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Argument {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub values: Vec<ArgumentValue>,
|
||||
pub optional: bool,
|
||||
pub arity: Option<Arity>,
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
pub fn is_variadic(&self) -> bool {
|
||||
self.arity
|
||||
.as_ref()
|
||||
.is_some_and(|arity| arity.limit.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Arity {
|
||||
pub limit: Option<usize>,
|
||||
pub delimiter: Option<ArgumentDelimiter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArgumentDelimiter(pub String);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ArgumentValue {
|
||||
Suggestion(Suggestion),
|
||||
Template {
|
||||
type_name: TemplateType,
|
||||
filter_name: Option<String>,
|
||||
},
|
||||
Generator(GeneratorFn),
|
||||
/// The argument itself is a root command.
|
||||
///
|
||||
/// This is the appropriate `ArgumentValue` for commands that take a full command as an
|
||||
/// argument: `time` or `sudo`, for example.
|
||||
RootCommand,
|
||||
}
|
||||
|
||||
/// The final set of results returned from a custom `GeneratorFn` or from a `GeneratorFn`'s
|
||||
/// `post_process` function.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneratorResults {
|
||||
/// The list of completion suggestions.
|
||||
pub suggestions: Vec<Suggestion>,
|
||||
|
||||
/// `true` if the order of `suggestions` should be preserved.
|
||||
///
|
||||
/// If `false`, `suggestions` may be re-ordered by the internal completions engine in the final
|
||||
/// result set.
|
||||
pub is_ordered: bool,
|
||||
}
|
||||
|
||||
/// The input struct passed to custom generator functions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneratorCompletionContext {
|
||||
/// The tokens in the input for which completion suggestions are being generated.
|
||||
pub tokens: Vec<String>,
|
||||
|
||||
/// The current working directory of the session.
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
/// The Rust representation of a JS Function used to generate argument value suggestions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GeneratorFn {
|
||||
/// A generator that computes suggestions by executing the given `script` (a sh command) and
|
||||
/// "post-processing" its stdout with the given `post_process` fn.
|
||||
ShellCommand {
|
||||
script: GeneratorScript,
|
||||
|
||||
/// If `None`, a "default" post process function implementation is used, where
|
||||
/// `Suggestion`s are created from each line in `script`'s stdout. The returned
|
||||
/// `GeneratorResults` object's `is_ordered` is set to `false`.
|
||||
post_process: Option<TypedJsFunctionRef<String, GeneratorResults>>,
|
||||
},
|
||||
/// An entirely user-specified JS function that generates suggestions based on the given
|
||||
/// `GeneratorCompletionContext`.
|
||||
Custom(TypedJsFunctionRef<GeneratorCompletionContext, GeneratorResults>),
|
||||
}
|
||||
|
||||
/// The command to be executed as part of a `GeneratorFn::ShellCommand` to generate argument
|
||||
/// suggestion values.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GeneratorScript {
|
||||
Static(String),
|
||||
/// A JS function that dynamically computes the command to be run based on the tokenized input
|
||||
/// for which suggestions are being generated.
|
||||
Dynamic(TypedJsFunctionRef<Vec<String>, String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Suggestion {
|
||||
pub value: String,
|
||||
pub display_value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
/// The lowest priority value a completion object can have.
|
||||
const MIN_PRIORITY: i32 = -100;
|
||||
|
||||
/// The priority value of a completion object if not otherwise specifiied.
|
||||
const DEFAULT_PRIORITY: i32 = 0;
|
||||
|
||||
/// The highest priority value a completion object can have.
|
||||
const MAX_PRIORITY: i32 = 100;
|
||||
|
||||
/// Priority is a property of Commands, Subcommands and Options that influences where in the
|
||||
/// suggestion list those objects appear. It is represented as an integer between -100 and 100
|
||||
/// (inclusive) with 0 as the default.
|
||||
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Copy, Clone)]
|
||||
pub struct Priority(i32);
|
||||
|
||||
impl Priority {
|
||||
/// Creates a Priority value clamped to the range [-100, 100].
|
||||
pub fn new(value: i32) -> Self {
|
||||
Self(value.clamp(MIN_PRIORITY, MAX_PRIORITY))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn min() -> Self {
|
||||
Self::new(MIN_PRIORITY)
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self::new(MAX_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Priority {
|
||||
fn default() -> Self {
|
||||
Self::new(DEFAULT_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Priority {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Priority {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TemplateType {
|
||||
Files,
|
||||
Folders,
|
||||
FilesAndFolders,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Opt {
|
||||
pub name: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub required: bool,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
/// Returns `true` if this `Opt` has the given name.
|
||||
///
|
||||
/// Note that the given `name` should not include any leading hyphens; for example, this
|
||||
/// returns true for an `Opt` with names ['-f', '--foo'] given name 'f' or 'foo'.
|
||||
pub fn has_name(&self, name: impl AsRef<str>) -> bool {
|
||||
self.name.iter().any(|option_name| {
|
||||
if let Some(rest) = option_name.strip_prefix("--") {
|
||||
rest == name.as_ref()
|
||||
} else if let Some(rest) = option_name.strip_prefix('-') {
|
||||
rest == name.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "signatures_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,55 @@
|
||||
//! `CommandRegistry` contains all registered `CommandSignature`s that are eligible for use in
|
||||
//! completion suggestion generation.
|
||||
//!
|
||||
//! Completion engine callers must supply a `CommandRegistry` in their `CompletionContext`
|
||||
//! implementation to generate suggestions for registered commands.
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use memo_map::MemoMap;
|
||||
|
||||
use super::CommandSignature;
|
||||
|
||||
static GLOBAL_REGISTRY: OnceLock<Arc<CommandRegistry>> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CommandRegistry {
|
||||
signatures: MemoMap<String, CommandSignature>,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signatures: MemoMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_signature(&self, command: impl AsRef<str>) -> Option<&CommandSignature> {
|
||||
self.signatures.get(command.as_ref())
|
||||
}
|
||||
|
||||
pub fn register_signature(&self, signature: CommandSignature) {
|
||||
self.signatures
|
||||
.insert(signature.command.name.clone(), signature);
|
||||
}
|
||||
|
||||
pub fn registered_commands(&self) -> impl Iterator<Item = &str> {
|
||||
self.signatures.keys().map(|key| key.as_str())
|
||||
}
|
||||
|
||||
pub fn global_instance() -> Arc<Self> {
|
||||
GLOBAL_REGISTRY
|
||||
.get_or_init(|| {
|
||||
// TODO(wasm): Determine how to asynchronously load command signatures on wasm.
|
||||
Arc::new(CommandRegistry::new())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "registry_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::signatures::{Command, Priority};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test".to_owned(),
|
||||
alias: vec![],
|
||||
description: None,
|
||||
arguments: vec![],
|
||||
subcommands: vec![],
|
||||
options: vec![],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_registry_registers_signature() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_signature());
|
||||
|
||||
let signature = registry
|
||||
.get_signature("test")
|
||||
.expect("Signature is registered.");
|
||||
assert_eq!(signature.command.name, "test");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use super::Priority;
|
||||
|
||||
#[test]
|
||||
fn test_priority_normalization() {
|
||||
let too_small = Priority::new(-201);
|
||||
assert_eq!(Priority::min(), too_small);
|
||||
|
||||
let too_large = Priority::new(201);
|
||||
assert_eq!(Priority::max(), too_large);
|
||||
|
||||
let fourty_two = Priority::new(42);
|
||||
assert_eq!(42, fourty_two.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_comparison() {
|
||||
let super_important = Priority::new(200);
|
||||
let important = Priority::new(40);
|
||||
let not_important = Priority::new(-80);
|
||||
|
||||
assert!(super_important == super_important);
|
||||
assert!(super_important > important);
|
||||
assert!(super_important > not_important);
|
||||
|
||||
assert!(important == important);
|
||||
assert!(important > not_important);
|
||||
|
||||
assert!(not_important == not_important);
|
||||
}
|
||||
Reference in New Issue
Block a user