1use crate::locator_error::PluginLocatorError;
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4use std::path::PathBuf;
5use std::str::FromStr;
6
7#[derive(Clone, Debug, Default, Eq, PartialEq)]
9pub struct FileLocator {
10 pub file: String,
12
13 pub path: Option<PathBuf>,
16}
17
18#[cfg(not(target_arch = "wasm32"))]
19impl FileLocator {
20 pub fn get_unresolved_path(&self) -> PathBuf {
21 PathBuf::from(self.file.strip_prefix("file://").unwrap_or(&self.file))
22 }
23
24 pub fn get_resolved_path(&self) -> PathBuf {
25 let mut path = self
26 .path
27 .clone()
28 .unwrap_or_else(|| self.get_unresolved_path());
29
30 if !path.is_absolute() {
31 path = std::env::current_dir()
32 .expect("Could not determine working directory!")
33 .join(path);
34 }
35
36 path
37 }
38}
39
40#[derive(Clone, Debug, Default, Eq, PartialEq)]
42pub struct GitHubLocator {
43 pub repo_slug: String,
45
46 pub tag: Option<String>,
48
49 pub project_name: Option<String>,
51}
52
53#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct UrlLocator {
56 pub url: String,
58}
59
60#[derive(Clone, Debug, Default, Eq, PartialEq)]
62pub struct RegistryLocator {
63 pub registry: Option<String>,
65
66 pub namespace: Option<String>,
68
69 pub image: String,
71
72 pub tag: Option<String>,
74}
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78#[serde(untagged, into = "String", try_from = "String")]
79pub enum PluginLocator {
80 File(Box<FileLocator>),
83
84 GitHub(Box<GitHubLocator>),
88
89 Url(Box<UrlLocator>),
91
92 Registry(Box<RegistryLocator>),
95}
96
97#[cfg(feature = "schematic")]
98impl schematic::Schematic for PluginLocator {
99 fn schema_name() -> Option<String> {
100 Some("PluginLocator".into())
101 }
102
103 fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
104 schema.set_description("Strategies and protocols for locating plugins.");
105 schema.string_default()
106 }
107}
108
109impl Display for PluginLocator {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 PluginLocator::File(file) => {
113 if file.file.starts_with("file://") {
114 write!(f, "{}", file.file)
115 } else {
116 write!(f, "file://{}", file.file)
117 }
118 }
119 PluginLocator::Url(url) => write!(f, "{}", url.url),
120 PluginLocator::GitHub(github) => write!(
121 f,
122 "github://{}{}{}",
123 github.repo_slug,
124 github
125 .project_name
126 .as_deref()
127 .map(|n| format!("/{n}"))
128 .unwrap_or_default(),
129 github
130 .tag
131 .as_deref()
132 .map(|t| format!("@{t}"))
133 .unwrap_or_default()
134 ),
135 PluginLocator::Registry(registry) => write!(
136 f,
137 "registry://{}:{}",
138 vec![
139 registry.registry.clone(),
140 registry.namespace.clone(),
141 Some(registry.image.clone())
142 ]
143 .into_iter()
144 .flatten()
145 .collect::<Vec<_>>()
146 .join("/"),
147 registry.tag.as_deref().unwrap_or("latest")
148 ),
149 }
150 }
151}
152
153impl FromStr for PluginLocator {
154 type Err = PluginLocatorError;
155
156 fn from_str(value: &str) -> Result<Self, Self::Err> {
157 PluginLocator::try_from(value.to_owned())
158 }
159}
160
161impl TryFrom<String> for PluginLocator {
162 type Error = PluginLocatorError;
163
164 fn try_from(value: String) -> Result<Self, Self::Error> {
165 if let Some(source) = value.strip_prefix("source:") {
167 if source.starts_with("http") {
168 return Self::try_from(source.to_owned());
169 } else {
170 return Self::try_from(format!("file://{source}"));
171 }
172 } else if value.starts_with("github:") && !value.contains("//") {
173 return Self::try_from(format!("github://{}", &value[7..]));
174 }
175
176 if !value.contains("://") {
177 return Err(PluginLocatorError::MissingProtocol);
178 }
179
180 let mut parts = value.splitn(2, "://");
181
182 let Some(protocol) = parts.next() else {
183 return Err(PluginLocatorError::MissingProtocol);
184 };
185
186 let Some(location) = parts.next() else {
187 return Err(PluginLocatorError::MissingLocation);
188 };
189
190 if location.is_empty() {
191 return Err(PluginLocatorError::MissingLocation);
192 }
193
194 match protocol {
195 "file" => Ok(PluginLocator::File(Box::new(FileLocator {
196 file: value,
197 path: None,
198 }))),
199 "github" => {
200 if !location.contains('/') {
201 return Err(PluginLocatorError::MissingGitHubOrg);
202 }
203
204 let mut github = GitHubLocator::default();
205 let mut query = location;
206
207 if let Some(index) = query.find('@') {
208 github.tag = Some(query[index + 1..].into());
209 query = &query[0..index];
210 }
211
212 let mut parts = query.split('/');
213 let org = parts.next().unwrap_or_default().to_owned();
214 let repo = parts.next().unwrap_or_default().to_owned();
215 let prefix = parts.next().map(|f| f.to_owned());
216
217 github.project_name = prefix;
218 github.repo_slug = format!("{org}/{repo}");
219
220 Ok(PluginLocator::GitHub(Box::new(github)))
221 }
222 "http" => Err(PluginLocatorError::SecureUrlsOnly),
223 "https" => Ok(PluginLocator::Url(Box::new(UrlLocator { url: value }))),
224 "registry" => {
225 let mut registry = RegistryLocator::default();
226 let mut query = location;
227
228 if let Some(index) = query.find(":") {
229 registry.tag = Some(query[index + 1..].into());
230 query = &query[0..index];
231 }
232
233 if let Some(index) = query.find("/") {
234 let inner = &query[0..index];
235
236 if inner.contains('.') {
238 registry.registry = Some(inner.into());
239 query = &query[index + 1..];
240 }
241 }
242
243 if let Some(index) = query.rfind('/') {
244 registry.image = query[index + 1..].into();
245 query = &query[0..index];
246 } else {
247 registry.image = query.into();
248 query = &query[0..0];
249 }
250
251 if !query.is_empty() {
252 registry.namespace = Some(query.into());
253 }
254
255 if registry.image.is_empty() {
256 return Err(PluginLocatorError::MissingRegistryImage);
257 }
258
259 Ok(PluginLocator::Registry(Box::new(registry)))
260 }
261 unknown => Err(PluginLocatorError::UnknownProtocol(unknown.to_owned())),
262 }
263 }
264}
265
266impl From<PluginLocator> for String {
267 fn from(locator: PluginLocator) -> Self {
268 locator.to_string()
269 }
270}
271
272impl AsRef<PluginLocator> for PluginLocator {
273 fn as_ref(&self) -> &Self {
274 self
275 }
276}