1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct PropelConfig {
26 #[serde(default)]
27 pub project: ProjectConfig,
28 #[serde(default)]
29 pub build: BuildConfig,
30 #[serde(default)]
31 pub cloud_run: CloudRunConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProjectConfig {
36 pub name: Option<String>,
38 #[serde(default = "default_region")]
40 pub region: String,
41 pub gcp_project_id: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct BuildConfig {
81 #[serde(default = "default_builder_image")]
85 pub base_image: String,
86 #[serde(default = "default_runtime_image")]
88 pub runtime_image: String,
89 #[serde(default)]
91 pub extra_packages: Vec<String>,
92 #[serde(default = "default_cargo_chef_version")]
94 pub cargo_chef_version: String,
95 #[serde(default)]
105 pub include: Option<Vec<String>>,
106 #[serde(default)]
117 pub env: HashMap<String, String>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct CloudRunConfig {
122 #[serde(default = "default_memory")]
124 pub memory: String,
125 #[serde(default = "default_cpu")]
127 pub cpu: u32,
128 #[serde(default)]
130 pub min_instances: u32,
131 #[serde(default = "default_max_instances")]
133 pub max_instances: u32,
134 #[serde(default = "default_concurrency")]
136 pub concurrency: u32,
137 #[serde(default = "default_port")]
139 pub port: u16,
140}
141
142impl Default for ProjectConfig {
143 fn default() -> Self {
144 Self {
145 name: None,
146 region: default_region(),
147 gcp_project_id: None,
148 }
149 }
150}
151
152impl Default for BuildConfig {
153 fn default() -> Self {
154 Self {
155 base_image: default_builder_image(),
156 runtime_image: default_runtime_image(),
157 extra_packages: Vec::new(),
158 cargo_chef_version: default_cargo_chef_version(),
159 include: None,
160 env: HashMap::new(),
161 }
162 }
163}
164
165impl Default for CloudRunConfig {
166 fn default() -> Self {
167 Self {
168 memory: default_memory(),
169 cpu: default_cpu(),
170 min_instances: 0,
171 max_instances: default_max_instances(),
172 concurrency: default_concurrency(),
173 port: default_port(),
174 }
175 }
176}
177
178impl PropelConfig {
179 pub fn load(project_dir: &std::path::Path) -> crate::Result<Self> {
181 let config_path = project_dir.join("propel.toml");
182 if config_path.exists() {
183 tracing::debug!(path = %config_path.display(), "loading propel.toml");
184 let content =
185 std::fs::read_to_string(&config_path).map_err(|e| crate::Error::ConfigLoad {
186 path: config_path.clone(),
187 source: e,
188 })?;
189 let config: Self = toml::from_str(&content).map_err(|e| crate::Error::ConfigParse {
190 path: config_path,
191 source: e,
192 })?;
193 config.build.validate_include_paths()?;
194 tracing::debug!(
195 region = %config.project.region,
196 port = config.cloud_run.port,
197 "config loaded"
198 );
199 Ok(config)
200 } else {
201 tracing::debug!(path = %config_path.display(), "propel.toml not found, using defaults");
202 Ok(Self::default())
203 }
204 }
205}
206
207impl BuildConfig {
208 fn validate_include_paths(&self) -> crate::Result<()> {
210 let paths = match &self.include {
211 Some(p) => p,
212 None => return Ok(()),
213 };
214 for path in paths {
215 let trimmed = path.trim();
216 if trimmed.is_empty() {
217 return Err(crate::Error::InvalidIncludePath {
218 path: path.clone(),
219 reason: "path must not be empty or whitespace-only",
220 });
221 }
222 if trimmed.trim_end_matches('/').is_empty() {
223 return Err(crate::Error::InvalidIncludePath {
224 path: path.clone(),
225 reason: "bare \"/\" is not a valid include path",
226 });
227 }
228 }
229 Ok(())
230 }
231}
232
233fn default_region() -> String {
234 "us-central1".to_owned()
235}
236
237fn default_builder_image() -> String {
238 "rust:1.93-bookworm".to_owned()
239}
240
241fn default_runtime_image() -> String {
242 "gcr.io/distroless/cc-debian12".to_owned()
243}
244
245fn default_cargo_chef_version() -> String {
246 "0.1.73".to_owned()
247}
248
249fn default_memory() -> String {
250 "512Mi".to_owned()
251}
252
253fn default_cpu() -> u32 {
254 1
255}
256
257fn default_max_instances() -> u32 {
258 10
259}
260
261fn default_concurrency() -> u32 {
262 80
263}
264
265fn default_port() -> u16 {
266 8080
267}