Skip to main content

lux_lib/operations/
gen_luarc.rs

1use crate::config::Config;
2use crate::fs;
3use crate::lockfile::LocalPackageLockType;
4use crate::tree::InstallTree;
5use crate::workspace::Workspace;
6use crate::workspace::WorkspaceError;
7use crate::workspace::WorkspaceTreeError;
8use crate::workspace::LUX_DIR_NAME;
9use bon::Builder;
10use itertools::Itertools;
11use miette::Diagnostic;
12use path_slash::PathBufExt;
13use pathdiff::diff_paths;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16use std::path::PathBuf;
17use thiserror::Error;
18
19#[derive(Error, Debug, Diagnostic)]
20pub enum GenLuaRcError {
21    #[error(transparent)]
22    #[diagnostic(transparent)]
23    Fs(#[from] fs::FsError),
24    #[error(transparent)]
25    #[diagnostic(transparent)]
26    Workspace(#[from] WorkspaceError),
27    #[error(transparent)]
28    #[diagnostic(transparent)]
29    WorkspaceTree(#[from] WorkspaceTreeError),
30    #[error("failed to serialize luarc content:\n{0}")]
31    Serialize(String),
32    #[error("failed to deserialize luarc content:\n{0}")]
33    Deserialize(String),
34}
35
36#[derive(Builder)]
37#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
38pub(crate) struct GenLuaRc<'a> {
39    config: &'a Config,
40    workspace: &'a Workspace,
41}
42
43impl<State> GenLuaRcBuilder<'_, State>
44where
45    State: gen_lua_rc_builder::State + gen_lua_rc_builder::IsComplete,
46{
47    pub async fn generate_luarc(self) -> Result<(), GenLuaRcError> {
48        do_generate_luarc(self._build()).await
49    }
50}
51
52#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
53#[serde(default)]
54struct LuaRC {
55    #[serde(flatten)] // <-- capture any unknown keys here
56    other: BTreeMap<String, serde_json::Value>,
57
58    #[serde(default)]
59    workspace: LuaRcWorkspace,
60}
61
62#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
63struct LuaRcWorkspace {
64    #[serde(flatten)] // <-- capture any unknown keys here
65    other: BTreeMap<String, serde_json::Value>,
66
67    #[serde(default)]
68    library: Vec<String>,
69}
70
71async fn do_generate_luarc(args: GenLuaRc<'_>) -> Result<(), GenLuaRcError> {
72    let config = args.config;
73    if !config.generate_luarc() {
74        return Ok(());
75    }
76    let workspace = args.workspace;
77    let lockfile = workspace.lockfile()?;
78    let luarc_path = workspace.luarc_path(config);
79
80    // read the existing .luarc file or initialise a new one if it doesn't exist
81    let luarc_content = fs::tokio::read_to_string(&luarc_path)
82        .await
83        .unwrap_or_else(|_| "{}".into());
84
85    let dependency_tree = workspace.tree(config)?;
86    let dependency_dirs = lockfile
87        .local_pkg_lock(&LocalPackageLockType::Regular)
88        .rocks()
89        .values()
90        .map(|dependency| dependency_tree.layout_for(dependency).src)
91        .filter(|dir| dir.is_dir())
92        .filter_map(|dependency_dir| diff_paths(dependency_dir, workspace.root()));
93
94    let test_dependency_tree = workspace.test_tree(config)?;
95    let test_dependency_dirs = lockfile
96        .local_pkg_lock(&LocalPackageLockType::Test)
97        .rocks()
98        .values()
99        .map(|dependency| test_dependency_tree.layout_for(dependency).src)
100        .filter(|dir| dir.is_dir())
101        .filter_map(|test_dependency_dir| diff_paths(test_dependency_dir, workspace.root()));
102
103    let library_dirs = dependency_dirs
104        .chain(test_dependency_dirs)
105        .sorted()
106        .collect_vec();
107
108    let luarc_content = update_luarc_content(&luarc_content, library_dirs)?;
109
110    fs::tokio::write(&luarc_path, luarc_content).await?;
111
112    Ok(())
113}
114
115fn update_luarc_content(
116    prev_contents: &str,
117    extra_paths: Vec<PathBuf>,
118) -> Result<String, GenLuaRcError> {
119    let mut luarc: LuaRC = serde_json::from_str(prev_contents)
120        .map_err(|err| GenLuaRcError::Deserialize(err.to_string()))?;
121
122    // remove any preexisting lux library paths
123    luarc
124        .workspace
125        .library
126        .retain(|path| !path.starts_with(&format!("{LUX_DIR_NAME}/")));
127
128    extra_paths
129        .iter()
130        .map(|path| path.to_slash_lossy().to_string())
131        .for_each(|path_str| luarc.workspace.library.push(path_str));
132
133    serde_json::to_string_pretty(&luarc).map_err(|err| GenLuaRcError::Serialize(err.to_string()))
134}
135
136#[cfg(test)]
137mod tests {
138
139    use super::*;
140
141    #[test]
142    fn test_generate_luarc_with_previous_libraries_parametrized() {
143        let cases = vec![
144            (
145                "Empty existing libraries, adding single lib", // 📝 Description
146                r#"{
147                    "workspace": {
148                        "library": []
149                    }
150                }"#,
151                vec![".lux/5.1/my-lib".into()],
152                r#"{
153                    "workspace": {
154                        "library": [".lux/5.1/my-lib"]
155                    }
156                }"#,
157            ),
158            (
159                "Other fields present, adding libs", // 📝 Description
160                r#"{
161                    "any-other-field": true,
162                    "workspace": {
163                        "library": []
164                    }
165                }"#,
166                vec![".lux/5.1/lib-A".into(), ".lux/5.1/lib-B".into()],
167                r#"{
168                    "any-other-field": true,
169                    "workspace": {
170                        "library": [".lux/5.1/lib-A", ".lux/5.1/lib-B"]
171                    }
172                }"#,
173            ),
174            (
175                "Removes not present libs, without removing others", // 📝 Description
176                r#"{
177                    "workspace": {
178                        "library": [".lux/5.1/lib-A", ".lux/5.4/lib-B"]
179                    }
180                }"#,
181                vec![".lux/5.1/lib-C".into()],
182                r#"{
183                    "workspace": {
184                        "library": [".lux/5.1/lib-C"]
185                    }
186                }"#,
187            ),
188        ];
189
190        for (description, initial, new_libs, expected) in cases {
191            let content = super::update_luarc_content(initial, new_libs.clone()).unwrap();
192
193            assert_eq!(
194                serde_json::from_str::<LuaRC>(&content).unwrap(),
195                serde_json::from_str::<LuaRC>(expected).unwrap(),
196                "Case failed: {}\nInitial input:\n{}\nNew libs: {:?}",
197                description,
198                initial,
199                new_libs
200            );
201        }
202    }
203}