use std::path::PathBuf;
use tokmd_settings::{Profile, TomlConfig, UserConfig, ViewProfile};
mod resolve;
pub use resolve::{
resolve_export, resolve_export_with_config, resolve_lang, resolve_lang_with_config,
resolve_module, resolve_module_with_config,
};
#[derive(Debug, Default)]
pub struct ConfigContext {
pub toml: Option<TomlConfig>,
pub toml_path: Option<PathBuf>,
pub json: Option<UserConfig>,
}
impl ConfigContext {
pub fn get_toml_view(&self, name: &str) -> Option<&ViewProfile> {
self.toml.as_ref().and_then(|t| t.view.get(name))
}
pub fn get_json_profile(&self, name: &str) -> Option<&Profile> {
self.json.as_ref().and_then(|c| c.profiles.get(name))
}
}
pub fn load_config() -> ConfigContext {
let toml_result = discover_toml_config();
let json = load_json_config();
ConfigContext {
toml: toml_result.as_ref().map(|(config, _)| config.clone()),
toml_path: toml_result.map(|(_, path)| path),
json,
}
}
fn sanitize_selector(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.chars().any(char::is_control) {
None
} else {
Some(trimmed.to_string())
}
}
fn discover_toml_config() -> Option<(TomlConfig, PathBuf)> {
if let Ok(config_path) = std::env::var("TOKMD_CONFIG")
&& let Some(config_path) = sanitize_selector(&config_path)
{
let path = PathBuf::from(&config_path);
if let Some(result) = try_load_toml(&path) {
return Some(result);
}
}
if let Ok(cwd) = std::env::current_dir() {
let mut dir = Some(cwd.as_path());
while let Some(d) = dir {
let config_path = d.join("tokmd.toml");
if let Some(result) = try_load_toml(&config_path) {
return Some(result);
}
dir = d.parent();
}
}
if let Some(config_dir) = dirs::config_dir() {
let user_config_path = config_dir.join("tokmd").join("tokmd.toml");
if let Some(result) = try_load_toml(&user_config_path) {
return Some(result);
}
}
None
}
fn try_load_toml(path: &std::path::Path) -> Option<(TomlConfig, PathBuf)> {
if path.exists() {
TomlConfig::from_file(path)
.ok()
.map(|config| (config, path.to_path_buf()))
} else {
None
}
}
fn load_json_config() -> Option<UserConfig> {
let config_dir = dirs::config_dir()?.join("tokmd");
let config_path = config_dir.join("config.json");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path).ok()?;
serde_json::from_str(&content).ok()
} else {
None
}
}
pub fn get_profile_name(cli_profile: Option<&String>) -> Option<String> {
if let Some(name) = cli_profile
&& let Some(name) = sanitize_selector(name)
{
return Some(name);
}
std::env::var("TOKMD_PROFILE")
.ok()
.and_then(|name| sanitize_selector(&name))
}
pub fn resolve_profile<'a>(
config: &'a Option<UserConfig>,
name: Option<&String>,
) -> Option<&'a Profile> {
config.as_ref().and_then(|c| {
let key = name.map(|s| s.as_str()).unwrap_or("default");
c.profiles.get(key)
})
}
#[derive(Debug, Default)]
pub struct ResolvedConfig<'a> {
pub toml_view: Option<&'a ViewProfile>,
pub json_profile: Option<&'a Profile>,
pub toml: Option<&'a TomlConfig>,
}
impl ResolvedConfig<'_> {
pub fn format(&self) -> Option<&str> {
self.toml_view
.and_then(|v| v.format.as_deref())
.or_else(|| self.json_profile.and_then(|p| p.format.as_deref()))
}
pub fn top(&self) -> Option<usize> {
self.toml_view
.and_then(|v| v.top)
.or_else(|| self.json_profile.and_then(|p| p.top))
}
pub fn files(&self) -> Option<bool> {
self.toml_view
.and_then(|v| v.files)
.or_else(|| self.json_profile.and_then(|p| p.files))
}
pub fn module_roots(&self) -> Option<Vec<String>> {
self.toml_view
.and_then(|v| v.module_roots.clone())
.or_else(|| self.toml.and_then(|t| t.module.roots.clone()))
.or_else(|| self.json_profile.and_then(|p| p.module_roots.clone()))
}
pub fn module_depth(&self) -> Option<usize> {
self.toml_view
.and_then(|v| v.module_depth)
.or_else(|| self.toml.and_then(|t| t.module.depth))
.or_else(|| self.json_profile.and_then(|p| p.module_depth))
}
pub fn children(&self) -> Option<&str> {
self.toml_view
.and_then(|v| v.children.as_deref())
.or_else(|| self.toml.and_then(|t| t.module.children.as_deref()))
.or_else(|| self.json_profile.and_then(|p| p.children.as_deref()))
}
pub fn min_code(&self) -> Option<usize> {
self.toml_view
.and_then(|v| v.min_code)
.or_else(|| self.toml.and_then(|t| t.export.min_code))
.or_else(|| self.json_profile.and_then(|p| p.min_code))
}
pub fn max_rows(&self) -> Option<usize> {
self.toml_view
.and_then(|v| v.max_rows)
.or_else(|| self.toml.and_then(|t| t.export.max_rows))
.or_else(|| self.json_profile.and_then(|p| p.max_rows))
}
pub fn redact(&self) -> Option<&str> {
self.toml_view
.and_then(|v| v.redact.as_deref())
.or_else(|| self.toml.and_then(|t| t.export.redact.as_deref()))
}
pub fn meta(&self) -> Option<bool> {
self.toml_view
.and_then(|v| v.meta)
.or_else(|| self.json_profile.and_then(|p| p.meta))
}
}
pub fn resolve_config<'a>(
ctx: &'a ConfigContext,
profile_name: Option<&str>,
) -> ResolvedConfig<'a> {
let toml_view = profile_name.and_then(|name| ctx.get_toml_view(name));
let json_profile = profile_name.and_then(|name| ctx.get_json_profile(name));
ResolvedConfig {
toml_view,
json_profile,
toml: ctx.toml.as_ref(),
}
}
#[cfg(test)]
mod tests {
use super::{get_profile_name, sanitize_selector};
#[test]
fn sanitize_selector_rejects_empty_and_control_values() {
assert_eq!(sanitize_selector(" "), None);
assert_eq!(sanitize_selector("bad\nvalue"), None);
assert_eq!(sanitize_selector("bad\0value"), None);
}
#[test]
fn sanitize_selector_trims_safe_values() {
assert_eq!(sanitize_selector(" default ").as_deref(), Some("default"));
}
#[test]
fn get_profile_name_sanitizes_cli_value() {
let cli_value = " secure-profile ".to_string();
assert_eq!(
get_profile_name(Some(&cli_value)).as_deref(),
Some("secure-profile")
);
}
#[test]
fn get_profile_name_rejects_control_char_cli_value() {
let cli_value = "bad\nprofile".to_string();
assert_eq!(get_profile_name(Some(&cli_value)), None);
}
}