1use crate::engine::{postgres_psql::PSQL, Engine, EngineType, TargetConfig};
2use crate::pinfile::LockData;
3use crate::variables::Variables;
4use anyhow::{anyhow, Context, Result};
5use opendal::Operator;
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10static PINFILE_LOCK_NAME: &str = "lock.toml";
11
12#[derive(Clone, Debug, Deserialize, Serialize)]
14pub struct ConfigLoaderSaver {
15 pub spawn_folder: String,
16 pub target: Option<String>,
17 pub environment: Option<String>,
18 pub targets: Option<HashMap<String, TargetConfig>>,
19 pub project_id: Option<String>,
21 #[serde(default = "default_telemetry", skip_serializing_if = "Option::is_none")]
23 pub telemetry: Option<bool>,
24}
25
26fn default_telemetry() -> Option<bool> {
27 None
28}
29
30impl ConfigLoaderSaver {
31 pub fn build(self, base_fs: Operator, spawn_fs: Option<Operator>) -> Config {
33 Config {
34 spawn_folder: self.spawn_folder,
35 target: self.target,
36 environment: self.environment,
37 targets: self.targets.unwrap_or_default(),
38 project_id: self.project_id,
39 telemetry: self.telemetry.unwrap_or(true),
40 base_fs,
41 spawn_fs,
42 }
43 }
44
45 pub async fn load(
46 path: &str,
47 op: &Operator,
48 target: Option<String>,
49 ) -> Result<ConfigLoaderSaver> {
50 let bytes = op
51 .read(path)
52 .await
53 .context(format!("No config found at path '{}'", &path))?
54 .to_bytes();
55 let main_config = String::from_utf8(bytes.to_vec())?;
56 let source = config::File::from_str(&main_config, config::FileFormat::Toml);
57
58 let mut settings = config::Config::builder().add_source(source);
59
60 match op.read(path).await {
62 Ok(data) => {
63 let bytes = String::from_utf8(data.to_bytes().to_vec())?;
64 let override_config = config::File::from_str(&bytes, config::FileFormat::Toml);
65 settings = settings.add_source(override_config);
66 }
67 Err(e) => match e.kind() {
68 opendal::ErrorKind::NotFound => {}
71 _ => return Err(e.into()),
72 },
73 };
74
75 let settings = settings
76 .add_source(config::Environment::with_prefix("SPAWN"))
79 .set_override_option("target", target)?
80 .set_default("environment", "prod")
81 .context("could not set default environment")?
82 .build()?
83 .try_deserialize()?;
84
85 Ok(settings)
86 }
87
88 pub async fn save(&self, path: &str, op: &Operator) -> Result<()> {
89 let toml_content = toml::to_string(self)?;
90 op.write(path, toml_content).await?;
91 Ok(())
92 }
93}
94
95#[derive(Clone)]
96pub struct FolderPather {
97 pub spawn_folder: String,
98}
99
100impl FolderPather {
101 pub fn spawn_folder_path(&self) -> &str {
102 self.spawn_folder.as_ref()
103 }
104
105 pub fn pinned_folder(&self) -> String {
106 let mut s = self.spawn_folder_path().to_string();
107 s.push_str("/pinned");
108 s
109 }
110
111 pub fn components_folder(&self) -> String {
112 let mut s = self.spawn_folder_path().to_string();
113 s.push_str("/components");
114 s
115 }
116
117 pub fn migrations_folder(&self) -> String {
118 let mut s = self.spawn_folder_path().to_string();
119 s.push_str("/migrations");
120 s
121 }
122
123 pub fn tests_folder(&self) -> String {
124 let mut s = self.spawn_folder_path().to_string();
125 s.push_str("/tests");
126 s
127 }
128
129 pub fn migration_folder(&self, script_path: &str) -> String {
130 let mut s = self.migrations_folder();
131 s.push('/');
132 s.push_str(script_path);
133 s
134 }
135
136 pub fn migration_script_file_path(&self, script_path: &str) -> String {
137 let mut s = self.migration_folder(script_path);
138 s.push_str("/up.sql");
139 s
140 }
141
142 pub fn test_folder(&self, test_path: &str) -> String {
143 let mut s = self.tests_folder();
144 s.push('/');
145 s.push_str(test_path);
146 s
147 }
148
149 pub fn test_file_path(&self, test_path: &str) -> String {
150 let mut s = self.test_folder(test_path);
151 s.push_str("/test.sql");
152 s
153 }
154
155 pub fn migration_lock_file_path(&self, script_path: &str) -> String {
156 let mut s = self.migrations_folder();
157 s.push('/');
158 s.push_str(script_path);
159 s.push('/');
160 s.push_str(PINFILE_LOCK_NAME);
161 s
162 }
163}
164
165#[derive(Debug, Clone)]
166pub struct Config {
167 spawn_folder: String,
168 pub target: Option<String>,
169 pub environment: Option<String>, pub targets: HashMap<String, TargetConfig>,
171 pub project_id: Option<String>,
173 pub telemetry: bool,
175
176 base_fs: Operator,
179 spawn_fs: Option<Operator>,
183}
184
185impl Config {
186 pub fn pather(&self) -> FolderPather {
187 FolderPather {
188 spawn_folder: self.spawn_folder.clone(),
189 }
190 }
191
192 pub async fn new_engine(&self) -> Result<Box<dyn Engine>> {
193 let target_config = self.target_config()?;
194
195 match target_config.engine {
196 EngineType::PostgresPSQL => Ok(PSQL::new(&target_config).await?),
197 }
198 }
199
200 pub fn target_config(&self) -> Result<TargetConfig> {
201 let target_name = self.target.as_ref().ok_or(anyhow!("no target selected"))?;
202 let mut conf = self
203 .targets
204 .get(target_name)
205 .ok_or(anyhow!("no target defined with name '{}'", target_name,))?
206 .clone();
207
208 if let Some(env) = &self.environment {
209 conf.environment = env.clone();
210 }
211
212 Ok(conf)
213 }
214
215 pub async fn load(path: &str, op: &Operator, target: Option<String>) -> Result<Config> {
216 let config_loader = ConfigLoaderSaver::load(path, op, target).await?;
217 Ok(config_loader.build(op.clone(), None))
218 }
219
220 pub fn operator(&self) -> &Operator {
221 if let Some(spawn_fs) = &self.spawn_fs {
222 &spawn_fs
223 } else {
224 &self.base_fs
225 }
226 }
227
228 pub async fn load_lock_file(&self, lock_file_path: &str) -> Result<LockData> {
229 let contents = self.operator().read(lock_file_path).await?.to_bytes();
230 let contents = String::from_utf8(contents.to_vec())?;
231 let lock_data: LockData = toml::from_str(&contents)?;
232
233 Ok(lock_data)
234 }
235
236 pub async fn load_variables_from_path(&self, path: &str) -> Result<Variables> {
239 let content = self
240 .operator()
241 .read(path)
242 .await
243 .context(format!("Failed to read variables file '{}'", path))?
244 .to_bytes();
245 let content_str =
246 String::from_utf8(content.to_vec()).context("Variables file is not valid UTF-8")?;
247
248 let extension = path.split('.').last().unwrap_or("");
249 Variables::from_str(extension, &content_str)
250 .context(format!("Failed to parse variables file '{}'", path))
251 }
252}