Skip to main content

cfgmatic_source/
lib.rs

1//! # cfgmatic-source
2//!
3//! Configuration sources (file, env, remote) for cfgmatic framework.
4//!
5//! This crate provides a unified interface for loading configuration
6//! from various sources: files, environment variables, and remote URLs.
7//!
8//! ## Architecture
9//!
10//! ```mermaid
11//! graph TB
12//!     subgraph "Public API"
13//!         LIB[lib.rs Facade]
14//!         PRELUDE[prelude]
15//!         FUNC[Convenience Functions]
16//!     end
17//!
18//!     subgraph "Config Layer"
19//!         OPT[LoaderOptions]
20//!         CTX[LoadContext]
21//!     end
22//!
23//!     subgraph "Application Layer"
24//!         LOADER[Loader]
25//!         DETECTOR[FormatDetector]
26//!     end
27//!
28//!     subgraph "Infrastructure Layer"
29//!         FS[FileSource]
30//!         ES[EnvSource]
31//!         MS[MemorySource]
32//!         CS[CompositeSource]
33//!         REG[SourceRegistry]
34//!     end
35//!
36//!     subgraph "Domain Layer"
37//!         SRC[Source Trait]
38//!         KIND[SourceKind]
39//!         META[SourceMetadata]
40//!         FMT[Format]
41//!         RAW[RawContent]
42//!         PARSED[ParsedContent]
43//!         ERR[SourceError]
44//!     end
45//!
46//!     LIB --> PRELUDE
47//!     LIB --> FUNC
48//!     LIB --> OPT
49//!     LIB --> LOADER
50//!     LIB --> SRC
51//!
52//!     FUNC --> LOADER
53//!     LOADER --> REG
54//!     REG --> FS
55//!     REG --> ES
56//!     REG --> MS
57//!     REG --> CS
58//!
59//!     FS --> SRC
60//!     ES --> SRC
61//!     MS --> SRC
62//!     CS --> SRC
63//!
64//!     SRC --> RAW
65//!     RAW --> FMT
66//!     FMT --> PARSED
67//!     DETECTOR --> FMT
68//!
69//!     OPT --> CTX
70//!     CTX --> LOADER
71//! ```
72//!
73//! ## Layer Dependencies
74//!
75//! The architecture follows the Onion (Layered) pattern with dependencies
76//! pointing inward:
77//!
78//! ```text
79//! +------------------------------------------+
80//! |              lib.rs (Facade)              |
81//! +------------------------------------------+
82//!                     |
83//! +------------------------------------------+
84//! |             Config Layer                  |
85//! |  Options, Context, Builders               |
86//! +------------------------------------------+
87//!                     |
88//! +------------------------------------------+
89//! |          Infrastructure Layer             |
90//! |  FileSource, EnvSource, MemorySource,     |
91//! |  CompositeSource, Registry                |
92//! +------------------------------------------+
93//!                     |
94//! +------------------------------------------+
95//! |           Application Layer               |
96//! |  Loader, FormatDetector                   |
97//! +------------------------------------------+
98//!                     |
99//! +------------------------------------------+
100//! |             Domain Layer                  |
101//! |  Source, SourceKind, Format,              |
102//! |  RawContent, ParsedContent, Error         |
103//! +------------------------------------------+
104//! ```
105//!
106//! ## Feature Flags
107//!
108//! | Feature   | Description                        | Default |
109//! |-----------|------------------------------------|---------|
110//! | `file`    | File-based sources                 | Yes     |
111//! | `env`     | Environment variable sources       | No      |
112//! | `remote`  | Remote HTTP/HTTPS sources          | No      |
113//! | `toml`    | TOML format support                | Yes     |
114//! | `json`    | JSON format support                | Yes     |
115//! | `yaml`    | YAML format support                | No      |
116//! | `async`   | Async source loading               | No      |
117//! | `watch`   | File watching support              | No      |
118//!
119//! ## Quick Start
120//!
121//! ### Load from a file
122//!
123//! ```rust,ignore
124//! use cfgmatic_source::prelude::*;
125//!
126//! let content = FileSource::new("config.toml")
127//!     .load()?
128//!     .parse_as::<MyConfig>()?;
129//! ```
130//!
131//! ### Load from environment
132//!
133//! ```rust,ignore
134//! use cfgmatic_source::prelude::*;
135//!
136//! let content = EnvSource::new("APP")
137//!     .load()?
138//!     .parse_as::<MyConfig>()?;
139//! ```
140//!
141//! ### Combine multiple sources
142//!
143//! ```rust,ignore
144//! use cfgmatic_source::prelude::*;
145//!
146//! let source = CompositeSource::new()
147//!     .with_source(FileSource::new("config.toml"), SourcePriority::High)
148//!     .with_source(EnvSource::new("APP"), SourcePriority::Low);
149//!
150//! let content = source.load()?.parse_as::<MyConfig>()?;
151//! ```
152//!
153//! ### Use convenience functions
154//!
155//! ```rust,ignore
156//! use cfgmatic_source::{load, load_from_file, load_from_env};
157//!
158//! // Auto-detect source and load
159//! let config: MyConfig = load()?;
160//!
161//! // Load from specific file
162//! let config: MyConfig = load_from_file("config.toml")?;
163//!
164//! // Load from environment
165//! let config: MyConfig = load_from_env("APP")?;
166//! ```
167
168#![warn(missing_docs)]
169// Allow some pedantic lints that are too noisy
170#![allow(clippy::missing_const_for_fn)]
171#![allow(clippy::doc_markdown)]
172#![allow(clippy::needless_pass_by_value)]
173#![allow(clippy::option_if_let_else)]
174#![allow(clippy::use_self)]
175#![allow(clippy::derivable_impls)]
176#![allow(clippy::trivially_copy_pass_by_ref)]
177#![allow(clippy::redundant_closure_for_method_calls)]
178#![allow(clippy::unused_self)]
179#![allow(clippy::cast_precision_loss)]
180#![allow(clippy::unnecessary_lazy_evaluations)]
181#![allow(clippy::uninlined_format_args)]
182#![allow(clippy::single_match_else)]
183#![allow(clippy::should_implement_trait)]
184#![allow(clippy::needless_continue)]
185#![allow(clippy::missing_fields_in_debug)]
186#![allow(clippy::struct_excessive_bools)]
187#![allow(clippy::cast_possible_truncation)]
188#![allow(clippy::map_unwrap_or)]
189#![allow(clippy::collapsible_if)]
190
191pub mod constants;
192pub mod domain;
193pub mod infrastructure;
194
195// Placeholder modules for future implementation
196/// Config layer - options and context.
197pub mod config;
198
199/// Application layer - services.
200pub mod application;
201
202// Re-exports from domain layer
203pub use domain::{
204    Format, ParsedContent, RawContent, Result, Source, SourceError, SourceKind, SourceMetadata,
205};
206
207// Re-exports from infrastructure layer
208pub use infrastructure::{
209    CompositeSource, CompositeSourceBuilder, MemorySource, MemorySourceBuilder, SourcePriority,
210};
211
212#[cfg(feature = "file")]
213pub use infrastructure::{FileSource, FileSourceBuilder};
214
215#[cfg(feature = "env")]
216pub use infrastructure::{EnvConfig, EnvSource, EnvSourceBuilder};
217
218// Re-exports from config layer
219pub use config::{
220    LoadOptions, LoadOptionsBuilder, MergeStrategy, SourceConfig, SourceConfigBuilder,
221};
222
223// Re-exports from application layer
224pub use application::{
225    LoadResult, Loader, LoaderBuilder, SourceCoordinator, SourceCoordinatorBuilder,
226};
227
228// Re-exports from constants
229pub use constants::{
230    DEFAULT_APP_NAME, DEFAULT_CONFIG_BASE_NAME, DEFAULT_DEBOUNCE_MS, DEFAULT_ENV_PREFIX,
231    DEFAULT_EXTENSIONS, DEFAULT_REMOTE_TIMEOUT_SECS, ENV_KEY_SEPARATOR, JSON_POINTER_ROOT,
232    MAX_REMOTE_RETRIES, MAX_SEARCH_DEPTH,
233};
234
235pub mod prelude {
236    //! Common imports for cfgmatic-source.
237    //!
238    //! This module re-exports the most commonly used types and traits
239    //! for convenience.
240
241    // Domain layer
242    pub use crate::domain::{
243        Format, ParsedContent, RawContent, Result, Source, SourceError, SourceKind, SourceMetadata,
244    };
245
246    // Infrastructure layer
247    pub use crate::infrastructure::{
248        CompositeSource, CompositeSourceBuilder, MemorySource, MemorySourceBuilder, SourcePriority,
249    };
250
251    #[cfg(feature = "file")]
252    pub use crate::infrastructure::{FileSource, FileSourceBuilder};
253
254    #[cfg(feature = "env")]
255    pub use crate::infrastructure::{EnvConfig, EnvSource, EnvSourceBuilder};
256
257    // Config layer
258    pub use crate::config::{
259        LoadOptions, LoadOptionsBuilder, MergeStrategy, SourceConfig, SourceConfigBuilder,
260    };
261
262    // Application layer
263    pub use crate::application::{
264        LoadResult, Loader, LoaderBuilder, SourceCoordinator, SourceCoordinatorBuilder,
265    };
266
267    // Constants
268    pub use crate::constants::{
269        DEFAULT_APP_NAME, DEFAULT_CONFIG_BASE_NAME, DEFAULT_DEBOUNCE_MS, DEFAULT_ENV_PREFIX,
270        DEFAULT_EXTENSIONS, DEFAULT_REMOTE_TIMEOUT_SECS, ENV_KEY_SEPARATOR, JSON_POINTER_ROOT,
271        MAX_REMOTE_RETRIES, MAX_SEARCH_DEPTH,
272    };
273}
274
275// =============================================================================
276// Convenience Functions
277// =============================================================================
278
279/// Load configuration from auto-detected source.
280///
281/// This function attempts to load configuration from the following sources
282/// in order of priority:
283///
284/// 1. Environment variables (if `env` feature is enabled)
285/// 2. Configuration file in current directory (if `file` feature is enabled)
286///
287/// # Errors
288///
289/// Returns [`SourceError`] if no configuration source can be found or
290/// if loading fails.
291///
292/// # Example
293///
294/// ```rust,ignore
295/// use cfgmatic_source::load;
296/// use serde::Deserialize;
297///
298/// #[derive(Deserialize)]
299/// struct Config {
300///     name: String,
301///     port: u16,
302/// }
303///
304/// let config: Config = load()?;
305/// ```
306#[cfg(feature = "file")]
307pub fn load<T>() -> crate::domain::Result<T>
308where
309    T: serde::de::DeserializeOwned,
310{
311    // Try to find and load config file
312    let source = FileSource::new("config.toml");
313    let raw = source.load_raw()?;
314    let content = raw
315        .as_str()
316        .map_err(|e| SourceError::serialization(&e.to_string()))?;
317    let format = source.detect_format().unwrap_or(Format::Toml);
318    format.parse_as(content.as_ref())
319}
320
321/// Load configuration from a specific file.
322///
323/// # Errors
324///
325/// Returns [`SourceError`] if the file cannot be read or parsed.
326///
327/// # Example
328///
329/// ```rust,ignore
330/// use cfgmatic_source::load_from_file;
331///
332/// let config: MyConfig = load_from_file("config.toml")?;
333/// ```
334#[cfg(feature = "file")]
335pub fn load_from_file<T, P>(path: P) -> crate::domain::Result<T>
336where
337    T: serde::de::DeserializeOwned,
338    P: AsRef<std::path::Path> + Into<std::path::PathBuf>,
339{
340    let source = FileSource::new(path);
341    let raw = source.load_raw()?;
342    let content = raw
343        .as_str()
344        .map_err(|e| SourceError::serialization(&e.to_string()))?;
345    let format = source.detect_format().unwrap_or(Format::Toml);
346    format.parse_as(content.as_ref())
347}
348
349/// Load configuration from environment variables.
350///
351/// # Errors
352///
353/// Returns [`SourceError`] if environment variables cannot be read or parsed.
354///
355/// # Example
356///
357/// ```rust,ignore
358/// use cfgmatic_source::load_from_env;
359///
360/// // Reads APP_NAME, APP_PORT, etc.
361/// let config: MyConfig = load_from_env("APP")?;
362/// ```
363#[cfg(feature = "env")]
364pub fn load_from_env<T>(prefix: &str) -> crate::domain::Result<T>
365where
366    T: serde::de::DeserializeOwned,
367{
368    let source = EnvSource::with_prefix(prefix);
369    let raw = source.load_raw()?;
370    let content = raw
371        .as_str()
372        .map_err(|e| SourceError::serialization(&e.to_string()))?;
373    // EnvSource always produces JSON
374    Format::Json.parse_as(content.as_ref())
375}
376
377/// Load configuration from memory (useful for testing).
378///
379/// # Errors
380///
381/// Returns [`SourceError`] if the content cannot be parsed.
382///
383/// # Example
384///
385/// ```rust,ignore
386/// use cfgmatic_source::load_from_memory;
387///
388/// let toml_content = r#"
389/// name = "test"
390/// port = 8080
391/// "#;
392///
393/// let config: MyConfig = load_from_memory(toml_content, Format::Toml)?;
394/// ```
395pub fn load_from_memory<T>(content: &str, format: Format) -> crate::domain::Result<T>
396where
397    T: serde::de::DeserializeOwned,
398{
399    format.parse_as(content)
400}