brack_plugin_manager/
add_plugin.rs

1use core::fmt;
2use std::{collections::HashMap, fs::File, io, path::Path};
3
4use anyhow::Result;
5use serde::{
6    de::{self, MapAccess, Visitor},
7    ser::SerializeStruct,
8    Deserialize, Deserializer, Serialize, Serializer,
9};
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct Config {
13    pub document: Document,
14    pub plugins: Option<HashMap<String, Plugin>>,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct Document {
19    pub name: String,
20    pub version: String,
21    pub backend: String,
22}
23
24#[derive(Debug)]
25pub enum Plugin {
26    GitHub {
27        owner: String,
28        repo: String,
29        version: String,
30    },
31}
32
33impl Serialize for Plugin {
34    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35    where
36        S: Serializer,
37    {
38        let mut s = serializer.serialize_struct("Plugin", 4)?;
39        match *self {
40            Plugin::GitHub {
41                ref owner,
42                ref repo,
43                ref version,
44            } => {
45                s.serialize_field("schema", "github")?;
46                s.serialize_field("owner", owner)?;
47                s.serialize_field("repo", repo)?;
48                s.serialize_field("version", version)?;
49            }
50        }
51        s.end()
52    }
53}
54
55impl<'de> Deserialize<'de> for Plugin {
56    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57    where
58        D: Deserializer<'de>,
59    {
60        struct PluginVisitor;
61
62        impl<'de> Visitor<'de> for PluginVisitor {
63            type Value = Plugin;
64
65            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
66                formatter.write_str("struct Plugin")
67            }
68
69            fn visit_map<V>(self, mut map: V) -> Result<Plugin, V::Error>
70            where
71                V: MapAccess<'de>,
72            {
73                let mut schema = None;
74                let mut owner = None;
75                let mut repo = None;
76                let mut version = None;
77
78                while let Some(key) = map.next_key::<String>()? {
79                    match key.as_str() {
80                        "schema" => {
81                            if schema.is_some() {
82                                return Err(de::Error::duplicate_field("schema"));
83                            }
84                            schema = Some(map.next_value()?);
85                        }
86                        "owner" => {
87                            if owner.is_some() {
88                                return Err(de::Error::duplicate_field("owner"));
89                            }
90                            owner = Some(map.next_value()?);
91                        }
92                        "repo" => {
93                            if repo.is_some() {
94                                return Err(de::Error::duplicate_field("repo"));
95                            }
96                            repo = Some(map.next_value()?);
97                        }
98                        "version" => {
99                            if version.is_some() {
100                                return Err(de::Error::duplicate_field("version"));
101                            }
102                            version = Some(map.next_value()?);
103                        }
104                        _ => return Err(de::Error::unknown_field(&key, FIELDS)),
105                    }
106                }
107
108                let schema: String = schema.ok_or_else(|| de::Error::missing_field("schema"))?;
109                let owner: String = owner.ok_or_else(|| de::Error::missing_field("owner"))?;
110                let repo: String = repo.ok_or_else(|| de::Error::missing_field("repo"))?;
111                let version: String = version.ok_or_else(|| de::Error::missing_field("version"))?;
112
113                match schema.as_str() {
114                    "github" => Ok(Plugin::GitHub {
115                        owner,
116                        repo,
117                        version,
118                    }),
119                    _ => Err(de::Error::invalid_value(
120                        de::Unexpected::Str(&schema),
121                        &"github",
122                    )),
123                }
124            }
125        }
126
127        const FIELDS: &'static [&'static str] = &["schema", "owner", "repo", "version"];
128        deserializer.deserialize_struct("Plugin", FIELDS, PluginVisitor)
129    }
130}
131
132fn check_existence_brack_toml() -> bool {
133    let path = Path::new("Brack.toml");
134    path.exists()
135}
136
137pub async fn add_plugin(schema: &str) -> Result<()> {
138    if !check_existence_brack_toml() {
139        return Err(anyhow::anyhow!("Brack.toml is not found."));
140    }
141
142    let repository_type = schema
143        .split(':')
144        .nth(0)
145        .ok_or_else(|| anyhow::anyhow!("Repository type is not found."))?;
146    let owner = schema
147        .split('/')
148        .nth(0)
149        .ok_or_else(|| anyhow::anyhow!("Owner is not found."))?
150        .split(':')
151        .nth(1)
152        .ok_or_else(|| anyhow::anyhow!("Owner is not found."))?;
153    let repo = schema
154        .split('/')
155        .nth(1)
156        .ok_or_else(|| anyhow::anyhow!("Repository is not found."))?
157        .split('@')
158        .nth(0)
159        .ok_or_else(|| anyhow::anyhow!("Repository is not found."))?;
160    let version = schema
161        .split('@')
162        .nth(1)
163        .ok_or_else(|| anyhow::anyhow!("Version is not found."))?;
164    let url = match repository_type {
165        "github" => format!("https://github.com/{}/{}", owner, repo),
166        _ => {
167            return Err(anyhow::anyhow!(
168                "Repository type '{}' is not supported.",
169                repository_type
170            ))
171        }
172    };
173
174    let mut config: Config = toml::from_str(&std::fs::read_to_string("Brack.toml")?)?;
175
176    let repository_name = url
177        .trim_end_matches('/')
178        .split('/')
179        .last()
180        .ok_or_else(|| anyhow::anyhow!("Last element of URL is not found."))?;
181    let plugin_name = repository_name
182        .split('.')
183        .next()
184        .ok_or_else(|| anyhow::anyhow!("First element of repository name is not found."))?;
185
186    if let Some(_) = config
187        .plugins
188        .as_ref()
189        .and_then(|plugins| plugins.get(plugin_name))
190    {
191        return Err(anyhow::anyhow!("Plugin already exists."));
192    }
193
194    let download_url = format!(
195        "{}/releases/download/{}/{}.wasm",
196        url, version, repository_name
197    );
198
199    let response = reqwest::get(&download_url).await?;
200
201    if !response.status().is_success() {
202        return Err(anyhow::anyhow!(
203            "Failed to download plugin from {}.\nStatus: {} - {}",
204            download_url,
205            response.status().as_str(),
206            response
207                .status()
208                .canonical_reason()
209                .unwrap_or("Unknown error")
210        ));
211    }
212
213    let bytes = response.bytes().await?;
214    std::fs::create_dir_all("plugins")?;
215    let mut out = File::create(format!("plugins/{}.wasm", repository_name))?;
216    io::copy(&mut bytes.as_ref(), &mut out)?;
217
218    config.plugins.get_or_insert_with(|| HashMap::new()).insert(
219        plugin_name.to_string(),
220        Plugin::GitHub {
221            owner: owner.to_string(),
222            repo: repo.to_string(),
223            version: version.to_string(),
224        },
225    );
226    let toml = toml::to_string(&config)?;
227    std::fs::write("Brack.toml", toml)?;
228
229    Ok(())
230}