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