1use std::collections::BTreeMap;
15use std::io::{BufRead, Write};
16
17use onetaskgraph_plugin_api::{
18 Page, Project, SecretResolver, SourceError, SourceName, Status, StatusCategory, Task,
19 TaskSource, TaskUpdateOutcome,
20};
21use secrecy::SecretString;
22use serde::Deserialize;
23use serde_json::{Value, json};
24
25use super::connection::{Line, MAX_LINE, read_line};
26use super::wire::{
27 AddCommentParams, CommentsParams, ContentParams, DeleteCommentParams, DeleteParams,
28 DeliveredByParams, DependencyParams, DocumentDir, DocumentQueryParams, DocumentWriteParams,
29 EditCommentParams, HandshakePluginKind, IdParams, InitializeParams, InitializeResult,
30 LabelParams, MetadataParams, PROTOCOL_VERSION, PriorityParams, ProjectQueryParams,
31 ProjectWriteParams, Request, Response, StatusParams, TaskQueryParams, TaskWriteParams,
32 UpdateParams, after_the_first_vocabulary, knows_every_category, vocabulary,
33};
34use crate::config::rebased;
35use crate::registry::PluginKind;
36
37#[derive(Debug, Clone, Deserialize)]
44struct HostedSettings {
45 kind: PluginKind,
47 #[serde(default)]
49 config: Value,
50}
51
52struct Hosted {
54 source: Box<dyn TaskSource>,
56 engine_knows_every_category: bool,
60}
61
62pub async fn serve(input: impl BufRead, output: impl Write) -> std::io::Result<()> {
72 serve_kind(input, output, None).await
73}
74
75pub async fn serve_plugin(
80 input: impl BufRead,
81 output: impl Write,
82 kind: PluginKind,
83) -> std::io::Result<()> {
84 serve_kind(input, output, Some(kind)).await
85}
86
87async fn serve_kind(
88 mut input: impl BufRead,
89 mut output: impl Write,
90 kind: Option<PluginKind>,
91) -> std::io::Result<()> {
92 let mut source: Option<Hosted> = None;
93 loop {
94 let line = match read_line(&mut input) {
95 Line::Read(line) => line,
96 Line::Ended => return Ok(()),
97 Line::Failed(error) => return Err(error),
98 Line::TooLong => {
102 eprintln!(
103 "onetaskgraph-source: a request ran past {MAX_LINE} bytes without \
104 ending its line; closing the connection"
105 );
106 return Ok(());
107 }
108 };
109 if line.trim().is_empty() {
110 continue;
111 }
112 let Some(id) = addressed(&line) else {
113 eprintln!("onetaskgraph-source: ignoring a line with no request id: {line}");
114 continue;
115 };
116 let response = match serde_json::from_str::<Request>(&line) {
117 Ok(request) => answer(&mut source, request, kind).await,
118 Err(error) => Response::failed(
119 id,
120 SourceError::Malformed {
121 message: format!("that is not a request envelope: {error}"),
122 },
123 ),
124 };
125 let finished = ended_the_connection(&response);
126 writeln!(
127 output,
128 "{}",
129 serde_json::to_string(&response).expect("a response is plain data")
131 )?;
132 output.flush()?;
133 if finished {
134 return Ok(());
135 }
136 }
137}
138
139fn addressed(line: &str) -> Option<String> {
146 serde_json::from_str::<Value>(line)
147 .ok()?
148 .get("id")?
149 .as_str()
150 .map(str::to_owned)
151}
152
153fn ended_the_connection(response: &Response) -> bool {
155 matches!(
156 response.error.as_ref(),
157 Some(SourceError::Config { message }) if message.starts_with(VERSION_REFUSAL)
158 )
159}
160
161const VERSION_REFUSAL: &str = "protocol version ";
164
165async fn answer(
167 source: &mut Option<Hosted>,
168 request: Request,
169 kind: Option<PluginKind>,
170) -> Response {
171 let Request { id, method, params } = request;
172 if method == "initialize" {
173 return match source {
174 Some(_) => Response::failed(
175 id,
176 SourceError::Malformed {
177 message: "this connection was already initialized".to_owned(),
178 },
179 ),
180 None => initialize(source, id, params, kind).await,
181 };
182 }
183 let Some(built) = source.as_ref() else {
184 return Response::failed(
185 id,
186 SourceError::Malformed {
187 message: format!("{method} arrived before the handshake"),
188 },
189 );
190 };
191 match dispatch(
192 built.source.as_ref(),
193 built.engine_knows_every_category,
194 &method,
195 params,
196 )
197 .await
198 {
199 Ok(result) => Response::ok(id, result),
200 Err(error) => Response::failed(id, error),
201 }
202}
203
204async fn initialize(
206 source: &mut Option<Hosted>,
207 id: String,
208 params: Value,
209 kind: Option<PluginKind>,
210) -> Response {
211 let params: InitializeParams = match serde_json::from_value(params) {
212 Ok(params) => params,
213 Err(error) => {
214 return Response::failed(
215 id,
216 SourceError::Config {
217 message: format!("that is not an initialize request: {error}"),
218 },
219 );
220 }
221 };
222 if params.protocol_version != PROTOCOL_VERSION {
223 return Response::failed(
224 id,
225 SourceError::Config {
226 message: format!(
227 "{VERSION_REFUSAL}{} is not supported by this plugin; it speaks \
228 version {PROTOCOL_VERSION}",
229 params.protocol_version
230 ),
231 },
232 );
233 }
234 match build(¶ms, kind) {
235 Ok(built) => {
236 let kind = match HandshakePluginKind::new(built.kind()) {
237 Ok(kind) => kind,
238 Err(error) => {
239 return Response::failed(
240 id,
241 SourceError::Malformed {
242 message: format!("the hosted plugin reported an invalid kind: {error}"),
243 },
244 );
245 }
246 };
247 let result = InitializeResult {
248 protocol_version: Some(PROTOCOL_VERSION),
249 kind,
250 capabilities: built.capabilities(),
251 writes: Some(built.writes()),
252 meters: matches!(built.metering().await, Ok(Some(_))),
255 statuses: Some(vocabulary()),
256 task_updates: true,
260 metadata_updates: true,
263 content_updates: true,
266 targeted_updates: true,
269 };
270 *source = Some(Hosted {
271 source: built,
272 engine_knows_every_category: knows_every_category(params.statuses.as_deref()),
273 });
274 Response::ok(
275 id,
276 serde_json::to_value(&result).expect("a result is plain data"),
277 )
278 }
279 Err(error) => Response::failed(id, error),
280 }
281}
282
283fn build(
285 params: &InitializeParams,
286 selected: Option<PluginKind>,
287) -> Result<Box<dyn TaskSource>, SourceError> {
288 let (kind, config) = match selected {
289 Some(kind) => (kind, ¶ms.config),
290 None => {
291 let settings: HostedSettings = serde_json::from_value(params.config.clone()).map_err(
292 |error| SourceError::Config {
293 message: format!(
294 "this host serves a plugin of this build, and its settings must name one \
295 as {{\"kind\": …, \"config\": …}}: {error}"
296 ),
297 },
298 )?;
299 return build_plugin(params, settings.kind, &settings.config);
300 }
301 };
302 build_plugin(params, kind, config)
303}
304
305fn build_plugin(
306 params: &InitializeParams,
307 kind: PluginKind,
308 config: &Value,
309) -> Result<Box<dyn TaskSource>, SourceError> {
310 let name = SourceName::new(params.source_name.clone())?;
311 let plugin = kind.plugin();
312 let config = measured_from_document(
313 params.document_dir.as_ref(),
314 plugin.document_relative_paths(),
315 config,
316 );
317 plugin.build(&name, &config, &Handshake(¶ms.secrets))
318}
319
320fn measured_from_document(
330 document_dir: Option<&DocumentDir>,
331 fields: &[&str],
332 config: &Value,
333) -> Value {
334 let mut config = config.clone();
335 let Some(directory) = document_dir.map(DocumentDir::as_path) else {
337 return config;
338 };
339 for field in fields {
340 let Some(value) = field
341 .split('.')
342 .try_fold(&mut config, |cursor, segment| cursor.get_mut(segment))
343 else {
344 continue;
345 };
346 let Some(rebased) = value.as_str().and_then(|raw| rebased(directory, raw)) else {
347 continue;
348 };
349 let rebased = rebased
351 .into_os_string()
352 .into_string()
353 .expect("a path joined from two strings is a string");
354 *value = Value::String(rebased);
355 }
356 config
357}
358
359struct Handshake<'a>(&'a BTreeMap<String, String>);
366
367impl SecretResolver for Handshake<'_> {
368 fn get(&self, var: &str) -> Option<SecretString> {
369 self.0
370 .get(var)
371 .map(|value| SecretString::from(value.clone()))
372 }
373}
374
375fn told(status: Status, known: bool) -> Status {
377 if known || !after_the_first_vocabulary(status.category) {
378 return status;
379 }
380 Status {
381 category: StatusCategory::Unknown,
382 name: status.name,
383 }
384}
385
386fn told_task(task: Task, known: bool) -> Task {
387 Task {
388 status: told(task.status.clone(), known),
389 ..task
390 }
391}
392
393fn told_project(project: Project, known: bool) -> Project {
394 Project {
395 status: told(project.status.clone(), known),
396 ..project
397 }
398}
399
400async fn dispatch(
405 source: &dyn TaskSource,
406 known: bool,
407 method: &str,
408 params: Value,
409) -> Result<Value, SourceError> {
410 match method {
411 "health" => encode(source.health().await?),
412 "get_task" => {
413 let params: IdParams = decode(method, params)?;
414 let task = source.get_task(¶ms.id).await?;
415 encode(json!({ "task": task.map(|task| told_task(task, known)) }))
416 }
417 "get_project" => {
418 let params: IdParams = decode(method, params)?;
419 let project = source.get_project(¶ms.id).await?;
420 encode(json!({ "project": project.map(|project| told_project(project, known)) }))
421 }
422 "query_tasks" => {
423 let params: TaskQueryParams = decode(method, params)?;
424 let page = source.query_tasks(¶ms.query, ¶ms.page).await?;
425 encode(Page {
426 items: page
427 .items
428 .into_iter()
429 .map(|task| told_task(task, known))
430 .collect(),
431 next: page.next,
432 })
433 }
434 "query_projects" => {
435 let params: ProjectQueryParams = decode(method, params)?;
436 let page = source.query_projects(¶ms.query, ¶ms.page).await?;
437 encode(Page {
438 items: page
439 .items
440 .into_iter()
441 .map(|project| told_project(project, known))
442 .collect(),
443 next: page.next,
444 })
445 }
446 "set_task_status" => {
447 let params: StatusParams = decode(method, params)?;
448 let status = source.set_task_status(¶ms.id, params.category).await?;
449 encode(json!({ "status": status.map(|status| told(status, known)) }))
450 }
451 "set_task_priority" => {
452 let params: PriorityParams = decode(method, params)?;
453 let priority = source
454 .set_task_priority(¶ms.id, params.priority)
455 .await?;
456 encode(json!({ "priority": priority }))
457 }
458 "set_task_content" => {
459 let params: ContentParams = decode(method, params)?;
460 let written = source.set_task_content(¶ms.id, ¶ms.content).await?;
461 encode(json!({ "id": written.map(|()| params.id) }))
462 }
463 "update_task" => {
464 let params: UpdateParams = decode(method, params)?;
465 let outcome = source.update_task(¶ms.id, ¶ms.update).await?;
466 encode(json!({ "outcome": outcome.map(|outcome| TaskUpdateOutcome {
467 task: told_task(outcome.task, known),
468 ..outcome
469 }) }))
470 }
471 "set_task_metadata" => {
472 let params: MetadataParams = decode(method, params)?;
473 let task = source
474 .set_task_metadata(¶ms.id, ¶ms.key, ¶ms.value)
475 .await?;
476 encode(json!({ "task": task.map(|task| told_task(task, known)) }))
477 }
478 "set_project_metadata" => {
479 let params: MetadataParams = decode(method, params)?;
480 let project = source
481 .set_project_metadata(¶ms.id, ¶ms.key, ¶ms.value)
482 .await?;
483 encode(json!({ "project": project.map(|project| told_project(project, known)) }))
484 }
485 "set_document_metadata" => {
486 let params: MetadataParams = decode(method, params)?;
487 let document = source
488 .set_document_metadata(¶ms.id, ¶ms.key, ¶ms.value)
489 .await?;
490 encode(json!({ "document": document }))
491 }
492 "set_delivered_by" => {
493 let params: DeliveredByParams = decode(method, params)?;
494 let held = source
495 .set_delivered_by(¶ms.id, ¶ms.delivered_by)
496 .await?;
497 encode(json!({ "delivered_by": held.map(|()| params.delivered_by) }))
498 }
499 "labels" => {
500 let params: LabelParams = decode(method, params)?;
501 encode(source.labels(¶ms.page).await?)
502 }
503 "task_dependencies" => {
504 let params: DependencyParams = decode(method, params)?;
505 encode(
506 source
507 .task_dependencies(¶ms.id, params.direction, ¶ms.page)
508 .await?,
509 )
510 }
511 "project_dependencies" => {
512 let params: DependencyParams = decode(method, params)?;
513 encode(
514 source
515 .project_dependencies(¶ms.id, params.direction, ¶ms.page)
516 .await?,
517 )
518 }
519 "write_task" => {
520 let params: TaskWriteParams = decode(method, params)?;
521 encode(json!({ "id": source.write_task(¶ms.write).await? }))
522 }
523 "write_project" => {
524 let params: ProjectWriteParams = decode(method, params)?;
525 encode(json!({ "id": source.write_project(¶ms.write).await? }))
526 }
527 "delete_task" => {
528 let params: DeleteParams = decode(method, params)?;
529 source.delete_task(¶ms.id).await?;
530 encode(json!({}))
531 }
532 "delete_project" => {
533 let params: DeleteParams = decode(method, params)?;
534 source.delete_project(¶ms.id).await?;
535 encode(json!({}))
536 }
537 "get_document" => {
538 let params: IdParams = decode(method, params)?;
539 encode(json!({ "document": source.get_document(¶ms.id).await? }))
540 }
541 "query_documents" => {
542 let params: DocumentQueryParams = decode(method, params)?;
543 encode(source.query_documents(¶ms.query, ¶ms.page).await?)
544 }
545 "write_document" => {
546 let params: DocumentWriteParams = decode(method, params)?;
547 encode(json!({ "id": source.write_document(¶ms.write).await? }))
548 }
549 "delete_document" => {
550 let params: DeleteParams = decode(method, params)?;
551 source.delete_document(¶ms.id).await?;
552 encode(json!({}))
553 }
554 "task_comments" => {
555 let params: CommentsParams = decode(method, params)?;
556 encode(json!({ "page": source.task_comments(¶ms.task, ¶ms.page).await? }))
557 }
558 "add_comment" => {
559 let params: AddCommentParams = decode(method, params)?;
560 encode(json!({ "comment": source.add_comment(¶ms.task, ¶ms.comment).await? }))
561 }
562 "edit_comment" => {
563 let params: EditCommentParams = decode(method, params)?;
564 encode(json!({
565 "comment": source
566 .edit_comment(¶ms.task, ¶ms.comment, ¶ms.body)
567 .await?
568 }))
569 }
570 "delete_comment" => {
571 let params: DeleteCommentParams = decode(method, params)?;
572 encode(json!({
573 "deleted": source.delete_comment(¶ms.task, ¶ms.comment).await?
574 }))
575 }
576 "metering" => encode(json!({ "metering": source.metering().await? })),
577 other => Err(SourceError::Malformed {
578 message: format!("protocol version {PROTOCOL_VERSION} has no method called {other:?}"),
579 }),
580 }
581}
582
583fn decode<T: for<'de> Deserialize<'de>>(method: &str, params: Value) -> Result<T, SourceError> {
585 serde_json::from_value(params).map_err(|error| SourceError::Malformed {
586 message: format!("the parameters of {method} are not the shape it takes: {error}"),
587 })
588}
589
590fn encode<T: serde::Serialize>(value: T) -> Result<Value, SourceError> {
592 serde_json::to_value(value).map_err(|error| SourceError::Malformed {
593 message: format!("this source returned data that will not serialize: {error}"),
594 })
595}