warpgate_api/
locator.rs

1use crate::locator_error::PluginLocatorError;
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4use std::path::PathBuf;
5use std::str::FromStr;
6
7/// A file system locator.
8#[derive(Clone, Debug, Default, Eq, PartialEq)]
9pub struct FileLocator {
10    /// Path explicitly configured by a user (with file://).
11    pub file: String,
12
13    /// The file (above) resolved to an absolute path.
14    /// This must be done manually on the host side.
15    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/// A GitHub release locator.
41#[derive(Clone, Debug, Default, Eq, PartialEq)]
42pub struct GitHubLocator {
43    /// Owner/org and repository name: `owner/repo`.
44    pub repo_slug: String,
45
46    /// Explicit release tag to use. Defaults to `latest`.
47    pub tag: Option<String>,
48
49    /// Project name to match tags against. Primarily used in monorepos.
50    pub project_name: Option<String>,
51}
52
53/// A HTTPS URL locator.
54#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct UrlLocator {
56    /// URL explicitly configured by a user (with https://).
57    pub url: String,
58}
59
60/// Strategies and protocols for locating plugins.
61#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
62#[serde(untagged, into = "String", try_from = "String")]
63pub enum PluginLocator {
64    /// file:///abs/path/to/file.wasm
65    /// file://../rel/path/to/file.wasm
66    File(Box<FileLocator>),
67
68    /// github://owner/repo
69    /// github://owner/repo@tag
70    /// github://owner/repo/project
71    GitHub(Box<GitHubLocator>),
72
73    /// https://url/to/file.wasm
74    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        // Legacy support
132        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}