brack_plugin_manager/
add_plugin.rs1use 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}