Skip to main content

dioxus_showcase_core/
config.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(default, deny_unknown_fields)]
7pub struct ShowcaseProjectConfig {
8    pub name: String,
9    pub entry_crate: String,
10    pub showcase_crate: String,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(default, deny_unknown_fields)]
15pub struct ShowcaseDevConfig {
16    pub port: u16,
17    pub host: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(default, deny_unknown_fields)]
22pub struct ShowcaseBuildConfig {
23    pub out_dir: String,
24    pub base_path: String,
25}
26
27#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(default, deny_unknown_fields)]
29pub struct ShowcaseConfig {
30    pub project: ShowcaseProjectConfig,
31    pub dev: ShowcaseDevConfig,
32    pub build: ShowcaseBuildConfig,
33}
34
35impl Default for ShowcaseProjectConfig {
36    fn default() -> Self {
37        Self {
38            name: "my-ui".to_owned(),
39            entry_crate: "web".to_owned(),
40            showcase_crate: "showcase".to_owned(),
41        }
42    }
43}
44
45impl Default for ShowcaseDevConfig {
46    fn default() -> Self {
47        Self { port: 6111, host: "127.0.0.1".to_owned() }
48    }
49}
50
51impl Default for ShowcaseBuildConfig {
52    fn default() -> Self {
53        Self { out_dir: "target/showcase".to_owned(), base_path: "/".to_owned() }
54    }
55}
56
57impl ShowcaseConfig {
58    /// Serializes the config into the on-disk `DioxusShowcase.toml` format.
59    pub fn as_toml_string(&self) -> String {
60        toml::to_string_pretty(self).expect("showcase config serialization should not fail")
61    }
62
63    /// Writes a default config file only when the target path does not already exist.
64    pub fn write_default_if_missing(path: impl AsRef<Path>) -> std::io::Result<bool> {
65        let path = path.as_ref();
66        if path.exists() {
67            return Ok(false);
68        }
69
70        std::fs::write(path, Self::default().as_toml_string())?;
71        Ok(true)
72    }
73
74    /// Loads and parses a config file from disk.
75    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, String> {
76        let content = std::fs::read_to_string(path.as_ref())
77            .map_err(|err| format!("failed to read {}: {err}", path.as_ref().display()))?;
78        Self::from_toml_str(&content)
79    }
80
81    /// Parses a config from TOML source text.
82    pub fn from_toml_str(content: &str) -> Result<Self, String> {
83        toml::from_str(content).map_err(|err| format!("failed to parse showcase config: {err}"))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::time::{SystemTime, UNIX_EPOCH};
91
92    #[test]
93    fn default_round_trips_through_toml() {
94        let config = ShowcaseConfig::default();
95        let parsed = ShowcaseConfig::from_toml_str(&config.as_toml_string()).expect("valid toml");
96        assert_eq!(parsed, config);
97    }
98
99    #[test]
100    fn parse_custom_values() {
101        let content = r#"
102[project]
103name = "demo"
104entry_crate = "client"
105showcase_crate = "ui-showcase"
106
107[dev]
108port = 7000
109host = "0.0.0.0"
110
111[build]
112out_dir = "dist/showcase"
113base_path = "/showcase"
114"#;
115        let parsed = ShowcaseConfig::from_toml_str(content).expect("should parse");
116        assert_eq!(parsed.project.name, "demo");
117        assert_eq!(parsed.project.entry_crate, "client");
118        assert_eq!(parsed.project.showcase_crate, "ui-showcase");
119        assert_eq!(parsed.dev.port, 7000);
120        assert_eq!(parsed.dev.host, "0.0.0.0");
121        assert_eq!(parsed.build.out_dir, "dist/showcase");
122        assert_eq!(parsed.build.base_path, "/showcase");
123    }
124
125    #[test]
126    fn parse_rejects_invalid_assignment() {
127        let err = ShowcaseConfig::from_toml_str("[project]\nname \"demo\"")
128            .expect_err("missing = should fail");
129        assert!(err.contains("failed to parse showcase config"));
130    }
131
132    #[test]
133    fn parse_rejects_unquoted_string() {
134        let err =
135            ShowcaseConfig::from_toml_str("[project]\nname = demo").expect_err("must be quoted");
136        assert!(err.contains("failed to parse showcase config"));
137    }
138
139    #[test]
140    fn parse_rejects_invalid_port() {
141        let err = ShowcaseConfig::from_toml_str("[dev]\nport = 99999").expect_err("invalid port");
142        assert!(err.contains("failed to parse showcase config"));
143    }
144
145    #[test]
146    fn parse_rejects_unknown_fields() {
147        let err = ShowcaseConfig::from_toml_str("[project]\nunknown = \"value\"")
148            .expect_err("unknown fields should fail");
149        assert!(err.contains("failed to parse showcase config"));
150    }
151
152    #[test]
153    fn parse_fills_missing_sections_from_defaults() {
154        let parsed = ShowcaseConfig::from_toml_str("[project]\nname = \"demo\"")
155            .expect("partial config should parse");
156
157        assert_eq!(parsed.project.name, "demo");
158        assert_eq!(parsed.project.entry_crate, "web");
159        assert_eq!(parsed.project.showcase_crate, "showcase");
160        assert_eq!(parsed.dev, ShowcaseDevConfig::default());
161        assert_eq!(parsed.build, ShowcaseBuildConfig::default());
162    }
163
164    #[test]
165    fn write_default_if_missing_only_writes_once() {
166        let unique = SystemTime::now()
167            .duration_since(UNIX_EPOCH)
168            .expect("clock should be monotonic")
169            .as_nanos();
170        let dir = std::env::temp_dir().join(format!("dioxus-showcase-config-test-{unique}"));
171        std::fs::create_dir_all(&dir).expect("create temp dir");
172        let path = dir.join("DioxusShowcase.toml");
173
174        let first = ShowcaseConfig::write_default_if_missing(&path).expect("first write");
175        let second = ShowcaseConfig::write_default_if_missing(&path).expect("second write");
176        let written = std::fs::read_to_string(&path).expect("read config");
177
178        assert!(first);
179        assert!(!second);
180        assert!(written.contains("[project]"));
181
182        let _ = std::fs::remove_dir_all(&dir);
183    }
184}