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