kcl_lib/settings/types/
mod.rs

1//! Types for kcl project and modeling-app settings.
2
3pub mod project;
4
5use anyhow::Result;
6use kittycad_modeling_cmds::units::UnitLength;
7use parse_display::{Display, FromStr};
8use schemars::JsonSchema;
9use serde::{Deserialize, Deserializer, Serialize};
10use validator::Validate;
11
12const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "untitled";
13
14/// User specific settings for the app.
15/// These live in `user.toml` in the app's configuration directory.
16/// Updating the settings in the app will update this file automatically.
17/// Do not edit this file manually, as it may be overwritten by the app.
18/// Manual edits can cause corruption of the settings file.
19#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
20#[ts(export)]
21#[serde(rename_all = "snake_case")]
22pub struct Configuration {
23    /// The settings for the Design Studio.
24    #[serde(default, skip_serializing_if = "is_default")]
25    #[validate(nested)]
26    pub settings: Settings,
27}
28
29impl Configuration {
30    pub fn parse_and_validate(toml_str: &str) -> Result<Self> {
31        let settings = toml::from_str::<Self>(toml_str)?;
32
33        settings.validate()?;
34
35        Ok(settings)
36    }
37}
38
39/// High level settings.
40#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
41#[ts(export)]
42#[serde(rename_all = "snake_case")]
43pub struct Settings {
44    /// The settings for the Design Studio.
45    #[serde(default, skip_serializing_if = "is_default")]
46    #[validate(nested)]
47    pub app: AppSettings,
48    /// Settings that affect the behavior while modeling.
49    #[serde(default, skip_serializing_if = "is_default")]
50    #[validate(nested)]
51    pub modeling: ModelingSettings,
52    /// Settings that affect the behavior of the KCL text editor.
53    #[serde(default, skip_serializing_if = "is_default")]
54    #[validate(nested)]
55    pub text_editor: TextEditorSettings,
56    /// Settings that affect the behavior of project management.
57    #[serde(default, skip_serializing_if = "is_default")]
58    #[validate(nested)]
59    pub project: ProjectSettings,
60    /// Settings that affect the behavior of the command bar.
61    #[serde(default, skip_serializing_if = "is_default")]
62    #[validate(nested)]
63    pub command_bar: CommandBarSettings,
64}
65
66/// Application wide settings.
67#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
68#[ts(export)]
69#[serde(rename_all = "snake_case")]
70pub struct AppSettings {
71    /// The settings for the appearance of the app.
72    #[serde(default, skip_serializing_if = "is_default")]
73    #[validate(nested)]
74    pub appearance: AppearanceSettings,
75    /// The onboarding status of the app.
76    #[serde(default, skip_serializing_if = "is_default")]
77    pub onboarding_status: OnboardingStatus,
78    /// When the user is idle, teardown the stream after some time.
79    #[serde(
80        default,
81        deserialize_with = "deserialize_stream_idle_mode",
82        alias = "streamIdleMode",
83        skip_serializing_if = "is_default"
84    )]
85    stream_idle_mode: Option<u32>,
86    /// Allow orbiting in sketch mode.
87    #[serde(default, skip_serializing_if = "is_default")]
88    pub allow_orbit_in_sketch_mode: bool,
89    /// Whether to show the debug panel, which lets you see various states
90    /// of the app to aid in development.
91    #[serde(default, skip_serializing_if = "is_default")]
92    pub show_debug_panel: bool,
93}
94
95/// Default to true.
96fn make_it_so() -> bool {
97    true
98}
99
100fn is_true(b: &bool) -> bool {
101    *b
102}
103
104fn deserialize_stream_idle_mode<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
105where
106    D: Deserializer<'de>,
107{
108    #[derive(Deserialize)]
109    #[serde(untagged)]
110    enum StreamIdleModeValue {
111        Number(u32),
112        String(String),
113        Boolean(bool),
114    }
115
116    const DEFAULT_TIMEOUT: u32 = 1000 * 60 * 5;
117
118    Ok(match StreamIdleModeValue::deserialize(deserializer) {
119        Ok(StreamIdleModeValue::Number(value)) => Some(value),
120        Ok(StreamIdleModeValue::String(value)) => Some(value.parse::<u32>().unwrap_or(DEFAULT_TIMEOUT)),
121        // The old type of this value. I'm willing to say no one used it but
122        // we can never guarantee it.
123        Ok(StreamIdleModeValue::Boolean(true)) => Some(DEFAULT_TIMEOUT),
124        Ok(StreamIdleModeValue::Boolean(false)) => None,
125        _ => None,
126    })
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
130#[ts(export)]
131#[serde(untagged)]
132pub enum FloatOrInt {
133    String(String),
134    Float(f64),
135    Int(i64),
136}
137
138impl From<FloatOrInt> for f64 {
139    fn from(float_or_int: FloatOrInt) -> Self {
140        match float_or_int {
141            FloatOrInt::String(s) => s.parse().unwrap(),
142            FloatOrInt::Float(f) => f,
143            FloatOrInt::Int(i) => i as f64,
144        }
145    }
146}
147
148/// The settings for the theme of the app.
149#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
150#[ts(export)]
151#[serde(rename_all = "snake_case")]
152pub struct AppearanceSettings {
153    /// The overall theme of the app.
154    #[serde(default, skip_serializing_if = "is_default")]
155    pub theme: AppTheme,
156}
157
158/// The overall appearance of the app.
159#[derive(
160    Debug, Default, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq,
161)]
162#[ts(export)]
163#[serde(rename_all = "snake_case")]
164#[display(style = "snake_case")]
165pub enum AppTheme {
166    /// A light theme.
167    Light,
168    /// A dark theme.
169    Dark,
170    /// Use the system theme.
171    /// This will use dark theme if the system theme is dark, and light theme if the system theme is light.
172    #[default]
173    System,
174}
175
176impl From<AppTheme> for kittycad::types::Color {
177    fn from(theme: AppTheme) -> Self {
178        match theme {
179            AppTheme::Light => kittycad::types::Color {
180                r: 249.0 / 255.0,
181                g: 249.0 / 255.0,
182                b: 249.0 / 255.0,
183                a: 1.0,
184            },
185            AppTheme::Dark => kittycad::types::Color {
186                r: 28.0 / 255.0,
187                g: 28.0 / 255.0,
188                b: 28.0 / 255.0,
189                a: 1.0,
190            },
191            AppTheme::System => {
192                // TODO: Check the system setting for the user.
193                todo!()
194            }
195        }
196    }
197}
198
199/// Settings that affect the behavior while modeling.
200#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
201#[serde(rename_all = "snake_case")]
202#[ts(export)]
203pub struct ModelingSettings {
204    /// The default unit to use in modeling dimensions.
205    #[serde(default = "default_length_unit_millimeters", skip_serializing_if = "is_default")]
206    pub base_unit: UnitLength,
207    /// The projection mode the camera should use while modeling.
208    #[serde(default, skip_serializing_if = "is_default")]
209    pub camera_projection: CameraProjectionType,
210    /// The methodology the camera should use to orbit around the model.
211    #[serde(default, skip_serializing_if = "is_default")]
212    pub camera_orbit: CameraOrbitType,
213    /// The controls for how to navigate the 3D view.
214    #[serde(default, skip_serializing_if = "is_default")]
215    pub mouse_controls: MouseControlType,
216    /// Which type of orientation gizmo to use.
217    #[serde(default, skip_serializing_if = "is_default")]
218    pub gizmo_type: GizmoType,
219    /// Toggle touch controls for 3D view navigation
220    #[serde(default, skip_serializing_if = "is_default")]
221    pub enable_touch_controls: DefaultTrue,
222    /// Toggle new sketch mode implementation
223    #[serde(default, skip_serializing_if = "is_default")]
224    pub use_new_sketch_mode: bool,
225    /// Highlight edges of 3D objects?
226    #[serde(default, skip_serializing_if = "is_default")]
227    pub highlight_edges: DefaultTrue,
228    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
229    #[serde(default, skip_serializing_if = "is_default")]
230    pub enable_ssao: DefaultTrue,
231    /// Whether or not to show a scale grid in the 3D modeling view
232    #[serde(default, skip_serializing_if = "is_default")]
233    pub show_scale_grid: bool,
234    /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.
235    /// If true, the grid cells will be fixed-size, where the width is your default length unit.
236    /// If false, the grid will get larger as you zoom out, and smaller as you zoom in.
237    #[serde(default = "make_it_so", skip_serializing_if = "is_true")]
238    pub fixed_size_grid: bool,
239    /// When enabled, tools like line, rectangle, etc. will snap to the grid.
240    #[serde(default, skip_serializing_if = "is_default")]
241    pub snap_to_grid: bool,
242    /// The space between major grid lines, specified in the current unit.
243    #[serde(default, skip_serializing_if = "is_default")]
244    pub major_grid_spacing: f64,
245    /// The number of minor grid lines per major grid line.
246    #[serde(default, skip_serializing_if = "is_default")]
247    pub minor_grids_per_major: f64,
248    /// The number of snaps between minor grid lines. 1 means snapping to each minor grid line.
249    #[serde(default, skip_serializing_if = "is_default")]
250    pub snaps_per_minor: f64,
251}
252
253fn default_length_unit_millimeters() -> UnitLength {
254    UnitLength::Millimeters
255}
256
257impl Default for ModelingSettings {
258    fn default() -> Self {
259        Self {
260            base_unit: UnitLength::Millimeters,
261            camera_projection: Default::default(),
262            camera_orbit: Default::default(),
263            mouse_controls: Default::default(),
264            gizmo_type: Default::default(),
265            enable_touch_controls: Default::default(),
266            use_new_sketch_mode: Default::default(),
267            highlight_edges: Default::default(),
268            enable_ssao: Default::default(),
269            show_scale_grid: Default::default(),
270            fixed_size_grid: true,
271            snap_to_grid: Default::default(),
272            major_grid_spacing: Default::default(),
273            minor_grids_per_major: Default::default(),
274            snaps_per_minor: Default::default(),
275        }
276    }
277}
278
279#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
280#[ts(export)]
281#[serde(transparent)]
282pub struct DefaultTrue(pub bool);
283
284impl Default for DefaultTrue {
285    fn default() -> Self {
286        Self(true)
287    }
288}
289
290impl From<DefaultTrue> for bool {
291    fn from(default_true: DefaultTrue) -> Self {
292        default_true.0
293    }
294}
295
296impl From<bool> for DefaultTrue {
297    fn from(b: bool) -> Self {
298        Self(b)
299    }
300}
301
302/// The types of controls for how to navigate the 3D view.
303#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
304#[ts(export)]
305#[serde(rename_all = "snake_case")]
306#[display(style = "snake_case")]
307pub enum MouseControlType {
308    #[default]
309    #[display("zoo")]
310    #[serde(rename = "zoo")]
311    Zoo,
312    #[display("onshape")]
313    #[serde(rename = "onshape")]
314    OnShape,
315    TrackpadFriendly,
316    Solidworks,
317    Nx,
318    Creo,
319    #[display("autocad")]
320    #[serde(rename = "autocad")]
321    AutoCad,
322}
323
324/// The types of camera projection for the 3D view.
325#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
326#[ts(export)]
327#[serde(rename_all = "snake_case")]
328#[display(style = "snake_case")]
329pub enum CameraProjectionType {
330    /// Perspective projection https://en.wikipedia.org/wiki/3D_projection#Perspective_projection
331    Perspective,
332    /// Orthographic projection https://en.wikipedia.org/wiki/3D_projection#Orthographic_projection
333    #[default]
334    Orthographic,
335}
336
337/// The types of camera orbit methods.
338#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
339#[ts(export)]
340#[serde(rename_all = "snake_case")]
341#[display(style = "snake_case")]
342pub enum CameraOrbitType {
343    /// Orbit using a spherical camera movement.
344    #[default]
345    #[display("spherical")]
346    Spherical,
347    /// Orbit using a trackball camera movement.
348    #[display("trackball")]
349    Trackball,
350}
351
352/// Which type of orientation gizmo to use.
353#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
354#[ts(export)]
355#[serde(rename_all = "snake_case")]
356#[display(style = "snake_case")]
357pub enum GizmoType {
358    /// 3D cube gizmo
359    #[default]
360    Cube,
361    /// 3-axis gizmo
362    Axis,
363}
364
365/// Settings that affect the behavior of the KCL text editor.
366#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
367#[serde(rename_all = "snake_case")]
368#[ts(export)]
369pub struct TextEditorSettings {
370    /// Whether to wrap text in the editor or overflow with scroll.
371    #[serde(default, skip_serializing_if = "is_default")]
372    pub text_wrapping: DefaultTrue,
373    /// Whether to make the cursor blink in the editor.
374    #[serde(default, skip_serializing_if = "is_default")]
375    pub blinking_cursor: DefaultTrue,
376}
377
378/// Same as TextEditorSettings but applies to a per-project basis.
379#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
380#[serde(rename_all = "snake_case")]
381#[ts(export)]
382pub struct ProjectTextEditorSettings {
383    /// Whether to wrap text in the editor or overflow with scroll.
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub text_wrapping: Option<bool>,
386    /// Whether to make the cursor blink in the editor.
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub blinking_cursor: Option<bool>,
389}
390
391/// Settings that affect the behavior of project management.
392#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
393#[serde(rename_all = "snake_case")]
394#[ts(export)]
395pub struct ProjectSettings {
396    /// The directory to save and load projects from.
397    #[serde(default, skip_serializing_if = "is_default")]
398    pub directory: std::path::PathBuf,
399    /// The default project name to use when creating a new project.
400    #[serde(default, skip_serializing_if = "is_default")]
401    pub default_project_name: ProjectNameTemplate,
402}
403
404#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
405#[ts(export)]
406#[serde(transparent)]
407pub struct ProjectNameTemplate(pub String);
408
409impl Default for ProjectNameTemplate {
410    fn default() -> Self {
411        Self(DEFAULT_PROJECT_NAME_TEMPLATE.to_string())
412    }
413}
414
415impl From<ProjectNameTemplate> for String {
416    fn from(project_name: ProjectNameTemplate) -> Self {
417        project_name.0
418    }
419}
420
421impl From<String> for ProjectNameTemplate {
422    fn from(s: String) -> Self {
423        Self(s)
424    }
425}
426
427/// Settings that affect the behavior of the command bar.
428#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
429#[serde(rename_all = "snake_case")]
430#[ts(export)]
431pub struct CommandBarSettings {
432    /// Whether to include settings in the command bar.
433    #[serde(default, skip_serializing_if = "is_default")]
434    pub include_settings: DefaultTrue,
435}
436
437/// Same as CommandBarSettings but applies to a per-project basis.
438#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
439#[serde(rename_all = "snake_case")]
440#[ts(export)]
441pub struct ProjectCommandBarSettings {
442    /// Whether to include settings in the command bar.
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub include_settings: Option<bool>,
445}
446
447/// The types of onboarding status.
448#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
449#[ts(export)]
450#[serde(rename_all = "snake_case")]
451#[display(style = "snake_case")]
452pub enum OnboardingStatus {
453    /// The unset state.
454    #[serde(rename = "")]
455    #[display("")]
456    Unset,
457    /// The user has completed onboarding.
458    Completed,
459    /// The user has not completed onboarding.
460    #[default]
461    Incomplete,
462    /// The user has dismissed onboarding.
463    Dismissed,
464
465    // Desktop Routes
466    #[serde(rename = "/desktop")]
467    #[display("/desktop")]
468    DesktopWelcome,
469    #[serde(rename = "/desktop/scene")]
470    #[display("/desktop/scene")]
471    DesktopScene,
472    #[serde(rename = "/desktop/toolbar")]
473    #[display("/desktop/toolbar")]
474    DesktopToolbar,
475    #[serde(rename = "/desktop/text-to-cad")]
476    #[display("/desktop/text-to-cad")]
477    DesktopTextToCadWelcome,
478    #[serde(rename = "/desktop/text-to-cad-prompt")]
479    #[display("/desktop/text-to-cad-prompt")]
480    DesktopTextToCadPrompt,
481    #[serde(rename = "/desktop/feature-tree-pane")]
482    #[display("/desktop/feature-tree-pane")]
483    DesktopFeatureTreePane,
484    #[serde(rename = "/desktop/code-pane")]
485    #[display("/desktop/code-pane")]
486    DesktopCodePane,
487    #[serde(rename = "/desktop/project-pane")]
488    #[display("/desktop/project-pane")]
489    DesktopProjectFilesPane,
490    #[serde(rename = "/desktop/other-panes")]
491    #[display("/desktop/other-panes")]
492    DesktopOtherPanes,
493    #[serde(rename = "/desktop/prompt-to-edit")]
494    #[display("/desktop/prompt-to-edit")]
495    DesktopPromptToEditWelcome,
496    #[serde(rename = "/desktop/prompt-to-edit-prompt")]
497    #[display("/desktop/prompt-to-edit-prompt")]
498    DesktopPromptToEditPrompt,
499    #[serde(rename = "/desktop/prompt-to-edit-result")]
500    #[display("/desktop/prompt-to-edit-result")]
501    DesktopPromptToEditResult,
502    #[serde(rename = "/desktop/imports")]
503    #[display("/desktop/imports")]
504    DesktopImports,
505    #[serde(rename = "/desktop/exports")]
506    #[display("/desktop/exports")]
507    DesktopExports,
508    #[serde(rename = "/desktop/conclusion")]
509    #[display("/desktop/conclusion")]
510    DesktopConclusion,
511
512    // Browser Routes
513    #[serde(rename = "/browser")]
514    #[display("/browser")]
515    BrowserWelcome,
516    #[serde(rename = "/browser/scene")]
517    #[display("/browser/scene")]
518    BrowserScene,
519    #[serde(rename = "/browser/toolbar")]
520    #[display("/browser/toolbar")]
521    BrowserToolbar,
522    #[serde(rename = "/browser/text-to-cad")]
523    #[display("/browser/text-to-cad")]
524    BrowserTextToCadWelcome,
525    #[serde(rename = "/browser/text-to-cad-prompt")]
526    #[display("/browser/text-to-cad-prompt")]
527    BrowserTextToCadPrompt,
528    #[serde(rename = "/browser/feature-tree-pane")]
529    #[display("/browser/feature-tree-pane")]
530    BrowserFeatureTreePane,
531    #[serde(rename = "/browser/prompt-to-edit")]
532    #[display("/browser/prompt-to-edit")]
533    BrowserPromptToEditWelcome,
534    #[serde(rename = "/browser/prompt-to-edit-prompt")]
535    #[display("/browser/prompt-to-edit-prompt")]
536    BrowserPromptToEditPrompt,
537    #[serde(rename = "/browser/prompt-to-edit-result")]
538    #[display("/browser/prompt-to-edit-result")]
539    BrowserPromptToEditResult,
540    #[serde(rename = "/browser/conclusion")]
541    #[display("/browser/conclusion")]
542    BrowserConclusion,
543}
544
545fn is_default<T: Default + PartialEq>(t: &T) -> bool {
546    t == &T::default()
547}
548
549#[cfg(test)]
550mod tests {
551    use pretty_assertions::assert_eq;
552
553    use super::{
554        AppSettings, AppTheme, AppearanceSettings, CameraProjectionType, CommandBarSettings, Configuration,
555        ModelingSettings, MouseControlType, OnboardingStatus, ProjectNameTemplate, ProjectSettings, Settings,
556        TextEditorSettings, UnitLength,
557    };
558
559    #[test]
560    fn test_settings_empty_file_parses() {
561        let empty_settings_file = r#""#;
562
563        let parsed = toml::from_str::<Configuration>(empty_settings_file).unwrap();
564        assert_eq!(parsed, Configuration::default());
565
566        // Write the file back out.
567        let serialized = toml::to_string(&parsed).unwrap();
568        assert_eq!(serialized, r#""#);
569
570        let parsed = Configuration::parse_and_validate(empty_settings_file).unwrap();
571        assert_eq!(parsed, Configuration::default());
572    }
573
574    #[test]
575    fn test_settings_parse_basic() {
576        let settings_file = r#"[settings.app]
577default_project_name = "untitled"
578directory = ""
579onboarding_status = "dismissed"
580
581  [settings.app.appearance]
582  theme = "dark"
583
584[settings.modeling]
585enable_ssao = false
586base_unit = "in"
587mouse_controls = "zoo"
588camera_projection = "perspective"
589
590[settings.project]
591default_project_name = "untitled"
592directory = ""
593
594[settings.text_editor]
595text_wrapping = true"#;
596
597        let expected = Configuration {
598            settings: Settings {
599                app: AppSettings {
600                    onboarding_status: OnboardingStatus::Dismissed,
601                    appearance: AppearanceSettings { theme: AppTheme::Dark },
602                    ..Default::default()
603                },
604                modeling: ModelingSettings {
605                    enable_ssao: false.into(),
606                    base_unit: UnitLength::Inches,
607                    mouse_controls: MouseControlType::Zoo,
608                    camera_projection: CameraProjectionType::Perspective,
609                    fixed_size_grid: true,
610                    ..Default::default()
611                },
612                project: ProjectSettings {
613                    default_project_name: ProjectNameTemplate("untitled".to_string()),
614                    directory: "".into(),
615                },
616                text_editor: TextEditorSettings {
617                    text_wrapping: true.into(),
618                    ..Default::default()
619                },
620                command_bar: CommandBarSettings {
621                    include_settings: true.into(),
622                },
623            },
624        };
625        let parsed = toml::from_str::<Configuration>(settings_file).unwrap();
626        assert_eq!(parsed, expected);
627
628        // Write the file back out.
629        let serialized = toml::to_string(&parsed).unwrap();
630        assert_eq!(
631            serialized,
632            r#"[settings.app]
633onboarding_status = "dismissed"
634
635[settings.app.appearance]
636theme = "dark"
637
638[settings.modeling]
639base_unit = "in"
640camera_projection = "perspective"
641enable_ssao = false
642"#
643        );
644
645        let parsed = Configuration::parse_and_validate(settings_file).unwrap();
646        assert_eq!(parsed, expected);
647    }
648}