From c3d2c768dbca24c04b151ecde9679895f839e873 Mon Sep 17 00:00:00 2001
From: daimond113 <72147841+daimond113@users.noreply.github.com>
Date: Mon, 30 Dec 2024 18:33:48 +0100
Subject: [PATCH] feat: add path dependencies

Fixes #13
---
 docs/src/content/docs/guides/dependencies.mdx |  18 +++
 docs/src/content/docs/reference/cli.mdx       |   4 +
 docs/src/content/docs/reference/manifest.mdx  |  13 ++
 registry/src/endpoints/publish_version.rs     |   5 +
 src/cli/auth.rs                               |   2 +-
 src/cli/commands/add.rs                       |  50 +++---
 src/cli/commands/outdated.rs                  |   5 +-
 src/cli/commands/publish.rs                   |   3 +
 src/cli/mod.rs                                |   3 +
 src/lib.rs                                    |   8 +-
 src/resolver.rs                               |   3 +
 src/source/git/mod.rs                         |   9 ++
 src/source/mod.rs                             |  35 ++++
 src/source/path/mod.rs                        | 152 ++++++++++++++++++
 src/source/path/pkg_ref.rs                    |  29 ++++
 src/source/path/specifier.rs                  |  17 ++
 src/source/refs.rs                            |   5 +
 src/source/specifiers.rs                      |   3 +
 src/source/workspace/mod.rs                   |   1 +
 19 files changed, 341 insertions(+), 24 deletions(-)
 create mode 100644 src/source/path/mod.rs
 create mode 100644 src/source/path/pkg_ref.rs
 create mode 100644 src/source/path/specifier.rs

diff --git a/docs/src/content/docs/guides/dependencies.mdx b/docs/src/content/docs/guides/dependencies.mdx
index 08a1128..13892fa 100644
--- a/docs/src/content/docs/guides/dependencies.mdx
+++ b/docs/src/content/docs/guides/dependencies.mdx
@@ -137,6 +137,24 @@ pesde add workspace:acme/bar
 	href="/guides/workspaces/"
 />
 
+## Path Dependencies
+
+Path dependencies are dependencies found anywhere available to the operating system.
+They are useful for local development, but are forbidden in published packages.
+
+The path must be absolute and point to a directory containing a `pesde.toml` file.
+
+```toml title="pesde.toml"
+[dependencies]
+foo = { path = "/home/user/foo" }
+```
+
+You can also add a path dependency by running the following command:
+
+```sh
+pesde add path:/home/user/foo
+```
+
 ## Peer Dependencies
 
 Peer dependencies are dependencies that are not installed automatically when
diff --git a/docs/src/content/docs/reference/cli.mdx b/docs/src/content/docs/reference/cli.mdx
index f24ca9e..f8a6a00 100644
--- a/docs/src/content/docs/reference/cli.mdx
+++ b/docs/src/content/docs/reference/cli.mdx
@@ -156,8 +156,12 @@ The following formats are supported:
 
 ```sh
 pesde add pesde/hello
+pesde add pesde/hello@1.2.3
 pesde add gh#acme/package#main
 pesde add https://git.acme.local/package.git#aeff6
+pesde add workspace:pesde/hello
+pesde add workspace:pesde/hello@1.2.3
+pesde add path:/home/user/package
 ```
 
 ## `pesde update`
diff --git a/docs/src/content/docs/reference/manifest.mdx b/docs/src/content/docs/reference/manifest.mdx
index 2496771..368a98b 100644
--- a/docs/src/content/docs/reference/manifest.mdx
+++ b/docs/src/content/docs/reference/manifest.mdx
@@ -419,6 +419,19 @@ foo = { workspace = "acme/foo", version = "^" }
 	href="/guides/workspaces/#workspace-dependencies"
 />
 
+### path
+
+```toml
+[dependencies]
+foo = { path = "/home/user/foo" }
+```
+
+**Path dependencies** contain the following fields:
+
+- `path`: The path to the package on the local filesystem.
+
+Path dependencies are forbidden in published packages.
+
 ## `[peer_dependencies]`
 
 The `[peer_dependencies]` section contains a list of peer dependencies for the
diff --git a/registry/src/endpoints/publish_version.rs b/registry/src/endpoints/publish_version.rs
index 9377938..58c24b9 100644
--- a/registry/src/endpoints/publish_version.rs
+++ b/registry/src/endpoints/publish_version.rs
@@ -348,6 +348,11 @@ pub async fn publish_package(
                         "non-transformed workspace dependency".into(),
                     ));
                 }
+                DependencySpecifiers::Path(_) => {
+                    return Err(Error::InvalidArchive(
+                        "path dependencies are not allowed".into(),
+                    ));
+                }
             }
         }
 
diff --git a/src/cli/auth.rs b/src/cli/auth.rs
index 21c4459..8bdda0b 100644
--- a/src/cli/auth.rs
+++ b/src/cli/auth.rs
@@ -80,7 +80,7 @@ pub async fn set_tokens(tokens: Tokens) -> anyhow::Result<()> {
 
     let mut config = read_config().await?;
     config.tokens = tokens;
-    write_config(&config).await.map_err(Into::into)
+    write_config(&config).await
 }
 
 pub async fn set_token(repo: &gix::Url, token: Option<&str>) -> anyhow::Result<()> {
diff --git a/src/cli/commands/add.rs b/src/cli/commands/add.rs
index c1fff29..3c5082a 100644
--- a/src/cli/commands/add.rs
+++ b/src/cli/commands/add.rs
@@ -11,10 +11,11 @@ use pesde::{
     names::PackageNames,
     source::{
         git::{specifier::GitDependencySpecifier, GitPackageSource},
+        path::{specifier::PathDependencySpecifier, PathPackageSource},
         pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource},
         specifiers::DependencySpecifiers,
         traits::{PackageSource, RefreshOptions, ResolveOptions},
-        workspace::WorkspacePackageSource,
+        workspace::{specifier::WorkspaceDependencySpecifier, WorkspacePackageSource},
         PackageSources,
     },
     Project, RefreshedSources, DEFAULT_INDEX_NAME,
@@ -119,13 +120,15 @@ impl AddCommand {
             ),
             AnyPackageIdentifier::Workspace(VersionedPackageName(name, version)) => (
                 PackageSources::Workspace(WorkspacePackageSource),
-                DependencySpecifiers::Workspace(
-                    pesde::source::workspace::specifier::WorkspaceDependencySpecifier {
-                        name: name.clone(),
-                        version: version.clone().unwrap_or_default(),
-                        target: self.target,
-                    },
-                ),
+                DependencySpecifiers::Workspace(WorkspaceDependencySpecifier {
+                    name: name.clone(),
+                    version: version.clone().unwrap_or_default(),
+                    target: self.target,
+                }),
+            ),
+            AnyPackageIdentifier::Path(path) => (
+                PackageSources::Path(PathPackageSource),
+                DependencySpecifiers::Path(PathDependencySpecifier { path: path.clone() }),
             ),
         };
 
@@ -187,6 +190,10 @@ impl AddCommand {
                 .map(|s| s.to_string())
                 .unwrap_or(url.path.to_string()),
             AnyPackageIdentifier::Workspace(versioned) => versioned.0.as_str().1.to_string(),
+            AnyPackageIdentifier::Path(path) => path
+                .file_name()
+                .map(|s| s.to_string_lossy().to_string())
+                .expect("path has no file name"),
         });
 
         let field = &mut manifest[dependency_key]
@@ -206,17 +213,17 @@ impl AddCommand {
                 }
 
                 println!(
-                    "added {}@{} {} to {}",
+                    "added {}@{} {} to {dependency_key}",
                     spec.name,
                     version_id.version(),
-                    version_id.target(),
-                    dependency_key
+                    version_id.target()
                 );
             }
             #[cfg(feature = "wally-compat")]
             DependencySpecifiers::Wally(spec) => {
-                field["wally"] =
-                    toml_edit::value(spec.name.clone().to_string().trim_start_matches("wally#"));
+                let name_str = spec.name.to_string();
+                let name_str = name_str.trim_start_matches("wally#");
+                field["wally"] = toml_edit::value(name_str);
                 field["version"] = toml_edit::value(format!("^{}", version_id.version()));
 
                 if let Some(index) = spec.index.filter(|i| i != DEFAULT_INDEX_NAME) {
@@ -224,17 +231,15 @@ impl AddCommand {
                 }
 
                 println!(
-                    "added wally {}@{} to {}",
-                    spec.name,
-                    version_id.version(),
-                    dependency_key
+                    "added wally {name_str}@{} to {dependency_key}",
+                    version_id.version()
                 );
             }
             DependencySpecifiers::Git(spec) => {
                 field["repo"] = toml_edit::value(spec.repo.to_bstring().to_string());
                 field["rev"] = toml_edit::value(spec.rev.clone());
 
-                println!("added git {}#{} to {}", spec.repo, spec.rev, dependency_key);
+                println!("added git {}#{} to {dependency_key}", spec.repo, spec.rev);
             }
             DependencySpecifiers::Workspace(spec) => {
                 field["workspace"] = toml_edit::value(spec.name.clone().to_string());
@@ -245,10 +250,15 @@ impl AddCommand {
                 }
 
                 println!(
-                    "added workspace {}@{} to {}",
-                    spec.name, spec.version, dependency_key
+                    "added workspace {}@{} to {dependency_key}",
+                    spec.name, spec.version
                 );
             }
+            DependencySpecifiers::Path(spec) => {
+                field["path"] = toml_edit::value(spec.path.to_string_lossy().to_string());
+
+                println!("added path {} to {dependency_key}", spec.path.display());
+            }
         }
 
         project
diff --git a/src/cli/commands/outdated.rs b/src/cli/commands/outdated.rs
index 3edd7bf..854af5b 100644
--- a/src/cli/commands/outdated.rs
+++ b/src/cli/commands/outdated.rs
@@ -53,7 +53,9 @@ impl OutdatedCommand {
 
                         if matches!(
                             specifier,
-                            DependencySpecifiers::Git(_) | DependencySpecifiers::Workspace(_)
+                            DependencySpecifiers::Git(_)
+                                | DependencySpecifiers::Workspace(_)
+                                | DependencySpecifiers::Path(_)
                         ) {
                             return Ok(true);
                         }
@@ -79,6 +81,7 @@ impl OutdatedCommand {
                                 }
                                 DependencySpecifiers::Git(_) => {}
                                 DependencySpecifiers::Workspace(_) => {}
+                                DependencySpecifiers::Path(_) => {}
                             };
                         }
 
diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs
index a1e0b96..8069f0a 100644
--- a/src/cli/commands/publish.rs
+++ b/src/cli/commands/publish.rs
@@ -471,6 +471,9 @@ info: otherwise, the file was deemed unnecessary, if you don't understand why, p
                         target: Some(spec.target.unwrap_or(manifest.target.kind())),
                     });
                 }
+                DependencySpecifiers::Path(_) => {
+                    anyhow::bail!("path dependencies are not allowed in published packages")
+                }
             }
         }
 
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 4faf068..eb1de0f 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -200,6 +200,7 @@ enum AnyPackageIdentifier<V: FromStr = VersionId, N: FromStr = PackageNames> {
     PackageName(VersionedPackageName<V, N>),
     Url((gix::Url, String)),
     Workspace(VersionedPackageName<VersionTypeOrReq, PackageName>),
+    Path(PathBuf),
 }
 
 impl<V: FromStr<Err = E>, E: Into<anyhow::Error>, N: FromStr<Err = F>, F: Into<anyhow::Error>>
@@ -218,6 +219,8 @@ impl<V: FromStr<Err = E>, E: Into<anyhow::Error>, N: FromStr<Err = F>, F: Into<a
             )))
         } else if let Some(rest) = s.strip_prefix("workspace:") {
             Ok(AnyPackageIdentifier::Workspace(rest.parse()?))
+        } else if let Some(rest) = s.strip_prefix("path:") {
+            Ok(AnyPackageIdentifier::Path(rest.into()))
         } else if s.contains(':') {
             let (url, rev) = s.split_once('#').context("missing revision")?;
 
diff --git a/src/lib.rs b/src/lib.rs
index a30a468..fbdb6d5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -182,8 +182,7 @@ impl Project {
     /// Deserialize the manifest file
     #[instrument(skip(self), ret(level = "trace"), level = "debug")]
     pub async fn deser_manifest(&self) -> Result<Manifest, errors::ManifestReadError> {
-        let string = fs::read_to_string(self.package_dir().join(MANIFEST_FILE_NAME)).await?;
-        Ok(toml::from_str(&string)?)
+        deser_manifest(self.package_dir()).await
     }
 
     /// Write the manifest file
@@ -345,6 +344,11 @@ impl RefreshedSources {
     }
 }
 
+async fn deser_manifest(path: &Path) -> Result<Manifest, errors::ManifestReadError> {
+    let string = fs::read_to_string(path.join(MANIFEST_FILE_NAME)).await?;
+    Ok(toml::from_str(&string)?)
+}
+
 /// Errors that can occur when using the pesde library
 pub mod errors {
     use std::path::PathBuf;
diff --git a/src/resolver.rs b/src/resolver.rs
index 441d3ae..d9da45a 100644
--- a/src/resolver.rs
+++ b/src/resolver.rs
@@ -239,6 +239,9 @@ impl Project {
                     DependencySpecifiers::Workspace(_) => {
                         PackageSources::Workspace(crate::source::workspace::WorkspacePackageSource)
                     }
+                    DependencySpecifiers::Path(_) => {
+                        PackageSources::Path(crate::source::path::PathPackageSource)
+                    }
                 };
 
                 refreshed_sources.refresh(
diff --git a/src/source/git/mod.rs b/src/source/git/mod.rs
index 8ab6890..2ea68f4 100644
--- a/src/source/git/mod.rs
+++ b/src/source/git/mod.rs
@@ -246,6 +246,11 @@ impl PackageSource for GitPackageSource {
                                     path: Some(path),
                                 })
                             }
+                            DependencySpecifiers::Path(_) => {
+                                return Err(errors::ResolveError::Path(Box::new(
+                                    self.repo_url.clone(),
+                                )))
+                            }
                         }
 
                         Ok((alias, (spec, ty)))
@@ -667,6 +672,10 @@ pub mod errors {
         /// No path for a workspace member was found in the lockfile
         #[error("no path found for workspace member {0} {1} in lockfile for repository {2}")]
         NoPathForWorkspaceMember(String, TargetKind, Box<gix::Url>),
+
+        /// The package depends on a path package
+        #[error("the package {0} depends on a path package")]
+        Path(Box<gix::Url>),
     }
 
     /// Errors that can occur when downloading a package from a Git package source
diff --git a/src/source/mod.rs b/src/source/mod.rs
index d6d7fdf..3004807 100644
--- a/src/source/mod.rs
+++ b/src/source/mod.rs
@@ -15,6 +15,8 @@ pub mod fs;
 pub mod git;
 /// Git index-based package source utilities
 pub mod git_index;
+/// The path package source
+pub mod path;
 /// The pesde package source
 pub mod pesde;
 /// Package references
@@ -52,6 +54,8 @@ pub enum PackageSources {
     Git(git::GitPackageSource),
     /// A workspace package source
     Workspace(workspace::WorkspacePackageSource),
+    /// A path package source
+    Path(path::PathPackageSource),
 }
 
 impl PackageSource for PackageSources {
@@ -77,6 +81,7 @@ impl PackageSource for PackageSources {
                 .await
                 .map_err(Self::RefreshError::Git),
             PackageSources::Workspace(source) => source.refresh(options).await.map_err(Into::into),
+            PackageSources::Path(source) => source.refresh(options).await.map_err(Into::into),
         }
     }
 
@@ -147,6 +152,20 @@ impl PackageSource for PackageSources {
                     .map_err(Into::into)
             }
 
+            (PackageSources::Path(source), DependencySpecifiers::Path(specifier)) => source
+                .resolve(specifier, options)
+                .await
+                .map(|(name, results)| {
+                    (
+                        name,
+                        results
+                            .into_iter()
+                            .map(|(version, pkg_ref)| (version, PackageRefs::Path(pkg_ref)))
+                            .collect(),
+                    )
+                })
+                .map_err(Into::into),
+
             _ => Err(errors::ResolveError::Mismatch),
         }
     }
@@ -174,6 +193,10 @@ impl PackageSource for PackageSources {
                 source.download(pkg_ref, options).await.map_err(Into::into)
             }
 
+            (PackageSources::Path(source), PackageRefs::Path(pkg_ref)) => {
+                source.download(pkg_ref, options).await.map_err(Into::into)
+            }
+
             _ => Err(errors::DownloadError::Mismatch),
         }
     }
@@ -203,6 +226,10 @@ pub mod errors {
         /// A workspace package source failed to refresh
         #[error("error refreshing workspace package source")]
         Workspace(#[from] crate::source::workspace::errors::RefreshError),
+
+        /// A path package source failed to refresh
+        #[error("error refreshing path package source")]
+        Path(#[from] crate::source::path::errors::RefreshError),
     }
 
     /// Errors that can occur when resolving a package
@@ -229,6 +256,10 @@ pub mod errors {
         /// A workspace package source failed to resolve
         #[error("error resolving workspace package")]
         Workspace(#[from] crate::source::workspace::errors::ResolveError),
+
+        /// A path package source failed to resolve
+        #[error("error resolving path package")]
+        Path(#[from] crate::source::path::errors::ResolveError),
     }
 
     /// Errors that can occur when downloading a package
@@ -255,5 +286,9 @@ pub mod errors {
         /// A workspace package source failed to download
         #[error("error downloading workspace package")]
         Workspace(#[from] crate::source::workspace::errors::DownloadError),
+
+        /// A path package source failed to download
+        #[error("error downloading path package")]
+        Path(#[from] crate::source::path::errors::DownloadError),
     }
 }
diff --git a/src/source/path/mod.rs b/src/source/path/mod.rs
new file mode 100644
index 0000000..d238131
--- /dev/null
+++ b/src/source/path/mod.rs
@@ -0,0 +1,152 @@
+use crate::{
+    deser_manifest,
+    manifest::target::Target,
+    names::PackageNames,
+    reporters::DownloadProgressReporter,
+    source::{
+        fs::PackageFS,
+        path::pkg_ref::PathPackageRef,
+        specifiers::DependencySpecifiers,
+        traits::{DownloadOptions, PackageSource, ResolveOptions},
+        version_id::VersionId,
+        ResolveResult,
+    },
+    DEFAULT_INDEX_NAME,
+};
+use std::collections::BTreeMap;
+use tracing::instrument;
+
+/// The path package reference
+pub mod pkg_ref;
+/// The path dependency specifier
+pub mod specifier;
+
+/// The path package source
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct PathPackageSource;
+
+impl PackageSource for PathPackageSource {
+    type Specifier = specifier::PathDependencySpecifier;
+    type Ref = PathPackageRef;
+    type RefreshError = errors::RefreshError;
+    type ResolveError = errors::ResolveError;
+    type DownloadError = errors::DownloadError;
+
+    #[instrument(skip_all, level = "debug")]
+    async fn resolve(
+        &self,
+        specifier: &Self::Specifier,
+        _options: &ResolveOptions,
+    ) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> {
+        let manifest = deser_manifest(&specifier.path).await?;
+
+        let pkg_ref = PathPackageRef {
+            path: specifier.path.clone(),
+            dependencies: manifest
+                .all_dependencies()?
+                .into_iter()
+                .map(|(alias, (mut spec, ty))| {
+                    match &mut spec {
+                        DependencySpecifiers::Pesde(spec) => {
+                            let index_name = spec.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME);
+
+                            spec.index = Some(
+                                manifest
+                                    .indices
+                                    .get(index_name)
+                                    .ok_or_else(|| {
+                                        errors::ResolveError::IndexNotFound(
+                                            index_name.to_string(),
+                                            specifier.path.clone(),
+                                        )
+                                    })?
+                                    .to_string(),
+                            )
+                        }
+                        #[cfg(feature = "wally-compat")]
+                        DependencySpecifiers::Wally(spec) => {
+                            let index_name = spec.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME);
+
+                            spec.index = Some(
+                                manifest
+                                    .wally_indices
+                                    .get(index_name)
+                                    .ok_or_else(|| {
+                                        errors::ResolveError::IndexNotFound(
+                                            index_name.to_string(),
+                                            specifier.path.clone(),
+                                        )
+                                    })?
+                                    .to_string(),
+                            )
+                        }
+                        DependencySpecifiers::Git(_) => {}
+                        DependencySpecifiers::Workspace(_) => {}
+                        DependencySpecifiers::Path(_) => {}
+                    }
+
+                    Ok((alias, (spec, ty)))
+                })
+                .collect::<Result<_, errors::ResolveError>>()?,
+        };
+
+        Ok((
+            PackageNames::Pesde(manifest.name),
+            BTreeMap::from([(
+                VersionId::new(manifest.version, manifest.target.kind()),
+                pkg_ref,
+            )]),
+        ))
+    }
+
+    #[instrument(skip_all, level = "debug")]
+    async fn download<R: DownloadProgressReporter>(
+        &self,
+        pkg_ref: &Self::Ref,
+        _options: &DownloadOptions<R>,
+    ) -> Result<(PackageFS, Target), Self::DownloadError> {
+        let manifest = deser_manifest(&pkg_ref.path).await?;
+
+        Ok((
+            PackageFS::Copy(pkg_ref.path.clone(), manifest.target.kind()),
+            manifest.target,
+        ))
+    }
+}
+
+/// Errors that can occur when using a path package source
+pub mod errors {
+    use std::path::PathBuf;
+    use thiserror::Error;
+
+    /// Errors that can occur when refreshing the path package source
+    #[derive(Debug, Error)]
+    #[non_exhaustive]
+    pub enum RefreshError {}
+
+    /// Errors that can occur when resolving a path package
+    #[derive(Debug, Error)]
+    #[non_exhaustive]
+    pub enum ResolveError {
+        /// Reading the manifest failed
+        #[error("error reading manifest")]
+        ManifestRead(#[from] crate::errors::ManifestReadError),
+
+        /// An error occurred getting all dependencies
+        #[error("failed to get all dependencies")]
+        AllDependencies(#[from] crate::manifest::errors::AllDependenciesError),
+
+        /// An index of the package was not found
+        #[error("index {0} not found in package {1}")]
+        IndexNotFound(String, PathBuf),
+    }
+
+    /// Errors that can occur when downloading a path package
+    #[derive(Debug, Error)]
+    #[non_exhaustive]
+    pub enum DownloadError {
+        /// Reading the manifest failed
+        #[error("error reading manifest")]
+        ManifestRead(#[from] crate::errors::ManifestReadError),
+    }
+}
diff --git a/src/source/path/pkg_ref.rs b/src/source/path/pkg_ref.rs
new file mode 100644
index 0000000..643783a
--- /dev/null
+++ b/src/source/path/pkg_ref.rs
@@ -0,0 +1,29 @@
+use crate::{
+    manifest::DependencyType,
+    source::{path::PathPackageSource, DependencySpecifiers, PackageRef, PackageSources},
+};
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeMap, path::PathBuf};
+
+/// A path package reference
+#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
+pub struct PathPackageRef {
+    /// The path of the package
+    pub path: PathBuf,
+    /// The dependencies of the package
+    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+    pub dependencies: BTreeMap<String, (DependencySpecifiers, DependencyType)>,
+}
+impl PackageRef for PathPackageRef {
+    fn dependencies(&self) -> &BTreeMap<String, (DependencySpecifiers, DependencyType)> {
+        &self.dependencies
+    }
+
+    fn use_new_structure(&self) -> bool {
+        true
+    }
+
+    fn source(&self) -> PackageSources {
+        PackageSources::Path(PathPackageSource)
+    }
+}
diff --git a/src/source/path/specifier.rs b/src/source/path/specifier.rs
new file mode 100644
index 0000000..bfb6263
--- /dev/null
+++ b/src/source/path/specifier.rs
@@ -0,0 +1,17 @@
+use crate::source::DependencySpecifier;
+use serde::{Deserialize, Serialize};
+use std::{fmt::Display, path::PathBuf};
+
+/// The specifier for a path dependency
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
+pub struct PathDependencySpecifier {
+    /// The path to the package
+    pub path: PathBuf,
+}
+impl DependencySpecifier for PathDependencySpecifier {}
+
+impl Display for PathDependencySpecifier {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "path:{}", self.path.display())
+    }
+}
diff --git a/src/source/refs.rs b/src/source/refs.rs
index 474ab11..8b268c9 100644
--- a/src/source/refs.rs
+++ b/src/source/refs.rs
@@ -18,6 +18,8 @@ pub enum PackageRefs {
     Git(crate::source::git::pkg_ref::GitPackageRef),
     /// A workspace package reference
     Workspace(crate::source::workspace::pkg_ref::WorkspacePackageRef),
+    /// A path package reference
+    Path(crate::source::path::pkg_ref::PathPackageRef),
 }
 
 impl PackageRefs {
@@ -40,6 +42,7 @@ impl PackageRef for PackageRefs {
             PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(),
             PackageRefs::Git(pkg_ref) => pkg_ref.dependencies(),
             PackageRefs::Workspace(pkg_ref) => pkg_ref.dependencies(),
+            PackageRefs::Path(pkg_ref) => pkg_ref.dependencies(),
         }
     }
 
@@ -50,6 +53,7 @@ impl PackageRef for PackageRefs {
             PackageRefs::Wally(pkg_ref) => pkg_ref.use_new_structure(),
             PackageRefs::Git(pkg_ref) => pkg_ref.use_new_structure(),
             PackageRefs::Workspace(pkg_ref) => pkg_ref.use_new_structure(),
+            PackageRefs::Path(pkg_ref) => pkg_ref.use_new_structure(),
         }
     }
 
@@ -60,6 +64,7 @@ impl PackageRef for PackageRefs {
             PackageRefs::Wally(pkg_ref) => pkg_ref.source(),
             PackageRefs::Git(pkg_ref) => pkg_ref.source(),
             PackageRefs::Workspace(pkg_ref) => pkg_ref.source(),
+            PackageRefs::Path(pkg_ref) => pkg_ref.source(),
         }
     }
 }
diff --git a/src/source/specifiers.rs b/src/source/specifiers.rs
index 9f7a603..ddea8b8 100644
--- a/src/source/specifiers.rs
+++ b/src/source/specifiers.rs
@@ -15,6 +15,8 @@ pub enum DependencySpecifiers {
     Git(crate::source::git::specifier::GitDependencySpecifier),
     /// A workspace dependency specifier
     Workspace(crate::source::workspace::specifier::WorkspaceDependencySpecifier),
+    /// A path dependency specifier
+    Path(crate::source::path::specifier::PathDependencySpecifier),
 }
 impl DependencySpecifier for DependencySpecifiers {}
 
@@ -26,6 +28,7 @@ impl Display for DependencySpecifiers {
             DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"),
             DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"),
             DependencySpecifiers::Workspace(specifier) => write!(f, "{specifier}"),
+            DependencySpecifiers::Path(specifier) => write!(f, "{specifier}"),
         }
     }
 }
diff --git a/src/source/workspace/mod.rs b/src/source/workspace/mod.rs
index 550a5b6..97b4e1c 100644
--- a/src/source/workspace/mod.rs
+++ b/src/source/workspace/mod.rs
@@ -110,6 +110,7 @@ impl PackageSource for WorkspacePackageSource {
                         }
                         DependencySpecifiers::Git(_) => {}
                         DependencySpecifiers::Workspace(_) => {}
+                        DependencySpecifiers::Path(_) => {}
                     }
 
                     Ok((alias, (spec, ty)))