moon_config 2.0.13

Core workspace, project, and moon configuration.
Documentation
use crate::is_glob_like;
use crate::shapes::{FilePath, GlobPath};
use regex::Regex;
use schematic::{ParseError, Schema, SchemaBuilder, Schematic, derive_enum};
use semver::Version;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;

static GIT: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new("^(?<url>[a-zA-Z@0-9.-]+/[a-zA-Z0-9-_./]+)#(?<revision>[a-z0-9-_.@]+)$").unwrap()
});

static NPM: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new("^(?<package>(@[a-z][a-z0-9-_.]*/)?[a-z][a-z0-9-_.]*)#(?<version>[a-z0-9-.+]+)$")
        .unwrap()
});

derive_enum!(
    #[serde(untagged, try_from = "String", into = "String")]
    pub enum TemplateLocator {
        Archive {
            url: String,
        },
        File {
            path: FilePath,
        },
        Glob {
            glob: GlobPath,
        },
        Git {
            remote_url: String,
            revision: String,
        },
        Npm {
            package: String,
            version: Version,
        },
    }
);

impl fmt::Display for TemplateLocator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TemplateLocator::Archive { url } => write!(f, "{url}"),
            TemplateLocator::File { path } => write!(f, "file://{path}"),
            TemplateLocator::Glob { glob } => write!(f, "glob://{glob}"),
            TemplateLocator::Git {
                remote_url,
                revision,
            } => write!(f, "git://{remote_url}#{revision}"),
            TemplateLocator::Npm { package, version } => write!(f, "npm://{package}#{version}"),
        }
    }
}

impl Schematic for TemplateLocator {
    fn build_schema(mut schema: SchemaBuilder) -> Schema {
        schema.string_default()
    }
}

impl FromStr for TemplateLocator {
    type Err = ParseError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if let Some(index) = value.find(':') {
            let protocol = &value[0..index];
            let mut inner_value = &value[index + 1..];

            if inner_value.starts_with("//") {
                inner_value = &value[index + 3..];
            }

            match protocol {
                "http" | "https" => {
                    // Keep in sync with starbase_archive
                    for ext in [
                        ".tar.gz", ".tar.xz", ".tar.bz2", ".tar", ".tgz", ".txz", ".tbz", ".tbz2",
                        ".tz2", ".zstd", ".zst", ".zip", ".gz",
                    ] {
                        if value.ends_with(ext) {
                            return Ok(TemplateLocator::Archive {
                                url: value.to_owned(),
                            });
                        }
                    }

                    return Err(ParseError::new(
                        "Invalid URL template locator, must contain a trailing file name with a supported archive extension",
                    ));
                }
                "git" | "git+http" | "git+https" => {
                    if let Some(result) = GIT.captures(inner_value) {
                        return Ok(TemplateLocator::Git {
                            remote_url: result.name("url").unwrap().as_str().to_owned(),
                            revision: result.name("revision").unwrap().as_str().to_owned(),
                        });
                    }

                    return Err(ParseError::new(format!(
                        "Invalid Git template locator, must be in the format of `{protocol}://url#revision`"
                    )));
                }
                "npm" | "pnpm" | "yarn" => {
                    if let Some(result) = NPM.captures(inner_value) {
                        return Ok(TemplateLocator::Npm {
                            package: result.name("package").unwrap().as_str().to_owned(),
                            version: Version::parse(result.name("version").unwrap().as_str())
                                .map_err(|error| ParseError::new(error.to_string()))?,
                        });
                    }

                    return Err(ParseError::new(format!(
                        "Invalid npm template locator, must be in the format of `{protocol}://package#version`"
                    )));
                }
                "file" => {
                    return Ok(TemplateLocator::File {
                        path: FilePath::from_str(inner_value)?,
                    });
                }
                "glob" => {
                    return Ok(TemplateLocator::Glob {
                        glob: GlobPath::from_str(inner_value)?,
                    });
                }
                other => {
                    return Err(ParseError::new(format!(
                        "Unknown template locator prefix `{other}`"
                    )));
                }
            };
        }

        Ok(if is_glob_like(value) {
            TemplateLocator::Glob {
                glob: GlobPath::from_str(value)?,
            }
        } else {
            TemplateLocator::File {
                path: FilePath::from_str(value)?,
            }
        })
    }
}

impl TryFrom<String> for TemplateLocator {
    type Error = ParseError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value)
    }
}

impl From<TemplateLocator> for String {
    fn from(value: TemplateLocator) -> String {
        value.to_string()
    }
}