1use build::BuildConfig;
2use directories::ProjectDirs;
3use external_deps::ExternalDependencySearchConfig;
4use itertools::Itertools;
5
6use miette::Diagnostic;
7use serde::{Deserialize, Serialize, Serializer};
8use std::ffi::OsStr;
9use std::path::Path;
10use std::{collections::HashMap, env, path::PathBuf, time::Duration};
11use thiserror::Error;
12use tokio::process::Command;
13use tree::RockLayoutConfig;
14use url::Url;
15
16use crate::config::access_tokens::AccessToken;
17use crate::fs;
18use crate::lua_version::LuaVersion;
19use crate::package::RemotePackageTypeFilterSpec;
20use crate::project::TomlDeError;
21use crate::tree::{Tree, TreeError};
22use crate::variables::GetVariableError;
23use crate::{build::utils, variables::HasVariables};
24
25pub mod access_tokens;
26pub mod build;
27pub mod external_deps;
28pub mod tree;
29
30const DEV_PATH: &str = "dev/";
31const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
32
33#[derive(Error, Debug, Diagnostic)]
34#[error("could not find a valid home directory")]
35#[diagnostic(
36 code(lux_lib::no_home_directory),
37 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
38)]
39pub struct NoValidHomeDirectory;
40
41#[derive(Debug, Clone)]
45pub struct Config {
46 enable_development_packages: bool,
47 server: Url,
48 extra_servers: Vec<Url>,
49 namespace: Option<String>,
50 lua_dir: Option<PathBuf>,
51 lua_version: Option<LuaVersion>,
52 user_tree: PathBuf,
53 workspace_tree: Option<PathBuf>,
54 verbose: bool,
55 no_progress: bool,
57 no_prompt: bool,
59 timeout: Duration,
60 max_jobs: usize,
61 variables: HashMap<String, String>,
62 access_tokens: HashMap<String, AccessToken>,
63 external_deps: ExternalDependencySearchConfig,
64
65 build: BuildConfig,
66 entrypoint_layout: RockLayoutConfig,
67
68 cache_dir: PathBuf,
69 data_dir: PathBuf,
70 vendor_dir: Option<PathBuf>,
71
72 user_agent: String,
73
74 generate_luarc: bool,
75 luarc_file_name: String,
76 wrap_bin_scripts: bool,
77 package_types: RemotePackageTypeFilterSpec,
78 no_tfa: bool,
79}
80
81impl Config {
82 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
84 directories::ProjectDirs::from("org", "lumenlabs", "lux")
85 .filter(|_| Self::home_dir_exists())
86 .ok_or(NoValidHomeDirectory)
87 }
88
89 fn home_dir_exists() -> bool {
90 directories::BaseDirs::new().is_some_and(|base| base.home_dir().is_dir())
91 }
92
93 fn default_cache_path() -> PathBuf {
95 match Config::project_dirs() {
96 Ok(project_dirs) => project_dirs.cache_dir().to_path_buf(),
97 Err(_) => Self::fallback_data_dir("cache"),
98 }
99 }
100
101 fn default_data_path() -> PathBuf {
103 match Config::project_dirs() {
104 Ok(project_dirs) => project_dirs.data_local_dir().to_path_buf(),
105 Err(_) => Self::fallback_data_dir("data"),
106 }
107 }
108
109 fn fallback_data_dir(subdir: &str) -> PathBuf {
111 let path = std::env::temp_dir().join("lux").join(subdir);
112 tracing::warn!(
113 "no valid home directory found; falling back to `{}`",
114 path.display()
115 );
116 path
117 }
118
119 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
121 Self {
122 lua_version: Some(lua_version),
123 ..self
124 }
125 }
126
127 pub fn with_tree(self, tree: PathBuf) -> Self {
129 Self {
130 user_tree: tree,
131 ..self
132 }
133 }
134
135 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
137 Self {
138 workspace_tree: tree,
139 ..self
140 }
141 }
142
143 pub fn server(&self) -> &Url {
145 &self.server
146 }
147
148 pub fn extra_servers(&self) -> &Vec<Url> {
150 self.extra_servers.as_ref()
151 }
152
153 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
155 let mut enabled_dev_servers = Vec::new();
156 if self.enable_development_packages {
157 let config_file = ConfigBuilder::config_file()
158 .map(|p| p.to_string_lossy().to_string())
159 .unwrap_or_default();
160 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
161 ConfigError::UrlParseError {
162 source,
163 help: Some(format!("check the `server` URL in {config_file}")),
164 }
165 })?);
166 for server in self.extra_servers() {
167 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
168 ConfigError::UrlParseError {
169 source,
170 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
171 }
172 })?);
173 }
174 }
175 Ok(enabled_dev_servers)
176 }
177
178 pub fn namespace(&self) -> Option<&String> {
180 self.namespace.as_ref()
181 }
182
183 pub fn lua_dir(&self) -> Option<&PathBuf> {
185 self.lua_dir.as_ref()
186 }
187
188 pub fn lua_version(&self) -> Option<&LuaVersion> {
190 self.lua_version.as_ref()
191 }
192
193 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
196 Tree::new(self.user_tree.clone(), version, self)
197 }
198
199 pub fn workspace_tree(&self) -> Option<&PathBuf> {
201 self.workspace_tree.as_ref()
202 }
203
204 pub fn verbose(&self) -> bool {
206 self.verbose
207 }
208
209 pub fn no_progress(&self) -> bool {
211 self.no_progress
212 }
213
214 pub fn no_prompt(&self) -> bool {
216 self.no_prompt
217 }
218
219 pub fn timeout(&self) -> &Duration {
222 &self.timeout
223 }
224
225 pub fn max_jobs(&self) -> usize {
228 self.max_jobs
229 }
230
231 pub fn make_cmd(&self) -> String {
233 match self.variables.get("MAKE") {
234 Some(make) => make.clone(),
235 None => "make".into(),
236 }
237 }
238
239 pub fn cmake_cmd(&self) -> String {
241 match self.variables.get("CMAKE") {
242 Some(cmake) => cmake.clone(),
243 None => "cmake".into(),
244 }
245 }
246
247 pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
253 where
254 P: AsRef<OsStr>,
255 A: IntoIterator,
256 A::Item: AsRef<OsStr>,
257 {
258 if self.build.runner.is_empty() {
259 let mut cmd = Command::new(program);
260 cmd.args(args);
261 cmd
262 } else {
263 let runner = &self.build.runner;
264 let mut cmd = Command::new(&runner[0]);
265 cmd.args(&runner[1..]).arg(program).args(args);
266 cmd
267 }
268 }
269
270 pub(crate) fn build_profile(&self) -> build::Profile {
272 self.build
273 .profile
274 .as_ref()
275 .cloned()
276 .unwrap_or(build::Profile::Release)
277 }
278
279 pub fn variables(&self) -> &HashMap<String, String> {
283 &self.variables
284 }
285
286 pub fn access_token(&self, host: &str) -> Option<AccessToken> {
291 access_tokens::env_access_tokens()
292 .get(host)
293 .cloned()
294 .or_else(|| self.access_tokens.get(host).cloned())
295 .or_else(|| {
296 (host == "github.com")
297 .then(|| env::var("GITHUB_TOKEN").ok())
298 .flatten()
299 .map(|raw| AccessToken::from(raw.as_str()))
300 })
301 }
302
303 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
304 &self.external_deps
305 }
306
307 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
310 &self.entrypoint_layout
311 }
312
313 pub fn cache_dir(&self) -> &PathBuf {
315 &self.cache_dir
316 }
317
318 pub fn data_dir(&self) -> &PathBuf {
320 &self.data_dir
321 }
322
323 pub fn vendor_dir(&self) -> Option<&PathBuf> {
327 self.vendor_dir.as_ref()
328 }
329
330 pub fn user_agent(&self) -> &str {
332 &self.user_agent
333 }
334
335 pub fn generate_luarc(&self) -> bool {
337 self.generate_luarc
338 }
339
340 pub fn luarc_file_name(&self) -> &str {
342 &self.luarc_file_name
343 }
344
345 pub fn wrap_bin_scripts(&self) -> bool {
349 self.wrap_bin_scripts
350 }
351
352 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
354 &self.package_types
355 }
356 pub fn no_tfa(&self) -> bool {
358 self.no_tfa
359 }
360}
361
362impl HasVariables for Config {
363 #[tracing::instrument(level = "trace")]
364 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
365 Ok(self.variables.get(input).cloned())
366 }
367}
368
369#[derive(Error, Debug, Diagnostic)]
370pub enum ConfigError {
371 #[error(transparent)]
372 #[diagnostic(transparent)]
373 Fs(#[from] fs::FsError),
374 #[error(transparent)]
375 #[diagnostic(transparent)]
376 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
377 #[error("error parsing {config_file}")]
378 Deserialize {
379 config_file: String,
380 #[diagnostic_source]
381 source: TomlDeError,
382 },
383 #[error("error parsing URL: {source}")]
384 UrlParseError {
385 source: url::ParseError,
386 #[help]
387 help: Option<String>,
388 },
389}
390
391#[derive(Debug, Clone, Default, Deserialize, Serialize)]
398pub struct ConfigBuilder {
399 #[serde(
400 default,
401 deserialize_with = "deserialize_url",
402 serialize_with = "serialize_url"
403 )]
404 server: Option<Url>,
405 #[serde(
406 default,
407 deserialize_with = "deserialize_url_vec",
408 serialize_with = "serialize_url_vec"
409 )]
410 extra_servers: Option<Vec<Url>>,
411 namespace: Option<String>,
412 lua_version: Option<LuaVersion>,
413 user_tree: Option<PathBuf>,
414 workspace_tree: Option<PathBuf>,
415 lua_dir: Option<PathBuf>,
416 cache_dir: Option<PathBuf>,
417 data_dir: Option<PathBuf>,
418 vendor_dir: Option<PathBuf>,
419 enable_development_packages: Option<bool>,
420 verbose: Option<bool>,
421 no_progress: Option<bool>,
422 no_prompt: Option<bool>,
423 timeout: Option<Duration>,
424 max_jobs: Option<usize>,
425 variables: Option<HashMap<String, String>>,
426 #[serde(default, skip_serializing)]
429 access_tokens: Option<HashMap<String, AccessToken>>,
430 #[serde(default)]
431 external_deps: ExternalDependencySearchConfig,
432 #[serde(default)]
433 build: BuildConfig,
434
435 #[serde(default)]
436 entrypoint_layout: RockLayoutConfig,
437 user_agent: Option<String>,
438 generate_luarc: Option<bool>,
439 luarc_file_name: Option<String>,
440 wrap_bin_scripts: Option<bool>,
441 package_types: Option<RemotePackageTypeFilterSpec>,
442 no_tfa: Option<bool>,
443}
444
445impl ConfigBuilder {
447 pub fn new() -> Result<Self, ConfigError> {
450 let config_file = Self::config_file()?;
451 if config_file.is_file() {
452 Self::from_file(&config_file)
453 } else {
454 Ok(Self::default())
455 }
456 }
457
458 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
459 let config_file_name = config_file.to_string_lossy().to_string();
460 let content = fs::sync::read_to_string(config_file)?;
461 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
462 ConfigError::Deserialize {
463 config_file: config_file_name,
464 source,
465 }
466 })
467 }
468
469 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
471 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
472 .ok_or(NoValidHomeDirectory)?;
473 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
474 }
475
476 pub fn dev(self, dev: Option<bool>) -> Self {
479 Self {
480 enable_development_packages: dev.or(self.enable_development_packages),
481 ..self
482 }
483 }
484
485 pub fn server(self, server: Option<Url>) -> Self {
488 Self {
489 server: server.or(self.server),
490 ..self
491 }
492 }
493
494 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
496 Self {
497 extra_servers: extra_servers.or(self.extra_servers),
498 ..self
499 }
500 }
501
502 pub fn namespace(self, namespace: Option<String>) -> Self {
504 Self {
505 namespace: namespace.or(self.namespace),
506 ..self
507 }
508 }
509
510 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
512 Self {
513 lua_dir: lua_dir.or(self.lua_dir),
514 ..self
515 }
516 }
517
518 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
521 Self {
522 lua_version: lua_version.or(self.lua_version),
523 ..self
524 }
525 }
526
527 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
529 Self {
530 user_tree: tree.or(self.user_tree),
531 ..self
532 }
533 }
534
535 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
538 Self {
539 workspace_tree: tree.or(self.workspace_tree),
540 ..self
541 }
542 }
543
544 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
548 Self {
549 variables: variables.or(self.variables),
550 ..self
551 }
552 }
553
554 pub fn verbose(self, verbose: Option<bool>) -> Self {
557 Self {
558 verbose: verbose.or(self.verbose),
559 ..self
560 }
561 }
562
563 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
566 Self {
567 no_progress: no_progress.or(self.no_progress),
568 ..self
569 }
570 }
571
572 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
575 Self {
576 no_prompt: no_prompt.or(self.no_prompt),
577 ..self
578 }
579 }
580
581 pub fn timeout(self, timeout: Option<Duration>) -> Self {
585 Self {
586 timeout: timeout.or(self.timeout),
587 ..self
588 }
589 }
590
591 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
595 Self {
596 max_jobs: max_jobs.or(self.max_jobs),
597 ..self
598 }
599 }
600
601 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
603 Self {
604 cache_dir: cache_dir.or(self.cache_dir),
605 ..self
606 }
607 }
608
609 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
611 Self {
612 data_dir: data_dir.or(self.data_dir),
613 ..self
614 }
615 }
616
617 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
621 Self {
622 vendor_dir: vendor_dir.or(self.vendor_dir),
623 ..self
624 }
625 }
626
627 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
630 Self {
631 entrypoint_layout: rock_layout,
632 ..self
633 }
634 }
635
636 pub fn user_agent(self, user_agent: Option<String>) -> Self {
639 Self {
640 user_agent: user_agent.or(self.user_agent),
641 ..self
642 }
643 }
644
645 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
648 Self {
649 generate_luarc: generate.or(self.generate_luarc),
650 ..self
651 }
652 }
653
654 pub fn luarc_file_name(self, file: Option<String>) -> Self {
657 Self {
658 luarc_file_name: file.or(self.luarc_file_name),
659 ..self
660 }
661 }
662
663 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
669 Self {
670 wrap_bin_scripts: generate.or(self.generate_luarc),
671 ..self
672 }
673 }
674 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
677 Self {
678 no_tfa: tfa.or(self.no_tfa),
679 ..self
680 }
681 }
682
683 pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
733 Self {
734 build: BuildConfig {
735 runner: runner.unwrap_or(self.build.runner),
736 ..self.build
737 },
738 ..self
739 }
740 }
741
742 pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
745 Self {
746 build: BuildConfig {
747 profile: profile.or(self.build.profile),
748 ..self.build
749 },
750 ..self
751 }
752 }
753
754 pub fn default_build_profile(self, profile: build::Profile) -> Self {
756 Self {
757 build: BuildConfig {
758 profile: self.build.profile.or(Some(profile)),
759 ..self.build
760 },
761 ..self
762 }
763 }
764
765 pub fn merge(self, other: Self) -> Self {
767 Self {
768 server: other.server.or(self.server),
769 extra_servers: other.extra_servers.or(self.extra_servers),
770 namespace: other.namespace.or(self.namespace),
771 lua_version: other.lua_version.or(self.lua_version),
772 user_tree: other.user_tree.or(self.user_tree),
773 workspace_tree: other.workspace_tree.or(self.workspace_tree),
774 lua_dir: other.lua_dir.or(self.lua_dir),
775 cache_dir: other.cache_dir.or(self.cache_dir),
776 data_dir: other.data_dir.or(self.data_dir),
777 vendor_dir: other.vendor_dir.or(self.vendor_dir),
778 enable_development_packages: other
779 .enable_development_packages
780 .or(self.enable_development_packages),
781 verbose: other.verbose.or(self.verbose),
782 no_progress: other.no_progress.or(self.no_progress),
783 no_prompt: other.no_prompt.or(self.no_prompt),
784 timeout: other.timeout.or(self.timeout),
785 max_jobs: other.max_jobs.or(self.max_jobs),
786 variables: other.variables.or(self.variables),
787 access_tokens: other.access_tokens.or(self.access_tokens),
788 external_deps: other.external_deps,
789 build: BuildConfig {
790 profile: other.build.profile.or(self.build.profile),
791 ..other.build
792 },
793 entrypoint_layout: other.entrypoint_layout,
794 user_agent: other.user_agent.or(self.user_agent),
795 generate_luarc: other.generate_luarc.or(self.generate_luarc),
796 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
797 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
798 package_types: other.package_types.or(self.package_types),
799 no_tfa: other.no_tfa.or(self.no_tfa),
800 }
801 }
802
803 #[tracing::instrument(level = "trace")]
804 pub fn build(self) -> Result<Config, ConfigError> {
805 let data_dir = self.data_dir.unwrap_or(Config::default_data_path());
806 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path());
807 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
808
809 let lua_version = self
810 .lua_version
811 .or(crate::lua_installation::detect_installed_lua_version());
812
813 Ok(Config {
814 enable_development_packages: self.enable_development_packages.unwrap_or(false),
815 server: self.server.unwrap_or_else(|| unsafe {
816 Url::parse("https://luarocks.org/").unwrap_unchecked()
817 }),
818 extra_servers: self.extra_servers.unwrap_or_default(),
819 namespace: self.namespace,
820 lua_dir: self.lua_dir,
821 lua_version,
822 user_tree,
823 workspace_tree: self.workspace_tree,
824 verbose: self.verbose.unwrap_or(false),
825 no_progress: self.no_progress.unwrap_or(false),
826 no_prompt: self.no_prompt.unwrap_or(false),
827 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
828 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
829 0 => usize::MAX,
830 max_jobs => max_jobs,
831 },
832 variables: default_variables()
833 .chain(self.variables.unwrap_or_default())
834 .collect(),
835 access_tokens: self.access_tokens.unwrap_or_default(),
836 external_deps: self.external_deps,
837 build: self.build,
838 entrypoint_layout: self.entrypoint_layout,
839 cache_dir,
840 data_dir,
841 vendor_dir: self.vendor_dir,
842 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
843 generate_luarc: self.generate_luarc.unwrap_or(true),
844 luarc_file_name: self
845 .luarc_file_name
846 .unwrap_or_else(|| ".luarc.json".to_string()),
847 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
848 package_types: self.package_types.unwrap_or_default(),
849 no_tfa: self.no_tfa.unwrap_or(false),
850 })
851 }
852}
853
854impl From<Config> for ConfigBuilder {
856 fn from(value: Config) -> Self {
857 ConfigBuilder {
858 enable_development_packages: Some(value.enable_development_packages),
859 server: Some(value.server),
860 extra_servers: Some(value.extra_servers),
861 namespace: value.namespace,
862 lua_dir: value.lua_dir,
863 lua_version: value.lua_version,
864 user_tree: Some(value.user_tree),
865 workspace_tree: value.workspace_tree,
866 verbose: Some(value.verbose),
867 no_progress: Some(value.no_progress),
868 no_prompt: Some(value.no_prompt),
869 timeout: Some(value.timeout),
870 max_jobs: if value.max_jobs == usize::MAX {
871 None
872 } else {
873 Some(value.max_jobs)
874 },
875 variables: Some(value.variables),
876 access_tokens: Some(value.access_tokens),
877 cache_dir: Some(value.cache_dir),
878 data_dir: Some(value.data_dir),
879 vendor_dir: value.vendor_dir,
880 external_deps: value.external_deps,
881 build: value.build,
882 entrypoint_layout: value.entrypoint_layout,
883 user_agent: Some(value.user_agent),
884 generate_luarc: Some(value.generate_luarc),
885 luarc_file_name: Some(value.luarc_file_name),
886 wrap_bin_scripts: Some(value.wrap_bin_scripts),
887 package_types: Some(value.package_types),
888 no_tfa: Some(value.no_tfa),
889 }
890 }
891}
892
893fn default_variables() -> impl Iterator<Item = (String, String)> {
894 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
895 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
896 vec![
897 ("MAKE".into(), "make".into()),
898 ("CMAKE".into(), "cmake".into()),
899 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
900 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
901 ("CFLAGS".into(), cflags),
902 ("LDFLAGS".into(), ldflags),
903 ("LIBFLAG".into(), utils::default_libflag().into()),
904 ]
905 .into_iter()
906}
907
908fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
909where
910 D: serde::Deserializer<'de>,
911{
912 let s = Option::<String>::deserialize(deserializer)?;
913 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
914 .transpose()
915}
916
917fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
918where
919 S: Serializer,
920{
921 match url {
922 Some(url) => serializer.serialize_some(url.as_str()),
923 None => serializer.serialize_none(),
924 }
925}
926
927fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
928where
929 D: serde::Deserializer<'de>,
930{
931 let s = Option::<Vec<String>>::deserialize(deserializer)?;
932 s.map(|v| {
933 v.into_iter()
934 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
935 .try_collect()
936 })
937 .transpose()
938}
939
940fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
941where
942 S: Serializer,
943{
944 match urls {
945 Some(urls) => {
946 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
947 serializer.serialize_some(&url_strings)
948 }
949 None => serializer.serialize_none(),
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 #[tokio::test]
958 async fn wrapped_command_without_runner() {
959 let echo = which::which("echo").unwrap();
960 let config = ConfigBuilder::default().build().unwrap();
961 let output = config
962 .wrapped_command(&echo, ["hello", "world"])
963 .output()
964 .await
965 .unwrap();
966 assert!(output.status.success());
967 assert_eq!(
968 String::from_utf8_lossy(&output.stdout).trim(),
969 "hello world"
970 );
971 }
972
973 #[tokio::test]
974 async fn wrapped_command_prepends_runner_argv() {
975 let echo = which::which("echo").unwrap();
976 let config = ConfigBuilder::default()
977 .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
978 .build()
979 .unwrap();
980 let output = config
981 .wrapped_command("world", Vec::<String>::new())
982 .output()
983 .await
984 .unwrap();
985 assert!(output.status.success());
986 assert_eq!(
987 String::from_utf8_lossy(&output.stdout).trim(),
988 "--prefix world"
989 );
990 }
991
992 #[test]
993 fn debug_redacts_access_tokens() {
994 let config: Config =
995 toml::from_str::<ConfigBuilder>("[access_tokens]\n\"github.com\" = \"supersecret\"\n")
996 .unwrap()
997 .build()
998 .unwrap();
999 let debug = format!("{config:?}");
1000 assert!(!debug.contains("supersecret"));
1001 assert!(debug.contains("access_tokens"));
1002 }
1003}