1use crate::cli::write::{Gate, Intent, check};
7use crate::cli::{Session, emit, report};
8use crate::exit::ExitCode;
9use crate::render::{Format, entity as render, machine};
10
11pub async fn list(kind: &str, query: Option<&str>, page: u32, session: &Session) -> ExitCode {
13 let client = match session.client() {
14 Ok(client) => client,
15 Err(code) => return code,
16 };
17
18 let display = session.display();
19 let Ok(per_page) = u32::try_from(display.limit.max(1)) else {
20 return report(&"limit is too large", ExitCode::ConfirmationRequired);
21 };
22
23 match client.entities(kind, query, page.max(1), per_page).await {
24 Ok(found) => {
25 let rendered = match session.render.format {
26 Format::Text => Ok(render::entities(&found, &session.render)),
27 Format::JsonRaw => machine(&found.items, Format::Json),
28 other => machine(&found.items, other),
29 };
30 finish(rendered)
31 }
32 Err(error) => {
33 let code = error.exit_code();
34 report(&error, code)
35 }
36 }
37}
38
39pub async fn contents(id: &str, page: u32, session: &Session) -> ExitCode {
41 let client = match session.client() {
42 Ok(client) => client,
43 Err(code) => return code,
44 };
45
46 let display = session.display();
47 let Ok(per_page) = u32::try_from(display.limit.max(1)) else {
48 return report(&"limit is too large", ExitCode::ConfirmationRequired);
49 };
50
51 match client.entities_in(id, page.max(1), per_page).await {
52 Ok(found) => {
53 let rendered = match session.render.format {
54 Format::Text => Ok(render::contents(&found, &session.render)),
55 Format::JsonRaw => machine(&found.items, Format::Json),
56 other => machine(&found.items, other),
57 };
58 finish(rendered)
59 }
60 Err(error) => {
61 let code = error.exit_code();
62 report(&error, code)
63 }
64 }
65}
66
67pub async fn get(kind: &str, id: &str, session: &Session) -> ExitCode {
69 let client = match session.client() {
70 Ok(client) => client,
71 Err(code) => return code,
72 };
73
74 match client.entity(kind, id).await {
75 Ok(found) => {
76 let rendered = match session.render.format {
77 Format::Text => Ok(render::entity(&found, &session.render)),
78 Format::JsonRaw => machine(&found, Format::Json),
79 other => machine(&found, other),
80 };
81 finish(rendered)
82 }
83 Err(error) => {
84 let code = error.exit_code();
85 report(&error, code)
86 }
87 }
88}
89
90fn finish(rendered: Result<String, crate::render::RenderError>) -> ExitCode {
91 match rendered {
92 Ok(text) => {
93 emit(&text);
94 ExitCode::Success
95 }
96 Err(error) => report(&error, ExitCode::Failure),
97 }
98}
99
100#[derive(Debug, Default, clap::Args)]
106pub struct Fields {
107 #[arg(long, short = 's')]
109 pub summary: Option<String>,
110 #[arg(long, short = 'd')]
112 pub description: Option<String>,
113 #[arg(long)]
115 pub lead: Option<String>,
116 #[arg(long)]
118 pub start: Option<String>,
119 #[arg(long)]
121 pub end: Option<String>,
122}
123
124impl Fields {
125 fn body(&self) -> serde_json::Map<String, serde_json::Value> {
127 let mut fields = serde_json::Map::new();
128 for (name, value) in [
129 ("summary", &self.summary),
130 ("description", &self.description),
131 ("lead", &self.lead),
132 ("start", &self.start),
133 ("end", &self.end),
134 ] {
135 if let Some(value) = value {
136 fields.insert(name.to_owned(), serde_json::json!(value));
137 }
138 }
139 fields
140 }
141}
142
143pub async fn create(kind: &str, fields: &Fields, session: &Session) -> ExitCode {
145 let client = match session.client() {
146 Ok(client) => client,
147 Err(code) => return code,
148 };
149
150 let Some(summary) = fields.summary.clone() else {
151 return report(
152 &format!("a {kind} needs a name: --summary"),
153 ExitCode::ConfirmationRequired,
154 );
155 };
156
157 let body = serde_json::Value::Object(fields.body());
158 let targets = [summary];
159 let intent = Intent {
160 action: &format!("create a {kind}"),
161 targets: &targets,
162 body: &body,
163 always_confirm: false,
164 };
165 if let Gate::Stop(code) = check(&intent, session) {
166 return code;
167 }
168
169 match client.create_entity(kind, &body).await {
170 Ok(created) => show(&created, session),
171 Err(error) => {
172 let code = error.exit_code();
173 report(&error, code)
174 }
175 }
176}
177
178pub async fn update(kind: &str, id: &str, fields: &Fields, session: &Session) -> ExitCode {
183 let client = match session.client() {
184 Ok(client) => client,
185 Err(code) => return code,
186 };
187
188 let body = fields.body();
189 if body.is_empty() {
190 return report(
191 &"nothing to change: pass --summary, --description, --lead, --start or --end",
192 ExitCode::ConfirmationRequired,
193 );
194 }
195 let body = serde_json::Value::Object(body);
196
197 let current = match client.entity(kind, id).await {
198 Ok(entity) => entity,
199 Err(error) => {
200 let code = error.exit_code();
201 return report(&error, code);
202 }
203 };
204
205 let intent = Intent {
206 action: &format!("change {kind} {id}"),
207 targets: std::slice::from_ref(¤t.id),
208 body: &body,
209 always_confirm: false,
210 };
211 if let Gate::Stop(code) = check(&intent, session) {
212 return code;
213 }
214
215 match client.update_entity(kind, id, &body, current.version).await {
216 Ok(updated) => show(&updated, session),
217 Err(error) => {
218 let code = error.exit_code();
219 report(&error, code)
220 }
221 }
222}
223
224pub async fn remove(kind: &str, id: &str, session: &Session) -> ExitCode {
230 let client = match session.client() {
231 Ok(client) => client,
232 Err(code) => return code,
233 };
234
235 let current = match client.entity(kind, id).await {
238 Ok(entity) => entity,
239 Err(error) => {
240 let code = error.exit_code();
241 return report(&error, code);
242 }
243 };
244
245 let body = serde_json::json!({ "delete": id });
246 let intent = Intent {
247 action: &format!("delete {kind} `{}`", current.summary),
248 targets: std::slice::from_ref(¤t.id),
249 body: &body,
250 always_confirm: true,
251 };
252 if let Gate::Stop(code) = check(&intent, session) {
253 return code;
254 }
255
256 match client.delete_entity(kind, id).await {
257 Ok(()) => {
258 emit(&format!("{kind} {id} deleted\n"));
259 ExitCode::Success
260 }
261 Err(error) => {
262 let code = error.exit_code();
263 report(&error, code)
264 }
265 }
266}
267
268fn show(entity: &crate::api::models::Entity, session: &Session) -> ExitCode {
270 let rendered = match session.render.format {
271 Format::Text => Ok(render::entity(entity, &session.render)),
272 Format::JsonRaw => machine(entity, Format::Json),
273 other => machine(entity, other),
274 };
275 finish(rendered)
276}
277
278pub async fn place(kind: &str, id: &str, parent: Option<&str>, session: &Session) -> ExitCode {
285 let client = match session.client() {
286 Ok(client) => client,
287 Err(code) => return code,
288 };
289
290 let current = match client.entity(kind, id).await {
291 Ok(entity) => entity,
292 Err(error) => {
293 let code = error.exit_code();
294 return report(&error, code);
295 }
296 };
297
298 let action = match parent {
299 Some(parent) => format!("put {kind} {id} into portfolio {parent}"),
300 None => format!("take {kind} {id} out of its portfolio"),
301 };
302 let body = serde_json::json!({
303 "fields": { "parentEntity": parent }
304 });
305 let intent = Intent {
306 action: &action,
307 targets: std::slice::from_ref(¤t.id),
308 body: &body,
309 always_confirm: false,
310 };
311 if let Gate::Stop(code) = check(&intent, session) {
312 return code;
313 }
314
315 match client.place_entity(kind, id, parent, current.version).await {
316 Ok(placed) => {
317 let rendered = match session.render.format {
318 Format::Text => Ok(render::entity(&placed, &session.render)),
319 Format::JsonRaw => machine(&placed, Format::Json),
320 other => machine(&placed, other),
321 };
322 finish(rendered)
323 }
324 Err(error) => {
325 let code = error.exit_code();
326 report(&error, code)
327 }
328 }
329}