Skip to main content

command_stream/
command_packages.rs

1//! Typed command packages, middleware and reloadable exact-argv manifests.
2use crate::commands::{CommandContext, VirtualCommandRegistry};
3use crate::{CommandBuilder, CommandResult, StdinOption};
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::future::Future;
7use std::path::Path;
8use std::pin::Pin;
9use std::sync::Arc;
10
11pub type HandlerFuture = Pin<Box<dyn Future<Output = CommandResult> + Send>>;
12pub type PackageHandler = Arc<dyn Fn(CommandContext) -> HandlerFuture + Send + Sync>;
13
14#[derive(Clone)]
15pub struct CommandPackage {
16    pub name: String,
17    pub version: String,
18    pub commands: HashMap<String, PackageHandler>,
19}
20
21#[derive(Deserialize)]
22struct Manifest {
23    name: String,
24    version: String,
25    commands: HashMap<String, Specification>,
26}
27#[derive(Deserialize)]
28struct Specification {
29    file: String,
30    #[serde(default)]
31    args: Vec<String>,
32}
33
34impl CommandPackage {
35    /// Loading a JSON manifest can be repeated after a file edit, without unsafe dynamic linking.
36    pub async fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
37        let manifest: Manifest = serde_json::from_str(&tokio::fs::read_to_string(path).await?)?;
38        let commands = manifest
39            .commands
40            .into_iter()
41            .map(|(name, specification)| {
42                let command = CommandBuilder::new(specification.file).args(specification.args);
43                let handler: PackageHandler = Arc::new(move |context| {
44                    let mut command = command
45                        .clone()
46                        .args(context.args)
47                        .stdin(StdinOption::Content(context.stdin.unwrap_or_default()));
48                    if let Some(cwd) = context.cwd {
49                        command = command.cwd(cwd);
50                    }
51                    if let Some(env) = context.env {
52                        for (key, value) in env {
53                            command = command.env(key, value);
54                        }
55                    }
56                    Box::pin(async move {
57                        command.run().await.unwrap_or_else(|error| {
58                            CommandResult::error_with_code(error.to_string(), 1)
59                        })
60                    })
61                });
62                (name, handler)
63            })
64            .collect();
65        Ok(Self {
66            name: manifest.name,
67            version: manifest.version,
68            commands,
69        })
70    }
71}
72
73#[derive(Default)]
74pub struct CommandEnvironment {
75    commands: HashMap<String, PackageHandler>,
76    owners: HashMap<String, String>,
77}
78
79impl CommandEnvironment {
80    pub fn new() -> Self {
81        Self::default()
82    }
83    pub fn create<F, Fut>(&mut self, name: &str, handler: F) -> Result<(), String>
84    where
85        F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
86        Fut: Future<Output = CommandResult> + Send + 'static,
87    {
88        validate(name)?;
89        self.owners.remove(name);
90        self.commands.insert(
91            name.into(),
92            Arc::new(move |context| Box::pin(handler(context))),
93        );
94        Ok(())
95    }
96    pub fn list(&self) -> Vec<String> {
97        let mut names = self.commands.keys().cloned().collect::<Vec<_>>();
98        names.sort();
99        names
100    }
101    pub fn get(&self, name: &str) -> Option<PackageHandler> {
102        self.commands.get(name).cloned().or_else(|| {
103            let handler = *VirtualCommandRegistry::with_builtins().get(name)?;
104            Some(Arc::new(handler) as PackageHandler)
105        })
106    }
107    pub async fn run(&self, name: &str, context: CommandContext) -> CommandResult {
108        match self.get(name) {
109            Some(handler) => handler(context).await,
110            None => CommandResult::error_with_code(format!("Unknown command: {name}"), 127),
111        }
112    }
113    pub fn extend<F>(&mut self, name: &str, middleware: F) -> Result<(), String>
114    where
115        F: Fn(CommandContext, PackageHandler) -> HandlerFuture + Send + Sync + 'static,
116    {
117        let parent = self
118            .get(name)
119            .ok_or_else(|| format!("Unknown command: {name}"))?;
120        self.commands.insert(
121            name.into(),
122            Arc::new(move |context| middleware(context, parent.clone())),
123        );
124        Ok(())
125    }
126    pub fn inherit(&mut self, name: &str, parent: &str, args: Vec<String>) -> Result<(), String> {
127        let handler = self
128            .get(parent)
129            .ok_or_else(|| format!("Unknown command: {parent}"))?;
130        self.create(name, move |mut context| {
131            let mut prefix = args.clone();
132            prefix.extend(context.args);
133            context.args = prefix;
134            handler(context)
135        })
136    }
137    pub fn compose(&mut self, name: &str, names: &[&str]) -> Result<(), String> {
138        if names.is_empty() {
139            return Err("Composition requires commands".into());
140        }
141        let handlers = names
142            .iter()
143            .map(|name| {
144                self.get(name)
145                    .ok_or_else(|| format!("Unknown command: {name}"))
146            })
147            .collect::<Result<Vec<_>, _>>()?;
148        self.create(name, move |context| {
149            let handlers = handlers.clone();
150            Box::pin(async move {
151                let mut result = CommandResult::success(context.stdin.unwrap_or_default());
152                for handler in handlers {
153                    let next = CommandContext {
154                        args: context.args.clone(),
155                        stdin: Some(result.stdout.to_string()),
156                        cwd: context.cwd.clone(),
157                        env: context.env.clone(),
158                        output_tx: context.output_tx.clone(),
159                        is_cancelled: None,
160                    };
161                    result = handler(next).await;
162                    if result.code != 0 {
163                        break;
164                    }
165                }
166                result
167            })
168        })
169    }
170    /// Validate all commands before replacing a package, including when hot reloading.
171    pub fn install(&mut self, package: CommandPackage) -> Result<(), String> {
172        if package.commands.is_empty() {
173            return Err("Empty command package".into());
174        }
175        for name in package.commands.keys() {
176            validate(name)?;
177            if self.commands.contains_key(name) && self.owners.get(name) != Some(&package.name) {
178                return Err(format!("Command already registered: {name}"));
179            }
180        }
181        self.uninstall(&package.name);
182        for (name, handler) in package.commands {
183            self.owners.insert(name.clone(), package.name.clone());
184            self.commands.insert(name, handler);
185        }
186        Ok(())
187    }
188    pub fn uninstall(&mut self, package: &str) {
189        let names = self
190            .owners
191            .iter()
192            .filter(|(_, owner)| owner.as_str() == package)
193            .map(|(name, _)| name.clone())
194            .collect::<Vec<_>>();
195        for name in names {
196            self.owners.remove(&name);
197            self.commands.remove(&name);
198        }
199    }
200}
201
202fn validate(name: &str) -> Result<(), String> {
203    if name.is_empty()
204        || !name
205            .chars()
206            .next()
207            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
208        || !name
209            .chars()
210            .all(|c| c.is_ascii_alphanumeric() || "_:.-".contains(c))
211    {
212        return Err(format!("Invalid command: {name}"));
213    }
214    Ok(())
215}