start_command/
execution_identity.rs1use crate::execution_store::{ExecutionRecord, ExecutionStore};
11use serde_json::{json, Value};
12
13fn option_str<'a>(record: &'a ExecutionRecord, key: &str) -> Option<&'a str> {
14 record.options.get(key).and_then(Value::as_str)
15}
16
17fn option_list<'a>(record: &'a ExecutionRecord, key: &str) -> Vec<&'a str> {
18 record
19 .options
20 .get(key)
21 .and_then(Value::as_array)
22 .map(|items| items.iter().filter_map(Value::as_str).collect())
23 .unwrap_or_default()
24}
25
26fn has_name(record: &ExecutionRecord, identifier: &str) -> bool {
27 option_str(record, "name") == Some(identifier)
28 || option_list(record, "aliases").contains(&identifier)
29}
30
31pub fn find_record<'a>(
34 records: &'a [ExecutionRecord],
35 identifier: &str,
36) -> Option<&'a ExecutionRecord> {
37 if identifier.is_empty() {
38 return None;
39 }
40 records
41 .iter()
42 .find(|r| r.uuid == identifier)
43 .or_else(|| {
44 records
45 .iter()
46 .find(|r| option_str(r, "sessionName") == Some(identifier))
47 })
48 .or_else(|| {
49 records
50 .iter()
51 .find(|r| option_list(r, "sessionNameHistory").contains(&identifier))
52 })
53 .or_else(|| records.iter().find(|r| has_name(r, identifier)))
54}
55
56pub fn execution_aliases(record: &ExecutionRecord) -> Vec<String> {
58 let mut names: Vec<String> = Vec::new();
59 let candidates = std::iter::once(Some(record.uuid.as_str()))
60 .chain(
61 option_list(record, "sessionNameHistory")
62 .into_iter()
63 .map(Some),
64 )
65 .chain(std::iter::once(option_str(record, "sessionName")))
66 .chain(option_list(record, "aliases").into_iter().map(Some))
67 .chain(std::iter::once(option_str(record, "name")));
68 for name in candidates.flatten() {
69 if !name.is_empty() && !names.iter().any(|n| n == name) {
70 names.push(name.to_string());
71 }
72 }
73 names
74}
75
76pub fn has_user_names(record: &ExecutionRecord) -> bool {
78 option_str(record, "name").is_some_and(|n| !n.is_empty())
79 || !option_list(record, "aliases").is_empty()
80}
81
82fn validate_name(name: &str) -> Result<(), String> {
83 if name.trim().is_empty() {
84 return Err("The new name must not be empty".into());
85 }
86 if name.starts_with('-') {
87 return Err(format!(
88 "Invalid name \"{}\": names cannot start with \"-\"",
89 name
90 ));
91 }
92 Ok(())
93}
94
95pub fn name_execution(
100 store: Option<&ExecutionStore>,
101 identifier: &str,
102 name: &str,
103 rename: bool,
104) -> Result<String, String> {
105 let store = store.ok_or("Execution tracking is disabled.")?;
106 validate_name(name)?;
107 let changed = store.with_lock(|records| {
108 let target = find_record(records, identifier)
109 .ok_or_else(|| {
110 format!(
111 "No execution found with UUID or session name: {}",
112 identifier
113 )
114 })?
115 .uuid
116 .clone();
117 let owner = find_record(records, name).map(|r| r.uuid.clone());
118 if let Some(owner) = owner.as_ref().filter(|o| **o != target) {
119 return Err(format!(
120 "Name \"{}\" already refers to execution {}",
121 name, owner
122 ));
123 }
124 let record = records.iter_mut().find(|r| r.uuid == target).unwrap();
125 let mut aliases: Vec<String> = option_list(record, "aliases")
126 .into_iter()
127 .map(str::to_string)
128 .collect();
129 if rename {
130 let current_name = option_str(record, "name").map(str::to_string);
131 let previous = current_name
132 .clone()
133 .or_else(|| option_str(record, "sessionName").map(str::to_string))
134 .unwrap_or_else(|| record.uuid.clone());
135 if previous == name {
136 return Ok(None);
137 }
138 if current_name.is_some() {
141 aliases.push(previous);
142 }
143 record.options.insert("name".into(), json!(name));
144 } else if owner.is_some() {
145 return Ok(None);
146 } else {
147 aliases.push(name.to_string());
148 }
149 let display = option_str(record, "name").map(str::to_string);
150 let mut unique: Vec<String> = Vec::new();
151 for alias in aliases {
152 if Some(&alias) != display.as_ref() && !unique.contains(&alias) {
153 unique.push(alias);
154 }
155 }
156 if unique.is_empty() {
157 record.options.remove("aliases");
158 } else {
159 record.options.insert("aliases".into(), json!(unique));
160 }
161 Ok(Some(record.clone()))
162 })?;
163 let unchanged = changed.is_none();
164 let current = match changed {
165 Some(record) => record,
166 None => store.get(identifier).ok_or_else(|| {
167 format!(
168 "No execution found with UUID or session name: {}",
169 identifier
170 )
171 })?,
172 };
173 let action = match (unchanged, rename) {
174 (true, _) => "Unchanged",
175 (false, true) => "Renamed",
176 (false, false) => "Aliased",
177 };
178 Ok(format!(
179 "{} execution {}\naliases: [{}]",
180 action,
181 current.uuid,
182 execution_aliases(¤t).join(", ")
183 ))
184}