use std::path::{Path, PathBuf};
use toml_edit::{DocumentMut, Item};
use crate::error::{CliError, CliResult};
const NESTRS_WORKSPACE_MARKERS: &[&str] = &["crates/*", "apps/*"];
pub const DEFAULT_PORT_BASE: u16 = 3000;
pub const DEFAULT_ENV_PREFIX: &str = "NESTRS";
pub const ENV_PREFIX_VAR: &str = "NESTRS_ENV_PREFIX";
#[derive(Debug, Clone)]
pub struct NestrsWorkspace {
pub root: PathBuf,
pub metadata: Metadata,
}
#[derive(Debug, Clone)]
pub struct Metadata {
pub port_base: u16,
}
impl Default for Metadata {
fn default() -> Self {
Self {
port_base: DEFAULT_PORT_BASE,
}
}
}
impl NestrsWorkspace {
pub fn discover(start: &Path) -> CliResult<Option<Self>> {
let mut dir = start.canonicalize().map_err(CliError::Io)?;
loop {
if let Some(ws) = read_workspace(&dir)? {
return Ok(Some(ws));
}
if !dir.pop() {
return Ok(None);
}
}
}
pub fn require(start: &Path) -> CliResult<Self> {
Self::discover(start)?.ok_or(CliError::NotNestrsWorkspace)
}
pub fn features_root(&self) -> PathBuf {
self.root.join("crates/features/src")
}
pub fn features_lib(&self) -> PathBuf {
self.root.join("crates/features/src/lib.rs")
}
pub fn features_cargo(&self) -> PathBuf {
self.root.join("crates/features/Cargo.toml")
}
pub fn apps_root(&self) -> PathBuf {
self.root.join("apps")
}
pub fn migrations_root(&self) -> PathBuf {
self.root.join("crates/migrations/src")
}
pub fn migrations_lib(&self) -> PathBuf {
self.migrations_root().join("lib.rs")
}
pub fn migrations_migrator(&self) -> PathBuf {
self.migrations_root().join("migrator.rs")
}
pub fn feature_root(&self, snake: &str) -> PathBuf {
self.features_root().join(snake)
}
pub fn feature_exists(&self, snake: &str) -> bool {
self.feature_root(snake).is_dir()
}
}
fn read_workspace(dir: &Path) -> CliResult<Option<NestrsWorkspace>> {
let manifest = dir.join("Cargo.toml");
if !manifest.is_file() {
return Ok(None);
}
let source = std::fs::read_to_string(&manifest).map_err(CliError::Io)?;
let doc = source
.parse::<DocumentMut>()
.map_err(|e| CliError::Anyhow(e.into()))?;
let Some(workspace) = doc.get("workspace").and_then(Item::as_table) else {
return Ok(None);
};
let Some(members) = workspace.get("members").and_then(Item::as_array) else {
return Ok(None);
};
let member_strings: Vec<String> = members
.iter()
.filter_map(|value| value.as_str().map(str::to_owned))
.collect();
let is_nestrs = NESTRS_WORKSPACE_MARKERS
.iter()
.all(|marker| member_strings.iter().any(|member| member == marker));
if !is_nestrs {
return Ok(None);
}
Ok(Some(NestrsWorkspace {
root: dir.to_path_buf(),
metadata: read_metadata(workspace),
}))
}
fn read_metadata(workspace: &toml_edit::Table) -> Metadata {
let mut meta = Metadata::default();
let Some(table) = workspace
.get("metadata")
.and_then(Item::as_table)
.and_then(|m| m.get("nestrs"))
.and_then(Item::as_table)
else {
return meta;
};
if let Some(port) = table.get("port-base").and_then(|v| v.as_integer())
&& let Ok(port) = u16::try_from(port)
{
meta.port_base = port;
}
meta
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum EnvPrefixSource {
#[default]
Unset,
Environment(String),
Invalid(String),
}
impl EnvPrefixSource {
pub fn detect() -> Self {
let Ok(value) = std::env::var(ENV_PREFIX_VAR) else {
return Self::Unset;
};
if value.is_empty() {
return Self::Unset;
}
match validate_env_prefix(&value) {
Ok(()) => Self::Environment(value),
Err(reason) => Self::Invalid(reason),
}
}
pub fn prefix(&self) -> &str {
match self {
Self::Environment(prefix) => prefix,
Self::Unset | Self::Invalid(_) => DEFAULT_ENV_PREFIX,
}
}
}
pub fn env_prefix() -> String {
EnvPrefixSource::detect().prefix().to_owned()
}
pub fn var_name(env_prefix: &str, namespace: &str, key: &str) -> String {
format!("{env_prefix}_{namespace}__{key}")
}
pub fn validate_env_prefix(prefix: &str) -> Result<(), String> {
let valid = !prefix.is_empty()
&& prefix.starts_with(|c: char| c.is_ascii_uppercase())
&& !prefix.ends_with('_')
&& prefix
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
if valid {
Ok(())
} else {
Err(format!(
"`{prefix}` is not a usable env prefix: use uppercase ASCII letters, digits and \
underscores, starting with a letter and not ending in `_` (e.g. `ACME`, which \
yields ACME_ENV and ACME_DATABASE__URL)"
))
}
}