lex_config/
lib.rs

1//! Shared configuration loader for the Lex toolchain.
2//!
3//! `defaults/lex.default.toml` is embedded into every binary so that docs and
4//! runtime behavior stay in sync. Applications layer user-specific files on top
5//! of those defaults via [`Loader`] before deserializing into [`LexConfig`].
6
7use config::builder::DefaultState;
8use config::{Config, ConfigBuilder, ConfigError, File, FileFormat, ValueKind};
9use lex_babel::formats::lex::formatting_rules::FormattingRules;
10use serde::Deserialize;
11use std::path::Path;
12
13const DEFAULT_TOML: &str = include_str!("../defaults/lex.default.toml");
14
15/// Top-level configuration consumed by Lex applications.
16#[derive(Debug, Clone, Deserialize)]
17pub struct LexConfig {
18    pub formatting: FormattingConfig,
19    pub inspect: InspectConfig,
20    pub convert: ConvertConfig,
21}
22
23/// Formatting-related configuration groups.
24#[derive(Debug, Clone, Deserialize)]
25pub struct FormattingConfig {
26    pub rules: FormattingRulesConfig,
27}
28
29/// Mirrors the knobs exposed by the Lex formatter.
30#[derive(Debug, Clone, Deserialize)]
31pub struct FormattingRulesConfig {
32    pub session_blank_lines_before: usize,
33    pub session_blank_lines_after: usize,
34    pub normalize_seq_markers: bool,
35    pub unordered_seq_marker: char,
36    pub max_blank_lines: usize,
37    pub indent_string: String,
38    pub preserve_trailing_blanks: bool,
39    pub normalize_verbatim_markers: bool,
40}
41
42impl From<FormattingRulesConfig> for FormattingRules {
43    fn from(config: FormattingRulesConfig) -> Self {
44        FormattingRules {
45            session_blank_lines_before: config.session_blank_lines_before,
46            session_blank_lines_after: config.session_blank_lines_after,
47            normalize_seq_markers: config.normalize_seq_markers,
48            unordered_seq_marker: config.unordered_seq_marker,
49            max_blank_lines: config.max_blank_lines,
50            indent_string: config.indent_string,
51            preserve_trailing_blanks: config.preserve_trailing_blanks,
52            normalize_verbatim_markers: config.normalize_verbatim_markers,
53        }
54    }
55}
56
57impl From<&FormattingRulesConfig> for FormattingRules {
58    fn from(config: &FormattingRulesConfig) -> Self {
59        FormattingRules {
60            session_blank_lines_before: config.session_blank_lines_before,
61            session_blank_lines_after: config.session_blank_lines_after,
62            normalize_seq_markers: config.normalize_seq_markers,
63            unordered_seq_marker: config.unordered_seq_marker,
64            max_blank_lines: config.max_blank_lines,
65            indent_string: config.indent_string.clone(),
66            preserve_trailing_blanks: config.preserve_trailing_blanks,
67            normalize_verbatim_markers: config.normalize_verbatim_markers,
68        }
69    }
70}
71
72/// Controls AST-related inspect output.
73#[derive(Debug, Clone, Deserialize)]
74pub struct InspectConfig {
75    pub ast: InspectAstConfig,
76    pub nodemap: NodemapConfig,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80pub struct InspectAstConfig {
81    pub include_all_properties: bool,
82    pub show_line_numbers: bool,
83}
84
85#[derive(Debug, Clone, Deserialize)]
86pub struct NodemapConfig {
87    pub color_blocks: bool,
88    pub color_characters: bool,
89    pub show_summary: bool,
90}
91
92/// Format-specific conversion knobs.
93#[derive(Debug, Clone, Deserialize)]
94pub struct ConvertConfig {
95    pub pdf: PdfConfig,
96    pub html: HtmlConfig,
97}
98
99#[derive(Debug, Clone, Deserialize)]
100pub struct PdfConfig {
101    pub size: PdfPageSize,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
105pub enum PdfPageSize {
106    #[serde(rename = "lexed")]
107    LexEd,
108    #[serde(rename = "mobile")]
109    Mobile,
110}
111
112#[derive(Debug, Clone, Deserialize)]
113pub struct HtmlConfig {
114    pub theme: String,
115    /// Optional path to a custom CSS file to append after the baseline CSS.
116    #[serde(default)]
117    pub custom_css: Option<String>,
118}
119
120/// Helper for layering user overrides over the built-in defaults.
121#[derive(Debug, Clone)]
122pub struct Loader {
123    builder: ConfigBuilder<DefaultState>,
124}
125
126impl Loader {
127    /// Start a loader seeded with the embedded defaults.
128    pub fn new() -> Self {
129        let builder = Config::builder().add_source(File::from_str(DEFAULT_TOML, FileFormat::Toml));
130        Self { builder }
131    }
132
133    /// Layer a configuration file. Missing files trigger an error.
134    pub fn with_file(mut self, path: impl AsRef<Path>) -> Self {
135        let source = File::from(path.as_ref())
136            .format(FileFormat::Toml)
137            .required(true);
138        self.builder = self.builder.add_source(source);
139        self
140    }
141
142    /// Layer an optional configuration file (ignored if the file is absent).
143    pub fn with_optional_file(mut self, path: impl AsRef<Path>) -> Self {
144        let source = File::from(path.as_ref())
145            .format(FileFormat::Toml)
146            .required(false);
147        self.builder = self.builder.add_source(source);
148        self
149    }
150
151    /// Apply a single key/value override (useful for CLI settings).
152    pub fn set_override<I>(mut self, key: &str, value: I) -> Result<Self, ConfigError>
153    where
154        I: Into<ValueKind>,
155    {
156        self.builder = self.builder.set_override(key, value)?;
157        Ok(self)
158    }
159
160    /// Finalize the builder and deserialize the resulting configuration.
161    pub fn build(self) -> Result<LexConfig, ConfigError> {
162        self.builder.build()?.try_deserialize()
163    }
164}
165
166impl Default for Loader {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// Convenience helper for callers that only need the defaults.
173pub fn load_defaults() -> Result<LexConfig, ConfigError> {
174    Loader::new().build()
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn loads_default_config() {
183        let config = load_defaults().expect("defaults to deserialize");
184        assert_eq!(config.formatting.rules.session_blank_lines_before, 1);
185        assert!(config.inspect.ast.show_line_numbers);
186        assert_eq!(config.convert.pdf.size, PdfPageSize::LexEd);
187    }
188
189    #[test]
190    fn supports_overrides() {
191        let config = Loader::new()
192            .set_override("convert.pdf.size", "mobile")
193            .expect("override to apply")
194            .build()
195            .expect("config to build");
196        assert_eq!(config.convert.pdf.size, PdfPageSize::Mobile);
197    }
198
199    #[test]
200    fn formatting_rules_config_converts_to_formatting_rules() {
201        let config = load_defaults().expect("defaults to deserialize");
202        let rules: FormattingRules = config.formatting.rules.into();
203        assert_eq!(rules.session_blank_lines_before, 1);
204        assert_eq!(rules.session_blank_lines_after, 1);
205        assert!(rules.normalize_seq_markers);
206        assert_eq!(rules.unordered_seq_marker, '-');
207        assert_eq!(rules.max_blank_lines, 2);
208        assert_eq!(rules.indent_string, "    ");
209        assert!(!rules.preserve_trailing_blanks);
210        assert!(rules.normalize_verbatim_markers);
211    }
212}