1use camino::{Utf8Path, Utf8PathBuf};
2use semifold_core::{
3 DependencyKind, EcosystemId, FileEdit, PackageId, PackageSnapshot, VersionMap, VersionSource,
4};
5use semver::Version;
6use std::path::PathBuf;
7
8use crate::error::ResolveError;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct PackageLocation {
13 pub id: PackageId,
14 pub project_root: Utf8PathBuf,
15 pub path: Utf8PathBuf,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct ManifestDependency {
21 pub manifest_name: String,
22 pub kind: DependencyKind,
23 pub requirement: Option<String>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub(crate) struct ParsedPackage {
29 pub name: String,
30 pub version: Version,
31 pub version_source: VersionSource,
32 pub path: PathBuf,
33 pub private: bool,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct PackageInspection {
39 pub id: PackageId,
40 pub manifest_name: String,
41 pub version: Version,
42 pub version_source: VersionSource,
43 pub ecosystem: EcosystemId,
44 pub path: Utf8PathBuf,
45 pub publishable: bool,
46 pub dependencies: Vec<ManifestDependency>,
47}
48
49#[derive(Clone, Copy, Debug)]
51pub struct EcosystemPlanInput<'input> {
52 pub project_root: &'input Utf8Path,
53 pub workspace_packages: &'input [PackageSnapshot],
54 pub released_packages: &'input [PackageId],
55 pub versions: &'input VersionMap,
56}
57
58pub trait EcosystemAdapter: Send + Sync {
60 fn ecosystem(&self) -> EcosystemId;
61
62 fn encode_version(&self, version: &Version) -> Result<String, AdapterError>;
64
65 fn discover(&self, root: &Utf8Path) -> Result<Vec<PackageInspection>, AdapterError>;
66
67 fn inspect(&self, package: &PackageLocation) -> Result<PackageInspection, AdapterError>;
68
69 fn plan_edits(&self, input: EcosystemPlanInput<'_>) -> Result<Vec<FileEdit>, AdapterError>;
70}
71
72#[derive(Debug, thiserror::Error)]
74pub enum AdapterError {
75 #[error(transparent)]
76 Manifest(#[from] ResolveError),
77 #[error(transparent)]
78 Plugin(#[from] crate::plugin::adapter::PluginAdapterError),
79 #[error("invalid adapter input: {reason}")]
80 InvalidInput { reason: String },
81 #[error(
82 "{ecosystem_name} cannot encode version {version}: {reason}",
83 ecosystem_name = .ecosystem.display_name()
84 )]
85 InvalidVersion {
86 ecosystem: EcosystemId,
87 version: Version,
88 reason: String,
89 },
90}
91
92#[cfg(test)]
93mod tests {
94 use std::collections::BTreeMap;
95
96 use semver::Version;
97
98 use super::*;
99
100 struct ContractAdapter;
101
102 impl EcosystemAdapter for ContractAdapter {
103 fn ecosystem(&self) -> EcosystemId {
104 EcosystemId::NODE
105 }
106
107 fn encode_version(&self, version: &Version) -> Result<String, AdapterError> {
108 Ok(version.to_string())
109 }
110
111 fn discover(&self, _root: &Utf8Path) -> Result<Vec<PackageInspection>, AdapterError> {
112 Ok(Vec::new())
113 }
114
115 fn inspect(&self, package: &PackageLocation) -> Result<PackageInspection, AdapterError> {
116 Ok(PackageInspection {
117 id: package.id.clone(),
118 manifest_name: "example".to_string(),
119 version: Version::new(1, 0, 0),
120 version_source: VersionSource::PackageManifest,
121 ecosystem: self.ecosystem(),
122 path: package.path.clone(),
123 publishable: true,
124 dependencies: Vec::new(),
125 })
126 }
127
128 fn plan_edits(&self, input: EcosystemPlanInput<'_>) -> Result<Vec<FileEdit>, AdapterError> {
129 if input
130 .released_packages
131 .iter()
132 .all(|package| input.versions.contains_key(package))
133 {
134 Ok(Vec::new())
135 } else {
136 Err(AdapterError::InvalidInput {
137 reason: "released package is missing from VersionMap".to_string(),
138 })
139 }
140 }
141 }
142
143 #[test]
144 fn adapter_contract_is_object_safe_and_receives_complete_plan_input() {
145 let adapter: Box<dyn EcosystemAdapter> = Box::new(ContractAdapter);
146 let location = PackageLocation {
147 id: PackageId::new("configured-id"),
148 project_root: "/project".into(),
149 path: "packages/example".into(),
150 };
151 let inspection = adapter.inspect(&location).unwrap();
152 let snapshot = PackageSnapshot {
153 id: inspection.id.clone(),
154 manifest_name: inspection.manifest_name.clone(),
155 version: inspection.version.clone(),
156 version_source: inspection.version_source.clone(),
157 ecosystem: inspection.ecosystem,
158 path: inspection.path.clone(),
159 publishable: inspection.publishable,
160 dependencies: Vec::new(),
161 };
162 let versions = BTreeMap::from([(PackageId::new("configured-id"), Version::new(1, 0, 1))]);
163
164 assert_eq!(inspection.id, PackageId::new("configured-id"));
165 assert_eq!(inspection.path, "packages/example");
166 assert!(
167 adapter
168 .plan_edits(EcosystemPlanInput {
169 project_root: &location.project_root,
170 workspace_packages: std::slice::from_ref(&snapshot),
171 released_packages: std::slice::from_ref(&snapshot.id),
172 versions: &versions,
173 })
174 .unwrap()
175 .is_empty()
176 );
177 }
178}