Skip to main content

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}
116
117/// Helper for layering user overrides over the built-in defaults.
118#[derive(Debug, Clone)]
119pub struct Loader {
120    builder: ConfigBuilder<DefaultState>,
121}
122
123impl Loader {
124    /// Start a loader seeded with the embedded defaults.
125    pub fn new() -> Self {
126        let builder = Config::builder().add_source(File::from_str(DEFAULT_TOML, FileFormat::Toml));
127        Self { builder }
128    }
129
130    /// Layer a configuration file. Missing files trigger an error.
131    pub fn with_file(mut self, path: impl AsRef<Path>) -> Self {
132        let source = File::from(path.as_ref())
133            .format(FileFormat::Toml)
134            .required(true);
135        self.builder = self.builder.add_source(source);
136        self
137    }
138
139    /// Layer an optional configuration file (ignored if the file is absent).
140    pub fn with_optional_file(mut self, path: impl AsRef<Path>) -> Self {
141        let source = File::from(path.as_ref())
142            .format(FileFormat::Toml)
143            .required(false);
144        self.builder = self.builder.add_source(source);
145        self
146    }
147
148    /// Apply a single key/value override (useful for CLI settings).
149    pub fn set_override<I>(mut self, key: &str, value: I) -> Result<Self, ConfigError>
150    where
151        I: Into<ValueKind>,
152    {
153        self.builder = self.builder.set_override(key, value)?;
154        Ok(self)
155    }
156
157    /// Finalize the builder and deserialize the resulting configuration.
158    pub fn build(self) -> Result<LexConfig, ConfigError> {
159        self.builder.build()?.try_deserialize()
160    }
161}
162
163impl Default for Loader {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169/// Convenience helper for callers that only need the defaults.
170pub fn load_defaults() -> Result<LexConfig, ConfigError> {
171    Loader::new().build()
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn loads_default_config() {
180        let config = load_defaults().expect("defaults to deserialize");
181        assert_eq!(config.formatting.rules.session_blank_lines_before, 1);
182        assert!(config.inspect.ast.show_line_numbers);
183        assert_eq!(config.convert.pdf.size, PdfPageSize::LexEd);
184    }
185
186    #[test]
187    fn supports_overrides() {
188        let config = Loader::new()
189            .set_override("convert.pdf.size", "mobile")
190            .expect("override to apply")
191            .build()
192            .expect("config to build");
193        assert_eq!(config.convert.pdf.size, PdfPageSize::Mobile);
194    }
195
196    #[test]
197    fn formatting_rules_config_converts_to_formatting_rules() {
198        let config = load_defaults().expect("defaults to deserialize");
199        let rules: FormattingRules = config.formatting.rules.into();
200        assert_eq!(rules.session_blank_lines_before, 1);
201        assert_eq!(rules.session_blank_lines_after, 1);
202        assert!(rules.normalize_seq_markers);
203        assert_eq!(rules.unordered_seq_marker, '-');
204        assert_eq!(rules.max_blank_lines, 2);
205        assert_eq!(rules.indent_string, "    ");
206        assert!(!rules.preserve_trailing_blanks);
207        assert!(rules.normalize_verbatim_markers);
208    }
209}