use crate::ocpp::{
ConnectorStatus as OcppConnectorStatus, OcppVersion, StopReason,
};
const COMMAND_WORDS: &[&str] = &[
"status",
"connect",
"disconnect",
"boot",
"authorize",
"data-transfer",
"start",
"stop",
"meter",
"send-meter",
"heartbeat",
"connector-status",
"clear",
"standards",
"help",
"exit",
];
const STATUS_WORDS_V1_6: &[OcppConnectorStatus] = OcppConnectorStatus::V1_6;
const STATUS_WORDS_V2_X: &[OcppConnectorStatus] = OcppConnectorStatus::V2_X;
const STOP_REASON_HINTS_V1_6: &[StopReason] = StopReason::V1_6;
const STOP_REASON_HINTS_V2_0_1: &[StopReason] = StopReason::V2_0_1;
const STOP_REASON_HINTS_V2_1: &[StopReason] = StopReason::V2_1;
const CONNECT_URL_PREFIXES: &[&str] = &["ws://", "wss://"];
#[derive(Debug, Clone)]
pub(super) struct CompletionState {
base: String,
candidates: Vec<String>,
index: usize,
}
impl CompletionState {
pub(super) fn new(
base: String,
candidates: Vec<String>,
index: usize,
) -> Self {
Self {
base,
candidates,
index,
}
}
pub(super) fn is_empty(&self) -> bool {
self.candidates.is_empty()
}
pub(super) fn next_value(&mut self, reverse: bool) -> String {
self.index =
next_completion_index(self.index, self.candidates.len(), reverse);
format!("{}{}", self.base, self.candidates[self.index])
}
}
pub(super) fn completion_seed(
input: &str,
protocol: OcppVersion,
known_connectors: &[u16],
profile_names: &[String],
) -> Option<(String, Vec<String>)> {
let ends_with_space = input.ends_with(char::is_whitespace);
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.is_empty() {
return Some((String::new(), complete_static("", COMMAND_WORDS)));
}
if parts.len() == 1 && !ends_with_space {
return Some((String::new(), complete_static(parts[0], COMMAND_WORDS)));
}
let command = parts[0].to_ascii_lowercase();
match command.as_str() {
"connect" => {
seed_for_connect_targets(&parts, ends_with_space, profile_names)
}
"start" | "meter" | "send-meter" => {
seed_for_connectors(&parts, ends_with_space, 1, known_connectors)
}
"stop" => seed_for_connectors(&parts, ends_with_space, 1, known_connectors)
.or_else(|| match protocol {
OcppVersion::V1_6 => seed_for_stop_reasons(
&parts,
ends_with_space,
2,
STOP_REASON_HINTS_V1_6,
),
OcppVersion::V2_0_1 => seed_for_stop_reasons(
&parts,
ends_with_space,
2,
STOP_REASON_HINTS_V2_0_1,
),
OcppVersion::V2_1 => seed_for_stop_reasons(
&parts,
ends_with_space,
2,
STOP_REASON_HINTS_V2_1,
),
}),
"heartbeat" => {
seed_for_position(&parts, ends_with_space, 1, &["start", "stop"])
}
"connector-status" => {
seed_for_connectors(&parts, ends_with_space, 1, known_connectors).or_else(
|| match protocol {
OcppVersion::V1_6 => {
seed_for_statuses(&parts, ends_with_space, 2, STATUS_WORDS_V1_6)
}
OcppVersion::V2_0_1 | OcppVersion::V2_1 => {
seed_for_statuses(&parts, ends_with_space, 2, STATUS_WORDS_V2_X)
}
},
)
}
_ => None,
}
}
fn seed_for_connect_targets(
parts: &[&str],
ends_with_space: bool,
profile_names: &[String],
) -> Option<(String, Vec<String>)> {
let token_index = if ends_with_space {
parts.len()
} else {
parts.len().saturating_sub(1)
};
if token_index != 1 {
return None;
}
let base = completion_base(parts, ends_with_space);
let prefix = if ends_with_space {
""
} else {
parts.last().copied().unwrap_or("")
};
let mut candidates = filter_words(prefix, profile_names)
.into_iter()
.map(|profile| format!("{profile} "))
.collect::<Vec<_>>();
candidates.extend(
CONNECT_URL_PREFIXES
.iter()
.filter(|prefix_candidate| prefix_candidate.starts_with(prefix))
.map(|prefix_candidate| (*prefix_candidate).to_string()),
);
Some((base, candidates))
}
fn complete_static(prefix: &str, words: &[&str]) -> Vec<String> {
let owned: Vec<String> = words.iter().map(ToString::to_string).collect();
let mut candidates = filter_words(prefix, &owned);
for candidate in &mut candidates {
candidate.push(' ');
}
candidates
}
fn seed_for_connectors(
parts: &[&str],
ends_with_space: bool,
arg_index: usize,
known_connectors: &[u16],
) -> Option<(String, Vec<String>)> {
let connector_words = connector_words(known_connectors);
seed_for_position_owned(parts, ends_with_space, arg_index, &connector_words)
}
fn seed_for_position(
parts: &[&str],
ends_with_space: bool,
arg_index: usize,
words: &[&str],
) -> Option<(String, Vec<String>)> {
let owned: Vec<String> = words.iter().map(ToString::to_string).collect();
seed_for_position_owned(parts, ends_with_space, arg_index, &owned)
}
fn seed_for_statuses(
parts: &[&str],
ends_with_space: bool,
arg_index: usize,
statuses: &[OcppConnectorStatus],
) -> Option<(String, Vec<String>)> {
let owned = statuses
.iter()
.map(|status| status.as_str().to_string())
.collect::<Vec<_>>();
seed_for_position_owned(parts, ends_with_space, arg_index, &owned)
}
fn seed_for_stop_reasons(
parts: &[&str],
ends_with_space: bool,
arg_index: usize,
reasons: &[StopReason],
) -> Option<(String, Vec<String>)> {
let owned = reasons
.iter()
.map(|reason| reason.as_str().to_string())
.collect::<Vec<_>>();
seed_for_position_owned(parts, ends_with_space, arg_index, &owned)
}
fn seed_for_position_owned(
parts: &[&str],
ends_with_space: bool,
arg_index: usize,
words: &[String],
) -> Option<(String, Vec<String>)> {
let token_index = if ends_with_space {
parts.len()
} else {
parts.len().saturating_sub(1)
};
if token_index != arg_index {
return None;
}
let base = completion_base(parts, ends_with_space);
let prefix = if ends_with_space {
""
} else {
parts.last().copied().unwrap_or("")
};
let mut candidates = filter_words(prefix, words);
for candidate in &mut candidates {
candidate.push(' ');
}
Some((base, candidates))
}
fn connector_words(known_connectors: &[u16]) -> Vec<String> {
if known_connectors.is_empty() {
return vec!["1".to_string()];
}
known_connectors.iter().map(ToString::to_string).collect()
}
fn filter_words(prefix: &str, words: &[String]) -> Vec<String> {
let prefix_lower = prefix.to_ascii_lowercase();
words
.iter()
.filter(|word| word.to_ascii_lowercase().starts_with(&prefix_lower))
.cloned()
.collect()
}
fn next_completion_index(current: usize, len: usize, reverse: bool) -> usize {
if len == 0 {
return 0;
}
if reverse {
if current == 0 { len - 1 } else { current - 1 }
} else {
(current + 1) % len
}
}
fn completion_base(parts: &[&str], ends_with_space: bool) -> String {
if parts.is_empty() {
return String::new();
}
if ends_with_space {
return format!("{} ", parts.join(" "));
}
if parts.len() == 1 {
return String::new();
}
format!("{} ", parts[..parts.len() - 1].join(" "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_completion_is_protocol_specific() {
let v1_6_words = completion_words("connector-status 1 ", OcppVersion::V1_6);
assert!(v1_6_words.contains(&"Charging".to_string()));
assert!(!v1_6_words.contains(&"Occupied".to_string()));
let v2_1_words = completion_words("connector-status 1 ", OcppVersion::V2_1);
assert!(v2_1_words.contains(&"Occupied".to_string()));
assert!(!v2_1_words.contains(&"Charging".to_string()));
}
#[test]
fn stop_reason_completion_is_protocol_specific() {
let v1_6_words = completion_words("stop 1 ", OcppVersion::V1_6);
assert!(v1_6_words.contains(&"UnlockCommand".to_string()));
assert!(!v1_6_words.contains(&"Timeout".to_string()));
let v2_0_1_words = completion_words("stop 1 ", OcppVersion::V2_0_1);
assert!(v2_0_1_words.contains(&"Timeout".to_string()));
assert!(!v2_0_1_words.contains(&"ReqEnergyTransferRejected".to_string(),));
let v2_1_words = completion_words("stop 1 ", OcppVersion::V2_1);
assert!(v2_1_words.contains(&"ReqEnergyTransferRejected".to_string(),));
}
#[test]
fn connect_completion_includes_profiles_and_url_prefixes() {
let profiles = vec!["demo".to_string(), "yard".to_string()];
let (_, candidates) =
completion_seed("connect ", OcppVersion::V1_6, &[1], &profiles)
.expect("completion candidates");
assert_eq!(
candidates,
vec![
"demo ".to_string(),
"yard ".to_string(),
"ws://".to_string(),
"wss://".to_string(),
]
);
}
#[test]
fn connect_completion_filters_profiles_and_url_prefixes() {
let profiles = vec!["demo".to_string(), "yard".to_string()];
let (_, profile_candidates) =
completion_seed("connect de", OcppVersion::V1_6, &[1], &profiles)
.expect("profile candidates");
assert_eq!(profile_candidates, vec!["demo ".to_string()]);
let (_, url_candidates) =
completion_seed("connect w", OcppVersion::V1_6, &[1], &profiles)
.expect("url candidates");
assert_eq!(
url_candidates,
vec!["ws://".to_string(), "wss://".to_string()]
);
}
fn completion_words(input: &str, protocol: OcppVersion) -> Vec<String> {
let (_, candidates) = completion_seed(input, protocol, &[1], &[])
.expect("completion candidates");
candidates
.into_iter()
.map(|candidate| candidate.trim_end().to_string())
.collect()
}
}