Commit

Author:

Hash:

Timestamp:

+504 -539 +/-30 browse

Kevin Schoon [me@kevinschoon.com]

86b027974fd2a875243fc21601c87f9e6353278f

Tue, 23 Jun 2026 18:45:18 +0000 (3 weeks ago)

clean up configuration handling considerably
clean up configuration handling considerably

This cleans up various config that has accumulated across the project
and makes the explicit decision that all Ayllu components can be run
with default configuration.
1diff --git a/ayllu-build/src/config.rs b/ayllu-build/src/config.rs
2index 7b25ff7..4245ca1 100644
3--- a/ayllu-build/src/config.rs
4+++ b/ayllu-build/src/config.rs
5 @@ -79,36 +79,36 @@ pub struct Build {
6
7 impl Build {
8 fn default_work_dir() -> PathBuf {
9- Self::default().work_dir
10+ data_dir().join("build")
11 }
12
13 fn default_oci_binary() -> PathBuf {
14- Self::default().oci_binary
15+ PathBuf::from("/usr/bin/crun")
16 }
17
18 fn default_interrupt_timeout() -> u64 {
19- Self::default().interrupt_timeout
20+ crate::handle::DEFAULT_INTERRUPT_TIMEOUT_SECS
21 }
22
23 fn default_init_script() -> String {
24- Self::default().init_script
25+ include_str!("../init.tmpl.sh").to_string()
26 }
27
28 fn default_runtime_dir() -> PathBuf {
29- Self::default().runtime_dir
30+ ayllu_config::runtime_dir().join("ayllu-build")
31 }
32 }
33
34 impl Default for Build {
35 fn default() -> Self {
36 Self {
37- work_dir: data_dir().join("build"),
38- pre_processor: Default::default(),
39+ work_dir: Build::default_work_dir(),
40+ pre_processor: None,
41+ oci_binary: Build::default_oci_binary(),
42 images: HashMap::default(),
43- oci_binary: Path::new("/usr/bin/crun").to_path_buf(),
44- interrupt_timeout: crate::handle::DEFAULT_INTERRUPT_TIMEOUT_SECS,
45- init_script: include_str!("../init.tmpl.sh").to_string(),
46- runtime_dir: ayllu_config::runtime_dir().join("ayllu-build"),
47+ interrupt_timeout: Build::default_interrupt_timeout(),
48+ init_script: Build::default_init_script(),
49+ runtime_dir: Build::default_runtime_dir(),
50 }
51 }
52 }
53 @@ -134,3 +134,16 @@ pub fn load(path: Option<&Path>) -> Result<Config, Error> {
54 Err(e) => Err(e),
55 }
56 }
57+
58+ #[cfg(test)]
59+ mod test {
60+ use ayllu_config::{EXAMPLE_CONFIG, Reader};
61+
62+ use crate::config::Config;
63+
64+ #[test]
65+ fn check_config() {
66+ Reader::<Config>::read(EXAMPLE_CONFIG).unwrap();
67+ Reader::<Config>::read("").unwrap();
68+ }
69+ }
70 diff --git a/ayllu-keys/src/main.rs b/ayllu-keys/src/main.rs
71index 7b16ffb..3c4b97b 100644
72--- a/ayllu-keys/src/main.rs
73+++ b/ayllu-keys/src/main.rs
74 @@ -90,6 +90,7 @@ fn main() -> ExitCode {
75 #[cfg(test)]
76 mod test {
77
78+ use ayllu_config::{EXAMPLE_CONFIG, Reader};
79 use ayllu_identity::{PublicKey, WrappedKey};
80
81 use super::*;
82 @@ -117,4 +118,10 @@ mod test {
83 .is_some_and(|identity| { identity.username == "hello" })
84 );
85 }
86+
87+ #[test]
88+ fn check_config() {
89+ Reader::<Config>::read(EXAMPLE_CONFIG).unwrap();
90+ Reader::<Config>::read("").unwrap();
91+ }
92 }
93 diff --git a/ayllu-migrate/src/main.rs b/ayllu-migrate/src/main.rs
94index c80817f..b822eea 100644
95--- a/ayllu-migrate/src/main.rs
96+++ b/ayllu-migrate/src/main.rs
97 @@ -3,7 +3,7 @@ use ayllu_config::{Configurable, Database};
98 use serde::{Deserialize, Serialize};
99 use tracing::Level;
100
101- #[derive(Deserialize, Serialize)]
102+ #[derive(Deserialize, Serialize, Default)]
103 pub struct Config {
104 #[serde(default = "Database::default")]
105 pub database: Database,
106 diff --git a/ayllu-shell/src/config.rs b/ayllu-shell/src/config.rs
107index 84d644d..920fb73 100644
108--- a/ayllu-shell/src/config.rs
109+++ b/ayllu-shell/src/config.rs
110 @@ -10,6 +10,9 @@ use serde::{Deserialize, Serialize};
111 pub struct Shell {
112 /// Message of the Day displayed for each interactive Ayllu session
113 pub motd: String,
114+ /// Path to a socket for communicating with ayllu-buld
115+ #[serde(default = "Shell::default_build_socket")]
116+ pub build_socket: PathBuf,
117 /// Path to remove removed repositories
118 #[serde(default = "Shell::default_trash_path")]
119 pub trash_path: PathBuf,
120 @@ -19,48 +22,35 @@ impl Shell {
121 fn default_trash_path() -> PathBuf {
122 Path::new("/usr/share/ayllu/.trash").to_path_buf()
123 }
124+
125+ fn default_build_socket() -> PathBuf {
126+ ayllu_config::runtime_dir().join("ayllu/build.socket")
127+ }
128 }
129
130- #[derive(Serialize, Deserialize, Clone)]
131+ #[derive(Serialize, Deserialize, Clone, Default)]
132 pub struct Config {
133- #[serde(default = "Config::default_log_level")]
134- pub log_level: String,
135- #[serde(default = "Config::default_branch")]
136- pub default_branch: String,
137- pub base_dir: Option<PathBuf>,
138- #[serde(default = "Vec::new")]
139- pub identities: Vec<Identity>,
140+ #[serde(flatten)]
141+ pub common: ayllu_config::Common,
142 #[serde(default = "Shell::default")]
143 pub shell: Shell,
144 #[serde(default = "Vec::new")]
145 pub collections: Vec<Collection>,
146- #[serde(default = "Config::default_build_socket")]
147- pub build_socket_path: PathBuf,
148+ #[serde(default = "Vec::new")]
149+ pub identities: Vec<Identity>,
150 }
151
152- impl Config {
153- fn default_log_level() -> String {
154- String::from("INFO")
155- }
156+ impl Configurable for Config {}
157
158- fn default_branch() -> String {
159- ayllu_git::DEFAULT_GIT_BRANCH.to_string()
160- }
161+ #[cfg(test)]
162+ mod test {
163+ use ayllu_config::{EXAMPLE_CONFIG, Reader};
164
165- fn default_build_socket() -> PathBuf {
166- ayllu_config::runtime_dir().join("ayllu/build.socket")
167- }
168- }
169+ use crate::config::Config;
170
171- impl Configurable for Config {
172- fn initialize(&mut self) -> Result<(), ayllu_config::Error> {
173- if let Some(base_dir) = &self.base_dir {
174- self.collections.iter_mut().for_each(|collection| {
175- if collection.path.is_relative() {
176- collection.path = base_dir.join(&collection.path);
177- };
178- });
179- };
180- Ok(())
181+ #[test]
182+ fn check_config() {
183+ Reader::<Config>::read(EXAMPLE_CONFIG).unwrap();
184+ Reader::<Config>::read("").unwrap();
185 }
186 }
187 diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs
188index 747c3b8..0a53b3a 100644
189--- a/ayllu-shell/src/main.rs
190+++ b/ayllu-shell/src/main.rs
191 @@ -16,7 +16,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> {
192 .find(|identity| identity.username == username);
193 match args.command {
194 Some(Subcommand::GitReceivePack { path }) => {
195- let resolved = if let Some(base_dir) = config.base_dir {
196+ let resolved = if let Some(base_dir) = config.common.base_path {
197 let trimmed = if path.is_absolute() {
198 path.strip_prefix("/").unwrap()
199 } else {
200 @@ -36,7 +36,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> {
201 if ayllu_git::git_dir(&resolved).is_ok_and(|is_git_dir| is_git_dir) {
202 Wrapper::new(&resolved)?
203 } else {
204- Wrapper::create(&resolved, Some(&config.default_branch), true)?
205+ Wrapper::create(&resolved, Some(&config.common.default_branch), true)?
206 };
207 repository.receive_pack()?;
208 }
209 @@ -47,7 +47,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> {
210 }
211 Some(Subcommand::GitUploadPack { ref path }) => {
212 let resolved = if path.is_relative() {
213- if let Some(base_dir) = config.base_dir {
214+ if let Some(base_dir) = config.common.base_path.as_ref() {
215 &base_dir.join(path)
216 } else {
217 path
218 diff --git a/ayllu-shell/src/ui/repository_build.rs b/ayllu-shell/src/ui/repository_build.rs
219index 186426e..56ac52c 100644
220--- a/ayllu-shell/src/ui/repository_build.rs
221+++ b/ayllu-shell/src/ui/repository_build.rs
222 @@ -92,7 +92,7 @@ impl MenuItem for Item {
223 ) {
224 self.0.collection(choice.collection());
225 self.0.repository(choice.clone());
226- self.0.socket_path(config.build_socket_path.clone());
227+ self.0.socket_path(config.shell.build_socket.clone());
228 Ok(Some(Action::Branch))
229 } else {
230 Ok(None)
231 diff --git a/ayllu-web/src/config.rs b/ayllu-web/src/config.rs
232index 618a57a..40bd162 100644
233--- a/ayllu-web/src/config.rs
234+++ b/ayllu-web/src/config.rs
235 @@ -4,7 +4,7 @@ use std::path::Path;
236 use std::{collections::HashMap, path::PathBuf};
237 use url::Url;
238
239- use ayllu_config::{Configurable, Database, Error};
240+ use ayllu_config::{Common, Configurable, Database, Error};
241 use ayllu_git::Collection;
242 use ayllu_identity::Identity;
243
244 @@ -78,24 +78,62 @@ pub struct Theme {
245
246 #[derive(Deserialize, Serialize, Clone, Debug)]
247 pub struct Web {
248+ pub origin: Url,
249+ #[serde(default = "Web::default_site_name")]
250+ pub site_name: String,
251+ #[serde(default = "Web::default_address")]
252+ pub address: String,
253 pub extra_css: Option<String>,
254- #[serde(default = "Web::default_default_theme")]
255+ #[serde(default = "Web::default_theme")]
256 pub default_theme: String,
257- #[serde(default = "Web::default_themes")]
258- pub themes: Vec<Theme>,
259 pub additional_themes: Option<Vec<Theme>>,
260 #[serde(default = "Web::default_unsafe_markdown")]
261 pub unsafe_markdown: bool,
262+ #[serde(default = "Git::default")]
263+ pub git: Git,
264+ /// If man pages should be hosted on the site
265+ #[serde(default = "Web::default_serve_docs")]
266+ pub serve_docs: bool,
267+ #[serde(default = "Web::default_about_blurb")]
268+ pub blurb: String,
269+ #[serde(default = "Web::default_robots_txt")]
270+ pub robots: String,
271+ #[serde(default = "default_false")]
272+ pub subpath_mode: bool,
273+ pub lfs: Option<Lfs>,
274+ #[serde(default = "Sites::default")]
275+ pub sites: Sites,
276+ #[serde(rename = "tree-sitter")]
277+ pub tree_sitter: Option<TreeSitter>,
278+ #[serde(default = "Vec::default")]
279+ pub languages: Vec<Language>,
280+ #[serde(default = "Web::default_themes")]
281+ pub themes: Vec<Theme>,
282+ #[serde(default = "Web::default_rss_time_to_live")]
283+ pub rss_time_to_live: i64,
284 }
285
286 impl Default for Web {
287 fn default() -> Self {
288 Self {
289+ origin: Web::default_origin(),
290+ site_name: Web::default_site_name(),
291+ address: Web::default_address(),
292 extra_css: None,
293- default_theme: Web::default_default_theme(),
294- themes: Web::default_themes(),
295+ default_theme: Web::default_theme(),
296 additional_themes: None,
297- unsafe_markdown: Web::default_unsafe_markdown(),
298+ unsafe_markdown: true,
299+ git: Git::default(),
300+ serve_docs: false,
301+ blurb: Web::default_about_blurb(),
302+ robots: Web::default_robots_txt(),
303+ subpath_mode: false,
304+ lfs: None,
305+ sites: Sites::default(),
306+ tree_sitter: None,
307+ languages: Vec::default(),
308+ themes: Web::default_themes(),
309+ rss_time_to_live: Web::default_rss_time_to_live(),
310 }
311 }
312 }
313 @@ -129,25 +167,40 @@ impl Web {
314 ]
315 }
316
317- fn default_default_theme() -> String {
318+ fn default_origin() -> Url {
319+ Url::parse("http://127.0.0.1:10000").unwrap()
320+ }
321+
322+ fn default_address() -> String {
323+ "127.0.0.1:10000".to_string()
324+ }
325+
326+ fn default_theme() -> String {
327 String::from("rose-pine")
328 }
329
330+ fn default_rss_time_to_live() -> i64 {
331+ 3600
332+ }
333+
334 fn default_unsafe_markdown() -> bool {
335 true
336 }
337- }
338
339- #[derive(Deserialize, Serialize, Clone, Debug)]
340- pub struct Http {
341- pub address: String,
342- }
343+ fn default_about_blurb() -> String {
344+ String::from(DEFAULT_ABOUT_BLURB)
345+ }
346
347- impl Default for Http {
348- fn default() -> Self {
349- Self {
350- address: String::from("127.0.0.1:10000"),
351- }
352+ fn default_robots_txt() -> String {
353+ String::from(DEFAULT_ROBOTS_TXT.trim_start())
354+ }
355+
356+ fn default_site_name() -> String {
357+ String::from("🌄 ayllu")
358+ }
359+
360+ fn default_serve_docs() -> bool {
361+ true
362 }
363 }
364
365 @@ -193,13 +246,6 @@ pub struct TreeSitter {
366 impl TreeSitter {
367 fn validate(&self) -> Result<(), Error> {
368 Ok(())
369- // self.languages.iter().try_for_each(|language| {
370- // let module_path = self.template.module(&self.base_path, language);
371- // if !module_path.exists() {
372- // return Err(Error::Validation(format!("{module_path:?} does not exist")));
373- // };
374- // Ok(())
375- // })
376 }
377 }
378
379 @@ -240,79 +286,32 @@ pub struct Git {
380 #[derive(Default, Deserialize, Serialize, Clone, Debug)]
381 pub struct Build {}
382
383- #[derive(Deserialize, Serialize, Clone, Debug)]
384+ #[derive(Deserialize, Serialize, Clone, Debug, Default)]
385 pub struct Config {
386- #[serde(default = "Config::default_site_name")]
387- pub site_name: String,
388- #[serde(default = "Config::default_origin")]
389- pub origin: String,
390- // TODO: Deprecate this
391- pub domain: Option<String>,
392- #[serde(default = "Git::default")]
393- pub git: Git,
394- pub sysadmin: Option<String>,
395- /// If man pages should be hosted on the site
396- #[serde(default = "Config::default_serve_docs")]
397- pub serve_docs: bool,
398- #[serde(default = "Config::default_about_blurb")]
399- pub blurb: String,
400- #[serde(default = "Config::default_robots_txt")]
401- pub robots: String,
402- #[serde(default = "default_false")]
403- pub subpath_mode: bool,
404- #[serde(default = "Config::default_log_level")]
405- pub log_level: String,
406- pub default_branch: Option<String>,
407- pub rss_time_to_live: Option<i64>,
408+ #[serde(flatten)]
409+ pub common: Common,
410 #[serde(default = "Web::default")]
411 pub web: Web,
412- #[serde(default = "Http::default")]
413- pub http: Http,
414- #[serde(default = "Vec::new")]
415- pub collections: Vec<Collection>,
416- // Optional base directory to resolve relative collections from
417- pub base_dir: Option<PathBuf>,
418- #[serde(default = "Sites::default")]
419- pub sites: Sites,
420- #[serde(rename = "tree-sitter")]
421- pub tree_sitter: Option<TreeSitter>,
422- #[serde(default = "Vec::default")]
423- pub languages: Vec<Language>,
424- pub lfs: Option<Lfs>,
425- #[serde(default = "Vec::new")]
426- pub identities: Vec<Identity>,
427+ pub collections: Option<Vec<Collection>>,
428+ pub identities: Option<Vec<Identity>>,
429 pub database: Option<Database>,
430 pub build: Option<Build>,
431 }
432
433 impl Configurable for Config {
434 fn validate(&mut self) -> Result<(), Error> {
435- let parsed_url = Url::parse(&self.origin)
436- .map_err(|e| Error::Validation(format!("Cannot parse URL: {}: {e:?}", self.origin)))?;
437- self.domain = parsed_url.domain().map(|domain| domain.to_string());
438-
439 // verify collection names are all valid
440- if let Some(collection) = self
441- .collections
442- .iter()
443- .find(|collection| BANNED_COLLECTION_NAMES.contains(&collection.name.as_str()))
444- {
445+ if let Some(collection) = self.collections.as_ref().and_then(|collections| {
446+ collections
447+ .iter()
448+ .find(|collection| BANNED_COLLECTION_NAMES.contains(&collection.name.as_str()))
449+ }) {
450 return Err(Error::Validation(format!(
451 "{} is a reserved name",
452 collection.name
453 )));
454 };
455
456- // validate that all collection directories exist
457- for collection in self.collections.iter() {
458- if let Err(err) = metadata(collection.path.clone()) {
459- return Err(Error::Validation(format!(
460- "Failed to load collection {}:\n{err}",
461- collection.name
462- )));
463- }
464- }
465-
466 if let Some(additional_themes) = self.web.additional_themes.as_ref() {
467 self.web
468 .themes
469 @@ -331,21 +330,13 @@ impl Configurable for Config {
470 )));
471 }
472
473- if let Some(base_dir) = &self.base_dir {
474- self.collections.iter_mut().for_each(|collection| {
475- if collection.path.is_relative() {
476- collection.path = base_dir.join(&collection.path);
477- };
478- });
479- };
480-
481 if self.build.is_some() && self.database.is_none() {
482 return Err(Error::Validation(
483 "Builder is enabled but the database in unconfigured".to_string(),
484 ));
485 }
486
487- if let Some(ts_config) = self.tree_sitter.as_ref() {
488+ if let Some(ts_config) = self.web.tree_sitter.as_ref() {
489 ts_config.validate()?;
490 }
491
492 @@ -353,32 +344,6 @@ impl Configurable for Config {
493 }
494 }
495
496- impl Config {
497- fn default_robots_txt() -> String {
498- String::from(DEFAULT_ROBOTS_TXT.trim_start())
499- }
500-
501- fn default_site_name() -> String {
502- String::from("🌄 ayllu")
503- }
504-
505- fn default_log_level() -> String {
506- String::from("info")
507- }
508-
509- fn default_origin() -> String {
510- format!("http://{}", Http::default().address)
511- }
512-
513- fn default_about_blurb() -> String {
514- String::from(DEFAULT_ABOUT_BLURB)
515- }
516-
517- fn default_serve_docs() -> bool {
518- true
519- }
520- }
521-
522 #[cfg(test)]
523 mod tests {
524 use super::*;
525 @@ -387,10 +352,6 @@ mod tests {
526 #[test]
527 fn test_example_config() {
528 Reader::<Config>::read(EXAMPLE_CONFIG).unwrap();
529- }
530-
531- #[test]
532- fn test_empty_config() {
533 Reader::<Config>::read("").unwrap();
534 }
535 }
536 diff --git a/ayllu-web/src/highlight.rs b/ayllu-web/src/highlight.rs
537index c8c7ffc..cf20c51 100644
538--- a/ayllu-web/src/highlight.rs
539+++ b/ayllu-web/src/highlight.rs
540 @@ -55,7 +55,7 @@ unsafe fn load_language(path: &str, hint: &Hint) -> Language {
541 language
542 }
543
544- pub struct Loader<'a>(pub &'a crate::config::Config);
545+ pub struct Loader<'a>(pub &'a crate::config::Web);
546
547 impl Loader<'_> {
548 pub unsafe fn load(&self) -> Result<(), std::io::Error> {
549 diff --git a/ayllu-web/src/main.rs b/ayllu-web/src/main.rs
550index b6f977f..178babf 100644
551--- a/ayllu-web/src/main.rs
552+++ b/ayllu-web/src/main.rs
553 @@ -37,32 +37,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
554 }
555 Subcommand::Serve {} => {
556 let mut cfg: config::Config = Reader::load(cli.config.as_deref())?;
557- if cfg.collections.is_empty()
558+ if cfg.collections.is_none()
559 && let Ok(path) = std::env::current_dir()
560 && ayllu_git::git_dir(path.as_path()).is_ok_and(|is_git_dir| is_git_dir)
561 {
562- cfg.collections.push(Collection {
563+ cfg.collections = Some(vec![Collection {
564 name: String::from("Default"),
565 description: Some(format!("Collection @ {}", path.to_string_lossy())),
566 path,
567 hidden: Default::default(),
568- })
569+ }]);
570 }
571
572 init_logger(
573 cli.level
574 .as_ref()
575 .map(|level| Level::from_str(level))
576- .unwrap_or(Level::from_str(&cfg.log_level))?,
577+ .unwrap_or(Level::from_str(&cfg.common.log_level))?,
578 );
579
580 unsafe {
581- if cfg.tree_sitter.is_some() {
582- Loader(&cfg).load()?;
583+ if cfg.web.tree_sitter.is_some() {
584+ Loader(&cfg.web).load()?;
585 }
586 }
587
588- LANGUAGE_TABLE.load(&cfg.languages);
589+ LANGUAGE_TABLE.load(&cfg.web.languages);
590 web2::serve(&cfg).await?;
591 Ok(())
592 }
593 diff --git a/ayllu-web/src/web2/middleware/error.rs b/ayllu-web/src/web2/middleware/error.rs
594index 209e599..fddbdcf 100644
595--- a/ayllu-web/src/web2/middleware/error.rs
596+++ b/ayllu-web/src/web2/middleware/error.rs
597 @@ -51,7 +51,11 @@ pub async fn middleware(
598 NotFoundPage {
599 base: Base {
600 title: String::from("Resource Not Found"),
601- nav_elements: crate::web2::navigation::global("", false, state.serve_docs),
602+ nav_elements: crate::web2::navigation::global(
603+ "",
604+ false,
605+ state.web.serve_docs,
606+ ),
607 logo: DEFAULT_LOGO.to_string(),
608 ..Default::default()
609 },
610 @@ -68,7 +72,11 @@ pub async fn middleware(
611 ErrorPage {
612 base: Base {
613 title: String::from("Internal Server Error"),
614- nav_elements: crate::web2::navigation::global("", false, state.serve_docs),
615+ nav_elements: crate::web2::navigation::global(
616+ "",
617+ false,
618+ state.web.serve_docs,
619+ ),
620 logo: DEFAULT_LOGO.to_string(),
621 ..Default::default()
622 },
623 diff --git a/ayllu-web/src/web2/middleware/repository.rs b/ayllu-web/src/web2/middleware/repository.rs
624index d184f38..197e5df 100644
625--- a/ayllu-web/src/web2/middleware/repository.rs
626+++ b/ayllu-web/src/web2/middleware/repository.rs
627 @@ -47,20 +47,21 @@ impl Preamble {
628 commitish: Option<String>,
629 file_path: Option<String>,
630 ) -> Result<Self, Error> {
631- let (collection_path, repo_path, hidden) = match system_config
632- .collections
633- .iter()
634- .find(|collection| collection.name == collection_name)
635- {
636- Some(collection) => (
637- collection.path.clone(),
638- collection.path.join(std::path::Path::new(&repo_name)),
639- collection.hidden.is_some_and(|hidden| hidden),
640- ),
641- None => {
642- return Err(Error::NotFound(String::from("collection not found")));
643- }
644- };
645+ let (collection_path, repo_path, hidden) =
646+ match system_config.collections.as_ref().and_then(|collections| {
647+ collections
648+ .iter()
649+ .find(|collection| collection.name == collection_name)
650+ }) {
651+ Some(collection) => (
652+ collection.path.clone(),
653+ collection.path.join(std::path::Path::new(&repo_name)),
654+ collection.hidden.is_some_and(|hidden| hidden),
655+ ),
656+ None => {
657+ return Err(Error::NotFound(String::from("collection not found")));
658+ }
659+ };
660 let repository = Repository::new(&repo_path)?;
661 let is_empty = repository.is_empty()?;
662 let config = repository.config()?;
663 @@ -73,9 +74,6 @@ impl Preamble {
664 } else if let Some(default_branch) = config.default_branch.clone() {
665 let commit = repository.resolve_commit(Some(default_branch.as_str()))?;
666 (default_branch.clone(), commit)
667- } else if let Some(system_default_branch) = &system_config.default_branch {
668- let commit = repository.resolve_commit(Some(system_default_branch.as_str()))?;
669- (system_default_branch.clone(), commit)
670 } else {
671 let default_branch = repository.default_ref()?;
672 let commit = repository.resolve_commit(Some(default_branch.as_str()))?;
673 diff --git a/ayllu-web/src/web2/middleware/sites.rs b/ayllu-web/src/web2/middleware/sites.rs
674index 7d846c8..e6aab53 100644
675--- a/ayllu-web/src/web2/middleware/sites.rs
676+++ b/ayllu-web/src/web2/middleware/sites.rs
677 @@ -37,19 +37,21 @@ fn repositories(collections: &[Collection]) -> Result<Vec<PathBuf>, Error> {
678
679 pub fn sites(cfg: &Config) -> Result<Sites, Error> {
680 let mut sites: Vec<(String, (String, String))> = Vec::new();
681- for path in repositories(&cfg.collections)? {
682- let repository = Repository::new(path.as_path())?;
683- let repo_config = repository.config()?;
684- if let Some(sites_cfg) = repo_config.sites {
685- let domain = sites_cfg.header.unwrap(); // FIXME
686- match domain.split_once('=') {
687- Some((key, value)) => {
688- let repo_path = path.to_str().unwrap().to_string();
689- sites.push((repo_path.to_string(), (key.to_string(), value.to_string())));
690- tracing::info!("serving static site {domain} -> {repo_path}");
691- }
692- None => panic!("bad key=value header"),
693- };
694+ if let Some(collections) = cfg.collections.as_ref() {
695+ for path in repositories(collections)? {
696+ let repository = Repository::new(path.as_path())?;
697+ let repo_config = repository.config()?;
698+ if let Some(sites_cfg) = repo_config.sites {
699+ let domain = sites_cfg.header.unwrap(); // FIXME
700+ match domain.split_once('=') {
701+ Some((key, value)) => {
702+ let repo_path = path.to_str().unwrap().to_string();
703+ sites.push((repo_path.to_string(), (key.to_string(), value.to_string())));
704+ tracing::info!("serving static site {domain} -> {repo_path}");
705+ }
706+ None => panic!("bad key=value header"),
707+ };
708+ }
709 }
710 }
711 Ok(sites)
712 @@ -60,7 +62,7 @@ pub async fn middleware(
713 req: extract::Request,
714 next: Next,
715 ) -> Result<Response, Error> {
716- if !cfg.sites.enabled {
717+ if !cfg.web.sites.enabled {
718 return Ok(next.run(req).await);
719 }
720
721 @@ -133,8 +135,7 @@ pub async fn middleware(
722
723 // special case if running a static site in-front of the forge
724 // where requests can fall through to the backend.
725- // TODO: Change this to use the origin URL
726- if hostname.is_some_and(|name| name == cfg.domain.unwrap()) {
727+ if hostname.is_some_and(|name| name == cfg.web.origin.host_str().unwrap()) {
728 let response = next.run(req).await;
729 Ok(response)
730 } else {
731 diff --git a/ayllu-web/src/web2/middleware/template.rs b/ayllu-web/src/web2/middleware/template.rs
732index 95341b0..44b1306 100644
733--- a/ayllu-web/src/web2/middleware/template.rs
734+++ b/ayllu-web/src/web2/middleware/template.rs
735 @@ -27,7 +27,7 @@ pub async fn middleware(
736 ) -> Response {
737 let request_uri = req.uri();
738 let template_base = Base {
739- subpath_mode: config.subpath_mode,
740+ subpath_mode: config.web.subpath_mode,
741 logo: DEFAULT_LOGO.to_string(),
742 feed_icon: DEFAULT_RSS_ICON.to_string(),
743 user_config,
744 @@ -39,7 +39,7 @@ pub async fn middleware(
745 .map(|theme| theme.name.clone())
746 .collect(),
747 },
748- global_nav: crate::web2::navigation::global("/", false, config.serve_docs),
749+ global_nav: crate::web2::navigation::global("/", false, config.web.serve_docs),
750 path: request_uri.path().to_string(),
751 menu_open: params.menu.is_some_and(|open| open > 0),
752 ..Default::default()
753 diff --git a/ayllu-web/src/web2/routes/about.rs b/ayllu-web/src/web2/routes/about.rs
754index 4464028..7b106ba 100644
755--- a/ayllu-web/src/web2/routes/about.rs
756+++ b/ayllu-web/src/web2/routes/about.rs
757 @@ -20,11 +20,11 @@ pub async fn serve(
758 Extension(adapter): Extension<TreeSitterAdapter>,
759 Extension(mut base): Extension<Base>,
760 ) -> Result<Html<String>, Error> {
761- base.global_nav = navigation::global("about", false, cfg.serve_docs);
762+ base.global_nav = navigation::global("about", false, cfg.web.serve_docs);
763 let options = ComrakOptions::default();
764 let mut plugins = ComrakPlugins::default();
765 plugins.render.codefence_syntax_highlighter = Some(&adapter);
766- let blurb = markdown_to_html_with_plugins(&cfg.blurb, &options, &plugins);
767+ let blurb = markdown_to_html_with_plugins(&cfg.web.blurb, &options, &plugins);
768 Ok(Html(
769 PageTemplate {
770 base,
771 diff --git a/ayllu-web/src/web2/routes/blob.rs b/ayllu-web/src/web2/routes/blob.rs
772index 66b5eea..708edd8 100644
773--- a/ayllu-web/src/web2/routes/blob.rs
774+++ b/ayllu-web/src/web2/routes/blob.rs
775 @@ -79,7 +79,7 @@ pub async fn serve(
776 } else if mime_type == "text/markdown" {
777 is_markdown = true;
778 let renderer = Renderer {
779- origin: &cfg.origin,
780+ origin: &cfg.web.origin.as_str(),
781 collection: &preamble.collection_name,
782 name: &preamble.repo_name,
783 refname: &preamble.refname,
784 @@ -151,7 +151,7 @@ pub async fn serve_raw(
785 }
786 let blob = blob.unwrap();
787 if blob.is_pointer {
788- let lfs_config = cfg.lfs.clone().unwrap();
789+ let lfs_config = cfg.web.lfs.clone().unwrap();
790 let location = util::lfs_url(
791 lfs_config.url_template.as_str(),
792 preamble.collection_name.as_str(),
793 diff --git a/ayllu-web/src/web2/routes/finger.rs b/ayllu-web/src/web2/routes/finger.rs
794index e0224c7..6a506d2 100644
795--- a/ayllu-web/src/web2/routes/finger.rs
796+++ b/ayllu-web/src/web2/routes/finger.rs
797 @@ -47,16 +47,24 @@ pub struct Resolver {
798
799 impl Resolver {
800 pub fn new(config: &Config) -> Self {
801- let identities = HashMap::from_iter(
802- config
803- .identities
804- .iter()
805- .map(|identity| (identity.email.clone(), identity.clone())),
806- );
807 Resolver {
808- origin: Url::parse(&config.origin).expect("Origin URL is invalid"),
809- collections: config.collections.clone(),
810- identities,
811+ origin: config.web.origin.clone(),
812+ collections: config
813+ .collections
814+ .as_ref()
815+ .map(|collections| collections.clone())
816+ .unwrap_or_default(),
817+ identities: config
818+ .identities
819+ .as_ref()
820+ .map(|identities| {
821+ HashMap::from_iter(
822+ identities
823+ .iter()
824+ .map(|identity| (identity.email.clone(), identity.clone())),
825+ )
826+ })
827+ .unwrap_or_default(),
828 }
829 }
830
831 diff --git a/ayllu-web/src/web2/routes/git.rs b/ayllu-web/src/web2/routes/git.rs
832index 3be7c1f..863e9d9 100644
833--- a/ayllu-web/src/web2/routes/git.rs
834+++ b/ayllu-web/src/web2/routes/git.rs
835 @@ -81,7 +81,7 @@ pub async fn handle(
836 headers: HeaderMap,
837 body: Body,
838 ) -> Result<Response, Error> {
839- if !cfg.git.smart_http {
840+ if !cfg.web.git.smart_http {
841 // if the smart_http server is disabled always return 404.
842 return Err(Error::NotFound("repository not found".to_string()));
843 }
844 @@ -123,7 +123,7 @@ pub async fn handle(
845
846 tracing::debug!("calling {cmd:?}");
847
848- if cfg.git.export_all {
849+ if cfg.web.git.export_all {
850 cmd = cmd.env("GIT_HTTP_EXPORT_ALL", "true");
851 };
852
853 diff --git a/ayllu-web/src/web2/routes/index.rs b/ayllu-web/src/web2/routes/index.rs
854index 3f85259..c366fb0 100644
855--- a/ayllu-web/src/web2/routes/index.rs
856+++ b/ayllu-web/src/web2/routes/index.rs
857 @@ -75,8 +75,8 @@ pub async fn index(
858 Extension(cfg): Extension<Config>,
859 Extension(mut base): Extension<Base>,
860 ) -> Result<Html<String>, Error> {
861- let collections: Vec<Collection> =
862- cfg.collections
863+ let collections: Vec<Collection> = if let Some(collections) = cfg.collections.as_ref() {
864+ collections
865 .iter()
866 .try_fold(Vec::new(), |mut accm, collection| {
867 if collection.hidden.is_some_and(|hidden| hidden) {
868 @@ -89,7 +89,11 @@ pub async fn index(
869 repositories,
870 });
871 Ok::<Vec<Collection>, Error>(accm)
872- })?;
873+ })?
874+ } else {
875+ Vec::default()
876+ };
877+
878 // let mut collections: Vec<Collection> = Vec::new();
879 // for collection in cfg.collections.iter() {
880 // if collection.hidden.is_some_and(|hidden| hidden) {
881 @@ -117,20 +121,22 @@ struct CollectionPageTemplate {
882
883 pub async fn collection(
884 Extension(cfg): Extension<Config>,
885- collection: Option<Path<String>>,
886+ collection_name: Path<String>,
887 Extension(mut base): Extension<Base>,
888 ) -> Result<Html<String>, Error> {
889- let entry = cfg.collections.iter().find(|entry| {
890- collection
891- .as_ref()
892- .is_some_and(|collection| entry.name == collection.0)
893- });
894+ let entry = if let Some(collections) = cfg.collections.as_ref() {
895+ collections
896+ .iter()
897+ .find(|collection| collection.name == collection_name.0)
898+ } else {
899+ None
900+ };
901 if entry.is_none() {
902 return Err(Error::NotFound("collection not found".to_string()));
903 }
904 let entry = entry.unwrap();
905 let repositories = load_repositories(entry.path.as_path())?;
906- base.title = format!("{collection:?}");
907+ base.title = entry.name.clone();
908 let page = CollectionPageTemplate {
909 base,
910 collection: Collection {
911 diff --git a/ayllu-web/src/web2/routes/man.rs b/ayllu-web/src/web2/routes/man.rs
912index 8a3b5b6..3cd13ba 100644
913--- a/ayllu-web/src/web2/routes/man.rs
914+++ b/ayllu-web/src/web2/routes/man.rs
915 @@ -25,7 +25,7 @@ pub async fn serve(
916 Extension(mut base): Extension<Base>,
917 name: Option<Path<String>>,
918 ) -> Result<Html<String>, Error> {
919- base.global_nav = navigation::global("man", false, cfg.serve_docs);
920+ base.global_nav = navigation::global("man", false, cfg.web.serve_docs);
921 let name = if let Some(Path(name)) = name.as_ref() {
922 name.as_str()
923 } else {
924 diff --git a/ayllu-web/src/web2/routes/repo.rs b/ayllu-web/src/web2/routes/repo.rs
925index 05766e2..bd5c57e 100644
926--- a/ayllu-web/src/web2/routes/repo.rs
927+++ b/ayllu-web/src/web2/routes/repo.rs
928 @@ -118,7 +118,7 @@ pub async fn serve(
929 let readme = match readme {
930 Some(readme) => {
931 let renderer = Renderer {
932- origin: &cfg.origin,
933+ origin: cfg.web.origin.as_str(),
934 collection: &preamble.collection_name,
935 name: &preamble.repo_name,
936 refname: &preamble.refname,
937 @@ -164,7 +164,9 @@ pub async fn serve(
938 rss_link_1m: &rss_link_1m,
939 http_clone_url: &format!(
940 "{}/{}/{}",
941- cfg.origin, preamble.collection_name, preamble.repo_name
942+ cfg.web.origin.as_str(),
943+ preamble.collection_name,
944+ preamble.repo_name
945 ),
946 git_clone_url: None, // FIXME
947 refname: &preamble.refname,
948 diff --git a/ayllu-web/src/web2/routes/rest/mod.rs b/ayllu-web/src/web2/routes/rest/mod.rs
949index cfdb010..ae01250 100644
950--- a/ayllu-web/src/web2/routes/rest/mod.rs
951+++ b/ayllu-web/src/web2/routes/rest/mod.rs
952 @@ -1,2 +1,3 @@
953- pub mod discovery;
954+ // TODO: Ayllu will never implement a bespoke REST API
955+ // pub mod discovery;
956 pub mod ping;
957 diff --git a/ayllu-web/src/web2/routes/robots.rs b/ayllu-web/src/web2/routes/robots.rs
958index 11b6c44..b1324b9 100644
959--- a/ayllu-web/src/web2/routes/robots.rs
960+++ b/ayllu-web/src/web2/routes/robots.rs
961 @@ -4,12 +4,12 @@ use crate::config::Config;
962 use crate::web2::error::Error;
963
964 pub async fn serve(Extension(cfg): Extension<Config>) -> Result<impl IntoResponse, Error> {
965- let robots_txt = cfg.sysadmin.clone().map_or(cfg.robots.clone(), |sysadmin| {
966- format!(
967- "# contact {} with questions\n{}",
968- sysadmin,
969- cfg.robots.clone()
970- )
971- });
972- Ok(robots_txt)
973+ if let Some(sysadmin) = cfg.common.sysadmin.as_ref() {
974+ Ok(format!(
975+ "# contact {sysadmin} with questions\n{}",
976+ cfg.web.robots,
977+ ))
978+ } else {
979+ Ok(cfg.web.robots.clone())
980+ }
981 }
982 diff --git a/ayllu-web/src/web2/routes/rss.rs b/ayllu-web/src/web2/routes/rss.rs
983index 030acdb..112cd97 100644
984--- a/ayllu-web/src/web2/routes/rss.rs
985+++ b/ayllu-web/src/web2/routes/rss.rs
986 @@ -286,13 +286,13 @@ impl Builder {
987
988 pub async fn feed_firehose(Extension(cfg): Extension<Config>) -> Result<Response, Error> {
989 let builder = Builder {
990- origin: cfg.origin.clone(),
991+ origin: cfg.web.origin.to_string(),
992 title: String::from("Firehose"),
993- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
994+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
995 current_time: OffsetDateTime::now_utc(),
996 };
997 let channel = builder.firehose(
998- builder.scan_repositories(&cfg.collections)?,
999+ builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?,
1000 Duration::days(7),
1001 )?;
1002 let response = Response::builder()
1003 @@ -304,13 +304,13 @@ pub async fn feed_firehose(Extension(cfg): Extension<Config>) -> Result<Response
1004
1005 pub async fn feed_1d(Extension(cfg): Extension<Config>) -> Result<Response, Error> {
1006 let builder = Builder {
1007- origin: cfg.origin.clone(),
1008+ origin: cfg.web.origin.to_string(),
1009 title: String::from("Daily Update Summary"),
1010- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1011+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1012 current_time: OffsetDateTime::now_utc(),
1013 };
1014 let channel = builder.summary(
1015- builder.scan_repositories(&cfg.collections)?,
1016+ builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?,
1017 Timeframe::Daily,
1018 )?;
1019 let response = Response::builder()
1020 @@ -322,13 +322,13 @@ pub async fn feed_1d(Extension(cfg): Extension<Config>) -> Result<Response, Erro
1021
1022 pub async fn feed_1w(Extension(cfg): Extension<Config>) -> Result<Response, Error> {
1023 let builder = Builder {
1024- origin: cfg.origin.clone(),
1025+ origin: cfg.web.origin.to_string(),
1026 title: String::from("Weekly Update Summary"),
1027- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1028+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1029 current_time: OffsetDateTime::now_utc(),
1030 };
1031 let channel = builder.summary(
1032- builder.scan_repositories(&cfg.collections)?,
1033+ builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?,
1034 Timeframe::Weekly,
1035 )?;
1036 let response = Response::builder()
1037 @@ -340,13 +340,13 @@ pub async fn feed_1w(Extension(cfg): Extension<Config>) -> Result<Response, Erro
1038
1039 pub async fn feed_1m(Extension(cfg): Extension<Config>) -> Result<Response, Error> {
1040 let builder = Builder {
1041- origin: cfg.origin.clone(),
1042+ origin: cfg.web.origin.to_string(),
1043 title: String::from("Monthly Update Summary"),
1044- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1045+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1046 current_time: OffsetDateTime::now_utc(),
1047 };
1048 let channel = builder.summary(
1049- builder.scan_repositories(&cfg.collections)?,
1050+ builder.scan_repositories(cfg.collections.as_deref().unwrap_or(&[]))?,
1051 Timeframe::Monthly,
1052 )?;
1053 let response = Response::builder()
1054 @@ -361,14 +361,14 @@ pub async fn feed_repository_firehose(
1055 Extension(cfg): Extension<Config>,
1056 Extension(preamble): Extension<Preamble>,
1057 ) -> Result<Response, Error> {
1058- let project_url = util::project_url(&uri, cfg.origin.as_str());
1059+ let project_url = util::project_url(&uri, cfg.web.origin.as_str());
1060 let builder = Builder {
1061- origin: cfg.origin,
1062+ origin: cfg.web.origin.to_string(),
1063 title: format!(
1064 "Firehose for {}/{}",
1065 preamble.collection_name, preamble.repo_name
1066 ),
1067- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1068+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1069 current_time: OffsetDateTime::now_utc(),
1070 };
1071 let channel = builder.firehose(vec![(preamble.repo_path, project_url)], Duration::days(7))?;
1072 @@ -384,14 +384,14 @@ pub async fn feed_repository_1d(
1073 Extension(cfg): Extension<Config>,
1074 Extension(preamble): Extension<Preamble>,
1075 ) -> Result<Response, Error> {
1076- let project_url = util::project_url(&uri, cfg.origin.as_str());
1077+ let project_url = util::project_url(&uri, cfg.web.origin.as_str());
1078 let builder = Builder {
1079- origin: cfg.origin,
1080+ origin: cfg.web.origin.to_string(),
1081 title: format!(
1082 "Daily Update Summary for {}/{}",
1083 preamble.collection_name, preamble.repo_name
1084 ),
1085- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1086+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1087 current_time: OffsetDateTime::now_utc(),
1088 };
1089 let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Daily)?;
1090 @@ -407,14 +407,14 @@ pub async fn feed_repository_1w(
1091 Extension(cfg): Extension<Config>,
1092 Extension(preamble): Extension<Preamble>,
1093 ) -> Result<Response, Error> {
1094- let project_url = util::project_url(&uri, cfg.origin.as_str());
1095+ let project_url = util::project_url(&uri, cfg.web.origin.as_str());
1096 let builder = Builder {
1097- origin: cfg.origin,
1098+ origin: cfg.web.origin.to_string(),
1099 title: format!(
1100 "Weekly Update Summary for {}/{}",
1101 preamble.collection_name, preamble.repo_name
1102 ),
1103- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1104+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1105 current_time: OffsetDateTime::now_utc(),
1106 };
1107 let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Weekly)?;
1108 @@ -430,14 +430,14 @@ pub async fn feed_repository_1m(
1109 Extension(cfg): Extension<Config>,
1110 Extension(preamble): Extension<Preamble>,
1111 ) -> Result<Response, Error> {
1112- let project_url = util::project_url(&uri, cfg.origin.as_str());
1113+ let project_url = util::project_url(&uri, cfg.web.origin.as_str());
1114 let builder = Builder {
1115- origin: cfg.origin,
1116+ origin: cfg.web.origin.to_string(),
1117 title: format!(
1118 "Monthly Update Summary for {}/{}",
1119 preamble.collection_name, preamble.repo_name
1120 ),
1121- time_to_live: cfg.rss_time_to_live.map(Duration::seconds),
1122+ time_to_live: Some(Duration::seconds(cfg.web.rss_time_to_live)),
1123 current_time: OffsetDateTime::now_utc(),
1124 };
1125 let channel = builder.summary(vec![(preamble.repo_path, project_url)], Timeframe::Monthly)?;
1126 diff --git a/ayllu-web/src/web2/server.rs b/ayllu-web/src/web2/server.rs
1127index a6be66b..d5d8d02 100644
1128--- a/ayllu-web/src/web2/server.rs
1129+++ b/ayllu-web/src/web2/server.rs
1130 @@ -16,8 +16,7 @@ use tower_http::{
1131 };
1132 use tracing::{Level, Span};
1133
1134- use crate::highlight::{Highlighter, Loader, TreeSitterAdapter};
1135- use crate::languages::{Hint, LANGUAGE_TABLE};
1136+ use crate::highlight::{Highlighter, TreeSitterAdapter};
1137 use crate::web2::middleware::database as db;
1138 use crate::web2::middleware::error;
1139 use crate::web2::middleware::repository;
1140 @@ -35,14 +34,14 @@ use crate::web2::routes::log as log_route;
1141 use crate::web2::routes::man;
1142 use crate::web2::routes::refs;
1143 use crate::web2::routes::repo;
1144- use crate::web2::routes::rest::discovery;
1145+ // use crate::web2::routes::rest::discovery;
1146 use crate::web2::routes::rest::ping;
1147 use crate::web2::routes::robots;
1148 use crate::web2::routes::rss;
1149 use crate::{config::Config, web2::middleware::template};
1150
1151 pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1152- let keywords = match &cfg.tree_sitter {
1153+ let keywords = match &cfg.web.tree_sitter {
1154 Some(ts_config) => ts_config.keywords.clone(),
1155 None => {
1156 tracing::warn!("tree-sitter is not configured, syntax highlighting will be disabled");
1157 @@ -53,7 +52,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1158 let highlighter = Highlighter::new(&keywords);
1159 let adapter = TreeSitterAdapter(highlighter.clone());
1160
1161- let site_mapping = if cfg.sites.enabled {
1162+ let site_mapping = if cfg.web.sites.enabled {
1163 sites::sites(cfg)?
1164 } else {
1165 Vec::new()
1166 @@ -70,7 +69,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1167 tracing::info!("SQLite Database is serving");
1168 }
1169
1170- let address: SocketAddrV4 = cfg.http.address.parse()?;
1171+ let address: SocketAddrV4 = cfg.web.address.parse()?;
1172 let app = NormalizePathLayer::trim_trailing_slash().layer(
1173 Router::new()
1174 .route("/robots.txt", routing::get(robots::serve))
1175 @@ -91,7 +90,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1176 .nest(
1177 "/0",
1178 Router::new()
1179- .route("/index", routing::get(discovery::serve))
1180+ // .route("/index", routing::get(discovery::serve))
1181 .route("/ping", routing::get(ping::serve)),
1182 )
1183 .nest(
1184 @@ -188,7 +187,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> {
1185 .on_response(DefaultOnResponse::new().level(Level::INFO)),
1186 ),
1187 );
1188- tracing::info!("listening @ {}", cfg.http.address);
1189+ tracing::info!("listening @ {}", cfg.web.address);
1190 let listener = TcpListener::bind(address).await?;
1191 axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await?;
1192 Ok(())
1193 diff --git a/ayllu.5.md b/ayllu.5.md
1194index d1077fe..636060b 100644
1195--- a/ayllu.5.md
1196+++ b/ayllu.5.md
1197 @@ -4,7 +4,8 @@ ayllu 5 "Configuration"
1198 # CONFIGURATION
1199
1200 Ayllu's configuration is managed by a single TOML file which can reside in
1201- a variety of places. All content stored in the configuration file is considered
1202+ a variety of places. All components of Ayllu are guaranteed to run without any
1203+ configuration. All content stored in the configuration file is considered
1204 public and appropriate for viewing by any users of the installation. The file
1205 is read by all components of Ayllu and contains a global configuration section
1206 which influences all applications as well as specific sections for different
1207 @@ -25,6 +26,4 @@ Finally two hard coded paths will be inspected:
1208 * `config.toml`
1209 * `/etc/ayllu/config.toml`
1210
1211- If no configuration file is found typically Ayllu can be run with only defaults.
1212-
1213 # ANNOTATED EXAMPLE CONFIGURATION
1214 diff --git a/config.example.toml b/config.example.toml
1215index 50dc005..83e13fe 100644
1216--- a/config.example.toml
1217+++ b/config.example.toml
1218 @@ -1,3 +1,7 @@
1219+ ###
1220+ ##### Global Configuration (effects multiple programs)
1221+ ###
1222+
1223 # site name used in various places across the instance
1224 site_name = "🌄 Ayllu"
1225
1226 @@ -5,50 +9,108 @@ site_name = "🌄 Ayllu"
1227 # default_branch = "master"
1228 default_branch = "main"
1229
1230- # A valid URI that identifies this server on the global internet
1231- origin = "localhost:10000"
1232+ # Global logging level
1233+ log_level = "INFO"
1234
1235- # sysadmin contact address
1236- sysadmin = "admin@example.org"
1237+ # When base_path is specified collection/repo pairs are evaluated relative to
1238+ # base path. So for example git clone ayllu@example.org:fuu/bar will
1239+ # automatically resolve to $basepath/fuu/bar.
1240+ base_path = "/var/lib/ayllu/repos"
1241
1242- # logging level
1243- log_level = "INFO"
1244+ # A collection refers to a group of repositories. Each collection may contain only a
1245+ # single level of repositories e.g. [$collection/repo-a, $collection/repo-b].
1246+ #
1247+ [[collections]]
1248+ # name of the code collection
1249+ name = "projects"
1250+ description = "Some really cool projects"
1251+ path = "/path/to/projects"
1252+ # If true the collection will not show up in the main index or RSS feeds
1253+ hidden = false
1254+
1255+ # Users are associated with "Identities" in Ayllu where each identity refers
1256+ # to a particular code author and system account on a given server.
1257+ [[identities]]
1258+ username = "demo"
1259+ authorized_keys = [
1260+ # public keys go here
1261+ ]
1262+ # E-mail address of the author
1263+ email = "example@example.org"
1264+ # Optional "tagline" associated with the author
1265+ tagline = "Programmer interested free software"
1266+ # Optional link to an avatar containing an image representing the author
1267+ avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" }
1268+ # Array of personal websites, social media, etc. associated with the author
1269+ profiles = [
1270+ { url = "https://example.com", mime_type = "text/html"},
1271+ { url = "https://example.org/@example", mime_type = "text/html"},
1272+ ]
1273+
1274+ [database]
1275+ # Path to the SQLite database
1276+ path = "/var/lib/ayllu/state.db"
1277+
1278+ ###
1279+ ##### Web Configuration (ayllu-web)
1280+ ###
1281+
1282+ [web]
1283+
1284+ # Interface and port to listen on
1285+ address = "127.0.0.1:10000"
1286+
1287+ # A valid URI that identifies this server on the global internet
1288+ origin = "localhost:10000"
1289
1290 # enable when ayllu is being served from a "subpath". Clicking the home
1291 # button will send the user back to /browse instead of /. This is useful if you
1292 # have a landing page at the root of your webserver path.
1293 subpath_mode = false
1294
1295- # When specified collections which do not have a relative path are
1296- # appended to this path. The default is None.
1297- # base_dir = /usr/share/ayllu/repos
1298-
1299 # friendly message to display on the about page of the main site which might
1300 # include details such as contact information, etc. markdown is supported.
1301 blurb = """
1302 # Welcome to Ayllu!
1303 """
1304
1305- # The default branch name to use when rendering repositories. If unspecified
1306- # the ref that HEAD is currently pointed at will be used.
1307- # default_branch = "main"
1308-
1309 # The number of threads to spawn by Tokio, typically this will be the number
1310 # of logical processors (cores) you have on your system
1311- # worker_threads = 8
1312+ worker_threads = 8
1313
1314 # The number of additional threads that can be launched to support synchronous
1315 # blocking operations.
1316- # max_blocking_threads = 512
1317+ max_blocking_threads = 512
1318
1319 # The number of suggested seconds to wait before polling the server again
1320 # across RSS feeds. The default of 1 hour is a reasonable default in most
1321 # cases.
1322 rss_time_to_live = 3600
1323
1324- # Git HTTP server options
1325+ # Themes are CSS files but you can also directly include some
1326+ # simple CSS right in your config which will be applied everywhere.
1327+ extra-css = """
1328+ body {
1329+ background-color: pink;
1330+ }
1331+ """
1332+ # # Set the theme which is served by default when no setting
1333+ # # is configured in client cookies.
1334+ default_theme = "stella"
1335
1336- [git]
1337+ # # Additional themes can be added such as below, see ayllu/themes for
1338+ # # examples of customization.
1339+ [[web.themes]]
1340+ # Name for your theme
1341+ name = "stella"
1342+ # All of the CSS content served as the theme.
1343+ css_content = """
1344+ body {
1345+ background-color: pink;
1346+ }
1347+ """
1348+ # Git HTTP server options
1349+ [web.git]
1350 # Global option to enable the Git "Smart HTTP" server which will serve
1351 # non-hidden repositories for cloning over HTTP. The default is true but if
1352 # set to false it will globally disable all cloning which you might want to do
1353 @@ -64,119 +126,6 @@ clone_url = "git@localhost"
1354 # for cloing to be permitted.
1355 export_all = false
1356
1357- # Job server configuration
1358-
1359- [jobs]
1360-
1361- # socket for the job server to listen to new requests on. This is typically
1362- # used to communicate via git hooks to perform new computation on a repository
1363- # after it has been updated.
1364- # jobs_socket_path = "/var/run/user/1000/ayllu-jobs.sock"
1365-
1366- # Maximum time to allow a job to run before killing it in seconds.
1367- # This value needs to be high for large repositories since we have to run a
1368- # job for every commit in the repository.
1369- timeout = 1800
1370-
1371- ##
1372- ## Identities
1373- ##
1374-
1375- # Users are associated with "Identities" in Ayllu where each identity refers
1376- # to a particular code author and system account on a given server. If the
1377- # Ayllu HTTP server configured with [[ identities ]] then the deligated account
1378- # may maintain it's own configuration and collection of repositories.
1379-
1380- # [[identities]]
1381- # username = "demo"
1382- # # optional, relative to the user's home directory
1383- # repositories_path = "repos"
1384-
1385- # A deligated account may also manage their own identity information by
1386- # creating another Ayllu configuration file at ~/.config/ayllu/config.toml.
1387- # A deligated account may not further deligate other identities and must
1388- # include a "me" section in their configuration.
1389- # [me]
1390- # # E-mail address of the author
1391- # email = "example@example.org"
1392- # # Optional "tagline" associated with the author
1393- # tagline = "Programmer interested free software"
1394- # # Optional link to an avatar containing an image representing the author
1395- # avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" }
1396- # # Array of personal websites, social media, etc. associated with the author
1397- # profiles = [
1398- # { url = "https://example.com", mime_type = "text/html"},
1399- # { url = "https://example.org/@example", mime_type = "text/html"},
1400- # ]
1401-
1402- # Deligated accounts may specify their own collections but they must be
1403- # relative to the global identities.repositories_path set by the site
1404- # administrator. Note that the repository path MUST be at least readable by
1405- # the system ayllu account such that the web server can serve the repository
1406- # contents.
1407- # [[ collection ]]
1408- # name = "my-code"
1409- # description = "random software"
1410- # path = "./projects/fuu"
1411-
1412- # A configuration that specifies both [[ identities ]] and [ me ] is considered
1413- # invalid and will prevent the server from starting up.
1414-
1415- # List of authors associated with this site
1416- # [[authors]]
1417- # # E-mail address of the author
1418- # email = "example@example.org"
1419- # # Optional "tagline" associated with the author
1420- # tagline = "Programmer interested free software"
1421- # # Optional link to an avatar containing an image representing the author
1422- # avatar = { url = "https://example.org/avatar.png", mime_type = "image/png" }
1423- # # Array of personal websites, social media, etc. associated with the author
1424- # profiles = [
1425- # { url = "https://example.com", mime_type = "text/html"},
1426- # { url = "https://example.org/@example", mime_type = "text/html"},
1427- # ]
1428-
1429- [http]
1430- # interface and port to listen on
1431- address = "127.0.0.1:10000"
1432-
1433- [web]
1434- # # Themes are CSS files but you can also directly include some
1435- # # simple CSS right in your config which will be applied everywhere.
1436- # extra-css = """
1437- # .fuu-bar {
1438- # color: pink;
1439- # }
1440- # """
1441- # # Set the theme which is served by default when no setting
1442- # # is configured in client cookies.
1443- # default-theme = "stella"
1444- # # Additional themes can be added such as below, see ayllu/themes for
1445- # # examples of customization.
1446- # [[web.themes]]
1447- # # Name for your theme
1448- # name = "stella"
1449- # # All of the CSS content served as the theme.
1450- # css-content = """
1451- # body {
1452- # background-color: pink;
1453- # }
1454- # """
1455- # # Additional themes is the same as themes but will be appended to the
1456- # # themes which are already shipped with the Ayllu binary
1457- # [[web.additional-themes]]
1458- # name = "fuu"
1459- # css-content = "body {color: pink}"
1460- # default theme that will be used when none is specified in user configuration.
1461- # additionally the default theme will be used to load assets that are not
1462- # overriden in the actively selected theme.
1463- # default_theme = "default"
1464- # [[web.themes]]
1465- # unique name to identify the theme in the web UI
1466- # name = "my-theme"
1467- # path to the directory containing the theme assets
1468- # path = "/usr/lib/ayllu/themes/my-theme"
1469-
1470 # static hosting
1471 # see an example Nginx configuration in contrib/nginx/nginx.conf
1472 [sites]
1473 @@ -189,149 +138,106 @@ enabled = false
1474 # Debian stable / testing has none. Archlinux has a few packages without
1475 # queries.
1476 #
1477- # [tree-sitter]
1478+ [web.tree-sitter]
1479
1480- # # Base directory to look for shared objects and other ts configuration in.
1481- # base_path = "/usr/lib/helix/runtime"
1482+ # Base directory to look for shared objects and other ts configuration in.
1483+ base_path = "/usr/lib/helix/runtime"
1484
1485 # tree-sitter highlighting keywords, these are attributes that will be
1486 # exposed as CSS classes which can be used to create themes.
1487- # keywords = [
1488- # "attribute",
1489- # "comment",
1490- # "constant",
1491- # "function.builtin",
1492- # "function",
1493- # "keyword",
1494- # "operator",
1495- # "property",
1496- # "punctuation",
1497- # "punctuation.bracket",
1498- # "punctuation.delimiter",
1499- # "string",
1500- # "string.special",
1501- # "tag",
1502- # "type",
1503- # "type.builtin",
1504- # "variable",
1505- # "variable.builtin",
1506- # "variable.parameter",
1507- # "keyword_insert",
1508- # "addition",
1509- # "removal"
1510- # ]
1511+ keywords = [
1512+ "attribute",
1513+ "comment",
1514+ "constant",
1515+ "function.builtin",
1516+ "function",
1517+ "keyword",
1518+ "operator",
1519+ "property",
1520+ "punctuation",
1521+ "punctuation.bracket",
1522+ "punctuation.delimiter",
1523+ "string",
1524+ "string.special",
1525+ "tag",
1526+ "type",
1527+ "type.builtin",
1528+ "variable",
1529+ "variable.builtin",
1530+ "variable.parameter",
1531+ "keyword_insert",
1532+ "addition",
1533+ "removal"
1534+ ]
1535
1536 # This section allows you to override any highlights with custom configuration.
1537- # [tree-sitter.highlights]
1538+ [web.tree-sitter.highlights]
1539
1540 # this results in two new CSS classes ts_addition and ts_removal which can
1541 # be configured in theme stylesheets.
1542- # diff = """
1543- # ; used to highlight diffs in the UI
1544- # [(addition) (new_file)] @addition
1545- # [(deletion) (old_file)] @removal
1546- #
1547- # (commit) @constant
1548- # (location) @attribute
1549- # (command) @variable.builtin
1550- # """
1551- #
1552+ diff = """
1553+ ; used to highlight diffs in the UI
1554+ [(addition) (new_file)] @addition
1555+ [(deletion) (old_file)] @removal
1556+
1557+ (commit) @constant
1558+ (location) @attribute
1559+ (command) @variable.builtin
1560+ """
1561+
1562 # Templates are used to control how shared objects and ts configuration is
1563 # loaded from the file system. In the strings below the value {language} will
1564 # be replaced with the language name as defined in the [[languages]] section
1565 # described below. NOTE that names are case sensitive and effect not only the
1566 # file name but the function name loaded from any shared object.
1567
1568- # [tree-sitter.template]
1569- # # required
1570- # module = "grammars/{language}.so"
1571- # # required
1572- # highlights = "queries/{language}/highlights.scm"
1573- # # optional
1574- # locals = "queries/{language}/locals.scm"
1575- # # optional
1576- # injections = "queries/{language}/injections.scm"
1577+ [web.tree-sitter.template]
1578+ # required
1579+ module = "grammars/{language}.so"
1580+ # required
1581+ highlights = "queries/{language}/highlights.scm"
1582+ # optional
1583+ locals = "queries/{language}/locals.scm"
1584+ # optional
1585+ injections = "queries/{language}/injections.scm"
1586
1587 # The [[languages]] setion controls how file names, their extensions, and
1588 # script content is associated with programming languages. Syntax highlighting
1589 # will only be enabled for languages which are specified below.
1590
1591 # Add a programming language and associate files and extensions with it.
1592- # [[languages]]
1593- # name = "FuuLang"
1594- # extension = ["bar", "baz"]
1595- # filenames = ["Filefile"]
1596- # # # deep pink
1597- # color = "#ff1493"
1598-
1599- # [[languages]]
1600- # name = "toml"
1601-
1602- # # Sometimes languages are associated with extensions that you might not expect
1603- # # and so you can override those such as below.
1604- # [[languages]]
1605- # name = "rust"
1606- # extensions = ["rs"]
1607+ [[web.languages]]
1608+ name = "FuuLang"
1609+ extension = ["bar", "baz"]
1610+ filenames = ["Filefile"]
1611+ # # deep pink
1612+ color = "#ff1493"
1613+
1614+ # Sometimes languages are associated with extensions that you might not expect
1615+ # and so you can override those such as below.
1616+ [[web.languages]]
1617+ name = "rust"
1618+ extensions = ["rs"]
1619
1620 # Large File Store configuration
1621- # [lfs]
1622+ [web.lfs]
1623 # template URL to construct an API request which will be used to lookup the
1624 # reference object of the file. Other LFS server implementations may work but
1625 # this has only been tested with https://github.com/jasonwhite/rudolfs
1626- # url_template = "/lfs/{collection}/{name}/object/{oid}"
1627-
1628- # path to directories of repositories, each collection will be seperated into
1629- # a unique section on the index page. recursive directories are not supported,
1630- # you may only have a single level of repositories at this time.
1631- # [[collections]]
1632- # # name of the code collection
1633- # name = "projects"
1634- # description = "software projects actively being worked on"
1635- # path = "/path/to/projects"
1636- # # If true the collection will not show up in the main index or RSS feeds
1637- # hidden = false
1638-
1639- # [[collections]]
1640- # name = "attic"
1641- # description = "archived code"
1642- # path = "/path/to/attic"
1643+ url_template = "/lfs/{collection}/{name}/object/{oid}"
1644+
1645+ ###
1646+ ##### Shell Configuration (ayllu-shell)
1647+ ###
1648
1649 [shell]
1650 motd = """
1651- ████ ████
1652- ░░███ ░░███
1653- ██████ █████ ████ ░███ ░███ █████ ████
1654- ░░░░░███ ░░███ ░███ ░███ ░███ ░░███ ░███
1655- ███████ ░███ ░███ ░███ ░███ ░███ ░███
1656- ███░░███ ░███ ░███ ░███ ░███ ░███ ░███
1657- ░░████████ ░░███████ █████ █████ ░░████████
1658- ░░░░░░░░ ░░░░░███ ░░░░░ ░░░░░ ░░░░░░░░
1659- ███ ░███
1660- ░░██████
1661- â–‘â–‘â–‘â–‘â–‘â–‘
1662-
1663 A Hyper Performant & Hackable Code Forge Built on Open Standards.
1664 """
1665
1666- # Identities associated with this Ayllu instance which may have an associated
1667- # system account.
1668-
1669- # [[identities]]
1670- # username = "demo"
1671- # If the user should be allocated a shell, if not specified the user may not
1672- # login.
1673- # shell = "/bin/sh"
1674- # authorized_keys = [
1675- # ".. your key here .."
1676- # ]
1677-
1678- # Global SQLite database used for all persistent storage outside of Git.
1679- # You need to run ayllu-migrate to apply any migrations
1680- [database]
1681- # Path to the SQLite database
1682- # path = "/usr/share/ayllu/state.db"
1683- # Directory of Migrations
1684- # migrations = "/usr/lib/ayllu/migrations"
1685+ ###
1686+ ##### Build Configuration
1687+ ###
1688
1689 # Global build configuration
1690 # Used to configure the runtime properties of ayllu-build
1691 @@ -347,3 +253,13 @@ source = ".ayllu-build.jsonnet"
1692 # Program and arguments to pass to the pre-processor
1693 program = "jsonnet"
1694 args = ["-"]
1695+
1696+ ###
1697+ ##### Quipu Configuration
1698+ ###
1699+
1700+ [quipu]
1701+
1702+ [[quipu.instances]]
1703+ name = "ayllu-forge"
1704+ url = "https://ayllu-forge.org"
1705 diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
1706index e899e1b..949eb5b 100644
1707--- a/crates/config/src/lib.rs
1708+++ b/crates/config/src/lib.rs
1709 @@ -1,9 +1,10 @@
1710+ use serde::{Deserialize, Serialize};
1711+
1712 use std::path::{Path, PathBuf};
1713
1714 pub use error::Error;
1715 pub use flags::Command;
1716 pub use reader::{Configurable, Reader, data_dir, runtime_dir};
1717- use serde::{Deserialize, Serialize};
1718
1719 mod edit;
1720 mod error;
1721 @@ -11,22 +12,39 @@ mod flags;
1722 mod reader;
1723
1724 pub const DEFAULT_BRANCH_NAME: &str = "master";
1725-
1726- fn default_branch() -> String {
1727- Common::default().default_branch
1728- }
1729+ pub const EXAMPLE_CONFIG: &str = include_str!("../../../config.example.toml");
1730
1731 /// Common configuration options across all binaries
1732 #[derive(Deserialize, Serialize, Clone, Debug)]
1733 pub struct Common {
1734- #[serde(default = "default_branch")]
1735+ #[serde(default = "Common::default_log_level")]
1736+ pub log_level: String,
1737+ #[serde(default = "Common::default_branch")]
1738 pub default_branch: String,
1739+ pub base_path: Option<PathBuf>,
1740+ #[serde(default = "Database::default")]
1741+ pub database: Database,
1742+ pub sysadmin: Option<String>,
1743+ }
1744+
1745+ impl Common {
1746+ fn default_log_level() -> String {
1747+ "INFO".to_string()
1748+ }
1749+
1750+ fn default_branch() -> String {
1751+ DEFAULT_BRANCH_NAME.to_string()
1752+ }
1753 }
1754
1755 impl Default for Common {
1756 fn default() -> Self {
1757 Self {
1758- default_branch: DEFAULT_BRANCH_NAME.to_string(),
1759+ log_level: Common::default_log_level(),
1760+ default_branch: Common::default_branch(),
1761+ database: Database::default(),
1762+ sysadmin: None,
1763+ base_path: None,
1764 }
1765 }
1766 }
1767 diff --git a/crates/config/src/reader.rs b/crates/config/src/reader.rs
1768index c36845a..e0b2f64 100644
1769--- a/crates/config/src/reader.rs
1770+++ b/crates/config/src/reader.rs
1771 @@ -2,11 +2,14 @@ use std::env;
1772 use std::path::{Path, PathBuf};
1773
1774 use serde::de::DeserializeOwned;
1775+ use serde::{Deserialize, Serialize};
1776 use toml::from_str;
1777
1778 use crate::error::Error;
1779
1780- pub trait Configurable {
1781+ /// All configuration across all binaries must support running with zero
1782+ /// configuration options.
1783+ pub trait Configurable: Default + Serialize + for<'a> Deserialize<'a> {
1784 /// initialize the configuration (typically set defaults)
1785 fn initialize(&mut self) -> Result<(), Error> {
1786 Ok(())
1787 @@ -61,7 +64,10 @@ where
1788 Some(path) => std::fs::read_to_string(path)?,
1789 None => match try_read() {
1790 Some((_, config_str)) => config_str,
1791- None => return Err(Error::ConfigNotFound),
1792+ None => {
1793+ eprintln!("No configuration was loaded, returning defaults");
1794+ return Ok(T::default());
1795+ }
1796 },
1797 };
1798 let mut config = from_str::<T>(&config_str)?;
1799 @@ -106,3 +112,21 @@ pub fn runtime_dir() -> PathBuf {
1800 .or_else(|| env::var("RUNTIME_DIRECTORY").ok().map(PathBuf::from))
1801 .unwrap_or(PathBuf::from("/run/ayllu"))
1802 }
1803+
1804+ // /// Used to validate individual binaries
1805+ // pub struct Validate;
1806+
1807+ // impl Validate {
1808+ // pub fn with_static_config<T>(input: &'static str)
1809+ // where
1810+ // T: Configurable + for<'a> Deserialize<'a> + Serialize,
1811+ // {
1812+ // match toml::from_str::<T>(input) {
1813+ // Ok(_) => {}
1814+ // Err(e) => {
1815+ // eprintln!("Example Config Validation Failed: {e:?}");
1816+ // eprintln!("Input:\n{input}");
1817+ // }
1818+ // }
1819+ // }
1820+ // }
1821 diff --git a/quipu/src/config.rs b/quipu/src/config.rs
1822index 000fa1e..8ae24d6 100644
1823--- a/quipu/src/config.rs
1824+++ b/quipu/src/config.rs
1825 @@ -15,9 +15,10 @@ pub struct Quipu {
1826 pub instances: Vec<Instance>,
1827 }
1828
1829- #[derive(Clone, Debug, Serialize, Deserialize)]
1830+ #[derive(Clone, Debug, Serialize, Deserialize, Default)]
1831 pub struct Config {
1832- pub log_level: String,
1833+ #[serde(flatten)]
1834+ pub common: ayllu_config::Common,
1835 pub quipu: Option<Quipu>,
1836 #[serde(default = "Database::default")]
1837 pub database: Database,
1838 @@ -50,3 +51,16 @@ impl Config {
1839 }
1840
1841 impl Configurable for Config {}
1842+
1843+ #[cfg(test)]
1844+ mod test {
1845+ use ayllu_config::{EXAMPLE_CONFIG, Reader};
1846+
1847+ use crate::config::Config;
1848+
1849+ #[test]
1850+ fn check_config() {
1851+ Reader::<Config>::read(EXAMPLE_CONFIG).unwrap();
1852+ Reader::<Config>::read("").unwrap();
1853+ }
1854+ }
1855 diff --git a/quipu/src/main.rs b/quipu/src/main.rs
1856index fadec1d..c67dcf6 100644
1857--- a/quipu/src/main.rs
1858+++ b/quipu/src/main.rs
1859 @@ -48,7 +48,7 @@ fn get_instance(
1860 async fn main() -> Result<(), error::QuipuError> {
1861 let cli = ayllu_cmd::parse::<Command>();
1862 let cfg: config::Config = Reader::load(cli.config.as_deref())?;
1863- let log_level = Level::from_str(&cfg.log_level)?;
1864+ let log_level = Level::from_str(&cfg.common.log_level)?;
1865 tracing_subscriber::fmt()
1866 .compact()
1867 .with_line_number(true)