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.installed_rock_layout(dependency))
91        .filter_map(Result::ok)
92        .map(|rock_layout| rock_layout.src)
93        .filter(|dir| dir.is_dir())
94        .filter_map(|dependency_dir| diff_paths(dependency_dir, workspace.root()));
95
96    let test_dependency_tree = workspace.test_tree(config)?;
97    let test_dependency_dirs = lockfile
98        .local_pkg_lock(&LocalPackageLockType::Test)
99        .rocks()
100        .values()
101        .map(|dependency| test_dependency_tree.installed_rock_layout(dependency))
102        .filter_map(Result::ok)
103        .map(|rock_layout| rock_layout.src)
104        .filter(|dir| dir.is_dir())
105        .filter_map(|test_dependency_dir| diff_paths(test_dependency_dir, workspace.root()));
106
107    let library_dirs = dependency_dirs
108        .chain(test_dependency_dirs)
109        .sorted()
110        .collect_vec();
111
112    let luarc_content = update_luarc_content(&luarc_content, library_dirs)?;
113
114    fs::tokio::write(&luarc_path, luarc_content).await?;
115
116    Ok(())
117}
118
119fn update_luarc_content(
120    prev_contents: &str,
121    extra_paths: Vec<PathBuf>,
122) -> Result<String, GenLuaRcError> {
123    let mut luarc: LuaRC = serde_json::from_str(prev_contents)
124        .map_err(|err| GenLuaRcError::Deserialize(err.to_string()))?;
125
126    // remove any preexisting lux library paths
127    luarc
128        .workspace
129        .library
130        .retain(|path| !path.starts_with(&format!("{LUX_DIR_NAME}/")));
131
132    extra_paths
133        .iter()
134        .map(|path| path.to_slash_lossy().to_string())
135        .for_each(|path_str| luarc.workspace.library.push(path_str));
136
137    serde_json::to_string_pretty(&luarc).map_err(|err| GenLuaRcError::Serialize(err.to_string()))
138}
139
140#[cfg(test)]
141mod tests {
142
143    use super::*;
144
145    #[test]
146    fn test_generate_luarc_with_previous_libraries_parametrized() {
147        let cases = vec![
148            (
149                "Empty existing libraries, adding single lib", // 📝 Description
150                r#"{
151                    "workspace": {
152                        "library": []
153                    }
154                }"#,
155                vec![".lux/5.1/my-lib".into()],
156                r#"{
157                    "workspace": {
158                        "library": [".lux/5.1/my-lib"]
159                    }
160                }"#,
161            ),
162            (
163                "Other fields present, adding libs", // 📝 Description
164                r#"{
165                    "any-other-field": true,
166                    "workspace": {
167                        "library": []
168                    }
169                }"#,
170                vec![".lux/5.1/lib-A".into(), ".lux/5.1/lib-B".into()],
171                r#"{
172                    "any-other-field": true,
173                    "workspace": {
174                        "library": [".lux/5.1/lib-A", ".lux/5.1/lib-B"]
175                    }
176                }"#,
177            ),
178            (
179                "Removes not present libs, without removing others", // 📝 Description
180                r#"{
181                    "workspace": {
182                        "library": [".lux/5.1/lib-A", ".lux/5.4/lib-B"]
183                    }
184                }"#,
185                vec![".lux/5.1/lib-C".into()],
186                r#"{
187                    "workspace": {
188                        "library": [".lux/5.1/lib-C"]
189                    }
190                }"#,
191            ),
192        ];
193
194        for (description, initial, new_libs, expected) in cases {
195            let content = super::update_luarc_content(initial, new_libs.clone()).unwrap();
196
197            assert_eq!(
198                serde_json::from_str::<LuaRC>(&content).unwrap(),
199                serde_json::from_str::<LuaRC>(expected).unwrap(),
200                "Case failed: {}\nInitial input:\n{}\nNew libs: {:?}",
201                description,
202                initial,
203                &new_libs
204            );
205        }
206    }
207}