1
//! BonsaiDb command line tools.
2

            
3
use std::{ffi::OsString, fmt::Debug, marker::PhantomData, path::PathBuf};
4

            
5
use bonsaidb_client::{fabruic::Certificate, Client};
6
use bonsaidb_core::async_trait::async_trait;
7
use bonsaidb_server::{Backend, CustomServer, NoBackend, ServerConfiguration};
8
use clap::{Parser, Subcommand};
9
use url::Url;
10

            
11
use crate::AnyServerConnection;
12

            
13
mod admin;
14

            
15
/// All available command line commands.
16
17
#[derive(Subcommand, Debug)]
17
pub enum Command<Cli: CommandLine> {
18
    /// Executes an admin command.
19
    #[clap(subcommand)]
20
    Admin(admin::Command),
21
    /// Execute a BonsaiDb server command.
22
    #[clap(subcommand)]
23
    Server(bonsaidb_server::cli::Command<Cli::Backend>),
24
    /// An external command.
25
    #[clap(flatten)]
26
    External(Cli::Subcommand),
27
}
28

            
29
impl<Cli> Command<Cli>
30
where
31
    Cli: CommandLine,
32
{
33
    /// Executes the command.
34
    // TODO add client builder insetad of server_url
35
7
    pub async fn execute(
36
7
        self,
37
7
        server_url: Option<Url>,
38
7
        pinned_certificate: Option<Certificate>,
39
7
        mut cli: Cli,
40
7
    ) -> anyhow::Result<()> {
41
7
        match self {
42
4
            Command::Server(server) => {
43
4
                if server_url.is_some() {
44
                    anyhow::bail!("server url provided for local-only command.")
45
4
                }
46
4

            
47
161
                server.execute(cli.configuration().await?).await?;
48
            }
49
3
            other => {
50
3
                let connection = if let Some(server_url) = server_url {
51
2
                    let mut client = Client::build(server_url)
52
2
                        .with_custom_api::<<Cli::Backend as Backend>::CustomApi>();
53

            
54
2
                    if let Some(certificate) = pinned_certificate {
55
2
                        client = client.with_certificate(certificate);
56
2
                    }
57

            
58
2
                    AnyServerConnection::Networked(client.finish().await?)
59
                } else {
60
15
                    AnyServerConnection::Local(cli.open_server().await?)
61
                };
62
3
                match other {
63
                    Command::Admin(admin) => admin.execute(connection).await?,
64
3
                    Command::External(external) => {
65
4
                        cli.execute(external, connection).await?;
66
                    }
67
                    Command::Server(_) => unreachable!(),
68
                }
69
            }
70
        }
71
6
        Ok(())
72
6
    }
73
}
74

            
75
/// The command line interface for `bonsaidb`.
76
7
#[derive(Parser, Debug)]
77
pub struct Args<Cli: CommandLine> {
78
4
    /// A url to a remote server.
79
2
    #[clap(long)]
80
2
    pub url: Option<Url>,
81
4
    /// A pinned certificate to use when connecting to `url`.
82
2
    #[clap(short = 'c', long)]
83
2
    pub pinned_certificate: Option<PathBuf>,
84
    /// The command to execute on the connection specified.
85
    #[clap(subcommand)]
86
    pub command: Command<Cli>,
87
}
88

            
89
impl<Cli: CommandLine> Args<Cli> {
90
    /// Executes the command.
91
7
    pub async fn execute(self, cli: Cli) -> anyhow::Result<()> {
92
7
        let pinned_certificate = if let Some(cert_path) = self.pinned_certificate {
93
2
            let bytes = tokio::fs::read(cert_path).await?;
94
2
            Some(Certificate::from_der(bytes)?)
95
        } else {
96
5
            None
97
        };
98
7
        self.command
99
180
            .execute(self.url, pinned_certificate, cli)
100
180
            .await
101
6
    }
102
}
103

            
104
/// A command line interface that can be executed with either a remote or local
105
/// connection to a server.
106
#[async_trait]
107
pub trait CommandLine: Sized + Send + Sync {
108
    /// The Backend for this command line.
109
    type Backend: Backend;
110
    /// The [`Subcommand`] which is embedded next to the built-in BonsaiDb
111
    /// commands.
112
    type Subcommand: Subcommand + Send + Sync + Debug;
113

            
114
    /// Runs the command-line interface using command-line arguments from the
115
    /// environment.
116
    async fn run(self) -> anyhow::Result<()> {
117
        Args::<Self>::parse().execute(self).await
118
    }
119

            
120
    /// Runs the command-line interface using the specified list of arguments.
121
7
    async fn run_from<I, T>(self, itr: I) -> anyhow::Result<()>
122
7
    where
123
7
        I: IntoIterator<Item = T> + Send,
124
7
        T: Into<OsString> + Clone + Send,
125
7
    {
126
182
        Args::<Self>::parse_from(itr).execute(self).await
127
13
    }
128

            
129
    /// Returns a new server initialized based on the same configuration used
130
    /// for [`CommandLine`].
131
2
    async fn open_server(&mut self) -> anyhow::Result<CustomServer<Self::Backend>> {
132
30
        Ok(CustomServer::<Self::Backend>::open(self.configuration().await?).await?)
133
4
    }
134

            
135
    /// Returns the server configuration to use when initializing a local server.
136
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration>;
137

            
138
    /// Execute the command on `connection`.
139
    async fn execute(
140
        &mut self,
141
        command: Self::Subcommand,
142
        connection: AnyServerConnection<Self::Backend>,
143
    ) -> anyhow::Result<()>;
144
}
145

            
146
#[async_trait]
147
impl CommandLine for NoBackend {
148
    type Backend = Self;
149
    type Subcommand = Self;
150

            
151
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration> {
152
        Ok(ServerConfiguration::default())
153
    }
154

            
155
    async fn execute(
156
        &mut self,
157
        _command: Self::Subcommand,
158
        _connection: AnyServerConnection<Self>,
159
    ) -> anyhow::Result<()> {
160
        unreachable!()
161
    }
162
}
163

            
164
/// Runs the command-line interface with only the built-in commands, using
165
/// `configuration` to launch a server if running a local command.
166
pub async fn run<B: Backend>(configuration: ServerConfiguration) -> anyhow::Result<()> {
167
    Args::parse()
168
        .execute(NoCommandLine::<B> {
169
            configuration: Some(configuration),
170
            _backend: PhantomData,
171
        })
172
        .await
173
}
174

            
175
#[derive(Debug)]
176
struct NoCommandLine<B: Backend> {
177
    configuration: Option<ServerConfiguration>,
178
    _backend: PhantomData<B>,
179
}
180

            
181
#[async_trait]
182
impl<B: Backend> CommandLine for NoCommandLine<B> {
183
    type Backend = B;
184
    type Subcommand = NoBackend;
185

            
186
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration> {
187
        self.configuration
188
            .take()
189
            .ok_or_else(|| anyhow::anyhow!("configuration already consumed"))
190
    }
191

            
192
    async fn execute(
193
        &mut self,
194
        _command: Self::Subcommand,
195
        _connection: AnyServerConnection<B>,
196
    ) -> anyhow::Result<()> {
197
        unreachable!()
198
    }
199
}