Skip to main content

lux_lib/operations/
gen_luarc.rs

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