Skip to main content

lux_lib/operations/
gen_luarc.rs

1use crate::config::Config;
2use crate::lockfile::LocalPackageLockType;
3use crate::tree::InstallTree;
4use crate::workspace::Workspace;
5use crate::workspace::WorkspaceError;
6use crate::workspace::WorkspaceTreeError;
7use crate::workspace::LUX_DIR_NAME;
8use bon::Builder;
9use itertools::Itertools;
10use miette::Diagnostic;
11use path_slash::PathBufExt;
12use pathdiff::diff_paths;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::io;
16use std::path::PathBuf;
17use thiserror::Error;
18use tokio::fs;
19
20#[derive(Error, Debug, Diagnostic)]
21pub enum GenLuaRcError {
22    #[error(transparent)]
23    #[diagnostic(transparent)]
24    Workspace(#[from] WorkspaceError),
25    #[error(transparent)]
26    #[diagnostic(transparent)]
27    WorkspaceTree(#[from] WorkspaceTreeError),
28    #[error("failed to serialize luarc content:\n{0}")]
29    Serialize(String),
30    #[error("failed to deserialize luarc content:\n{0}")]
31    Deserialize(String),
32    #[error("failed to write {0}:\n{1}")]
33    Write(PathBuf, io::Error),
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::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::write(&luarc_path, luarc_content)
115        .await
116        .map_err(|err| GenLuaRcError::Write(luarc_path, err))?;
117
118    Ok(())
119}
120
121fn update_luarc_content(
122    prev_contents: &str,
123    extra_paths: Vec<PathBuf>,
124) -> Result<String, GenLuaRcError> {
125    let mut luarc: LuaRC = serde_json::from_str(prev_contents)
126        .map_err(|err| GenLuaRcError::Deserialize(err.to_string()))?;
127
128    // remove any preexisting lux library paths
129    luarc
130        .workspace
131        .library
132        .retain(|path| !path.starts_with(&format!("{LUX_DIR_NAME}/")));
133
134    extra_paths
135        .iter()
136        .map(|path| path.to_slash_lossy().to_string())
137        .for_each(|path_str| luarc.workspace.library.push(path_str));
138
139    serde_json::to_string_pretty(&luarc).map_err(|err| GenLuaRcError::Serialize(err.to_string()))
140}
141
142#[cfg(test)]
143mod tests {
144
145    use super::*;
146
147    #[test]
148    fn test_generate_luarc_with_previous_libraries_parametrized() {
149        let cases = vec![
150            (
151                "Empty existing libraries, adding single lib", // 📝 Description
152                r#"{
153                    "workspace": {
154                        "library": []
155                    }
156                }"#,
157                vec![".lux/5.1/my-lib".into()],
158                r#"{
159                    "workspace": {
160                        "library": [".lux/5.1/my-lib"]
161                    }
162                }"#,
163            ),
164            (
165                "Other fields present, adding libs", // 📝 Description
166                r#"{
167                    "any-other-field": true,
168                    "workspace": {
169                        "library": []
170                    }
171                }"#,
172                vec![".lux/5.1/lib-A".into(), ".lux/5.1/lib-B".into()],
173                r#"{
174                    "any-other-field": true,
175                    "workspace": {
176                        "library": [".lux/5.1/lib-A", ".lux/5.1/lib-B"]
177                    }
178                }"#,
179            ),
180            (
181                "Removes not present libs, without removing others", // 📝 Description
182                r#"{
183                    "workspace": {
184                        "library": [".lux/5.1/lib-A", ".lux/5.4/lib-B"]
185                    }
186                }"#,
187                vec![".lux/5.1/lib-C".into()],
188                r#"{
189                    "workspace": {
190                        "library": [".lux/5.1/lib-C"]
191                    }
192                }"#,
193            ),
194        ];
195
196        for (description, initial, new_libs, expected) in cases {
197            let content = super::update_luarc_content(initial, new_libs.clone()).unwrap();
198
199            assert_eq!(
200                serde_json::from_str::<LuaRC>(&content).unwrap(),
201                serde_json::from_str::<LuaRC>(expected).unwrap(),
202                "Case failed: {}\nInitial input:\n{}\nNew libs: {:?}",
203                description,
204                initial,
205                &new_libs
206            );
207        }
208    }
209}