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    pub fn as_toml_string(&self) -> String {
59        toml::to_string_pretty(self).expect("showcase config serialization should not fail")
60    }
61
62    pub fn write_default_if_missing(path: impl AsRef<Path>) -> std::io::Result<bool> {
63        let path = path.as_ref();
64        if path.exists() {
65            return Ok(false);
66        }
67
68        std::fs::write(path, Self::default().as_toml_string())?;
69        Ok(true)
70    }
71
72    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, String> {
73        let content = std::fs::read_to_string(path.as_ref())
74            .map_err(|err| format!("failed to read {}: {err}", path.as_ref().display()))?;
75        Self::from_toml_str(&content)
76    }
77
78    pub fn from_toml_str(content: &str) -> Result<Self, String> {
79        toml::from_str(content).map_err(|err| format!("failed to parse showcase config: {err}"))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::time::{SystemTime, UNIX_EPOCH};
87
88    #[test]
89    fn default_round_trips_through_toml() {
90        let config = ShowcaseConfig::default();
91        let parsed = ShowcaseConfig::from_toml_str(&config.as_toml_string()).expect("valid toml");
92        assert_eq!(parsed, config);
93    }
94
95    #[test]
96    fn parse_custom_values() {
97        let content = r#"
98[project]
99name = "demo"
100entry_crate = "client"
101showcase_crate = "ui-showcase"
102
103[dev]
104port = 7000
105host = "0.0.0.0"
106
107[build]
108out_dir = "dist/showcase"
109base_path = "/showcase"
110"#;
111        let parsed = ShowcaseConfig::from_toml_str(content).expect("should parse");
112        assert_eq!(parsed.project.name, "demo");
113        assert_eq!(parsed.project.entry_crate, "client");
114        assert_eq!(parsed.project.showcase_crate, "ui-showcase");
115        assert_eq!(parsed.dev.port, 7000);
116        assert_eq!(parsed.dev.host, "0.0.0.0");
117        assert_eq!(parsed.build.out_dir, "dist/showcase");
118        assert_eq!(parsed.build.base_path, "/showcase");
119    }
120
121    #[test]
122    fn parse_rejects_invalid_assignment() {
123        let err = ShowcaseConfig::from_toml_str("[project]\nname \"demo\"")
124            .expect_err("missing = should fail");
125        assert!(err.contains("failed to parse showcase config"));
126    }
127
128    #[test]
129    fn parse_rejects_unquoted_string() {
130        let err =
131            ShowcaseConfig::from_toml_str("[project]\nname = demo").expect_err("must be quoted");
132        assert!(err.contains("failed to parse showcase config"));
133    }
134
135    #[test]
136    fn parse_rejects_invalid_port() {
137        let err = ShowcaseConfig::from_toml_str("[dev]\nport = 99999").expect_err("invalid port");
138        assert!(err.contains("failed to parse showcase config"));
139    }
140
141    #[test]
142    fn parse_rejects_unknown_fields() {
143        let err = ShowcaseConfig::from_toml_str("[project]\nunknown = \"value\"")
144            .expect_err("unknown fields should fail");
145        assert!(err.contains("failed to parse showcase config"));
146    }
147
148    #[test]
149    fn parse_fills_missing_sections_from_defaults() {
150        let parsed = ShowcaseConfig::from_toml_str("[project]\nname = \"demo\"")
151            .expect("partial config should parse");
152
153        assert_eq!(parsed.project.name, "demo");
154        assert_eq!(parsed.project.entry_crate, "web");
155        assert_eq!(parsed.project.showcase_crate, "showcase");
156        assert_eq!(parsed.dev, ShowcaseDevConfig::default());
157        assert_eq!(parsed.build, ShowcaseBuildConfig::default());
158    }
159
160    #[test]
161    fn write_default_if_missing_only_writes_once() {
162        let unique = SystemTime::now()
163            .duration_since(UNIX_EPOCH)
164            .expect("clock should be monotonic")
165            .as_nanos();
166        let dir = std::env::temp_dir().join(format!("dioxus-showcase-config-test-{unique}"));
167        std::fs::create_dir_all(&dir).expect("create temp dir");
168        let path = dir.join("DioxusShowcase.toml");
169
170        let first = ShowcaseConfig::write_default_if_missing(&path).expect("first write");
171        let second = ShowcaseConfig::write_default_if_missing(&path).expect("second write");
172        let written = std::fs::read_to_string(&path).expect("read config");
173
174        assert!(first);
175        assert!(!second);
176        assert!(written.contains("[project]"));
177
178        let _ = std::fs::remove_dir_all(&dir);
179    }
180}