1use serde::Deserialize;
2use serde_json::json;
3
4use crate::api::{Client, Projection};
5use crate::error::Result;
6use crate::fields::{choices, closest, same, usage};
7
8#[derive(Deserialize)]
9struct Tag {
10 name: String,
11}
12
13impl Projection for Tag {
14 const FIELDS: &'static str = "name";
15}
16
17pub async fn visible(client: &Client) -> Result<Vec<String>> {
19 let tags: Vec<Tag> = client.get_all("tags").await?;
20 Ok(tags.into_iter().map(|tag| tag.name).collect())
21}
22
23pub async fn create(client: &Client, name: &str) -> Result<()> {
25 client.post_unit("tags", &json!({"name": name})).await
26}
27
28pub fn resolve(tags: &[String], name: &str, hint: &str) -> Result<String> {
30 if let Some(tag) = tags.iter().find(|tag| same(tag, name)) {
31 return Ok(tag.clone());
32 }
33 let mut message = format!("there is no tag '{name}'");
34 if let Some(close) = closest(name, tags.iter().map(String::as_str)) {
35 message.push_str(&format!(" (did you mean '{close}'?)"));
36 } else if !tags.is_empty() {
37 message.push_str(&format!("; tags: {}", choices(tags)));
38 }
39 message.push_str(&format!("; {hint}"));
40 Err(usage(message))
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 fn tags() -> Vec<String> {
48 vec!["Needs review".into(), "ui".into()]
49 }
50
51 #[test]
52 fn tags_take_the_instance_spelling() {
53 assert_eq!(
54 resolve(&tags(), "needs REVIEW", "").unwrap(),
55 "Needs review"
56 );
57 }
58
59 #[test]
60 fn unknown_tags_suggest_the_closest() {
61 let error = resolve(&tags(), "Neds review", "pass --no-validate to create it").unwrap_err();
62 assert!(
63 error.to_string().contains("did you mean 'Needs review'"),
64 "{error}"
65 );
66 assert!(error.to_string().contains("--no-validate"), "{error}");
67 }
68}