waddling_errors_hash/
config_loader.rs

1//! Configuration loader for reading global settings from Cargo.toml
2//!
3//! This module provides functionality to read hash and doc generation configuration from
4//! `[package.metadata.waddling-errors]` in Cargo.toml at compile time.
5//!
6//! # Configuration Format
7//!
8//! ```toml
9//! [package.metadata.waddling-errors]
10//! # Hash configuration (optional - defaults to WDP standard seed)
11//! hash_seed = "0x000031762D706477"  # WDP v1 seed (default)
12//!
13//! # Doc generation configuration
14//! doc_formats = ["json", "html"]
15//! doc_output_dir = "target/doc"
16//! ```
17//!
18//! # Note
19//!
20//! WDP specifies xxHash3 as the only algorithm. Custom seeds are only
21//! for testing/isolation purposes - production should use the WDP seed.
22
23#[cfg(not(feature = "std"))]
24extern crate alloc;
25
26#[cfg(feature = "std")]
27use std::string::String;
28
29#[cfg(not(feature = "std"))]
30use alloc::string::String;
31
32use crate::algorithm::HashConfig;
33
34#[cfg(feature = "std")]
35use std::vec::Vec;
36
37#[cfg(not(feature = "std"))]
38use alloc::vec::Vec;
39
40/// Documentation generation configuration
41///
42/// Specifies which formats to generate and where to output them.
43#[derive(Debug, Clone)]
44pub struct DocGenConfig {
45    /// Formats to generate (e.g., ["json", "html"])
46    pub formats: Vec<String>,
47    /// Output directory for generated documentation
48    pub output_dir: String,
49    /// Optional namespace identifier for WDP Part 7 support
50    ///
51    /// When set, enables combined IDs (e.g., "nsHash-codeHash") for
52    /// multi-service environments. Format: lowercase with underscores.
53    ///
54    /// # Example
55    /// ```toml
56    /// [package.metadata.waddling-errors]
57    /// namespace = "auth_service"
58    /// ```
59    pub namespace: Option<String>,
60}
61
62impl Default for DocGenConfig {
63    fn default() -> Self {
64        Self {
65            formats: Vec::new(),
66            output_dir: String::from("target/doc"),
67            namespace: None,
68        }
69    }
70}
71
72/// Load global hash configuration from available sources
73///
74/// WDP specifies xxHash3 as the only algorithm. This function loads
75/// a custom seed if configured, otherwise uses the WDP standard seed.
76///
77/// # Priority Order
78/// 1. Environment variable `WADDLING_HASH_SEED` (highest priority)
79/// 2. Cargo.toml metadata `hash_seed`
80/// 3. Default WDP seed (0x000031762D706477)
81///
82/// # Examples
83///
84/// ```
85/// use waddling_errors_hash::config_loader::load_global_config;
86///
87/// let config = load_global_config();
88/// // Returns: xxHash3 + WDP seed (if no custom seed configured)
89/// ```
90pub fn load_global_config() -> HashConfig {
91    // Priority 1: Environment variables
92    if let Some(config) = load_from_env() {
93        return config;
94    }
95
96    // Priority 2: Cargo.toml metadata (compile-time only)
97    #[cfg(feature = "std")]
98    if let Some(config) = load_from_cargo_toml() {
99        return config;
100    }
101
102    // Priority 3: Default WDP config
103    HashConfig::wdp_compliant()
104}
105
106/// Load hash configuration from environment variables
107///
108/// Checks for:
109/// - `WADDLING_HASH_SEED` - Custom seed as hex (e.g., "0x12345678") or decimal
110///
111/// # Examples
112///
113/// ```bash
114/// WADDLING_HASH_SEED=0x12345678 cargo build
115/// ```
116fn load_from_env() -> Option<HashConfig> {
117    // Check if seed is set
118    let seed_str = option_env!("WADDLING_HASH_SEED")?;
119
120    // Parse seed (hex or decimal)
121    let seed = parse_seed(seed_str)?;
122
123    Some(HashConfig::with_seed(seed))
124}
125
126/// Parse a seed string into u64
127///
128/// Supports:
129/// - Hex: "0x12345678" or "0X12345678"
130/// - Decimal: "12345678"
131fn parse_seed(s: &str) -> Option<u64> {
132    let s = s.trim();
133    if s.starts_with("0x") || s.starts_with("0X") {
134        u64::from_str_radix(&s[2..], 16).ok()
135    } else {
136        s.parse().ok()
137    }
138}
139
140/// Load hash configuration from Cargo.toml metadata
141///
142/// Reads from `[package.metadata.waddling-errors]`:
143/// ```toml
144/// [package.metadata.waddling-errors]
145/// hash_seed = "0x000031762D706477"
146/// ```
147#[cfg(feature = "std")]
148fn load_from_cargo_toml() -> Option<HashConfig> {
149    // Try to read CARGO_MANIFEST_DIR environment variable
150    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
151    let manifest_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
152
153    // Read Cargo.toml file
154    let cargo_toml_content = std::fs::read_to_string(manifest_path).ok()?;
155
156    // Parse hash_seed
157    let seed_str = parse_toml_value(&cargo_toml_content, "hash_seed")?;
158    let seed = parse_seed(&seed_str)?;
159
160    Some(HashConfig::with_seed(seed))
161}
162
163/// Simple TOML parser for extracting values from `[package.metadata.waddling-errors]`
164///
165/// This is a minimal parser that looks for lines like:
166/// ```toml
167/// key = "value"
168/// ```
169///
170/// within the `[package.metadata.waddling-errors]` section.
171#[cfg(feature = "std")]
172fn parse_toml_value(toml_content: &str, key: &str) -> Option<String> {
173    let mut in_section = false;
174
175    for line in toml_content.lines() {
176        let line = line.trim();
177
178        // Check if we're entering the waddling-errors metadata section
179        if line == "[package.metadata.waddling-errors]" {
180            in_section = true;
181            continue;
182        }
183
184        // Check if we're leaving the section (new section starts)
185        if in_section && line.starts_with('[') {
186            break;
187        }
188
189        // Parse key-value pair if we're in the section
190        if in_section && line.contains('=') {
191            let parts: Vec<&str> = line.splitn(2, '=').collect();
192            if parts.len() == 2 {
193                let found_key = parts[0].trim();
194                let value = parts[1].trim();
195
196                if found_key == key {
197                    // Remove quotes from value
198                    let value = value.trim_matches('"').trim_matches('\'');
199                    return Some(value.to_string());
200                }
201            }
202        }
203    }
204
205    None
206}
207
208/// Create a hash config with optional seed override
209///
210/// This helper function applies a per-diagnostic seed override on top of the global config.
211///
212/// # Examples
213///
214/// ```
215/// use waddling_errors_hash::config_loader::{load_global_config, apply_overrides};
216///
217/// let global = load_global_config();
218///
219/// // Override seed
220/// let config = apply_overrides(&global, Some(0x12345678));
221///
222/// // No override
223/// let config = apply_overrides(&global, None);
224/// ```
225pub fn apply_overrides(global: &HashConfig, seed: Option<u64>) -> HashConfig {
226    match seed {
227        Some(s) => HashConfig::with_seed(s),
228        None => global.clone(),
229    }
230}
231
232/// Load global namespace configuration from available sources
233///
234/// This function checks multiple sources in priority order:
235/// 1. Environment variable `WADDLING_NAMESPACE` (highest priority)
236/// 2. Cargo.toml metadata `namespace` field
237/// 3. None (no namespace)
238///
239/// # Examples
240///
241/// ```toml
242/// [package.metadata.waddling-errors]
243/// namespace = "auth_service"
244/// ```
245///
246/// ```
247/// use waddling_errors_hash::config_loader::load_namespace;
248///
249/// let namespace = load_namespace();
250/// // Returns Some("auth_service") if configured, None otherwise
251/// ```
252#[cfg(feature = "std")]
253pub fn load_namespace() -> Option<String> {
254    // Priority 1: Environment variable
255    if let Ok(ns) = std::env::var("WADDLING_NAMESPACE")
256        && !ns.is_empty()
257    {
258        return Some(ns);
259    }
260
261    // Priority 2: Cargo.toml metadata
262    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
263    let manifest_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
264    let cargo_toml_content = std::fs::read_to_string(manifest_path).ok()?;
265
266    parse_toml_value(&cargo_toml_content, "namespace")
267}
268
269/// Load global documentation generation configuration from available sources
270///
271/// This function checks multiple sources in priority order:
272/// 1. Environment variables (highest priority)
273/// 2. Cargo.toml metadata (if available)
274/// 3. Default (no doc generation)
275///
276/// # Examples
277///
278/// ```
279/// use waddling_errors_hash::config_loader::load_doc_gen_config;
280///
281/// let config = load_doc_gen_config();
282/// // Returns default: empty formats, "target/doc" output
283/// ```
284pub fn load_doc_gen_config() -> DocGenConfig {
285    // Priority 1: Environment variables
286    #[cfg(feature = "std")]
287    {
288        if let Ok(env_formats) = std::env::var("WADDLING_DOC_FORMATS") {
289            let formats: Vec<String> = env_formats
290                .split(',')
291                .map(|s| s.trim().to_string())
292                .filter(|s| !s.is_empty())
293                .collect();
294
295            let output_dir = std::env::var("WADDLING_DOC_OUTPUT_DIR")
296                .unwrap_or_else(|_| "target/doc".to_string());
297
298            let namespace = std::env::var("WADDLING_NAMESPACE").ok();
299
300            return DocGenConfig {
301                formats,
302                output_dir,
303                namespace,
304            };
305        }
306    }
307
308    // Priority 2: Cargo.toml metadata
309    #[cfg(feature = "std")]
310    if let Some(config) = load_doc_config_from_cargo_toml() {
311        return config;
312    }
313
314    // Priority 3: Default
315    DocGenConfig::default()
316}
317
318/// Load doc config from Cargo.toml
319#[cfg(feature = "std")]
320fn load_doc_config_from_cargo_toml() -> Option<DocGenConfig> {
321    // Try to read CARGO_MANIFEST_DIR environment variable
322    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
323    let manifest_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
324
325    // Read Cargo.toml file
326    let cargo_toml_content = std::fs::read_to_string(manifest_path).ok()?;
327
328    // Parse doc_formats (array)
329    let formats = parse_toml_array(&cargo_toml_content, "doc_formats").unwrap_or_default();
330
331    // Parse doc_output_dir (string, optional)
332    let output_dir = parse_toml_value(&cargo_toml_content, "doc_output_dir")
333        .unwrap_or_else(|| "target/doc".to_string());
334
335    // Parse namespace (string, optional) - WDP Part 7 support
336    let namespace = parse_toml_value(&cargo_toml_content, "namespace");
337
338    // Return config if namespace is set OR formats are specified
339    if formats.is_empty() && namespace.is_none() {
340        None
341    } else {
342        Some(DocGenConfig {
343            formats,
344            output_dir,
345            namespace,
346        })
347    }
348}
349
350/// Parse TOML array values from `[package.metadata.waddling-errors]`
351///
352/// This is a minimal parser that looks for lines like:
353/// ```toml
354/// key = ["value1", "value2"]
355/// ```
356///
357/// within the `[package.metadata.waddling-errors]` section.
358#[cfg(feature = "std")]
359fn parse_toml_array(toml_content: &str, key: &str) -> Option<Vec<String>> {
360    let mut in_section = false;
361
362    for line in toml_content.lines() {
363        let line = line.trim();
364
365        // Check if we're entering the waddling-errors metadata section
366        if line == "[package.metadata.waddling-errors]" {
367            in_section = true;
368            continue;
369        }
370
371        // Check if we're leaving the section (new section starts)
372        if in_section && line.starts_with('[') {
373            break;
374        }
375
376        // Parse key-value pair if we're in the section
377        if in_section && line.contains('=') {
378            let parts: Vec<&str> = line.splitn(2, '=').collect();
379            if parts.len() == 2 {
380                let found_key = parts[0].trim();
381                let value = parts[1].trim();
382
383                if found_key == key {
384                    // Parse array: ["item1", "item2"]
385                    if value.starts_with('[') && value.ends_with(']') {
386                        let array_content =
387                            value.trim_start_matches('[').trim_end_matches(']').trim();
388
389                        let items: Vec<String> = array_content
390                            .split(',')
391                            .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string())
392                            .filter(|s| !s.is_empty())
393                            .collect();
394
395                        return Some(items);
396                    }
397                }
398            }
399        }
400    }
401
402    None
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::algorithm::{HashAlgorithm, WDP_SEED};
409
410    #[test]
411    fn test_load_global_config_default() {
412        // Without any config, should return default (WDP-compliant)
413        let config = load_global_config();
414        assert_eq!(config.algorithm, HashAlgorithm::XxHash3);
415        assert_eq!(config.seed, WDP_SEED);
416    }
417
418    #[test]
419    fn test_apply_overrides_with_seed() {
420        let global = HashConfig::default();
421        let config = apply_overrides(&global, Some(0x12345678));
422
423        assert_eq!(config.algorithm, HashAlgorithm::XxHash3);
424        assert_eq!(config.seed, 0x12345678);
425    }
426
427    #[test]
428    fn test_apply_overrides_none() {
429        let global = HashConfig::with_seed(0x87654321);
430        let config = apply_overrides(&global, None);
431
432        assert_eq!(config.algorithm, HashAlgorithm::XxHash3);
433        assert_eq!(config.seed, 0x87654321);
434    }
435
436    #[test]
437    fn test_parse_seed_hex() {
438        assert_eq!(parse_seed("0x12345678"), Some(0x12345678));
439        assert_eq!(parse_seed("0X12345678"), Some(0x12345678));
440        assert_eq!(parse_seed("0x000031762D706477"), Some(WDP_SEED));
441    }
442
443    #[test]
444    fn test_parse_seed_decimal() {
445        assert_eq!(parse_seed("12345678"), Some(12345678));
446        assert_eq!(parse_seed("0"), Some(0));
447    }
448
449    #[cfg(feature = "std")]
450    #[test]
451    fn test_parse_toml_value() {
452        let toml = r#"
453[package]
454name = "test"
455
456[package.metadata.waddling-errors]
457hash_seed = "0x12345678"
458namespace = "auth_service"
459
460[dependencies]
461some_dep = "1.0"
462"#;
463
464        assert_eq!(
465            parse_toml_value(toml, "hash_seed"),
466            Some("0x12345678".to_string())
467        );
468        assert_eq!(
469            parse_toml_value(toml, "namespace"),
470            Some("auth_service".to_string())
471        );
472        assert_eq!(parse_toml_value(toml, "nonexistent"), None);
473    }
474
475    #[cfg(feature = "std")]
476    #[test]
477    fn test_parse_toml_value_single_quotes() {
478        let toml = r#"
479[package.metadata.waddling-errors]
480hash_seed = '0x87654321'
481"#;
482
483        assert_eq!(
484            parse_toml_value(toml, "hash_seed"),
485            Some("0x87654321".to_string())
486        );
487    }
488
489    #[cfg(feature = "std")]
490    #[test]
491    fn test_parse_toml_value_section_boundary() {
492        let toml = r#"
493[package.metadata.waddling-errors]
494hash_seed = "0x12345678"
495
496[package.metadata.other]
497hash_seed = "ShouldNotMatch"
498"#;
499
500        // Should only get the value from waddling-errors section
501        assert_eq!(
502            parse_toml_value(toml, "hash_seed"),
503            Some("0x12345678".to_string())
504        );
505    }
506}