aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-05 20:34:27 +0900
committernsfisis <nsfisis@gmail.com>2026-05-05 20:44:37 +0900
commit5254a9e9b698c3618229f4f802b39a82baf9169a (patch)
treed89127778b56ee0421d9838a1dc462eaa7599eab /crates/mozart-core
parent884d9ab32bbca7a8ec5c7ee7d42cbde0e7e6babf (diff)
downloadphp-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')
-rw-r--r--crates/mozart-core/src/composer.rs3
-rw-r--r--crates/mozart-core/src/factory.rs211
-rw-r--r--crates/mozart-core/src/lib.rs1
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;