Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use miette::Diagnostic;
6use serde::{Deserialize, Serialize, Serializer};
7use std::{collections::HashMap, env, path::PathBuf, time::Duration};
8use thiserror::Error;
9use tree::RockLayoutConfig;
10use url::Url;
11
12use crate::fs;
13use crate::lua_version::LuaVersion;
14use crate::package::RemotePackageTypeFilterSpec;
15use crate::project::TomlDeError;
16use crate::tree::{Tree, TreeError};
17use crate::variables::GetVariableError;
18use crate::{build::utils, variables::HasVariables};
19
20pub mod external_deps;
21pub mod tree;
22
23const DEV_PATH: &str = "dev/";
24const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
25
26#[derive(Error, Debug, Diagnostic)]
27#[error("could not find a valid home directory")]
28#[diagnostic(
29    code(lux_lib::no_home_directory),
30    help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
31)]
32pub struct NoValidHomeDirectory;
33
34/// The resolved configuration for a Lux session.
35/// Can be constructed via [`ConfigBuilder`], which supports layering multiple
36/// configuration sources (config file, CLI flags, environment variables).
37#[derive(Debug, Clone)]
38pub struct Config {
39    enable_development_packages: bool,
40    server: Url,
41    extra_servers: Vec<Url>,
42    namespace: Option<String>,
43    lua_dir: Option<PathBuf>,
44    lua_version: Option<LuaVersion>,
45    user_tree: PathBuf,
46    workspace_tree_root: Option<PathBuf>,
47    verbose: bool,
48    /// Don't display progress bars
49    no_progress: bool,
50    /// Skip prompts (choosing the default choice)
51    no_prompt: bool,
52    timeout: Duration,
53    max_jobs: usize,
54    variables: HashMap<String, String>,
55    external_deps: ExternalDependencySearchConfig,
56    entrypoint_layout: RockLayoutConfig,
57
58    cache_dir: PathBuf,
59    data_dir: PathBuf,
60    vendor_dir: Option<PathBuf>,
61
62    user_agent: String,
63
64    generate_luarc: bool,
65    luarc_file_name: String,
66    wrap_bin_scripts: bool,
67    package_types: RemotePackageTypeFilterSpec,
68    no_tfa: bool,
69}
70
71impl Config {
72    /// Lux application directories
73    pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
74        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
75    }
76
77    /// Lux cache directory
78    fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
79        let project_dirs = Config::project_dirs()?;
80        Ok(project_dirs.cache_dir().to_path_buf())
81    }
82
83    /// Lux data directory
84    fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
85        let project_dirs = Config::project_dirs()?;
86        Ok(project_dirs.data_local_dir().to_path_buf())
87    }
88
89    /// Create a copy of this config for the specified Lua version
90    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
91        Self {
92            lua_version: Some(lua_version),
93            ..self
94        }
95    }
96
97    /// Create a copy of this config with the specified install tree
98    pub fn with_tree(self, tree: PathBuf) -> Self {
99        Self {
100            user_tree: tree,
101            ..self
102        }
103    }
104
105    /// Create a copy of this config with the specified workspace tree root
106    pub fn with_workspace_tree_root(self, tree: Option<PathBuf>) -> Self {
107        Self {
108            workspace_tree_root: tree,
109            ..self
110        }
111    }
112
113    /// The luarocks repository server
114    pub fn server(&self) -> &Url {
115        &self.server
116    }
117
118    /// Additional luarocks repository servers
119    pub fn extra_servers(&self) -> &Vec<Url> {
120        self.extra_servers.as_ref()
121    }
122
123    /// Enabled luarocks repository servers that provide dev/scm rocks
124    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
125        let mut enabled_dev_servers = Vec::new();
126        if self.enable_development_packages {
127            let config_file = ConfigBuilder::config_file()
128                .map(|p| p.to_string_lossy().to_string())
129                .unwrap_or_default();
130            enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
131                ConfigError::UrlParseError {
132                    source,
133                    help: Some(format!("check the `server` URL in {config_file}")),
134                }
135            })?);
136            for server in self.extra_servers() {
137                enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
138                    ConfigError::UrlParseError {
139                        source,
140                        help: Some(format!("check the `extra_servers` URLs in {config_file}")),
141                    }
142                })?);
143            }
144        }
145        Ok(enabled_dev_servers)
146    }
147
148    /// The luarocks server namespace to use
149    pub fn namespace(&self) -> Option<&String> {
150        self.namespace.as_ref()
151    }
152
153    /// The directory in which to install Lua{n} if not found
154    pub fn lua_dir(&self) -> Option<&PathBuf> {
155        self.lua_dir.as_ref()
156    }
157
158    // TODO(vhyrro): Remove `LuaVersion::from(&config)` and keep this only.
159    pub fn lua_version(&self) -> Option<&LuaVersion> {
160        self.lua_version.as_ref()
161    }
162
163    /// The tree in which to install rocks.
164    /// If installing packages for a project, use `Project::tree` instead.
165    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
166        Tree::new(self.user_tree.clone(), version, self)
167    }
168
169    /// The detached workspace tree root, if set.
170    pub(crate) fn workspace_tree_root(&self) -> Option<&PathBuf> {
171        self.workspace_tree_root.as_ref()
172    }
173
174    /// Whether to display verbose output of commands executed
175    pub fn verbose(&self) -> bool {
176        self.verbose
177    }
178
179    /// Whether to disable printing progress bars and spinners
180    pub fn no_progress(&self) -> bool {
181        self.no_progress
182    }
183
184    /// Whether to skip prompts, selecting the default option
185    pub fn no_prompt(&self) -> bool {
186        self.no_prompt
187    }
188
189    /// Timeout on network operations, in seconds.
190    /// 0 means no timeout (wait forever).
191    pub fn timeout(&self) -> &Duration {
192        &self.timeout
193    }
194
195    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
196    /// 0 means no limit.
197    pub fn max_jobs(&self) -> usize {
198        self.max_jobs
199    }
200
201    /// Command to use for running `make` builds
202    pub fn make_cmd(&self) -> String {
203        match self.variables.get("MAKE") {
204            Some(make) => make.clone(),
205            None => "make".into(),
206        }
207    }
208
209    /// Command to use for running `cmake` builds
210    pub fn cmake_cmd(&self) -> String {
211        match self.variables.get("CMAKE") {
212            Some(cmake) => cmake.clone(),
213            None => "cmake".into(),
214        }
215    }
216
217    /// Variable names, mapped to their values.
218    /// Lux populates variables in the `lux.toml` and in RockSpecs
219    /// with these before building.
220    pub fn variables(&self) -> &HashMap<String, String> {
221        &self.variables
222    }
223
224    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
225        &self.external_deps
226    }
227
228    /// The rock layout for entrypoints of new install trees.
229    /// Does not affect existing install trees or dependency rock layouts.
230    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
231        &self.entrypoint_layout
232    }
233
234    /// The Lux cache directory
235    pub fn cache_dir(&self) -> &PathBuf {
236        &self.cache_dir
237    }
238
239    /// The Lux data directory
240    pub fn data_dir(&self) -> &PathBuf {
241        &self.data_dir
242    }
243
244    /// Specifies a directory with locally vendored sources and RockSpecs.
245    /// When building or installing a package with this flag,
246    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
247    pub fn vendor_dir(&self) -> Option<&PathBuf> {
248        self.vendor_dir.as_ref()
249    }
250
251    /// The user agent to use when making web requests.
252    pub fn user_agent(&self) -> &str {
253        &self.user_agent
254    }
255
256    /// Whether to generate a `.luarc.json` on build.
257    pub fn generate_luarc(&self) -> bool {
258        self.generate_luarc
259    }
260
261    // Lua runtime configuration file name
262    pub fn luarc_file_name(&self) -> &str {
263        &self.luarc_file_name
264    }
265
266    /// Whether to wrap installed Lua bin scripts to be executed with
267    /// the detected or configured Lua installation.
268    /// If `true`, individual rocks can still disable wrapping of their own bin scripts.
269    pub fn wrap_bin_scripts(&self) -> bool {
270        self.wrap_bin_scripts
271    }
272
273    /// Filter specification for package types to include in searches.
274    pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
275        &self.package_types
276    }
277    /// Whether to disable prompts for two-factor authentication (2FA) codes.
278    pub fn no_tfa(&self) -> bool {
279        self.no_tfa
280    }
281}
282
283impl HasVariables for Config {
284    #[tracing::instrument(level = "trace")]
285    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
286        Ok(self.variables.get(input).cloned())
287    }
288}
289
290#[derive(Error, Debug, Diagnostic)]
291pub enum ConfigError {
292    #[error(transparent)]
293    #[diagnostic(transparent)]
294    Fs(#[from] fs::FsError),
295    #[error(transparent)]
296    #[diagnostic(transparent)]
297    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
298    #[error("error parsing {config_file}")]
299    Deserialize {
300        config_file: String,
301        #[diagnostic_source]
302        source: TomlDeError,
303    },
304    #[error("error parsing URL: {source}")]
305    UrlParseError {
306        source: url::ParseError,
307        #[help]
308        help: Option<String>,
309    },
310}
311
312/// Incrementally builds a [`Config`] by layering configuration sources.
313///
314/// - Call [`ConfigBuilder::default`] to start with a blank slate,
315///   or call [`ConfigBuilder::new`] to start from a deserialised configuration file.
316/// - Populate the fields from overriding sources (e.g. CLI arguments).
317/// - Finish with [`ConfigBuilder::build`].
318#[derive(Debug, Clone, Default, Deserialize, Serialize)]
319pub struct ConfigBuilder {
320    #[serde(
321        default,
322        deserialize_with = "deserialize_url",
323        serialize_with = "serialize_url"
324    )]
325    server: Option<Url>,
326    #[serde(
327        default,
328        deserialize_with = "deserialize_url_vec",
329        serialize_with = "serialize_url_vec"
330    )]
331    extra_servers: Option<Vec<Url>>,
332    namespace: Option<String>,
333    lua_version: Option<LuaVersion>,
334    user_tree: Option<PathBuf>,
335    workspace_tree: Option<PathBuf>,
336    lua_dir: Option<PathBuf>,
337    cache_dir: Option<PathBuf>,
338    data_dir: Option<PathBuf>,
339    vendor_dir: Option<PathBuf>,
340    enable_development_packages: Option<bool>,
341    verbose: Option<bool>,
342    no_progress: Option<bool>,
343    no_prompt: Option<bool>,
344    timeout: Option<Duration>,
345    max_jobs: Option<usize>,
346    variables: Option<HashMap<String, String>>,
347    #[serde(default)]
348    external_deps: ExternalDependencySearchConfig,
349    #[serde(default)]
350    entrypoint_layout: RockLayoutConfig,
351    user_agent: Option<String>,
352    generate_luarc: Option<bool>,
353    luarc_file_name: Option<String>,
354    wrap_bin_scripts: Option<bool>,
355    package_types: Option<RemotePackageTypeFilterSpec>,
356    no_tfa: Option<bool>,
357}
358
359/// A builder for the lux `Config`.
360impl ConfigBuilder {
361    /// Create a new `ConfigBuilder` by deserializing from a config file
362    /// if present, or otherwise by instantiating the default config.
363    pub fn new() -> Result<Self, ConfigError> {
364        let config_file = Self::config_file()?;
365        if config_file.is_file() {
366            let config_file_name = config_file.to_string_lossy().to_string();
367            let content = fs::sync::read_to_string(&config_file)?;
368            crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
369                ConfigError::Deserialize {
370                    config_file: config_file_name,
371                    source,
372                }
373            })
374        } else {
375            Ok(Self::default())
376        }
377    }
378
379    /// Get the path to the lux config file.
380    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
381        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
382            .ok_or(NoValidHomeDirectory)?;
383        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
384    }
385
386    /// Whether to enable development packages
387    /// Default: `false`
388    pub fn dev(self, dev: Option<bool>) -> Self {
389        Self {
390            enable_development_packages: dev.or(self.enable_development_packages),
391            ..self
392        }
393    }
394
395    /// Fetch rocks/rockspecs from this luarocks server
396    /// Default: `"https://luarocks.org/"`
397    pub fn server(self, server: Option<Url>) -> Self {
398        Self {
399            server: server.or(self.server),
400            ..self
401        }
402    }
403
404    /// Fetch rocks/rockspecs from these servers in addition to the main server
405    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
406        Self {
407            extra_servers: extra_servers.or(self.extra_servers),
408            ..self
409        }
410    }
411
412    /// The luarocks server namespace to use
413    pub fn namespace(self, namespace: Option<String>) -> Self {
414        Self {
415            namespace: namespace.or(self.namespace),
416            ..self
417        }
418    }
419
420    /// The directory in which to install Lua if not found
421    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
422        Self {
423            lua_dir: lua_dir.or(self.lua_dir),
424            ..self
425        }
426    }
427
428    /// Which Lua version to use.
429    /// Default: The installed Lua version, if detected
430    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
431        Self {
432            lua_version: lua_version.or(self.lua_version),
433            ..self
434        }
435    }
436
437    /// Which tree to operate on
438    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
439        Self {
440            user_tree: tree.or(self.user_tree),
441            ..self
442        }
443    }
444
445    /// Which tree to operate on when in a workspace
446    /// Default: A `.lux` directory in the workspace root.
447    pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
448        Self {
449            workspace_tree: tree.or(self.workspace_tree),
450            ..self
451        }
452    }
453
454    /// Variable names, mapped to their values.
455    /// Lux populates variables in the `lux.toml` and in RockSpecs
456    /// with these before building.
457    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
458        Self {
459            variables: variables.or(self.variables),
460            ..self
461        }
462    }
463
464    /// Whether to display verbose output of commands executed.
465    /// Default: `false`
466    pub fn verbose(self, verbose: Option<bool>) -> Self {
467        Self {
468            verbose: verbose.or(self.verbose),
469            ..self
470        }
471    }
472
473    /// Whether to disable printing progress bars and spinners
474    /// Default: `false`
475    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
476        Self {
477            no_progress: no_progress.or(self.no_progress),
478            ..self
479        }
480    }
481
482    /// Whether to disable user prompts
483    /// Default: `false`
484    pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
485        Self {
486            no_prompt: no_prompt.or(self.no_prompt),
487            ..self
488        }
489    }
490
491    /// Timeout on network operations, in seconds.
492    /// 0 means no timeout (wait forever).
493    /// Default: 30 s
494    pub fn timeout(self, timeout: Option<Duration>) -> Self {
495        Self {
496            timeout: timeout.or(self.timeout),
497            ..self
498        }
499    }
500
501    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
502    /// 0 means no limit.
503    /// Default: 0
504    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
505        Self {
506            max_jobs: max_jobs.or(self.max_jobs),
507            ..self
508        }
509    }
510
511    /// The cache directory, e.g. for luarocks manifests.
512    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
513        Self {
514            cache_dir: cache_dir.or(self.cache_dir),
515            ..self
516        }
517    }
518
519    /// The data directory, in which the default user install tree resides.
520    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
521        Self {
522            data_dir: data_dir.or(self.data_dir),
523            ..self
524        }
525    }
526
527    /// Specifies a directory with locally vendored sources and RockSpecs.
528    /// When building or installing a package with this flag,
529    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
530    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
531        Self {
532            vendor_dir: vendor_dir.or(self.vendor_dir),
533            ..self
534        }
535    }
536
537    /// The rock layout for entrypoints of new install trees.
538    /// Does not affect existing install trees or dependency rock layouts.
539    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
540        Self {
541            entrypoint_layout: rock_layout,
542            ..self
543        }
544    }
545
546    /// The user agent to set when making web requests.
547    /// Default: "lux-lib/<version>".
548    pub fn user_agent(self, user_agent: Option<String>) -> Self {
549        Self {
550            user_agent: user_agent.or(self.user_agent),
551            ..self
552        }
553    }
554
555    /// Whether to generate a `.luarc.json` on build.
556    /// Default: `true`
557    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
558        Self {
559            generate_luarc: generate.or(self.generate_luarc),
560            ..self
561        }
562    }
563
564    /// Lua runtime configuration file name
565    /// Default: `.luarc.json`
566    pub fn luarc_file_name(self, file: Option<String>) -> Self {
567        Self {
568            luarc_file_name: file.or(self.luarc_file_name),
569            ..self
570        }
571    }
572
573    /// Whether to wrap installed Lua bin scripts to be executed with
574    /// the detected or configured Lua installation.
575    /// Setting this to `false` disables wrapping globally.
576    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
577    /// Default: `true`.
578    pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
579        Self {
580            wrap_bin_scripts: generate.or(self.generate_luarc),
581            ..self
582        }
583    }
584    /// Whether to disable prompts for two-factor authentication (2FA) codes.
585    /// Default: `false`.
586    pub fn no_tfa(self, tfa: Option<bool>) -> Self {
587        Self {
588            no_tfa: tfa.or(self.no_tfa),
589            ..self
590        }
591    }
592
593    #[tracing::instrument(level = "trace")]
594    pub fn build(self) -> Result<Config, ConfigError> {
595        let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
596        let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
597        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
598
599        let lua_version = self
600            .lua_version
601            .or(crate::lua_installation::detect_installed_lua_version());
602
603        Ok(Config {
604            enable_development_packages: self.enable_development_packages.unwrap_or(false),
605            server: self.server.unwrap_or_else(|| unsafe {
606                Url::parse("https://luarocks.org/").unwrap_unchecked()
607            }),
608            extra_servers: self.extra_servers.unwrap_or_default(),
609            namespace: self.namespace,
610            lua_dir: self.lua_dir,
611            lua_version,
612            user_tree,
613            workspace_tree_root: self.workspace_tree,
614            verbose: self.verbose.unwrap_or(false),
615            no_progress: self.no_progress.unwrap_or(false),
616            no_prompt: self.no_prompt.unwrap_or(false),
617            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
618            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
619                0 => usize::MAX,
620                max_jobs => max_jobs,
621            },
622            variables: default_variables()
623                .chain(self.variables.unwrap_or_default())
624                .collect(),
625            external_deps: self.external_deps,
626            entrypoint_layout: self.entrypoint_layout,
627            cache_dir,
628            data_dir,
629            vendor_dir: self.vendor_dir,
630            user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
631            generate_luarc: self.generate_luarc.unwrap_or(true),
632            luarc_file_name: self
633                .luarc_file_name
634                .unwrap_or_else(|| ".luarc.json".to_string()),
635            wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
636            package_types: self.package_types.unwrap_or_default(),
637            no_tfa: self.no_tfa.unwrap_or(false),
638        })
639    }
640}
641
642/// Useful for printing the current config
643impl From<Config> for ConfigBuilder {
644    fn from(value: Config) -> Self {
645        ConfigBuilder {
646            enable_development_packages: Some(value.enable_development_packages),
647            server: Some(value.server),
648            extra_servers: Some(value.extra_servers),
649            namespace: value.namespace,
650            lua_dir: value.lua_dir,
651            lua_version: value.lua_version,
652            user_tree: Some(value.user_tree),
653            workspace_tree: value.workspace_tree_root,
654            verbose: Some(value.verbose),
655            no_progress: Some(value.no_progress),
656            no_prompt: Some(value.no_prompt),
657            timeout: Some(value.timeout),
658            max_jobs: if value.max_jobs == usize::MAX {
659                None
660            } else {
661                Some(value.max_jobs)
662            },
663            variables: Some(value.variables),
664            cache_dir: Some(value.cache_dir),
665            data_dir: Some(value.data_dir),
666            vendor_dir: value.vendor_dir,
667            external_deps: value.external_deps,
668            entrypoint_layout: value.entrypoint_layout,
669            user_agent: Some(value.user_agent),
670            generate_luarc: Some(value.generate_luarc),
671            luarc_file_name: Some(value.luarc_file_name),
672            wrap_bin_scripts: Some(value.wrap_bin_scripts),
673            package_types: Some(value.package_types),
674            no_tfa: Some(value.no_tfa),
675        }
676    }
677}
678
679fn default_variables() -> impl Iterator<Item = (String, String)> {
680    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
681    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
682    vec![
683        ("MAKE".into(), "make".into()),
684        ("CMAKE".into(), "cmake".into()),
685        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
686        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
687        ("CFLAGS".into(), cflags),
688        ("LDFLAGS".into(), ldflags),
689        ("LIBFLAG".into(), utils::default_libflag().into()),
690    ]
691    .into_iter()
692}
693
694fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
695where
696    D: serde::Deserializer<'de>,
697{
698    let s = Option::<String>::deserialize(deserializer)?;
699    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
700        .transpose()
701}
702
703fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
704where
705    S: Serializer,
706{
707    match url {
708        Some(url) => serializer.serialize_some(url.as_str()),
709        None => serializer.serialize_none(),
710    }
711}
712
713fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
714where
715    D: serde::Deserializer<'de>,
716{
717    let s = Option::<Vec<String>>::deserialize(deserializer)?;
718    s.map(|v| {
719        v.into_iter()
720            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
721            .try_collect()
722    })
723    .transpose()
724}
725
726fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
727where
728    S: Serializer,
729{
730    match urls {
731        Some(urls) => {
732            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
733            serializer.serialize_some(&url_strings)
734        }
735        None => serializer.serialize_none(),
736    }
737}