1
//! Shows how to utilize the built-in CLI helpers.
2

            
3
use std::convert::Infallible;
4

            
5
use bonsaidb::cli::CommandLine;
6
use bonsaidb::core::actionable::async_trait;
7
use bonsaidb::core::connection::AsyncStorageConnection;
8
use bonsaidb::core::schema::{SerializedCollection, SerializedView};
9
use bonsaidb::local::config::Builder;
10
use bonsaidb::server::{
11
    Backend, BackendError, CustomServer, DefaultPermissions, ServerConfiguration,
12
};
13
use bonsaidb::AnyServerConnection;
14
use clap::Subcommand;
15

            
16
mod support;
17
use support::schema::{Shape, ShapesByNumberOfSides};
18

            
19
#[tokio::main]
20
async fn main() -> anyhow::Result<()> {
21
    env_logger::init();
22
    CliBackend.run().await
23
}
24

            
25
6
#[derive(Debug, Default)]
26
struct CliBackend;
27

            
28
15
#[derive(Debug, Subcommand)]
29
enum Cli {
30
    Add { sides: u32 },
31
    Count { sides: Option<u32> },
32
}
33

            
34
#[async_trait]
35
impl Backend for CliBackend {
36
    type ClientData = ();
37
    type Error = Infallible;
38

            
39
6
    async fn initialize(&self, server: &CustomServer<Self>) -> Result<(), BackendError> {
40
12
        server.create_database::<Shape>("shapes", true).await?;
41
6
        Ok(())
42
12
    }
43
}
44

            
45
#[async_trait]
46
impl CommandLine for CliBackend {
47
    type Backend = Self;
48
    type Subcommand = Cli;
49

            
50
6
    async fn configuration(&mut self) -> anyhow::Result<ServerConfiguration<CliBackend>> {
51
6
        Ok(ServerConfiguration::new("cli.bonsaidb")
52
6
            .default_permissions(DefaultPermissions::AllowAll)
53
6
            .with_schema::<Shape>()?)
54
12
    }
55

            
56
3
    async fn execute(
57
3
        &mut self,
58
3
        command: Self::Subcommand,
59
3
        connection: AnyServerConnection<Self>,
60
3
    ) -> anyhow::Result<()> {
61
3
        let database = connection.database::<Shape>("shapes").await?;
62
3
        match command {
63
2
            Cli::Add { sides } => {
64
2
                let new_shape = Shape::new(sides).push_into_async(&database).await?;
65
2
                println!(
66
2
                    "Shape #{} inserted with {} sides",
67
2
                    new_shape.header.id, sides
68
2
                );
69
            }
70
1
            Cli::Count { sides } => {
71
1
                if let Some(sides) = sides {
72
                    let count = ShapesByNumberOfSides::entries_async(&database)
73
                        .with_key(&sides)
74
                        .reduce()
75
                        .await?;
76
                    println!("Found {count} shapes with {sides} sides");
77
1
                } else {
78
1
                    let count = ShapesByNumberOfSides::entries_async(&database)
79
1
                        .reduce()
80
1
                        .await?;
81
1
                    println!("Found {count} shapes with any number of sides");
82
                }
83
            }
84
        }
85
3
        Ok(())
86
6
    }
87
}
88

            
89
1
#[tokio::test]
90
1
async fn test() -> anyhow::Result<()> {
91
1
    if std::path::Path::new("cli.bonsaidb").exists() {
92
        tokio::fs::remove_dir_all("cli.bonsaidb").await?;
93
1
    }
94
1
    if std::path::Path::new("cli-backup").exists() {
95
        tokio::fs::remove_dir_all("cli-backup").await?;
96
1
    }
97

            
98
1
    CliBackend
99
1
        .run_from(["executable", "certificate", "install-self-signed"])
100
15
        .await?;
101

            
102
    // Execute a command locally (no server running).
103
7
    CliBackend.run_from(["executable", "add", "3"]).await?;
104

            
105
    // Spawn the server so that we can communicate with it over the network. We
106
    // need to be able to shut this server down, but since we're launching it
107
    // through a command-line interface, we have no way to call the shutdown()
108
    // function. Instead, we'll spawn a thread to run its own runtime, which
109
    // allows us to fully control the task cleanup as the runtime is cleaned up.
110
    // This ensures all file locks are dropped.
111
1
    let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel();
112
1
    std::thread::spawn(move || {
113
1
        let rt = tokio::runtime::Runtime::new().unwrap();
114
1
        rt.spawn(async {
115
1
            CliBackend
116
1
                .run_from(["executable", "serve", "--listen-on", "[::1]:6004"])
117
7
                .await
118
                .unwrap();
119
1
        });
120
1
        rt.block_on(async {
121
1
            drop(shutdown_receiver.await);
122
1
        });
123
1
    });
124
1

            
125
1
    tokio::time::sleep(std::time::Duration::from_millis(250)).await;
126

            
127
    // Execute the same command as before, but this time use the network.
128
1
    CliBackend
129
1
        .run_from([
130
1
            "executable",
131
1
            "--url",
132
1
            "bonsaidb://localhost:6004",
133
1
            "--pinned-certificate",
134
1
            "cli.bonsaidb/pinned-certificate.der",
135
1
            "add",
136
1
            "3",
137
1
        ])
138
2
        .await
139
1
        .unwrap();
140
1

            
141
1
    CliBackend
142
1
        .run_from([
143
1
            "executable",
144
1
            "--url",
145
1
            "bonsaidb://localhost:6004",
146
1
            "--pinned-certificate",
147
1
            "cli.bonsaidb/pinned-certificate.der",
148
1
            "count",
149
1
        ])
150
2
        .await
151
1
        .unwrap();
152
1

            
153
1
    // Close the server
154
1
    shutdown_sender.send(()).unwrap();
155
1

            
156
1
    // Back up the database
157
1
    CliBackend
158
1
        .run_from(["executable", "backup", "path", "cli-backup"])
159
6
        .await?;
160

            
161
    // Remove the database and restore from backup.
162
1
    tokio::fs::remove_dir_all("cli.bonsaidb").await?;
163

            
164
    // Restore the database
165
1
    CliBackend
166
1
        .run_from(["executable", "restore", "path", "cli-backup"])
167
6
        .await?;
168

            
169
    // Re-open the database and verify the operations were made.
170
    {
171
5
        let server = CliBackend.open_server().await?;
172
1
        let database = server.database::<Shape>("shapes").await?;
173
1
        assert_eq!(
174
1
            ShapesByNumberOfSides::entries_async(&database)
175
1
                .reduce()
176
1
                .await?,
177
            2
178
        );
179
    }
180
1
    tokio::fs::remove_dir_all("cli-backup").await?;
181

            
182
1
    Ok(())
183
}