use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
use std::{fmt, fs};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use storm_workspace::utils::get_workspace_root;
use crate::types::PackageJson;
use crate::{Config, ConfigError, Environment, File, Value};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum WorkspaceMode {
Development,
Test,
Production,
}
impl Default for WorkspaceMode {
fn default() -> Self {
WorkspaceMode::Production
}
}
impl FromStr for WorkspaceMode {
type Err = ();
fn from_str(input: &str) -> Result<WorkspaceMode, Self::Err> {
match input {
"development" => Ok(WorkspaceMode::Development),
"test" => Ok(WorkspaceMode::Test),
"production" => Ok(WorkspaceMode::Production),
_ => Err(()),
}
}
}
impl fmt::Display for WorkspaceMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
WorkspaceMode::Development => write!(f, "development"),
WorkspaceMode::Test => write!(f, "test"),
WorkspaceMode::Production => write!(f, "production"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum WorkspaceVariant {
Minimal,
Monorepo,
}
impl Default for WorkspaceVariant {
fn default() -> Self {
WorkspaceVariant::Monorepo
}
}
impl FromStr for WorkspaceVariant {
type Err = ();
fn from_str(input: &str) -> Result<WorkspaceVariant, Self::Err> {
match input {
"minimal" => Ok(WorkspaceVariant::Minimal),
"monorepo" => Ok(WorkspaceVariant::Monorepo),
_ => Err(()),
}
}
}
impl fmt::Display for WorkspaceVariant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
WorkspaceVariant::Minimal => write!(f, "minimal"),
WorkspaceVariant::Monorepo => write!(f, "monorepo"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PackageManagerType {
Npm,
Yarn,
Pnpm,
Bun,
}
impl FromStr for PackageManagerType {
type Err = ();
fn from_str(input: &str) -> Result<PackageManagerType, Self::Err> {
match input {
"npm" => Ok(PackageManagerType::Npm),
"yarn" => Ok(PackageManagerType::Yarn),
"pnpm" => Ok(PackageManagerType::Pnpm),
"bun" => Ok(PackageManagerType::Bun),
_ => Err(()),
}
}
}
impl fmt::Display for PackageManagerType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
PackageManagerType::Npm => write!(f, "npm"),
PackageManagerType::Yarn => write!(f, "yarn"),
PackageManagerType::Pnpm => write!(f, "pnpm"),
PackageManagerType::Bun => write!(f, "bun"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Silent,
Fatal,
Error,
Warn,
Success,
Info,
Debug,
Trace,
All,
}
impl FromStr for LogLevel {
type Err = ();
fn from_str(input: &str) -> Result<LogLevel, Self::Err> {
match input {
"silent" => Ok(LogLevel::Silent),
"fatal" => Ok(LogLevel::Fatal),
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"success" => Ok(LogLevel::Success),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
"all" => Ok(LogLevel::All),
_ => Err(()),
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
LogLevel::Silent => write!(f, "silent"),
LogLevel::Fatal => write!(f, "fatal"),
LogLevel::Error => write!(f, "error"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::Success => write!(f, "success"),
LogLevel::Info => write!(f, "info"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Trace => write!(f, "trace"),
LogLevel::All => write!(f, "all"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ExtendsConfig {
Single(String),
Multiple(Vec<String>),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct OrganizationDetails {
pub name: Option<String>,
pub description: Option<String>,
pub logo: Option<String>,
pub icon: Option<String>,
pub url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum WorkspaceOrganizationConfig {
Details(OrganizationDetails),
Name(String),
}
impl Default for WorkspaceOrganizationConfig {
fn default() -> Self {
WorkspaceOrganizationConfig::Name("storm-software".to_string())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ColorPaletteConfig {
pub dark: String,
pub light: String,
pub brand: String,
pub alternate: Option<String>,
pub accent: Option<String>,
pub link: String,
pub help: String,
pub success: String,
pub info: String,
pub warning: String,
pub danger: String,
pub fatal: Option<String>,
pub positive: String,
pub negative: String,
pub gradient: Option<Vec<String>>,
}
impl Default for ColorPaletteConfig {
fn default() -> Self {
Self {
dark: "#151718".to_string(),
light: "#cbd5e1".to_string(),
brand: "#1fb2a6".to_string(),
alternate: None,
accent: None,
link: "#3fa6ff".to_string(),
help: "#818cf8".to_string(),
success: "#45b27e".to_string(),
info: "#38bdf8".to_string(),
warning: "#f3d371".to_string(),
danger: "#d8314a".to_string(),
fatal: None,
positive: "#4ade80".to_string(),
negative: "#ef4444".to_string(),
gradient: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ColorSchemaConfig {
pub foreground: String,
pub background: String,
pub brand: String,
pub alternate: Option<String>,
pub accent: Option<String>,
pub link: String,
pub help: String,
pub success: String,
pub info: String,
pub warning: String,
pub danger: String,
pub fatal: Option<String>,
pub positive: String,
pub negative: String,
pub gradient: Option<Vec<String>>,
}
impl ColorSchemaConfig {
fn default_dark() -> Self {
Self {
foreground: "#cbd5e1".to_string(),
background: "#151718".to_string(),
brand: "#1fb2a6".to_string(),
alternate: None,
accent: None,
link: "#3fa6ff".to_string(),
help: "#818cf8".to_string(),
success: "#45b27e".to_string(),
info: "#38bdf8".to_string(),
warning: "#f3d371".to_string(),
danger: "#d8314a".to_string(),
fatal: None,
positive: "#4ade80".to_string(),
negative: "#ef4444".to_string(),
gradient: None,
}
}
fn default_light() -> Self {
Self {
foreground: "#151718".to_string(),
background: "#cbd5e1".to_string(),
brand: "#1fb2a6".to_string(),
alternate: None,
accent: None,
link: "#3fa6ff".to_string(),
help: "#818cf8".to_string(),
success: "#45b27e".to_string(),
info: "#38bdf8".to_string(),
warning: "#f3d371".to_string(),
danger: "#d8314a".to_string(),
fatal: None,
positive: "#4ade80".to_string(),
negative: "#ef4444".to_string(),
gradient: None,
}
}
}
impl Default for ColorSchemaConfig {
fn default() -> Self {
ColorSchemaConfig::default_dark()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ColorThemeConfig {
pub light: ColorSchemaConfig,
pub dark: ColorSchemaConfig,
}
impl Default for ColorThemeConfig {
fn default() -> Self {
Self { light: ColorSchemaConfig::default_light(), dark: ColorSchemaConfig::default_dark() }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ColorThemeEntry {
Palette(ColorPaletteConfig),
Theme(ColorThemeConfig),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum WorkspaceColorsConfig {
Palette(ColorPaletteConfig),
Theme(ColorThemeConfig),
Collection(HashMap<String, ColorThemeEntry>),
}
impl Default for WorkspaceColorsConfig {
fn default() -> Self {
WorkspaceColorsConfig::Palette(ColorPaletteConfig::default())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceRegistryUrlConfig {
pub github: Option<String>,
pub npm: Option<String>,
pub cargo: Option<String>,
pub cyclone: Option<String>,
pub container: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceBotConfig {
pub name: String,
pub email: String,
}
impl Default for WorkspaceBotConfig {
fn default() -> Self {
Self { name: "stormie-bot".to_string(), email: "stormie-bot@stormsoftware.com".to_string() }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceReleaseBannerConfig {
pub url: Option<String>,
pub alt: String,
}
impl Default for WorkspaceReleaseBannerConfig {
fn default() -> Self {
Self { url: None, alt: "The workspace's banner image".to_string() }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum ReleaseBannerConfig {
Url(String),
Details(WorkspaceReleaseBannerConfig),
}
impl Default for ReleaseBannerConfig {
fn default() -> Self {
ReleaseBannerConfig::Details(WorkspaceReleaseBannerConfig::default())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceReleaseConfig {
pub banner: ReleaseBannerConfig,
pub header: Option<String>,
pub footer: Option<String>,
}
impl Default for WorkspaceReleaseConfig {
fn default() -> Self {
Self { banner: ReleaseBannerConfig::default(), header: None, footer: None }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSocialsConfig {
pub twitter: Option<String>,
pub discord: Option<String>,
pub telegram: Option<String>,
pub slack: Option<String>,
pub medium: Option<String>,
pub github: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceErrorConfig {
pub codes_file: String,
pub url: Option<String>,
}
impl Default for WorkspaceErrorConfig {
fn default() -> Self {
Self { codes_file: "tools/errors/codes.json".to_string(), url: None }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceDirectoriesConfig {
pub cache: Option<String>,
pub data: Option<String>,
pub config: Option<String>,
pub temp: Option<String>,
pub log: Option<String>,
pub build: String,
}
impl Default for WorkspaceDirectoriesConfig {
fn default() -> Self {
Self { cache: None, data: None, config: None, temp: None, log: None, build: "dist".to_string() }
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceConfig {
pub schema: String,
pub config: Option<Config>,
pub config_file: Option<String>,
pub extends: Option<ExtendsConfig>,
pub workspace_root: String,
pub name: String,
pub namespace: String,
pub variant: WorkspaceVariant,
pub organization: WorkspaceOrganizationConfig,
pub repository: String,
pub license: String,
pub homepage: String,
pub docs: Option<String>,
pub portal: Option<String>,
pub licensing: Option<String>,
pub contact: Option<String>,
pub support: Option<String>,
pub branch: String,
pub preid: Option<String>,
pub owner: String,
pub bot: WorkspaceBotConfig,
pub mode: WorkspaceMode,
pub colors: WorkspaceColorsConfig,
pub release: WorkspaceReleaseConfig,
pub socials: WorkspaceSocialsConfig,
pub error: WorkspaceErrorConfig,
pub skip_cache: bool,
pub registry: WorkspaceRegistryUrlConfig,
pub directories: WorkspaceDirectoriesConfig,
pub package_manager: PackageManagerType,
pub timezone: String,
pub locale: String,
pub log_level: LogLevel,
pub skip_config_logging: bool,
pub extensions: RefCell<HashMap<String, HashMap<String, Value>>>,
}
impl WorkspaceConfig {
pub fn new() -> Result<Self, ConfigError> {
let workspace_root = get_workspace_root().expect("No workspace root could be found");
let workspace_config = Self::from_workspace_root(&workspace_root)?;
Ok(workspace_config)
}
pub fn from_workspace_root(workspace_root: &Path) -> Result<Self, ConfigError> {
let mut workspace_config = Self::default();
workspace_config.workspace_root = workspace_root.to_str().unwrap().to_string();
match fs::metadata(format!("{}/package.json", workspace_root.to_string_lossy())) {
Ok(_) => {
let package_json_path = format!("{}/package.json", workspace_root.to_string_lossy());
let package_json: PackageJson = serde_json::from_reader(
std::fs::File::open(Path::new(&package_json_path))
.expect("Unable to read package.json file"),
)
.expect("error while reading or parsing");
workspace_config.config = Some(
Config::builder()
.set_default("name", package_json.name)?
.set_default("namespace", package_json.namespace)?
.set_default("repository", package_json.repository.get("url").unwrap().to_string())?
.set_default("license", package_json.license)?
.set_default("homepage", package_json.homepage)?
.set_default("workspace_root", workspace_root.to_str().unwrap().to_string())?
.add_source(
File::with_name(&format!(
"{}/storm-workspace.json",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/storm-workspace.jsonc",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!("{}/.storm/config.json", workspace_root.to_string_lossy()))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/.storm-workspace/config.json",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/storm-workspace.toml",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!("{}/.storm/config.toml", workspace_root.to_string_lossy()))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/.storm-workspace/config.toml",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/storm-workspace.yaml",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!("{}/.storm/config.yaml", workspace_root.to_string_lossy()))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/.storm-workspace/config.yaml",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(
File::with_name(&format!("{}/storm-workspace.yml", workspace_root.to_string_lossy()))
.required(false),
)
.add_source(
File::with_name(&format!("{}/.storm/config.yml", workspace_root.to_string_lossy()))
.required(false),
)
.add_source(
File::with_name(&format!(
"{}/.storm-workspace/config.yml",
workspace_root.to_string_lossy()
))
.required(false),
)
.add_source(Environment::with_prefix("storm"))
.build()?,
);
let config = workspace_config.config.as_ref().unwrap();
if let Ok(found) = config.get_string("config_file") {
workspace_config.config_file = Some(found);
}
if let Ok(found) = config.get_string("schema") {
workspace_config.schema = found;
}
if let Ok(found) = config.get::<ExtendsConfig>("extends") {
workspace_config.extends = Some(found);
}
if let Ok(found) = config.get_string("workspace_root") {
workspace_config.workspace_root = found;
}
if let Ok(found) = config.get_string("name") {
workspace_config.name = found;
}
if let Ok(found) = config.get_string("namespace") {
workspace_config.namespace = found;
}
if let Ok(found) = config.get::<WorkspaceVariant>("variant") {
workspace_config.variant = found;
}
if let Ok(found) = config.get::<WorkspaceOrganizationConfig>("organization") {
workspace_config.organization = found;
} else if let Ok(found) = config.get::<WorkspaceOrganizationConfig>("org") {
workspace_config.organization = found;
} else if let Ok(found) = config.get::<WorkspaceOrganizationConfig>("organization_config") {
workspace_config.organization = found;
}
if let Ok(found) = config.get_string("repository") {
workspace_config.repository = found;
}
if let Ok(found) = config.get_string("license") {
workspace_config.license = found;
}
if let Ok(found) = config.get_string("homepage") {
workspace_config.homepage = found;
}
if let Ok(found) = config.get_string("docs") {
workspace_config.docs = Some(found);
}
if let Ok(found) = config.get_string("portal") {
workspace_config.portal = Some(found);
}
if let Ok(found) = config.get_string("licensing") {
workspace_config.licensing = Some(found);
}
if let Ok(found) = config.get_string("contact") {
workspace_config.contact = Some(found);
}
if let Ok(found) = config.get_string("support") {
workspace_config.support = Some(found);
}
if let Ok(found) = config.get_string("branch") {
workspace_config.branch = found;
}
if let Ok(found) = config.get_string("preid") {
workspace_config.preid = Some(found);
}
if let Ok(found) = config.get_string("owner") {
workspace_config.owner = found;
}
if let Ok(found) = config.get_string("mode") {
workspace_config.mode = WorkspaceMode::from_str(&found).unwrap();
}
if let Ok(found) = config.get_bool("skip_cache") {
workspace_config.skip_cache = found;
}
if let Ok(found) = config.get_string("package_manager") {
workspace_config.package_manager = PackageManagerType::from_str(&found).unwrap();
}
if let Ok(found) = config.get_string("timezone") {
workspace_config.timezone = found;
}
if let Ok(found) = config.get_string("locale") {
workspace_config.locale = found;
}
if let Ok(found) = config.get_string("log_level") {
workspace_config.log_level = LogLevel::from_str(&found).unwrap();
}
if let Ok(found) = config.get_bool("skip_config_logging") {
workspace_config.skip_config_logging = found;
}
if let Ok(found) = config.get::<WorkspaceReleaseConfig>("release") {
workspace_config.release = found;
}
if let Ok(found) = config.get::<WorkspaceColorsConfig>("colors") {
workspace_config.colors = found;
}
if let Ok(found) = config.get::<WorkspaceBotConfig>("bot") {
workspace_config.bot = found;
} else if let Ok(found) = config.get::<WorkspaceBotConfig>("bot_config") {
workspace_config.bot = found;
} else if let Ok(found) = config.get::<WorkspaceBotConfig>("workspaceBot") {
workspace_config.bot = found;
}
if let Ok(found) = config.get::<WorkspaceSocialsConfig>("socials") {
workspace_config.socials = found;
}
if let Ok(found) = config.get::<WorkspaceErrorConfig>("error") {
workspace_config.error = found;
}
if let Ok(found) = config.get::<WorkspaceRegistryUrlConfig>("registry") {
workspace_config.registry = found;
} else if let Ok(found) = config.get::<WorkspaceRegistryUrlConfig>("registry_urls") {
workspace_config.registry = found;
} else if let Ok(found) = config.get::<WorkspaceRegistryUrlConfig>("registryUrls") {
workspace_config.registry = found;
}
if let Ok(found) = config.get::<WorkspaceDirectoriesConfig>("directories") {
workspace_config.directories = found;
}
if let Ok(found) = config.get_string("package_manager") {
workspace_config.package_manager = PackageManagerType::from_str(&found).unwrap();
}
Ok(workspace_config)
}
Err(_) => {
return Err(ConfigError::NotFound(format!(
"{}/package.json",
workspace_root.to_string_lossy()
)));
}
}
}
pub fn get_extension(&self, name: &str) -> Option<HashMap<String, Value>> {
if let Some(existing) = self.extensions.borrow().get(name) {
return Some(existing.clone());
}
let extension = self.config.as_ref().expect("Config value must be determined").get_table(name);
match extension.is_ok() {
true => {
self.extensions.borrow_mut().insert(name.to_string(), extension.ok().unwrap());
return self.extensions.borrow().get(name).cloned();
}
false => None,
}
}
}
impl Default for WorkspaceConfig {
fn default() -> Self {
WorkspaceConfig {
schema: "https://stormsoftware.com/schemas/storm-workspace.json".to_string(),
config: None,
config_file: None,
extends: None,
workspace_root: get_workspace_root().unwrap().to_str().unwrap().to_string(),
mode: WorkspaceMode::Production,
variant: WorkspaceVariant::Monorepo,
name: "storm-monorepo".to_string(),
namespace: "storm-software".to_string(),
organization: WorkspaceOrganizationConfig::default(),
repository: "https://github.com/storm-software/storm-monorepo".to_string(),
license: "Apache-2.0".to_string(),
homepage: "https://stormsoftware.com".to_string(),
branch: "main".to_string(),
preid: None,
docs: None,
portal: None,
licensing: None,
contact: None,
support: None,
release: WorkspaceReleaseConfig::default(),
socials: WorkspaceSocialsConfig::default(),
error: WorkspaceErrorConfig::default(),
directories: WorkspaceDirectoriesConfig::default(),
owner: "@storm-software/admin".to_string(),
bot: WorkspaceBotConfig::default(),
log_level: LogLevel::Info,
skip_config_logging: true,
skip_cache: false,
package_manager: PackageManagerType::Npm,
registry: WorkspaceRegistryUrlConfig::default(),
timezone: "America/New_York".to_string(),
locale: "en-US".to_string(),
colors: WorkspaceColorsConfig::default(),
extensions: HashMap::new().into(),
}
}
}
impl FromStr for WorkspaceConfig {
type Err = ();
fn from_str(input: &str) -> Result<WorkspaceConfig, Self::Err> {
let workspace_root = Path::new(input);
WorkspaceConfig::from_workspace_root(workspace_root).map_err(|_| ())
}
}