1use itertools::Itertools;
2use lets_find_up::{find_up_with, FindUpKind, FindUpOptions};
3use mlua::{ExternalResult, UserData};
4use path_slash::PathBufExt;
5use project_toml::{
6 LocalProjectTomlValidationError, PartialProjectToml, RemoteProjectTomlValidationError,
7};
8use std::{
9 io,
10 ops::Deref,
11 path::{Path, PathBuf},
12 str::FromStr,
13};
14use thiserror::Error;
15use toml_edit::{DocumentMut, Item};
16
17use crate::{
18 build,
19 config::{Config, LuaVersion},
20 git::{self, shorthand::GitUrlShorthand, utils::GitError},
21 lockfile::{LockfileError, ProjectLockfile, ReadOnly},
22 lua::lua_runtime,
23 lua_rockspec::{
24 LocalLuaRockspec, LuaRockspecError, LuaVersionError, PartialLuaRockspec,
25 PartialRockspecError, RemoteLuaRockspec,
26 },
27 progress::Progress,
28 remote_package_db::RemotePackageDB,
29 rockspec::{
30 lua_dependency::{DependencyType, LuaDependencySpec, LuaDependencyType},
31 LuaVersionCompatibility,
32 },
33 tree::{Tree, TreeError},
34};
35use crate::{
36 lockfile::PinnedState,
37 package::{PackageName, PackageReq},
38};
39
40pub(crate) mod gen;
41pub mod project_toml;
42
43pub use project_toml::PROJECT_TOML;
44
45pub const EXTRA_ROCKSPEC: &str = "extra.rockspec";
46
47#[derive(Error, Debug)]
48#[error(transparent)]
49pub enum ProjectError {
50 Io(#[from] io::Error),
51 Lockfile(#[from] LockfileError),
52 Project(#[from] LocalProjectTomlValidationError),
53 Toml(#[from] toml::de::Error),
54 #[error("error when parsing `extra.rockspec`: {0}")]
55 Rockspec(#[from] PartialRockspecError),
56 #[error("not in a lux project directory")]
57 NotAProjectDir,
58}
59
60#[derive(Error, Debug)]
61#[error(transparent)]
62pub enum IntoLocalRockspecError {
63 LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
64 RockspecError(#[from] LuaRockspecError),
65}
66
67#[derive(Error, Debug)]
68#[error(transparent)]
69pub enum IntoRemoteRockspecError {
70 RocksTomlValidationError(#[from] RemoteProjectTomlValidationError),
71 RockspecError(#[from] LuaRockspecError),
72}
73
74#[derive(Error, Debug)]
75pub enum ProjectEditError {
76 #[error(transparent)]
77 Io(#[from] tokio::io::Error),
78 #[error(transparent)]
79 Toml(#[from] toml_edit::TomlError),
80 #[error("error parsing lux.toml after edit. This is probably a bug.")]
81 TomlDe(#[from] toml::de::Error),
82 #[error(transparent)]
83 Git(#[from] GitError),
84 #[error("unable to query latest version for {0}")]
85 LatestVersionNotFound(PackageName),
86 #[error("expected field to be a value, but got {0}")]
87 ExpectedValue(toml_edit::Item),
88 #[error("expected string, but got {0}")]
89 ExpectedString(toml_edit::Value),
90 #[error(transparent)]
91 GitUrlShorthandParse(#[from] git::shorthand::ParseError),
92}
93
94#[derive(Error, Debug)]
95#[error(transparent)]
96pub enum ProjectTreeError {
97 Tree(#[from] TreeError),
98 LuaVersionError(#[from] LuaVersionError),
99}
100
101#[derive(Error, Debug)]
102pub enum PinError {
103 #[error("package {0} not found in dependencies")]
104 PackageNotFound(PackageName),
105 #[error("dependency {dep} is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
106 PinStateUnchanged {
107 pin_state: PinnedState,
108 dep: PackageName,
109 },
110 #[error(transparent)]
111 Toml(#[from] toml_edit::TomlError),
112 #[error("error parsing lux.toml after edit. This is probably a bug.")]
113 TomlDe(#[from] toml::de::Error),
114 #[error(transparent)]
115 Io(#[from] tokio::io::Error),
116}
117
118#[derive(Clone, Debug)]
121#[cfg_attr(test, derive(Default))]
122pub struct ProjectRoot(PathBuf);
123
124impl ProjectRoot {
125 pub(crate) fn new() -> Self {
126 Self(PathBuf::new())
127 }
128}
129
130impl AsRef<Path> for ProjectRoot {
131 fn as_ref(&self) -> &Path {
132 self.0.as_ref()
133 }
134}
135
136impl Deref for ProjectRoot {
137 type Target = PathBuf;
138
139 fn deref(&self) -> &Self::Target {
140 &self.0
141 }
142}
143
144#[derive(Clone, Debug)]
145pub struct Project {
146 root: ProjectRoot,
148 toml: PartialProjectToml,
150}
151
152impl UserData for Project {
153 fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
154 methods.add_method("toml_path", |_, this, ()| Ok(this.toml_path()));
155 methods.add_method("extra_rockspec_path", |_, this, ()| {
156 Ok(this.extra_rockspec_path())
157 });
158 methods.add_method("lockfile_path", |_, this, ()| Ok(this.lockfile_path()));
159 methods.add_method("root", |_, this, ()| Ok(this.root().0.clone()));
160 methods.add_method("toml", |_, this, ()| Ok(this.toml().clone()));
161 methods.add_method("local_rockspec", |_, this, ()| {
162 this.local_rockspec().into_lua_err()
163 });
164 methods.add_method("remote_rockspec", |_, this, ()| {
165 this.remote_rockspec().into_lua_err()
166 });
167 methods.add_method("tree", |_, this, config: Config| {
168 this.tree(&config).into_lua_err()
169 });
170 methods.add_method("test_tree", |_, this, config: Config| {
171 this.test_tree(&config).into_lua_err()
172 });
173 methods.add_method("lua_version", |_, this, config: Config| {
174 this.lua_version(&config).into_lua_err()
175 });
176 methods.add_method("extra_rockspec", |_, this, ()| {
177 this.extra_rockspec().into_lua_err()
178 });
179
180 methods.add_async_method_mut(
181 "add",
182 |_, mut this, (deps, config): (DependencyType<PackageReq>, Config)| async move {
183 let _guard = lua_runtime().enter();
189
190 let package_db = RemotePackageDB::from_config(&config, &Progress::NoProgress)
191 .await
192 .into_lua_err()?;
193 this.add(deps, &package_db).await.into_lua_err()
194 },
195 );
196
197 methods.add_async_method_mut(
198 "remove",
199 |_, mut this, deps: DependencyType<PackageName>| async move {
200 let _guard = lua_runtime().enter();
201
202 this.remove(deps).await.into_lua_err()
203 },
204 );
205
206 methods.add_async_method_mut(
207 "upgrade",
208 |_, mut this, (deps, package_db): (LuaDependencyType<PackageName>, RemotePackageDB)| async move {
209 let _guard = lua_runtime().enter();
210
211 this.upgrade(deps, &package_db).await.into_lua_err()
212 },
213 );
214
215 methods.add_async_method_mut(
216 "upgrade_all",
217 |_, mut this, package_db: RemotePackageDB| async move {
218 let _guard = lua_runtime().enter();
219
220 this.upgrade_all(&package_db).await.into_lua_err()
221 },
222 );
223
224 methods.add_async_method_mut(
225 "set_pinned_state",
226 |_, mut this, (deps, pin): (LuaDependencyType<PackageName>, PinnedState)| async move {
227 let _guard = lua_runtime().enter();
228
229 this.set_pinned_state(deps, pin).await.into_lua_err()
230 },
231 );
232
233 methods.add_method("project_files", |_, this, ()| Ok(this.project_files()));
234
235 }
242}
243
244impl Project {
245 pub fn current() -> Result<Option<Self>, ProjectError> {
246 Self::from(&std::env::current_dir()?)
247 }
248
249 pub fn current_or_err() -> Result<Self, ProjectError> {
250 Self::current()?.ok_or(ProjectError::NotAProjectDir)
251 }
252
253 pub fn from_exact(start: impl AsRef<Path>) -> Result<Option<Self>, ProjectError> {
254 if !start.as_ref().exists() {
255 return Ok(None);
256 }
257
258 if start.as_ref().join(PROJECT_TOML).exists() {
259 let toml_content = std::fs::read_to_string(start.as_ref().join(PROJECT_TOML))?;
260 let root = start.as_ref();
261
262 let mut project = Project {
263 root: ProjectRoot(root.to_path_buf()),
264 toml: PartialProjectToml::new(&toml_content, ProjectRoot(root.to_path_buf()))?,
265 };
266
267 if let Some(extra_rockspec) = project.extra_rockspec()? {
268 project.toml = project.toml.merge(extra_rockspec);
269 }
270
271 Ok(Some(project))
272 } else {
273 Ok(None)
274 }
275 }
276
277 pub fn from(start: impl AsRef<Path>) -> Result<Option<Self>, ProjectError> {
278 if !start.as_ref().exists() {
279 return Ok(None);
280 }
281
282 match find_up_with(
283 PROJECT_TOML,
284 FindUpOptions {
285 cwd: start.as_ref(),
286 kind: FindUpKind::File,
287 },
288 ) {
289 Ok(Some(path)) => {
290 let toml_content = std::fs::read_to_string(&path)?;
291 let root = path.parent().unwrap();
292
293 let mut project = Project {
294 root: ProjectRoot(root.to_path_buf()),
295 toml: PartialProjectToml::new(&toml_content, ProjectRoot(root.to_path_buf()))?,
296 };
297
298 if let Some(extra_rockspec) = project.extra_rockspec()? {
299 project.toml = project.toml.merge(extra_rockspec);
300 }
301
302 std::fs::create_dir_all(root)?;
303
304 Ok(Some(project))
305 }
306 _ => Ok(None),
310 }
311 }
312
313 pub fn toml_path(&self) -> PathBuf {
315 self.root.join(PROJECT_TOML)
316 }
317
318 pub fn extra_rockspec_path(&self) -> PathBuf {
320 self.root.join(EXTRA_ROCKSPEC)
321 }
322
323 pub fn lockfile_path(&self) -> PathBuf {
325 self.root.join("lux.lock")
326 }
327
328 pub fn lockfile(&self) -> Result<ProjectLockfile<ReadOnly>, ProjectError> {
330 Ok(ProjectLockfile::new(self.lockfile_path())?)
331 }
332
333 pub fn try_lockfile(&self) -> Result<Option<ProjectLockfile<ReadOnly>>, ProjectError> {
335 let path = self.lockfile_path();
336 if path.is_file() {
337 Ok(Some(ProjectLockfile::load(path)?))
338 } else {
339 Ok(None)
340 }
341 }
342
343 pub fn root(&self) -> &ProjectRoot {
344 &self.root
345 }
346
347 pub fn toml(&self) -> &PartialProjectToml {
348 &self.toml
349 }
350
351 pub fn local_rockspec(&self) -> Result<LocalLuaRockspec, IntoLocalRockspecError> {
352 Ok(self.toml().into_local()?.to_lua_rockspec()?)
353 }
354
355 pub fn remote_rockspec(&self) -> Result<RemoteLuaRockspec, IntoRemoteRockspecError> {
356 Ok(self.toml().into_remote()?.to_lua_rockspec()?)
357 }
358
359 pub fn extra_rockspec(&self) -> Result<Option<PartialLuaRockspec>, PartialRockspecError> {
360 if self.extra_rockspec_path().exists() {
361 Ok(Some(PartialLuaRockspec::new(&std::fs::read_to_string(
362 self.extra_rockspec_path(),
363 )?)?))
364 } else {
365 Ok(None)
366 }
367 }
368
369 pub(crate) fn default_tree_root_dir(&self) -> PathBuf {
370 self.root.join(".lux")
371 }
372
373 pub fn tree(&self, config: &Config) -> Result<Tree, ProjectTreeError> {
374 self.lua_version_tree(self.lua_version(config)?, config)
375 }
376
377 pub(crate) fn lua_version_tree(
378 &self,
379 lua_version: LuaVersion,
380 config: &Config,
381 ) -> Result<Tree, ProjectTreeError> {
382 Ok(Tree::new(
383 self.default_tree_root_dir(),
384 lua_version,
385 config,
386 )?)
387 }
388
389 pub fn test_tree(&self, config: &Config) -> Result<Tree, ProjectTreeError> {
390 Ok(self.tree(config)?.test_tree(config)?)
391 }
392
393 pub fn build_tree(&self, config: &Config) -> Result<Tree, ProjectTreeError> {
394 Ok(self.tree(config)?.build_tree(config)?)
395 }
396
397 pub fn lua_version(&self, config: &Config) -> Result<LuaVersion, LuaVersionError> {
398 self.toml().lua_version_matches(config)
399 }
400
401 pub async fn add(
402 &mut self,
403 dependencies: DependencyType<PackageReq>,
404 package_db: &RemotePackageDB,
405 ) -> Result<(), ProjectEditError> {
406 let mut project_toml =
407 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
408
409 prepare_dependency_tables(&mut project_toml);
410 let table = match dependencies {
411 DependencyType::Regular(_) => &mut project_toml["dependencies"],
412 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
413 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
414 DependencyType::External(_) => &mut project_toml["external_dependencies"],
415 };
416
417 match dependencies {
418 DependencyType::Regular(ref deps)
419 | DependencyType::Build(ref deps)
420 | DependencyType::Test(ref deps) => {
421 for dep in deps {
422 let dep_version_str = if dep.version_req().is_any() {
423 package_db
424 .latest_version(dep.name())
425 .expect("unable to query latest version for package")
431 .to_string()
432 } else {
433 dep.version_req().to_string()
434 };
435 table[dep.name().to_string()] = toml_edit::value(dep_version_str);
436 }
437 }
438 DependencyType::External(ref deps) => {
439 for (name, dep) in deps {
440 if let Some(path) = &dep.header {
441 table[name]["header"] = toml_edit::value(path.to_slash_lossy().to_string());
442 }
443 if let Some(path) = &dep.library {
444 table[name]["library"] =
445 toml_edit::value(path.to_slash_lossy().to_string());
446 }
447 }
448 }
449 };
450
451 let toml_content = project_toml.to_string();
452 tokio::fs::write(self.toml_path(), &toml_content).await?;
453 self.toml = PartialProjectToml::new(&toml_content, self.root.clone())?;
454
455 Ok(())
456 }
457
458 pub async fn add_git(
459 &mut self,
460 dependencies: LuaDependencyType<GitUrlShorthand>,
461 ) -> Result<(), ProjectEditError> {
462 let mut project_toml =
463 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
464
465 prepare_dependency_tables(&mut project_toml);
466 let table = match dependencies {
467 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
468 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
469 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
470 };
471
472 match dependencies {
473 LuaDependencyType::Regular(ref urls)
474 | LuaDependencyType::Build(ref urls)
475 | LuaDependencyType::Test(ref urls) => {
476 for url in urls {
477 let git_url: git_url_parse::GitUrl = url.clone().into();
478 let rev = git::utils::latest_semver_tag_or_commit_sha(&git_url)?;
479 table[git_url.name.clone()]["version"] = Item::Value(rev.into());
480 table[git_url.name.clone()]["git"] = Item::Value(url.to_string().into());
481 }
482 }
483 }
484
485 let toml_content = project_toml.to_string();
486 tokio::fs::write(self.toml_path(), &toml_content).await?;
487 self.toml = PartialProjectToml::new(&toml_content, self.root.clone())?;
488
489 Ok(())
490 }
491
492 pub async fn remove(
493 &mut self,
494 dependencies: DependencyType<PackageName>,
495 ) -> Result<(), ProjectEditError> {
496 let mut project_toml =
497 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
498
499 prepare_dependency_tables(&mut project_toml);
500 let table = match dependencies {
501 DependencyType::Regular(_) => &mut project_toml["dependencies"],
502 DependencyType::Build(_) => &mut project_toml["build_dependencies"],
503 DependencyType::Test(_) => &mut project_toml["test_dependencies"],
504 DependencyType::External(_) => &mut project_toml["external_dependencies"],
505 };
506
507 match dependencies {
508 DependencyType::Regular(ref deps)
509 | DependencyType::Build(ref deps)
510 | DependencyType::Test(ref deps) => {
511 for dep in deps {
512 table[dep.to_string()] = Item::None;
513 }
514 }
515 DependencyType::External(ref deps) => {
516 for (name, dep) in deps {
517 if dep.header.is_some() {
518 table[name]["header"] = Item::None;
519 }
520 if dep.library.is_some() {
521 table[name]["library"] = Item::None;
522 }
523 }
524 }
525 };
526
527 let toml_content = project_toml.to_string();
528 tokio::fs::write(self.toml_path(), &toml_content).await?;
529 self.toml = PartialProjectToml::new(&toml_content, self.root.clone())?;
530
531 Ok(())
532 }
533
534 pub async fn upgrade(
535 &mut self,
536 dependencies: LuaDependencyType<PackageName>,
537 package_db: &RemotePackageDB,
538 ) -> Result<(), ProjectEditError> {
539 let mut project_toml =
540 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
541
542 prepare_dependency_tables(&mut project_toml);
543 let table = match dependencies {
544 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
545 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
546 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
547 };
548
549 match dependencies {
550 LuaDependencyType::Regular(ref deps)
551 | LuaDependencyType::Build(ref deps)
552 | LuaDependencyType::Test(ref deps) => {
553 let latest_rock_version_str =
554 |dep: &PackageName| -> Result<String, ProjectEditError> {
555 Ok(package_db
556 .latest_version(dep)
557 .ok_or(ProjectEditError::LatestVersionNotFound(dep.clone()))?
558 .to_string())
559 };
560 for dep in deps {
561 let mut dep_item = table[dep.to_string()].clone();
562 match &dep_item {
563 Item::Value(_) => {
564 let dep_version_str = latest_rock_version_str(dep)?;
565 table[dep.to_string()] = toml_edit::value(dep_version_str);
566 }
567 Item::Table(tbl) => {
568 if tbl.contains_key("git") {
569 let git_value = tbl
570 .get("git")
571 .expect("expected 'git' field")
572 .clone()
573 .into_value()
574 .map_err(ProjectEditError::ExpectedValue)?;
575 let git_url_str = git_value
576 .as_str()
577 .ok_or(ProjectEditError::ExpectedString(git_value.clone()))?;
578 let shorthand: GitUrlShorthand = git_url_str.parse()?;
579 let latest_rev =
580 git::utils::latest_semver_tag_or_commit_sha(&shorthand.into())?;
581 let key = if tbl.contains_key("rev") {
582 "rev".to_string()
583 } else {
584 "version".to_string()
585 };
586 dep_item[key] = toml_edit::value(latest_rev);
587 table[dep.to_string()] = dep_item;
588 } else {
589 let dep_version_str = latest_rock_version_str(dep)?;
590 dep_item["version".to_string()] = toml_edit::value(dep_version_str);
591 table[dep.to_string()] = dep_item;
592 }
593 }
594 _ => {}
595 }
596 }
597 }
598 }
599
600 let toml_content = project_toml.to_string();
601 tokio::fs::write(self.toml_path(), &toml_content).await?;
602 self.toml = PartialProjectToml::new(&toml_content, self.root.clone())?;
603
604 Ok(())
605 }
606
607 pub async fn upgrade_all(
608 &mut self,
609 package_db: &RemotePackageDB,
610 ) -> Result<(), ProjectEditError> {
611 if let Some(dependencies) = &self.toml().dependencies {
612 let packages = dependencies
613 .iter()
614 .map(|dep| dep.name())
615 .cloned()
616 .collect_vec();
617 self.upgrade(LuaDependencyType::Regular(packages), package_db)
618 .await?;
619 }
620 if let Some(dependencies) = &self.toml().build_dependencies {
621 let packages = dependencies
622 .iter()
623 .map(|dep| dep.name())
624 .cloned()
625 .collect_vec();
626 self.upgrade(LuaDependencyType::Build(packages), package_db)
627 .await?;
628 }
629 if let Some(dependencies) = &self.toml().test_dependencies {
630 let packages = dependencies
631 .iter()
632 .map(|dep| dep.name())
633 .cloned()
634 .collect_vec();
635 self.upgrade(LuaDependencyType::Test(packages), package_db)
636 .await?;
637 }
638 Ok(())
639 }
640
641 pub async fn set_pinned_state(
642 &mut self,
643 dependencies: LuaDependencyType<PackageName>,
644 pin: PinnedState,
645 ) -> Result<(), PinError> {
646 let mut project_toml =
647 toml_edit::DocumentMut::from_str(&tokio::fs::read_to_string(self.toml_path()).await?)?;
648
649 prepare_dependency_tables(&mut project_toml);
650 let table = match dependencies {
651 LuaDependencyType::Regular(_) => &mut project_toml["dependencies"],
652 LuaDependencyType::Build(_) => &mut project_toml["build_dependencies"],
653 LuaDependencyType::Test(_) => &mut project_toml["test_dependencies"],
654 };
655
656 match dependencies {
657 LuaDependencyType::Regular(ref _deps) => {
658 self.toml.dependencies = Some(
659 self.toml
660 .dependencies
661 .take()
662 .unwrap_or_default()
663 .into_iter()
664 .map(|dep| LuaDependencySpec { pin, ..dep })
665 .collect(),
666 )
667 }
668 LuaDependencyType::Build(ref _deps) => {
669 self.toml.build_dependencies = Some(
670 self.toml
671 .build_dependencies
672 .take()
673 .unwrap_or_default()
674 .into_iter()
675 .map(|dep| LuaDependencySpec { pin, ..dep })
676 .collect(),
677 )
678 }
679 LuaDependencyType::Test(ref _deps) => {
680 self.toml.test_dependencies = Some(
681 self.toml
682 .test_dependencies
683 .take()
684 .unwrap_or_default()
685 .into_iter()
686 .map(|dep| LuaDependencySpec { pin, ..dep })
687 .collect(),
688 )
689 }
690 }
691
692 match dependencies {
693 LuaDependencyType::Regular(ref deps)
694 | LuaDependencyType::Build(ref deps)
695 | LuaDependencyType::Test(ref deps) => {
696 for dep in deps {
697 let mut dep_item = table[dep.to_string()].clone();
698 match dep_item {
699 version @ Item::Value(_) => match &pin {
700 PinnedState::Unpinned => {}
701 PinnedState::Pinned => {
702 let mut dep_entry = toml_edit::table().into_table().unwrap();
703 dep_entry.set_implicit(true);
704 dep_entry["version"] = version;
705 dep_entry["pin"] = toml_edit::value(true);
706 table[dep.to_string()] = toml_edit::Item::Table(dep_entry);
707 }
708 },
709 Item::Table(_) => {
710 dep_item["pin".to_string()] = toml_edit::value(pin.as_bool());
711 table[dep.to_string()] = dep_item;
712 }
713 _ => {}
714 }
715 }
716 }
717 }
718
719 let toml_content = project_toml.to_string();
720 tokio::fs::write(self.toml_path(), &toml_content).await?;
721 self.toml = PartialProjectToml::new(&toml_content, self.root.clone())?;
722
723 Ok(())
724 }
725
726 pub fn project_files(&self) -> Vec<PathBuf> {
727 build::utils::project_files(&self.root().0)
728 }
729}
730
731fn prepare_dependency_tables(project_toml: &mut DocumentMut) {
732 if !project_toml.contains_table("dependencies") {
733 let mut table = toml_edit::table().into_table().unwrap();
734 table.set_implicit(true);
735
736 project_toml["dependencies"] = toml_edit::Item::Table(table);
737 }
738 if !project_toml.contains_table("build_dependencies") {
739 let mut table = toml_edit::table().into_table().unwrap();
740 table.set_implicit(true);
741
742 project_toml["build_dependencies"] = toml_edit::Item::Table(table);
743 }
744 if !project_toml.contains_table("test_dependencies") {
745 let mut table = toml_edit::table().into_table().unwrap();
746 table.set_implicit(true);
747
748 project_toml["test_dependencies"] = toml_edit::Item::Table(table);
749 }
750 if !project_toml.contains_table("external_dependencies") {
751 let mut table = toml_edit::table().into_table().unwrap();
752 table.set_implicit(true);
753
754 project_toml["external_dependencies"] = toml_edit::Item::Table(table);
755 }
756}
757
758#[cfg(test)]
760mod tests {
761 use std::collections::HashMap;
762
763 use assert_fs::prelude::PathCopy;
764 use url::Url;
765
766 use super::*;
767 use crate::{
768 lua_rockspec::ExternalDependencySpec,
769 manifest::{Manifest, ManifestMetadata},
770 package::PackageReq,
771 rockspec::Rockspec,
772 };
773
774 #[tokio::test]
775 async fn test_add_various_dependencies() {
776 let sample_project: PathBuf = "resources/test/sample-project-no-build-spec/".into();
777 let project_root = assert_fs::TempDir::new().unwrap();
778 project_root.copy_from(&sample_project, &["**"]).unwrap();
779 let project_root: PathBuf = project_root.path().into();
780 let mut project = Project::from(&project_root).unwrap().unwrap();
781 let add_dependencies =
782 vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into())).unwrap()];
783 let expected_dependencies = vec![PackageReq::new("busted".into(), Some(">= 1.0.0".into()))
784 .unwrap()
785 .into()];
786
787 let test_manifest_path =
788 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
789 let content = String::from_utf8(std::fs::read(&test_manifest_path).unwrap()).unwrap();
790 let metadata = ManifestMetadata::new(&content).unwrap();
791 let package_db = Manifest::new(Url::parse("https://example.com").unwrap(), metadata).into();
792
793 project
794 .add(
795 DependencyType::Regular(add_dependencies.clone()),
796 &package_db,
797 )
798 .await
799 .unwrap();
800
801 project
802 .add(DependencyType::Build(add_dependencies.clone()), &package_db)
803 .await
804 .unwrap();
805 project
806 .add(DependencyType::Test(add_dependencies.clone()), &package_db)
807 .await
808 .unwrap();
809
810 project
811 .add(
812 DependencyType::External(HashMap::from([(
813 "lib".into(),
814 ExternalDependencySpec {
815 library: Some("path.so".into()),
816 header: None,
817 },
818 )])),
819 &package_db,
820 )
821 .await
822 .unwrap();
823
824 let project = Project::from(&project_root).unwrap().unwrap();
827 let validated_toml = project.toml().into_remote().unwrap();
828
829 assert_eq!(
830 validated_toml.dependencies().current_platform(),
831 &expected_dependencies
832 );
833 assert_eq!(
834 validated_toml.build_dependencies().current_platform(),
835 &expected_dependencies
836 );
837 assert_eq!(
838 validated_toml.test_dependencies().current_platform(),
839 &expected_dependencies
840 );
841 assert_eq!(
842 validated_toml
843 .external_dependencies()
844 .current_platform()
845 .get("lib")
846 .unwrap(),
847 &ExternalDependencySpec {
848 library: Some("path.so".into()),
849 header: None
850 }
851 );
852 }
853
854 #[tokio::test]
855 async fn test_remove_dependencies() {
856 let sample_project: PathBuf = "resources/test/sample-project-dependencies/".into();
857 let project_root = assert_fs::TempDir::new().unwrap();
858 project_root.copy_from(&sample_project, &["**"]).unwrap();
859 let project_root: PathBuf = project_root.path().into();
860 let mut project = Project::from(&project_root).unwrap().unwrap();
861 let remove_dependencies = vec!["lua-cjson".into(), "plenary.nvim".into()];
862 project
863 .remove(DependencyType::Regular(remove_dependencies.clone()))
864 .await
865 .unwrap();
866 let check = |project: &Project| {
867 for name in &remove_dependencies {
868 assert!(!project
869 .toml()
870 .dependencies
871 .clone()
872 .unwrap_or_default()
873 .iter()
874 .any(|dep| dep.name() == name));
875 }
876 };
877 check(&project);
878 let reloaded_project = Project::from(&project_root).unwrap().unwrap();
880 check(&reloaded_project);
881 }
882
883 #[tokio::test]
884 async fn test_extra_rockspec_parsing() {
885 let sample_project: PathBuf = "resources/test/sample-project-extra-rockspec".into();
886 let project_root = assert_fs::TempDir::new().unwrap();
887 project_root.copy_from(&sample_project, &["**"]).unwrap();
888 let project_root: PathBuf = project_root.path().into();
889 let project = Project::from(project_root).unwrap().unwrap();
890
891 let extra_rockspec = project.extra_rockspec().unwrap();
892
893 assert!(extra_rockspec.is_some());
894
895 let rocks = project.toml().into_remote().unwrap();
896
897 assert_eq!(rocks.package().to_string(), "custom-package");
898 }
899
900 #[tokio::test]
901 async fn test_pin_dependencies() {
902 test_pin_unpin_dependencies(PinnedState::Pinned).await
903 }
904
905 #[tokio::test]
906 async fn test_unpin_dependencies() {
907 test_pin_unpin_dependencies(PinnedState::Unpinned).await
908 }
909
910 async fn test_pin_unpin_dependencies(pin: PinnedState) {
911 let sample_project: PathBuf = "resources/test/sample-project-dependencies/".into();
912 let project_root = assert_fs::TempDir::new().unwrap();
913 project_root.copy_from(&sample_project, &["**"]).unwrap();
914 let project_root: PathBuf = project_root.path().into();
915 let mut project = Project::from(&project_root).unwrap().unwrap();
916 let pin_dependencies = vec!["lua-cjson".into(), "plenary.nvim".into()];
917 project
918 .set_pinned_state(LuaDependencyType::Regular(pin_dependencies.clone()), pin)
919 .await
920 .unwrap();
921 let check = |project: &Project| {
922 for name in &pin_dependencies {
923 assert!(project
924 .toml()
925 .dependencies
926 .clone()
927 .unwrap_or_default()
928 .iter()
929 .any(|dep| dep.name() == name && dep.pin == pin));
930 }
931 };
932 check(&project);
933 let reloaded_project = Project::from(&project_root).unwrap().unwrap();
935 check(&reloaded_project);
936 }
937}