Skip to content

Commit

Permalink
chore: pkg crate init and dependency feature
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelramos committed Nov 19, 2024
1 parent b27601d commit 0735c23
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 1 deletion.
57 changes: 57 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/standard", "crates/git"]
members = ["crates/standard", "crates/git", "crates/pkg"]
resolver = "2"

[workspace.package]
Expand Down
26 changes: 26 additions & 0 deletions crates/pkg/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "ws-pkg"
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lib]
doctest = false

[lints]
workspace = true

[dependencies]
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"] }
petgraph = "0.6.5"
semver = { version = "1.0.23", features = ["serde"] }
145 changes: 145 additions & 0 deletions crates/pkg/src/dependency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use petgraph::{stable_graph::StableDiGraph, Direction};
use std::fmt::Display;

/// Must be implemented by the type you wish
pub trait Node {
/// Encodes a dependency relationship. In a Package Manager dependency graph for instance, this might be a (package name, version) tuple.
/// It might also just be the exact same type as the one that implements the Node trait, in which case `Node::matches` can be implemented through simple equality.
type DependencyType;

/// Returns a slice of dependencies for this Node
fn dependencies(&self) -> &[Self::DependencyType];

/// Returns true if the `dependency` can be met by us.
fn matches(&self, dependency: &Self::DependencyType) -> bool;
}

/// Wrapper around dependency graph nodes.
/// Since a graph might have dependencies that cannot be resolved internally,
/// this wrapper is necessary to differentiate between internally resolved and
/// externally (unresolved) dependencies.
/// An Unresolved dependency does not necessarily mean that it *cannot* be resolved,
/// only that no Node within the graph fulfills it.
#[derive(Debug, Clone)]
pub enum Step<'a, N: Node> {
Resolved(&'a N),
Unresolved(&'a N::DependencyType),
}

impl<'a, N: Node> Step<'a, N> {
pub fn is_resolved(&self) -> bool {
match self {
Step::Resolved(_) => true,
Step::Unresolved(_) => false,
}
}

pub fn as_resolved(&self) -> Option<&N> {
match self {
Step::Resolved(node) => Some(node),
Step::Unresolved(_) => None,
}
}

pub fn as_unresolved(&self) -> Option<&N::DependencyType> {
match self {
Step::Resolved(_) => None,
Step::Unresolved(dependency) => Some(dependency),
}
}
}

impl<'a, N: Node> Display for Step<'a, N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Step::Resolved(_node) => write!(f, "Resolved"),
Step::Unresolved(_dependency) => write!(f, "Unresolved"),
}
}
}

/// The [`DependencyGraph`] structure builds an internal [Directed Graph](`petgraph::stable_graph::StableDiGraph`), which can then be traversed
/// in an order which ensures that dependent Nodes are visited before their parents.
#[derive(Debug, Clone)]
pub struct DependencyGraph<'a, N: Node> {
pub graph: StableDiGraph<Step<'a, N>, &'a N::DependencyType>,
}

/// The only way to build a [`DependencyGraph`] is from a slice of objects implementing [`Node`].
/// The graph references the original items, meaning the objects cannot be modified while
/// the [`DependencyGraph`] holds a reference to them.
impl<'a, N> From<&'a [N]> for DependencyGraph<'a, N>
where
N: Node,
{
fn from(nodes: &'a [N]) -> Self {
let mut graph = StableDiGraph::<Step<'a, N>, &'a N::DependencyType>::new();

// Insert the input nodes into the graph, and record their positions.
// We'll be adding the edges next, and filling in any unresolved
// steps we find along the way.
let nodes: Vec<(_, _)> =
nodes.iter().map(|node| (node, graph.add_node(Step::Resolved(node)))).collect();

for (node, index) in &nodes {
for dependency in node.dependencies() {
// Check to see if we can resolve this dependency internally.
if let Some((_, dependent)) = nodes.iter().find(|(dep, _)| dep.matches(dependency))
{
// If we can, just add an edge between the two nodes.
graph.add_edge(*index, *dependent, dependency);
} else {
// If not, create a new "Unresolved" node, and create an edge to that.
let unresolved = graph.add_node(Step::Unresolved(dependency));
graph.add_edge(*index, unresolved, dependency);
}
}
}

Self { graph }
}
}

impl<'a, N> DependencyGraph<'a, N>
where
N: Node,
{
/// True if all graph [`Node`]s have only references to other internal [`Node`]s.
/// That is, there are no unresolved dependencies between nodes.
pub fn is_internally_resolvable(&self) -> bool {
self.graph.node_weights().all(Step::is_resolved)
}

/// Get an iterator over unresolved dependencies, without traversing the whole graph.
/// Useful for doing pre-validation or pre-fetching of external dependencies before
/// starting to resolve internal dependencies.
pub fn unresolved_dependencies(&self) -> impl Iterator<Item = &N::DependencyType> {
self.graph.node_weights().filter_map(Step::as_unresolved)
}

pub fn resolved_dependencies(&self) -> impl Iterator<Item = &N> {
self.graph.node_weights().filter_map(Step::as_resolved)
}
}

/// Iterate over the DependencyGraph in an order which ensures dependencies are resolved before each Node is visited.
/// Note: If a `Step::Unresolved` node is returned, it is the caller's responsibility to ensure the dependency is resolved
/// before continuing.
impl<'a, N> Iterator for DependencyGraph<'a, N>
where
N: Node,
{
type Item = Step<'a, N>;

fn next(&mut self) -> Option<Self::Item> {
// Returns the first node, which does not have any Outgoing
// edges, which means it is terminal.
for index in self.graph.node_indices().rev() {
if self.graph.neighbors_directed(index, Direction::Outgoing).count() == 0 {
return self.graph.remove_node(index);
}
}

None
}
}
1 change: 1 addition & 0 deletions crates/pkg/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod dependency;
Loading

0 comments on commit 0735c23

Please sign in to comment.