1use super::*;
2use coil_auth::{LoadedAuthModelPackage, load_auth_model_package_at};
3use coil_config::{CustomerAppBootstrapManifest, CustomerAppBootstrapManifestError};
4use coil_i18n::TranslationCatalog;
5use std::collections::BTreeSet;
6use std::env;
7use std::path::Path;
8use std::path::PathBuf;
9
10pub struct CustomerRootRuntimeBuilder<P> {
15 inner: RuntimeBuilder<P>,
16 config: PlatformConfig,
17 app_root: Option<PathBuf>,
18 enabled_modules: Vec<String>,
19}
20
21pub struct CustomerRootBootstrapInputs {
22 pub app_root: PathBuf,
23 pub manifest_path: PathBuf,
24 pub enabled_modules: Vec<String>,
25 pub config_path: PathBuf,
26 pub config: PlatformConfig,
27 pub auth_package_name: String,
28 pub auth_package: LoadedAuthModelPackage,
29 pub translation_catalogs: Vec<TranslationCatalog>,
30}
31
32pub fn customer_root_runtime<P>(
33 config: PlatformConfig,
34 auth_package: P,
35) -> CustomerRootRuntimeBuilder<P>
36where
37 P: AuthModelPackage + 'static,
38{
39 CustomerRootRuntimeBuilder::new(config, auth_package)
40}
41
42pub fn customer_root_runtime_from_env()
43-> Result<CustomerRootRuntimeBuilder<LoadedAuthModelPackage>, RuntimeBootstrapError> {
44 CustomerRootRuntimeBuilder::from_env()
45}
46
47pub fn customer_root_runtime_from_paths(
48 app_root: impl AsRef<Path>,
49 config_path: impl AsRef<Path>,
50) -> Result<CustomerRootRuntimeBuilder<LoadedAuthModelPackage>, RuntimeBootstrapError> {
51 CustomerRootRuntimeBuilder::from_paths(app_root, config_path)
52}
53
54pub fn customer_root_bootstrap_inputs_from_env()
55-> Result<CustomerRootBootstrapInputs, RuntimeBootstrapError> {
56 CustomerRootBootstrapInputs::from_env()
57}
58
59pub fn customer_root_bootstrap_inputs_from_paths(
60 app_root: impl AsRef<Path>,
61 config_path: impl AsRef<Path>,
62) -> Result<CustomerRootBootstrapInputs, RuntimeBootstrapError> {
63 CustomerRootBootstrapInputs::from_paths(app_root, config_path)
64}
65
66impl<P> CustomerRootRuntimeBuilder<P>
67where
68 P: AuthModelPackage + 'static,
69{
70 pub fn new(config: PlatformConfig, auth_package: P) -> Self {
71 Self {
72 inner: RuntimeBuilder::new(config.clone(), auth_package),
73 config,
74 app_root: None,
75 enabled_modules: Vec::new(),
76 }
77 }
78
79 pub fn with_customer_root<A>(mut self, root: A) -> Self
83 where
84 A: Into<PathBuf>,
85 {
86 let root = root.into();
87 self.app_root = Some(root.clone());
88 self.inner = self.inner.with_template_root(root);
89 self
90 }
91
92 pub fn with_linked_customer_plugin<C>(mut self, plugin: C) -> Self
94 where
95 C: CustomerBackendPlugin,
96 {
97 self.inner = self.inner.register_customer_plugin(plugin);
98 self
99 }
100
101 pub fn with_boxed_linked_customer_plugin(
102 mut self,
103 plugin: Box<dyn CustomerBackendPlugin>,
104 ) -> Self {
105 self.inner = self.inner.with_boxed_customer_plugin(plugin);
106 self
107 }
108
109 pub fn with_module<M>(mut self, module: M) -> Self
110 where
111 M: PlatformModule + 'static,
112 {
113 self.inner = self.inner.with_module(module);
114 self
115 }
116
117 pub fn register_module<M>(self, module: M) -> Self
118 where
119 M: PlatformModule + 'static,
120 {
121 self.with_module(module)
122 }
123
124 pub fn with_boxed_module(mut self, module: Box<dyn PlatformModule>) -> Self {
125 self.inner = self.inner.with_boxed_module(module);
126 self
127 }
128
129 pub fn register_customer_plugin<C>(self, plugin: C) -> Self
130 where
131 C: CustomerBackendPlugin,
132 {
133 self.with_linked_customer_plugin(plugin)
134 }
135
136 pub fn with_installed_extension(mut self, extension: InstalledExtension) -> Self {
137 self.inner = self.inner.with_installed_extension(extension);
138 self
139 }
140
141 pub fn with_template(mut self, template: coil_template::TemplateDefinition) -> Self {
142 self.inner = self.inner.with_template(template);
143 self
144 }
145
146 pub fn with_templates<I>(mut self, templates: I) -> Self
147 where
148 I: IntoIterator<Item = coil_template::TemplateDefinition>,
149 {
150 self.inner = self.inner.with_templates(templates);
151 self
152 }
153
154 pub fn with_storage_policy_rule(mut self, rule: PathPolicyRule) -> Self {
155 self.inner = self.inner.with_storage_policy_rule(rule);
156 self
157 }
158
159 pub fn with_storage_policies(mut self, policies: StoragePolicySet) -> Self {
160 self.inner = self.inner.with_storage_policies(policies);
161 self
162 }
163
164 pub fn with_route(mut self, route: RouteDefinition) -> Self {
165 self.inner = self.inner.with_route(route);
166 self
167 }
168
169 pub fn with_handler(mut self, handler: HandlerDefinition) -> Self {
170 self.inner = self.inner.with_handler(handler);
171 self
172 }
173
174 pub fn with_feature_flag(mut self, feature_flag: FeatureFlag) -> Self {
175 self.inner = self.inner.with_feature_flag(feature_flag);
176 self
177 }
178
179 pub fn with_translation_catalog(mut self, catalog: TranslationCatalog) -> Self {
180 self.inner = self.inner.with_translation_catalog(catalog);
181 self
182 }
183
184 pub fn with_translation_catalogs<I>(mut self, catalogs: I) -> Self
185 where
186 I: IntoIterator<Item = TranslationCatalog>,
187 {
188 self.inner = self.inner.with_translation_catalogs(catalogs);
189 self
190 }
191
192 pub fn with_maintenance_mode(mut self, maintenance_mode: MaintenanceMode) -> Self {
193 self.inner = self.inner.with_maintenance_mode(maintenance_mode);
194 self
195 }
196
197 pub fn into_runtime_builder(self) -> RuntimeBuilder<P> {
200 self.inner
201 }
202
203 pub fn build(self) -> Result<RuntimePlan, RuntimeBuildError> {
204 let enabled_modules = if self.enabled_modules.is_empty() {
205 self.resolve_enabled_modules_from_customer_root()?
206 } else {
207 self.enabled_modules
208 };
209 self.inner
210 .resolve_enabled_customer_modules(&enabled_modules)?
211 .build()
212 }
213
214 pub fn run_from_env(self) -> Result<(), RuntimeBootstrapError> {
215 self.build()?.serve_from_env(env::var("COIL_BIND").ok())
216 }
217}
218
219impl CustomerRootRuntimeBuilder<LoadedAuthModelPackage> {
220 pub fn from_env() -> Result<Self, RuntimeBootstrapError> {
221 let app_root = env::current_dir().map_err(RuntimeBootstrapError::CurrentDirectory)?;
222 let config_path = discover_default_config_path(&app_root).ok_or_else(|| {
223 RuntimeBootstrapError::ConfigNotFound {
224 app_root: app_root.clone(),
225 }
226 })?;
227 Self::from_paths(app_root, config_path)
228 }
229
230 pub fn from_paths(
231 app_root: impl AsRef<Path>,
232 config_path: impl AsRef<Path>,
233 ) -> Result<Self, RuntimeBootstrapError> {
234 let inputs = CustomerRootBootstrapInputs::from_paths(app_root, config_path)?;
235 Ok(Self::from_bootstrap_inputs(inputs))
236 }
237
238 pub fn from_bootstrap_inputs(inputs: CustomerRootBootstrapInputs) -> Self {
239 let mut builder = Self::new(inputs.config, inputs.auth_package)
240 .with_customer_root(inputs.app_root)
241 .with_translation_catalogs(inputs.translation_catalogs);
242 builder.enabled_modules = inputs.enabled_modules;
243 builder
244 }
245}
246
247impl<P> CustomerRootRuntimeBuilder<P>
248where
249 P: AuthModelPackage + 'static,
250{
251 fn resolve_enabled_modules_from_customer_root(&self) -> Result<Vec<String>, RuntimeBuildError> {
252 let Some(app_root) = &self.app_root else {
253 return Err(RuntimeBuildError::CustomerRootNotConfigured);
254 };
255 let manifest_path = app_root.join("app.toml");
256 let manifest = load_customer_root_manifest(&manifest_path)
257 .map_err(|error| customer_root_manifest_error_into_build(&manifest_path, error))?;
258 manifest
259 .validate_runtime_config_alignment(&self.config)
260 .map_err(|error| customer_root_manifest_error_into_build(&manifest_path, error))?;
261 Ok(manifest.enabled_modules().to_vec())
262 }
263}
264
265impl CustomerRootBootstrapInputs {
266 pub fn from_env() -> Result<Self, RuntimeBootstrapError> {
267 let app_root = env::current_dir().map_err(RuntimeBootstrapError::CurrentDirectory)?;
268 let config_path = discover_default_config_path(&app_root).ok_or_else(|| {
269 RuntimeBootstrapError::ConfigNotFound {
270 app_root: app_root.clone(),
271 }
272 })?;
273 Self::from_paths(app_root, config_path)
274 }
275
276 pub fn from_paths(
277 app_root: impl AsRef<Path>,
278 config_path: impl AsRef<Path>,
279 ) -> Result<Self, RuntimeBootstrapError> {
280 let app_root = app_root.as_ref().to_path_buf();
281 let config_path = resolve_path(&app_root, config_path.as_ref());
282 let manifest_path = app_root.join("app.toml");
283 let config = PlatformConfig::from_file(&config_path).map_err(|error| {
284 RuntimeBootstrapError::ConfigLoad {
285 path: config_path.clone(),
286 reason: error.to_string(),
287 }
288 })?;
289 let manifest = load_customer_root_manifest(&manifest_path)
290 .map_err(|error| customer_root_manifest_error_into_bootstrap(&manifest_path, error))?;
291 manifest
292 .validate_runtime_config_alignment(&config)
293 .map_err(|error| customer_root_manifest_error_into_bootstrap(&manifest_path, error))?;
294 let translation_catalogs = load_customer_translation_catalogs(&app_root, &manifest)
295 .map_err(|reason| RuntimeBootstrapError::ManifestLoad {
296 path: manifest_path.clone(),
297 reason,
298 })?;
299 let auth_package_name = config.auth.package.clone();
300 let auth_package =
301 load_auth_model_package_at(&auth_package_name, &app_root).map_err(|error| {
302 RuntimeBootstrapError::AuthPackageLoad {
303 package: auth_package_name.clone(),
304 app_root: app_root.clone(),
305 reason: error.to_string(),
306 }
307 })?;
308
309 Ok(Self {
310 app_root,
311 manifest_path,
312 enabled_modules: manifest.enabled_modules().to_vec(),
313 config_path,
314 config,
315 auth_package_name,
316 auth_package,
317 translation_catalogs,
318 })
319 }
320}
321
322fn resolve_path(app_root: &Path, path: &Path) -> PathBuf {
323 if path.is_absolute() {
324 path.to_path_buf()
325 } else {
326 app_root.join(path)
327 }
328}
329
330fn discover_default_config_path(app_root: &Path) -> Option<PathBuf> {
331 env::var("COIL_CONFIG")
332 .ok()
333 .map(PathBuf::from)
334 .map(|path| resolve_path(app_root, &path))
335 .filter(|path| path.is_file())
336 .or_else(|| {
337 [
338 PathBuf::from("platform.toml"),
339 PathBuf::from("platform.dev.toml"),
340 PathBuf::from("config/platform.toml"),
341 PathBuf::from("coil.toml"),
342 PathBuf::from("config/coil.toml"),
343 ]
344 .into_iter()
345 .map(|path| app_root.join(path))
346 .find(|path| path.is_file())
347 })
348}
349
350fn load_customer_root_manifest(
351 manifest_path: &Path,
352) -> Result<CustomerAppBootstrapManifest, CustomerAppBootstrapManifestError> {
353 CustomerAppBootstrapManifest::from_file(manifest_path)
354}
355
356fn customer_root_manifest_error_into_bootstrap(
357 manifest_path: &Path,
358 error: CustomerAppBootstrapManifestError,
359) -> RuntimeBootstrapError {
360 match error {
361 CustomerAppBootstrapManifestError::Read { path, reason }
362 | CustomerAppBootstrapManifestError::Parse { path, reason } => {
363 RuntimeBootstrapError::ManifestLoad { path, reason }
364 }
365 other => RuntimeBootstrapError::ManifestLoad {
366 path: manifest_path.to_path_buf(),
367 reason: other.to_string(),
368 },
369 }
370}
371
372fn customer_root_manifest_error_into_build(
373 manifest_path: &Path,
374 error: CustomerAppBootstrapManifestError,
375) -> RuntimeBuildError {
376 match error {
377 CustomerAppBootstrapManifestError::Read { path, reason }
378 | CustomerAppBootstrapManifestError::Parse { path, reason } => {
379 RuntimeBuildError::CustomerManifestLoad { path, reason }
380 }
381 other => RuntimeBuildError::CustomerManifestLoad {
382 path: manifest_path.to_path_buf(),
383 reason: other.to_string(),
384 },
385 }
386}
387
388fn load_customer_translation_catalogs(
389 app_root: &Path,
390 manifest: &CustomerAppBootstrapManifest,
391) -> Result<Vec<TranslationCatalog>, String> {
392 let supported_locales = manifest.supported_locales();
393 let mut seen_locales = BTreeSet::new();
394 manifest
395 .translation_catalogs()
396 .iter()
397 .map(|catalog| {
398 if !supported_locales.iter().any(|locale| locale == catalog.locale()) {
399 return Err(format!(
400 "translation catalog locale `{}` is not declared in the customer app supported locale set",
401 catalog.locale()
402 ));
403 }
404 if !seen_locales.insert(catalog.locale().to_string()) {
405 return Err(format!(
406 "translation catalog for locale `{}` is declared more than once",
407 catalog.locale()
408 ));
409 }
410 let path = app_root.join(catalog.path());
411 TranslationCatalog::from_toml_file(
412 coil_i18n::LocaleTag::new(catalog.locale()).map_err(|error| error.to_string())?,
413 &path,
414 )
415 .map_err(|error| {
416 format!(
417 "failed to load translation catalog `{}` for locale `{}`: {error}",
418 catalog.path(),
419 catalog.locale()
420 )
421 })
422 })
423 .collect()
424}
425
426pub(crate) fn resolve_enabled_customer_modules(
427 enabled_modules: &[String],
428 modules: Vec<Box<dyn PlatformModule>>,
429) -> Result<Vec<Box<dyn PlatformModule>>, RuntimeBuildError> {
430 let enabled = enabled_modules.iter().cloned().collect::<BTreeSet<_>>();
431 let mut linked = BTreeSet::new();
432 let mut selected = Vec::new();
433 for module in modules {
434 let name = module.manifest().name;
435 linked.insert(name.clone());
436 if enabled.contains(&name) {
437 selected.push(module);
438 }
439 }
440 let missing = enabled
441 .into_iter()
442 .filter(|module| !linked.contains(module))
443 .collect::<Vec<_>>();
444 if !missing.is_empty() {
445 return Err(RuntimeBuildError::CustomerManifestMissingLinkedModules { modules: missing });
446 }
447 Ok(selected)
448}