use kernel::{Address, IdemKey};
pub(super) const CONTROL: [&str; 6] = ["help", "web", "at", "quit", "serving", "remote"];
const OFF_THE_SOCKET: [&str; 2] = ["PutSecret", "Auth"];
pub(super) fn carried_commands() -> impl Iterator<Item = &'static str> {
wire::COMMAND_NAMES
.iter()
.copied()
.filter(|name| !OFF_THE_SOCKET.contains(name))
}
#[derive(Debug, PartialEq)]
pub(crate) enum Line {
Nothing,
Help,
OpenWeb,
Serving,
Select(Address),
Quit,
Remote(crate::outside::console::RemoteLine),
Frame(Box<wire::ClientFrame>),
Work(String),
Unknown {
verb: String,
nearest: Vec<String>,
},
Malformed {
verb: String,
reason: String,
},
}
pub(crate) fn snake(camel: &str) -> String {
let mut out = String::with_capacity(camel.len().saturating_add(4));
for (index, ch) in camel.char_indices() {
if ch.is_ascii_uppercase() && index > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
}
out
}
pub(crate) fn verbs() -> Vec<String> {
let mut out: Vec<String> = CONTROL.iter().map(|name| (*name).to_owned()).collect();
out.extend(carried_commands().map(snake));
out.extend(wire::QUERY_NAMES.iter().map(|name| snake(name)));
out
}
fn nearest(verb: &str) -> Vec<String> {
let known = verbs();
let typed: Vec<char> = verb.chars().collect();
for length in (1..=typed.len().min(4)).rev() {
let head: String = typed.iter().take(length).collect();
let mut close: Vec<String> = known
.iter()
.filter(|name| name.starts_with(&head))
.cloned()
.collect();
if !close.is_empty() {
close.truncate(6);
return close;
}
}
Vec::new()
}
pub(crate) fn parse(line: &str, selected: Option<&Address>, idem: IdemKey) -> Line {
let trimmed = line.trim();
if trimmed.is_empty() {
return Line::Nothing;
}
let Some(rest) = trimmed.strip_prefix('/') else {
return match selected {
Some(_) => Line::Work(trimmed.to_owned()),
None => Line::Unknown {
verb: "(no room selected)".to_owned(),
nearest: vec!["at".to_owned()],
},
};
};
let (verb, tail) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
let tail = tail.trim();
match verb {
"help" | "?" => Line::Help,
"web" => Line::OpenWeb,
"serving" => Line::Serving,
"quit" | "exit" => Line::Quit,
"remote" => Line::Remote(crate::outside::console::parse(tail)),
"at" => match Address::parse(tail) {
Ok(addr) => Line::Select(addr),
Err(_) => Line::Unknown {
verb: format!("at {tail}"),
nearest: vec!["at <building>/<room>".to_owned()],
},
},
other => wire_frame(other, tail, idem),
}
}
fn wire_frame(verb: &str, tail: &str, idem: IdemKey) -> Line {
let body = if tail.is_empty() {
Ok(serde_json::Value::Null)
} else {
serde_json::from_str::<serde_json::Value>(tail)
};
let known_command = carried_commands().find(|name| snake(name) == verb);
let known_query = wire::QUERY_NAMES.iter().find(|name| snake(name) == verb);
let framed = match (known_command, known_query, body) {
(None, None, _) => {
return Line::Unknown {
verb: verb.to_owned(),
nearest: nearest(verb),
};
}
(_, _, Err(err)) => return malformed(verb, &err),
(Some(name), _, Ok(mut body)) => {
if let serde_json::Value::Object(fields) = &mut body {
fields
.entry("idem")
.or_insert_with(|| serde_json::Value::String(idem.to_string()));
}
keyed("command", keyed(&snake(name), body))
}
(None, Some(name), Ok(serde_json::Value::Null)) => {
asked(serde_json::Value::String(snake(name)))
}
(None, Some(name), Ok(body)) => asked(keyed(&snake(name), body)),
};
match serde_json::from_value::<wire::ClientFrame>(framed) {
Ok(frame) => Line::Frame(Box::new(frame)),
Err(err) => malformed(verb, &err),
}
}
fn keyed(key: &str, value: serde_json::Value) -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::from_iter([(key.to_owned(), value)]))
}
fn malformed(verb: &str, err: &serde_json::Error) -> Line {
Line::Malformed {
verb: verb.to_owned(),
reason: err.to_string(),
}
}
pub(crate) fn help(selected: Option<&Address>) -> String {
let commands: Vec<String> = carried_commands().map(snake).collect();
let queries: Vec<String> = wire::QUERY_NAMES.iter().map(|n| snake(n)).collect();
let room = selected.map_or_else(
|| "no room selected - `/at <building>/<room>` first".to_owned(),
|addr| format!("work goes to {}", addr.as_str()),
);
format!(
"\n {room}\n\n \
/help this\n \
/at <building>/<room> choose where plain lines go\n \
/serving where this city listens, and what is running in it\n \
/web open the WebUI, token included\n \
/remote <verb> open|pair|close|devices|revoke: the door a device reaches the city through\n \
/quit close this console; the city keeps serving\n\n \
anything else work, dispatched to the chosen room\n\n \
/<query> {}\n \
/<command> <json> {}\n",
queries.join(", "),
commands.join(", ")
)
}
fn asked(query: serde_json::Value) -> serde_json::Value {
serde_json::json!({ "ask": { "ask_id": 0, "query": query } })
}