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, Deserialize, Eq, PartialEq, Serialize)]
62#[serde(untagged, into = "String", try_from = "String")]
63pub enum PluginLocator {
64 File(Box<FileLocator>),
67
68 GitHub(Box<GitHubLocator>),
72
73 Url(Box<UrlLocator>),
75}
76
77#[cfg(feature = "schematic")]
78impl schematic::Schematic for PluginLocator {
79 fn schema_name() -> Option<String> {
80 Some("PluginLocator".into())
81 }
82
83 fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
84 schema.set_description("Strategies and protocols for locating plugins.");
85 schema.string_default()
86 }
87}
88
89impl Display for PluginLocator {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 PluginLocator::File(file) => {
93 if file.file.starts_with("file://") {
94 write!(f, "{}", file.file)
95 } else {
96 write!(f, "file://{}", file.file)
97 }
98 }
99 PluginLocator::Url(url) => write!(f, "{}", url.url),
100 PluginLocator::GitHub(github) => write!(
101 f,
102 "github://{}{}{}",
103 github.repo_slug,
104 github
105 .project_name
106 .as_deref()
107 .map(|n| format!("/{n}"))
108 .unwrap_or_default(),
109 github
110 .tag
111 .as_deref()
112 .map(|t| format!("@{t}"))
113 .unwrap_or_default()
114 ),
115 }
116 }
117}
118
119impl FromStr for PluginLocator {
120 type Err = PluginLocatorError;
121
122 fn from_str(value: &str) -> Result<Self, Self::Err> {
123 PluginLocator::try_from(value.to_owned())
124 }
125}
126
127impl TryFrom<String> for PluginLocator {
128 type Error = PluginLocatorError;
129
130 fn try_from(value: String) -> Result<Self, Self::Error> {
131 if let Some(source) = value.strip_prefix("source:") {
133 if source.starts_with("http") {
134 return Self::try_from(source.to_owned());
135 } else {
136 return Self::try_from(format!("file://{source}"));
137 }
138 } else if value.starts_with("github:") && !value.contains("//") {
139 return Self::try_from(format!("github://{}", &value[7..]));
140 }
141
142 if !value.contains("://") {
143 return Err(PluginLocatorError::MissingProtocol);
144 }
145
146 let mut parts = value.splitn(2, "://");
147
148 let Some(protocol) = parts.next() else {
149 return Err(PluginLocatorError::MissingProtocol);
150 };
151
152 let Some(location) = parts.next() else {
153 return Err(PluginLocatorError::MissingLocation);
154 };
155
156 if location.is_empty() {
157 return Err(PluginLocatorError::MissingLocation);
158 }
159
160 match protocol {
161 "file" => Ok(PluginLocator::File(Box::new(FileLocator {
162 file: value,
163 path: None,
164 }))),
165 "github" => {
166 if !location.contains('/') {
167 return Err(PluginLocatorError::MissingGitHubOrg);
168 }
169
170 let mut github = GitHubLocator::default();
171 let mut query = location;
172
173 if let Some(index) = query.find('@') {
174 github.tag = Some(query[index + 1..].into());
175 query = &query[0..index];
176 }
177
178 let mut parts = query.split('/');
179 let org = parts.next().unwrap_or_default().to_owned();
180 let repo = parts.next().unwrap_or_default().to_owned();
181 let prefix = parts.next().map(|f| f.to_owned());
182
183 github.project_name = prefix;
184 github.repo_slug = format!("{org}/{repo}");
185
186 Ok(PluginLocator::GitHub(Box::new(github)))
187 }
188 "http" => Err(PluginLocatorError::SecureUrlsOnly),
189 "https" => Ok(PluginLocator::Url(Box::new(UrlLocator { url: value }))),
190 unknown => Err(PluginLocatorError::UnknownProtocol(unknown.to_owned())),
191 }
192 }
193}
194
195impl From<PluginLocator> for String {
196 fn from(locator: PluginLocator) -> Self {
197 locator.to_string()
198 }
199}
200
201impl AsRef<PluginLocator> for PluginLocator {
202 fn as_ref(&self) -> &Self {
203 self
204 }
205}