diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-05 20:34:27 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-05 20:44:37 +0900 |
| commit | 5254a9e9b698c3618229f4f802b39a82baf9169a (patch) | |
| tree | d89127778b56ee0421d9838a1dc462eaa7599eab /crates/mozart-core/src | |
| parent | 884d9ab32bbca7a8ec5c7ee7d42cbde0e7e6babf (diff) | |
| download | php-mozart-5254a9e9b698c3618229f4f802b39a82baf9169a.tar.gz php-mozart-5254a9e9b698c3618229f4f802b39a82baf9169a.tar.zst php-mozart-5254a9e9b698c3618229f4f802b39a82baf9169a.zip | |
feat(core): port Factory::createConfig() as factory::create_config()
Adds crates/mozart-core/src/factory.rs with get_cache_dir(),
get_data_dir(), and create_config() — a Rust port of
Composer\Factory::createConfig() (auth loading and htaccess creation
are out of scope for now).
Also fixes a correctness bug on Linux: the previous Config::default()
resolved cache-dir to $XDG_CONFIG_HOME/composer/cache via the
{$home}/cache placeholder, whereas Composer uses the XDG cache base
($XDG_CACHE_HOME or ~/.cache), giving ~/.cache/composer.
Callers updated:
- Composer::load() uses create_config() as the global baseline before
merging project-level config.
- config command execute_read() builds the global baseline with
create_config() and overlays local config on top when not --global,
matching Composer's actual layering order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src')
| -rw-r--r-- | crates/mozart-core/src/composer.rs | 3 | ||||
| -rw-r--r-- | crates/mozart-core/src/factory.rs | 211 | ||||
| -rw-r--r-- | crates/mozart-core/src/lib.rs | 1 |
3 files changed, 214 insertions, 1 deletions
diff --git a/crates/mozart-core/src/composer.rs b/crates/mozart-core/src/composer.rs index 76427a7..2e252c6 100644 --- a/crates/mozart-core/src/composer.rs +++ b/crates/mozart-core/src/composer.rs @@ -12,6 +12,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use crate::config::{Config, resolve_references}; +use crate::factory::create_config; /// Return the Composer home directory, respecting `COMPOSER_HOME` and falling /// back to the platform default using Composer-compatible logic. @@ -109,7 +110,7 @@ impl Composer { fn load(project_dir: PathBuf, composer_json: &Path) -> anyhow::Result<Self> { let content = std::fs::read_to_string(composer_json)?; let value: serde_json::Value = serde_json::from_str(&content)?; - let mut config = Config::default(); + let mut config = create_config()?; if let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) { let overrides: BTreeMap<String, serde_json::Value> = cfg_obj .iter() diff --git a/crates/mozart-core/src/factory.rs b/crates/mozart-core/src/factory.rs new file mode 100644 index 0000000..faa98d8 --- /dev/null +++ b/crates/mozart-core/src/factory.rs @@ -0,0 +1,211 @@ +//! Factory helpers for constructing Composer configuration. +//! +//! Ports the static factory methods from `Composer\Factory` that deal with +//! default and global configuration. Auth loading and htaccess creation are +//! intentionally omitted as they are out of scope for the current port. + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use crate::composer::composer_home; +use crate::config::Config; + +/// Rust port of `Factory::getCacheDir()`. +/// +/// Priority: +/// 1. `$COMPOSER_CACHE_DIR` env var +/// 2. Windows: `%LOCALAPPDATA%/Composer` +/// 3. macOS: `$HOME/Library/Caches/composer` +/// 4. Linux/other: `$XDG_CACHE_HOME/composer` (or `$HOME/.cache/composer`) +fn get_cache_dir(home: &std::path::Path) -> PathBuf { + if let Ok(val) = std::env::var("COMPOSER_CACHE_DIR") + && !val.is_empty() + { + return PathBuf::from(val); + } + + #[cfg(target_os = "windows")] + { + if let Ok(local) = std::env::var("LOCALAPPDATA") + && !local.is_empty() + { + return PathBuf::from(local).join("Composer"); + } + return home.join("cache"); + } + + #[cfg(target_os = "macos")] + { + if let Ok(h) = std::env::var("HOME") + && !h.is_empty() + { + return PathBuf::from(h) + .join("Library") + .join("Caches") + .join("composer"); + } + return home.join("cache"); + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + let cache_base = std::env::var("XDG_CACHE_HOME") + .ok() + .filter(|v| !v.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| { + std::env::var("HOME") + .map(|h| PathBuf::from(h).join(".cache")) + .unwrap_or_else(|_| home.join("cache")) + }); + cache_base.join("composer") + } +} + +/// Rust port of `Factory::getDataDir()`. +/// +/// Priority: +/// 1. `$COMPOSER_HOME` is set → use `home` (same path) as data dir +/// 2. Windows: `home` +/// 3. Linux/macOS: `$XDG_DATA_HOME/composer` (or `$HOME/.local/share/composer`) +fn get_data_dir(home: &std::path::Path) -> PathBuf { + if std::env::var("COMPOSER_HOME").is_ok_and(|v| !v.is_empty()) { + return home.to_path_buf(); + } + + #[cfg(target_os = "windows")] + { + return home.to_path_buf(); + } + + #[cfg(not(target_os = "windows"))] + { + let data_base = std::env::var("XDG_DATA_HOME") + .ok() + .filter(|v| !v.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| { + std::env::var("HOME") + .map(|h| PathBuf::from(h).join(".local").join("share")) + .unwrap_or_else(|_| PathBuf::from("/tmp")) + }); + data_base.join("composer") + } +} + +/// Rust port of `Factory::createConfig()`. +/// +/// Builds the effective global [`Config`] by: +/// 1. Starting from `Config::default()` +/// 2. Setting `home`, `cache-dir`, and `data-dir` based on platform conventions +/// 3. Loading and merging `$COMPOSER_HOME/config.json` if it exists +/// +/// Auth loading (`auth.json`, `COMPOSER_AUTH`) and htaccess-protect directory +/// creation are intentionally omitted. +/// +/// **Callers must call [`crate::config::resolve_references`] after any +/// additional project-level merges.** This function does not call it +/// internally so that callers can overlay project config first. +pub fn create_config() -> anyhow::Result<Config> { + let home = composer_home(); + let cache_dir = get_cache_dir(&home); + let data_dir = get_data_dir(&home); + + let mut config = Config::default(); + + // Inject home/cache-dir/data-dir as the platform-computed baseline. + // `home` and `data-dir` have no dedicated fields on Config and land in `extra`. + let mut defaults: BTreeMap<String, serde_json::Value> = BTreeMap::new(); + defaults.insert( + "home".to_string(), + serde_json::json!(home.to_string_lossy().as_ref()), + ); + defaults.insert( + "cache-dir".to_string(), + serde_json::json!(cache_dir.to_string_lossy().as_ref()), + ); + defaults.insert( + "data-dir".to_string(), + serde_json::json!(data_dir.to_string_lossy().as_ref()), + ); + config.merge(&defaults)?; + + // Load $COMPOSER_HOME/config.json global config + let global_config_path = home.join("config.json"); + if global_config_path.exists() { + let content = std::fs::read_to_string(&global_config_path)?; + let json: serde_json::Value = serde_json::from_str(&content).map_err(|e| { + anyhow::anyhow!("Failed to parse {}: {e}", global_config_path.display()) + })?; + if let Some(obj) = json.get("config").and_then(|v| v.as_object()) { + let overrides: BTreeMap<String, serde_json::Value> = + obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + config.merge(&overrides)?; + } + } + + Ok(config) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_config_cache_dir_has_no_placeholder() { + let config = create_config().unwrap(); + assert!( + !config.cache_dir.contains("{$home}"), + "cache_dir should not contain placeholder, got: {}", + config.cache_dir + ); + assert!(!config.cache_dir.is_empty()); + } + + #[test] + fn test_create_config_home_accessible_via_get() { + let config = create_config().unwrap(); + let home_val = config.get("home"); + assert!(home_val.is_some(), "config.get('home') should return Some"); + assert!( + home_val + .unwrap() + .as_str() + .map(|s| !s.is_empty()) + .unwrap_or(false), + "home should be a non-empty string" + ); + } + + #[test] + fn test_create_config_data_dir_accessible_via_get() { + let config = create_config().unwrap(); + assert!(config.get("data-dir").is_some()); + } + + #[test] + fn test_get_cache_dir_ends_with_composer() { + let home = std::path::PathBuf::from("/tmp/test-home"); + let result = get_cache_dir(&home); + assert!( + result.to_string_lossy().contains("composer"), + "cache dir should contain 'composer', got: {}", + result.display() + ); + } + + #[test] + fn test_get_data_dir_ends_with_composer_when_no_composer_home() { + // Only valid when COMPOSER_HOME is not set in the test environment. + if std::env::var("COMPOSER_HOME").is_ok_and(|v| !v.is_empty()) { + return; + } + let home = std::path::PathBuf::from("/tmp/test-home"); + let result = get_data_dir(&home); + assert!( + result.to_string_lossy().contains("composer"), + "data dir should contain 'composer', got: {}", + result.display() + ); + } +} diff --git a/crates/mozart-core/src/lib.rs b/crates/mozart-core/src/lib.rs index 1264292..74f3512 100644 --- a/crates/mozart-core/src/lib.rs +++ b/crates/mozart-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod composer; pub mod config; pub mod console; pub mod exit_code; +pub mod factory; pub mod http; pub mod package; pub mod platform; |
