1use crate::locator_error::PluginLocatorError;
2use serde::{Deserialize, Serialize};
3use std::fmt::{self, Debug, Display};
4use std::path::PathBuf;
5use std::str::FromStr;
6
7#[derive(Clone, Default, Eq, PartialEq)]
9pub struct DataLocator {
10 pub data: String,
12
13 pub bytes: Option<Vec<u8>>,
16}
17
18impl Display for DataLocator {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 if self.data.starts_with("data://") {
21 write!(f, "{}", self.data)
22 } else {
23 write!(f, "data://{}", self.data)
24 }
25 }
26}
27
28impl Debug for DataLocator {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let mut data = self.data.chars().take(39).collect::<String>();
32
33 if self.data.len() > 39 {
34 data.push_str("...");
35 }
36
37 f.debug_struct("DataLocator").field("data", &data).finish()
38 }
39}
40
41#[derive(Clone, Debug, Default, Eq, PartialEq)]
43pub struct FileLocator {
44 pub file: String,
46
47 pub path: Option<PathBuf>,
50}
51
52#[cfg(not(target_arch = "wasm32"))]
53impl FileLocator {
54 pub fn get_unresolved_path(&self) -> PathBuf {
57 PathBuf::from(self.file.strip_prefix("file://").unwrap_or(&self.file))
58 }
59
60 pub fn get_resolved_path(&self) -> PathBuf {
63 let mut path = self
64 .path
65 .clone()
66 .unwrap_or_else(|| self.get_unresolved_path());
67
68 if !path.is_absolute() {
69 path = std::env::current_dir()
70 .expect("Could not determine working directory!")
71 .join(path);
72 }
73
74 path
75 }
76}
77
78impl Display for FileLocator {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 if self.file.starts_with("file://") {
81 write!(f, "{}", self.file)
82 } else {
83 write!(f, "file://{}", self.file)
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default, Eq, PartialEq)]
90pub struct GitHubLocator {
91 pub repo_slug: String,
93
94 pub tag: Option<String>,
96
97 pub project_name: Option<String>,
99}
100
101impl Display for GitHubLocator {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(
104 f,
105 "github://{}{}{}",
106 self.repo_slug,
107 self.project_name
108 .as_deref()
109 .map(|n| format!("/{n}"))
110 .unwrap_or_default(),
111 self.tag
112 .as_deref()
113 .map(|t| format!("@{t}"))
114 .unwrap_or_default()
115 )
116 }
117}
118
119#[derive(Clone, Debug, Default, Eq, PartialEq)]
121pub struct UrlLocator {
122 pub url: String,
124}
125
126impl Display for UrlLocator {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(f, "{}", self.url)
129 }
130}
131
132#[derive(Clone, Debug, Default, Eq, PartialEq)]
134pub struct RegistryLocator {
135 pub registry: Option<String>,
137
138 pub namespace: Option<String>,
140
141 pub image: String,
143
144 pub tag: Option<String>,
146}
147
148impl Display for RegistryLocator {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(
151 f,
152 "registry://{}:{}",
153 vec![
154 self.registry.clone(),
155 self.namespace.clone(),
156 Some(self.image.clone())
157 ]
158 .into_iter()
159 .flatten()
160 .collect::<Vec<_>>()
161 .join("/"),
162 self.tag.as_deref().unwrap_or("latest")
163 )
164 }
165}
166
167#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
169#[serde(untagged, into = "String", try_from = "String")]
170pub enum PluginLocator {
171 Data(Box<DataLocator>),
173
174 File(Box<FileLocator>),
177
178 GitHub(Box<GitHubLocator>),
182
183 Url(Box<UrlLocator>),
185
186 Registry(Box<RegistryLocator>),
189}
190
191#[cfg(feature = "schematic")]
192impl schematic::Schematic for PluginLocator {
193 fn schema_name() -> Option<String> {
194 Some("PluginLocator".into())
195 }
196
197 fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
198 schema.set_description("Strategies and protocols for locating plugins.");
199 schema.string_default()
200 }
201}
202
203impl Display for PluginLocator {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 match self {
206 PluginLocator::Data(data) => write!(f, "{data}"),
207 PluginLocator::File(file) => write!(f, "{file}"),
208 PluginLocator::Url(url) => write!(f, "{url}"),
209 PluginLocator::GitHub(github) => write!(f, "{github}"),
210 PluginLocator::Registry(registry) => write!(f, "{registry}"),
211 }
212 }
213}
214
215impl FromStr for PluginLocator {
216 type Err = PluginLocatorError;
217
218 fn from_str(value: &str) -> Result<Self, Self::Err> {
219 PluginLocator::try_from(value.to_owned())
220 }
221}
222
223impl TryFrom<String> for PluginLocator {
224 type Error = PluginLocatorError;
225
226 fn try_from(value: String) -> Result<Self, Self::Error> {
227 if let Some(source) = value.strip_prefix("source:") {
229 if source.starts_with("http") {
230 return Self::try_from(source.to_owned());
231 } else {
232 return Self::try_from(format!("file://{source}"));
233 }
234 } else if value.starts_with("github:") && !value.contains("//") {
235 return Self::try_from(format!("github://{}", &value[7..]));
236 }
237
238 if !value.contains("://") {
239 return Err(PluginLocatorError::MissingProtocol);
240 }
241
242 let mut parts = value.splitn(2, "://");
243
244 let Some(protocol) = parts.next() else {
245 return Err(PluginLocatorError::MissingProtocol);
246 };
247
248 let Some(location) = parts.next() else {
249 return Err(PluginLocatorError::MissingLocation);
250 };
251
252 if location.is_empty() {
253 return Err(PluginLocatorError::MissingLocation);
254 }
255
256 match protocol {
257 "data" => Ok(PluginLocator::Data(Box::new(DataLocator {
258 data: value,
259 bytes: None,
260 }))),
261 "file" => Ok(PluginLocator::File(Box::new(FileLocator {
262 file: value,
263 path: None,
264 }))),
265 "github" => {
266 if !location.contains('/') {
267 return Err(PluginLocatorError::MissingGitHubOrg);
268 }
269
270 let mut github = GitHubLocator::default();
271 let mut query = location;
272
273 if let Some(index) = query.find('@') {
274 github.tag = Some(query[index + 1..].into());
275 query = &query[0..index];
276 }
277
278 let mut parts = query.split('/');
279 let org = parts.next().unwrap_or_default().to_owned();
280 let repo = parts.next().unwrap_or_default().to_owned();
281 let prefix = parts.next().map(|f| f.to_owned());
282
283 github.project_name = prefix;
284 github.repo_slug = format!("{org}/{repo}");
285
286 Ok(PluginLocator::GitHub(Box::new(github)))
287 }
288 "http" => Err(PluginLocatorError::SecureUrlsOnly),
289 "https" => Ok(PluginLocator::Url(Box::new(UrlLocator { url: value }))),
290 "registry" => {
291 let mut registry = RegistryLocator::default();
292 let mut query = location;
293
294 if let Some(index) = query.find(":") {
295 registry.tag = Some(query[index + 1..].into());
296 query = &query[0..index];
297 }
298
299 if let Some(index) = query.find("/") {
300 let inner = &query[0..index];
301
302 if inner.contains('.') {
304 registry.registry = Some(inner.into());
305 query = &query[index + 1..];
306 }
307 }
308
309 if let Some(index) = query.rfind('/') {
310 registry.image = query[index + 1..].into();
311 query = &query[0..index];
312 } else {
313 registry.image = query.into();
314 query = &query[0..0];
315 }
316
317 if !query.is_empty() {
318 registry.namespace = Some(query.into());
319 }
320
321 if registry.image.is_empty() {
322 return Err(PluginLocatorError::MissingRegistryImage);
323 }
324
325 Ok(PluginLocator::Registry(Box::new(registry)))
326 }
327 unknown => Err(PluginLocatorError::UnknownProtocol(unknown.to_owned())),
328 }
329 }
330}
331
332impl From<PluginLocator> for String {
333 fn from(locator: PluginLocator) -> Self {
334 locator.to_string()
335 }
336}
337
338impl AsRef<PluginLocator> for PluginLocator {
339 fn as_ref(&self) -> &Self {
340 self
341 }
342}