use crate::locator_error::PluginLocatorError;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug, Display};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Clone, Default, Eq, PartialEq)]
pub struct DataLocator {
pub data: String,
pub bytes: Option<Vec<u8>>,
}
impl Display for DataLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.data.starts_with("data://") {
write!(f, "{}", self.data)
} else {
write!(f, "data://{}", self.data)
}
}
}
impl Debug for DataLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut data = self.data.chars().take(39).collect::<String>();
if self.data.len() > 39 {
data.push_str("...");
}
f.debug_struct("DataLocator").field("data", &data).finish()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct FileLocator {
pub file: String,
pub path: Option<PathBuf>,
}
#[cfg(not(target_arch = "wasm32"))]
impl FileLocator {
pub fn get_unresolved_path(&self) -> PathBuf {
PathBuf::from(self.file.strip_prefix("file://").unwrap_or(&self.file))
}
pub fn get_resolved_path(&self) -> PathBuf {
let mut path = self
.path
.clone()
.unwrap_or_else(|| self.get_unresolved_path());
if !path.is_absolute() {
path = std::env::current_dir()
.expect("Could not determine working directory!")
.join(path);
}
path
}
}
impl Display for FileLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.file.starts_with("file://") {
write!(f, "{}", self.file)
} else {
write!(f, "file://{}", self.file)
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GitHubLocator {
pub repo_slug: String,
pub tag: Option<String>,
pub project_name: Option<String>,
}
impl Display for GitHubLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"github://{}{}{}",
self.repo_slug,
self.project_name
.as_deref()
.map(|n| format!("/{n}"))
.unwrap_or_default(),
self.tag
.as_deref()
.map(|t| format!("@{t}"))
.unwrap_or_default()
)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct UrlLocator {
pub url: String,
}
impl Display for UrlLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.url)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RegistryLocator {
pub registry: Option<String>,
pub namespace: Option<String>,
pub image: String,
pub tag: Option<String>,
}
impl Display for RegistryLocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"registry://{}:{}",
vec![
self.registry.clone(),
self.namespace.clone(),
Some(self.image.clone())
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("/"),
self.tag.as_deref().unwrap_or("latest")
)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged, into = "String", try_from = "String")]
pub enum PluginLocator {
Data(Box<DataLocator>),
File(Box<FileLocator>),
GitHub(Box<GitHubLocator>),
Url(Box<UrlLocator>),
Registry(Box<RegistryLocator>),
}
#[cfg(feature = "schematic")]
impl schematic::Schematic for PluginLocator {
fn schema_name() -> Option<String> {
Some("PluginLocator".into())
}
fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
schema.set_description("Strategies and protocols for locating plugins.");
schema.string_default()
}
}
impl Display for PluginLocator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginLocator::Data(data) => write!(f, "{data}"),
PluginLocator::File(file) => write!(f, "{file}"),
PluginLocator::Url(url) => write!(f, "{url}"),
PluginLocator::GitHub(github) => write!(f, "{github}"),
PluginLocator::Registry(registry) => write!(f, "{registry}"),
}
}
}
impl FromStr for PluginLocator {
type Err = PluginLocatorError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
PluginLocator::try_from(value.to_owned())
}
}
impl TryFrom<String> for PluginLocator {
type Error = PluginLocatorError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if let Some(source) = value.strip_prefix("source:") {
if source.starts_with("http") {
return Self::try_from(source.to_owned());
} else {
return Self::try_from(format!("file://{source}"));
}
} else if value.starts_with("github:") && !value.contains("//") {
return Self::try_from(format!("github://{}", &value[7..]));
}
if !value.contains("://") {
return Err(PluginLocatorError::MissingProtocol);
}
let mut parts = value.splitn(2, "://");
let Some(protocol) = parts.next() else {
return Err(PluginLocatorError::MissingProtocol);
};
let Some(location) = parts.next() else {
return Err(PluginLocatorError::MissingLocation);
};
if location.is_empty() {
return Err(PluginLocatorError::MissingLocation);
}
match protocol {
"data" => Ok(PluginLocator::Data(Box::new(DataLocator {
data: value,
bytes: None,
}))),
"file" => Ok(PluginLocator::File(Box::new(FileLocator {
file: value,
path: None,
}))),
"github" => {
if !location.contains('/') {
return Err(PluginLocatorError::MissingGitHubOrg);
}
let mut github = GitHubLocator::default();
let mut query = location;
if let Some(index) = query.find('@') {
github.tag = Some(query[index + 1..].into());
query = &query[0..index];
}
let mut parts = query.split('/');
let org = parts.next().unwrap_or_default().to_owned();
let repo = parts.next().unwrap_or_default().to_owned();
let prefix = parts.next().map(|f| f.to_owned());
github.project_name = prefix;
github.repo_slug = format!("{org}/{repo}");
Ok(PluginLocator::GitHub(Box::new(github)))
}
"http" => Err(PluginLocatorError::SecureUrlsOnly),
"https" => Ok(PluginLocator::Url(Box::new(UrlLocator { url: value }))),
"registry" => {
let mut registry = RegistryLocator::default();
let mut query = location;
if let Some(index) = query.find(":") {
registry.tag = Some(query[index + 1..].into());
query = &query[0..index];
}
if let Some(index) = query.find("/") {
let inner = &query[0..index];
if inner.contains('.') {
registry.registry = Some(inner.into());
query = &query[index + 1..];
}
}
if let Some(index) = query.rfind('/') {
registry.image = query[index + 1..].into();
query = &query[0..index];
} else {
registry.image = query.into();
query = &query[0..0];
}
if !query.is_empty() {
registry.namespace = Some(query.into());
}
if registry.image.is_empty() {
return Err(PluginLocatorError::MissingRegistryImage);
}
Ok(PluginLocator::Registry(Box::new(registry)))
}
unknown => Err(PluginLocatorError::UnknownProtocol(unknown.to_owned())),
}
}
}
impl From<PluginLocator> for String {
fn from(locator: PluginLocator) -> Self {
locator.to_string()
}
}
impl AsRef<PluginLocator> for PluginLocator {
fn as_ref(&self) -> &Self {
self
}
}