Skip to main content

onetaskgraph_core/
registry.rs

1//! The compile-time registry of plugin kinds.
2//!
3//! Every plugin this binary can build is named here, whether or not its source is
4//! implemented yet. That is deliberate: a configuration naming `linear` gets the
5//! plugin's own "not implemented yet" message rather than "unknown plugin", and
6//! landing the real source is an additive change to that one crate with no edit
7//! here.
8
9use std::{fmt, str::FromStr};
10
11use onetaskgraph_plugin_api::SourcePlugin;
12use serde::{Deserialize, Serialize};
13
14/// One of the plugin kinds this build has.
15///
16/// A [`SourceConfig`](crate::SourceConfig) holds one of these rather than the string a
17/// document spelled, so a configuration naming a plugin nothing answers to cannot exist
18/// past [`Config::from_document`](crate::Config::from_document). Resolution therefore has
19/// no "what if the registry does not have it" branch left to get wrong, and the refusal
20/// happens at the one place that can name the offending key.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22#[serde(into = "String", try_from = "String")]
23pub enum PluginKind {
24    /// GitHub Projects.
25    GithubProjects,
26    /// The in-memory source the journeys are written against.
27    InMemory,
28    /// Linear.
29    Linear,
30    /// A folder of Markdown files.
31    LocalMd,
32    /// A program of its own, speaking `docs/plugin-protocol.md` over stdio.
33    Subprocess,
34}
35
36impl PluginKind {
37    /// Every kind, in the stable order [`registry`] reports them in.
38    pub const ALL: [Self; 5] = [
39        Self::GithubProjects,
40        Self::InMemory,
41        Self::Linear,
42        Self::LocalMd,
43        Self::Subprocess,
44    ];
45
46    /// The name a configuration document's `plugin:` field names this kind by.
47    ///
48    /// Spelled here rather than read from the plugin so that matching a name costs no
49    /// allocation. `every_plugin_kind_names_the_kind_its_own_plugin_reports` is what
50    /// keeps the two from drifting.
51    #[must_use]
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::GithubProjects => "github-projects",
55            Self::InMemory => "in-memory",
56            Self::Linear => "linear",
57            Self::LocalMd => "local-md",
58            Self::Subprocess => "subprocess",
59        }
60    }
61
62    /// The kind called `name`, or `None` when nothing in this build answers to it.
63    #[must_use]
64    pub fn parse(name: &str) -> Option<Self> {
65        Self::ALL.into_iter().find(|kind| kind.as_str() == name)
66    }
67
68    /// This kind's factory.
69    ///
70    /// Total, which is the point of the type: a `PluginKind` is one of exactly five
71    /// things, so there is no absent-plugin case for a caller to handle or forget.
72    #[must_use]
73    pub fn plugin(self) -> Box<dyn SourcePlugin> {
74        match self {
75            Self::GithubProjects => Box::new(onetaskgraph_github_projects::Plugin),
76            Self::InMemory => Box::new(onetaskgraph_in_memory::Plugin),
77            Self::Linear => Box::new(onetaskgraph_linear::Plugin),
78            Self::LocalMd => Box::new(onetaskgraph_local_md::Plugin),
79            Self::Subprocess => Box::new(crate::subprocess::SubprocessPlugin),
80        }
81    }
82}
83
84impl TryFrom<String> for PluginKind {
85    type Error = String;
86
87    /// Read a kind this build has, and refuse one it does not — naming what it does have.
88    ///
89    /// Where a document or a protocol message carries a plugin kind, this is what keeps
90    /// "a kind" and "a kind this binary can build" the same thing: an unknown name stops
91    /// being representable at the field rather than at a lookup somewhere later.
92    fn try_from(value: String) -> Result<Self, Self::Error> {
93        Self::parse(&value).ok_or_else(|| {
94            format!(
95                "no plugin of this build is called {value:?}; it knows {}",
96                plugin_kinds().join(", ")
97            )
98        })
99    }
100}
101
102impl From<PluginKind> for String {
103    fn from(value: PluginKind) -> Self {
104        value.as_str().to_owned()
105    }
106}
107
108impl fmt::Display for PluginKind {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.as_str())
111    }
112}
113
114impl FromStr for PluginKind {
115    type Err = String;
116
117    fn from_str(value: &str) -> Result<Self, Self::Err> {
118        value.to_owned().try_into()
119    }
120}
121
122/// Every plugin kind this build knows, in a stable order.
123#[must_use]
124pub fn registry() -> Vec<Box<dyn SourcePlugin>> {
125    PluginKind::ALL.map(PluginKind::plugin).into()
126}
127
128/// The kind names in [`registry`], for help text and error messages.
129#[must_use]
130pub fn plugin_kinds() -> Vec<&'static str> {
131    registry().iter().map(|plugin| plugin.kind()).collect()
132}
133
134/// The plugin registered for `kind`, or `None` when nothing answers to that name.
135#[must_use]
136pub fn plugin_for(kind: &str) -> Option<Box<dyn SourcePlugin>> {
137    PluginKind::parse(kind).map(PluginKind::plugin)
138}