assemble_core/dependencies/
project_dependency.rs

1//! Provides the project dependency trait for dependency containers
2
3use crate::dependencies::{
4    AcquisitionError, Dependency, DependencyType, Registry, ResolvedDependency,
5    ResolvedDependencyBuilder,
6};
7use crate::flow::shared::Artifact;
8use crate::identifier::{Id, InvalidId};
9use crate::plugins::Plugin;
10use crate::prelude::ProjectId;
11use crate::project::buildable::{Buildable, BuildableObject, GetBuildable};
12use crate::project::error::ProjectResult;
13use crate::project::GetProjectId;
14use crate::resources::{ProjectResourceExt, ResourceLocation};
15use crate::Project;
16use crate::__export::TaskId;
17use crate::plugins::PluginAware;
18
19use itertools::Itertools;
20use once_cell::sync::Lazy;
21use regex::Regex;
22use std::collections::HashSet;
23use std::fmt::Debug;
24use std::path::Path;
25
26use crate::project::shared::SharedProject;
27use url::Url;
28
29/// Get access to project dependencies
30pub trait CreateProjectDependencies {
31    /// Creates an inter-project dependency with the default configuration
32    fn project<S: AsRef<str>>(&self, path: S) -> ProjectDependency;
33    /// Creates an inter-project dependency with a given configuration
34    fn project_with<P: AsRef<str>, C: AsRef<str>>(&self, path: P, config: C) -> ProjectDependency;
35}
36
37impl CreateProjectDependencies for Project {
38    fn project<S: AsRef<str>>(&self, path: S) -> ProjectDependency {
39        ProjectDependency {
40            parent: self.as_shared(),
41            location: ResourceLocation::find(self.id(), path.as_ref(), None)
42                .expect("no project found"),
43        }
44    }
45
46    fn project_with<P: AsRef<str>, C: AsRef<str>>(&self, path: P, config: C) -> ProjectDependency {
47        ProjectDependency {
48            parent: self.as_shared(),
49            location: ResourceLocation::find(self.id(), path.as_ref(), config.as_ref())
50                .expect("no project found"),
51        }
52    }
53}
54
55impl CreateProjectDependencies for SharedProject {
56    fn project<S: AsRef<str>>(&self, path: S) -> ProjectDependency {
57        ProjectDependency {
58            parent: self.clone(),
59            location: ResourceLocation::find(&self.project_id(), path.as_ref(), None)
60                .expect("no project found"),
61        }
62    }
63
64    fn project_with<P: AsRef<str>, C: AsRef<str>>(&self, path: P, config: C) -> ProjectDependency {
65        ProjectDependency {
66            parent: self.clone(),
67            location: ResourceLocation::find(&self.project_id(), path.as_ref(), config.as_ref())
68                .expect("no project found"),
69        }
70    }
71}
72
73#[derive(Debug)]
74pub struct ProjectDependency {
75    parent: SharedProject,
76    location: ResourceLocation,
77}
78
79impl Buildable for ProjectDependency {
80    fn get_dependencies(&self, project: &Project) -> ProjectResult<HashSet<TaskId>> {
81        let location = self.location.clone();
82        self.parent.with(|p| {
83            let resource = p
84                .get_resource(location)
85                .map_err(|e| AcquisitionError::custom(e.to_string()))
86                .unwrap();
87
88            match resource.buildable() {
89                None => Ok(HashSet::new()),
90                Some(buildable) => buildable.get_dependencies(project),
91            }
92        })
93    }
94}
95
96impl GetBuildable for ProjectDependency {
97    fn as_buildable(&self) -> BuildableObject {
98        let location = self.location.clone();
99        self.parent.with(|p| {
100            let resource = p
101                .get_resource(location)
102                .map_err(|e| AcquisitionError::custom(e.to_string()))
103                .unwrap();
104
105            match resource.buildable() {
106                None => BuildableObject::None,
107                Some(buildable) => BuildableObject::from(buildable),
108            }
109        })
110    }
111}
112
113impl Dependency for ProjectDependency {
114    fn id(&self) -> String {
115        format!("{}", self.location.project())
116    }
117
118    fn dep_type(&self) -> DependencyType {
119        PROJECT_DEPENDENCY_TYPE.clone()
120    }
121
122    fn try_resolve(
123        &self,
124        _: &dyn Registry,
125        _: &Path,
126    ) -> Result<ResolvedDependency, AcquisitionError> {
127        let location = self.location.clone();
128        self.parent.with(|p| {
129            let resource = p
130                .get_resource(location)
131                .map_err(|e| AcquisitionError::custom(e.to_string()))?;
132
133            Ok(ResolvedDependencyBuilder::new(resource).finish())
134        })
135    }
136
137    // fn maybe_buildable(&self) -> Option<Box<dyn Buildable>> {
138    //     let location = self.location.clone();
139    //     self.parent.with(|p| {
140    //         let resource = p
141    //             .get_resource(location)
142    //             .map_err(|e| AcquisitionError::custom(e.to_string()))
143    //             .unwrap();
144    //
145    //         resource.buildable()
146    //     })
147    // }
148}
149
150/// The dependency type of project outgoing variants
151pub static PROJECT_DEPENDENCY_TYPE: Lazy<DependencyType> =
152    Lazy::new(|| DependencyType::new("project", "project_variant_artifact", vec!["*"]));
153
154/// Allows using projects to resolve project dependencies
155pub struct ProjectRegistry;
156
157impl ProjectRegistry {
158    fn new() -> Self {
159        Self
160    }
161}
162
163impl Registry for ProjectRegistry {
164    fn url(&self) -> Url {
165        Url::parse("https://localhost:80/").unwrap()
166    }
167
168    fn supported(&self) -> Vec<DependencyType> {
169        vec![PROJECT_DEPENDENCY_TYPE.clone()]
170    }
171}
172
173#[derive(Debug, Default)]
174pub struct ProjectDependencyPlugin;
175
176impl Plugin<Project> for ProjectDependencyPlugin {
177    fn apply_to(&self, project: &mut Project) -> ProjectResult {
178        for sub in project.subprojects() {
179            sub.apply_plugin::<Self>()?;
180        }
181        project.registries_mut(|reg| {
182            reg.add_registry(ProjectRegistry::new());
183            Ok(())
184        })?;
185        Ok(())
186    }
187}
188
189pub static PROJECT_SCHEME: &str = "assemble";
190
191pub fn project_url<P: GetProjectId>(project: &P) -> Url {
192    let id = project.project_id();
193    _project_url(id)
194}
195
196fn _project_url(id: ProjectId) -> Url {
197    let project_as_path = id.iter().join("/");
198    let host = "project.assemble.rs";
199    Url::parse(&format!(
200        "{scheme}://{host}/{path}/",
201        scheme = PROJECT_SCHEME,
202        path = project_as_path
203    ))
204    .unwrap()
205}
206
207pub fn subproject_url<P: GetProjectId>(
208    base_project: &P,
209    path: &str,
210    configuration: impl Into<Option<String>>,
211) -> Result<Url, ProjectUrlError> {
212    static PROJECT_LOCATOR_REGEX: Lazy<Regex> =
213        Lazy::new(|| Regex::new(r"(:{0,2})([a-zA-Z]\w*)").unwrap());
214
215    let project_ptr = if path.is_empty() {
216        base_project.project_id()
217    } else {
218        let mut project_ptr: Option<Id> = None;
219
220        for captures in PROJECT_LOCATOR_REGEX.captures_iter(path) {
221            let mechanism = &captures[1];
222            let id = &captures[2];
223
224            match mechanism {
225                ":" => match project_ptr {
226                    None => {
227                        project_ptr = Some(Id::new(id)?);
228                    }
229                    Some(s) => {
230                        project_ptr = Some(s.join(id)?);
231                    }
232                },
233                "::" => match project_ptr {
234                    None => {
235                        project_ptr = Some(
236                            base_project
237                                .parent_id()
238                                .ok_or(InvalidId::new("No parent id"))
239                                .and_then(|parent| parent.join(id))?,
240                        )
241                    }
242                    Some(s) => {
243                        project_ptr = Some(
244                            s.parent()
245                                .ok_or(InvalidId::new("No parent id"))
246                                .and_then(|parent| parent.join(id))?,
247                        )
248                    }
249                },
250                "" => {
251                    match project_ptr {
252                        None => project_ptr = Some(base_project.project_id().join(id)?),
253                        Some(_) => {
254                            panic!("Shouldn't be possible to access a non :: or : access after the first")
255                        }
256                    }
257                }
258                s => {
259                    panic!("{:?} should not be matchable", s)
260                }
261            }
262        }
263        project_ptr.unwrap().into()
264    };
265
266    let output: Url = project_url(&project_ptr);
267
268    if let Some(configuration) = configuration.into() {
269        Ok(output.join(&configuration)?)
270    } else {
271        Ok(output)
272    }
273}
274
275impl GetProjectId for Url {
276    fn project_id(&self) -> ProjectId {
277        if self.scheme() != PROJECT_SCHEME {
278            panic!("only assemble: scheme supported for project id urls")
279        }
280        ProjectId::from_path(self.path()).expect("url is not valid project id")
281    }
282
283    fn parent_id(&self) -> Option<ProjectId> {
284        self.project_id().parent().cloned().map(ProjectId::from)
285    }
286
287    fn root_id(&self) -> ProjectId {
288        let id = self.project_id();
289        id.ancestors().last().cloned().map(ProjectId::from).unwrap()
290    }
291}
292
293#[derive(Debug, thiserror::Error)]
294pub enum ProjectUrlError {
295    #[error("No parent project to resolve non-absolute project path (path = {0:?})")]
296    NoParentProject(String),
297    #[error(transparent)]
298    ParseUrlError(#[from] url::ParseError),
299    #[error(transparent)]
300    InvalidId(#[from] InvalidId),
301    #[error("No project was found")]
302    NoProjectFound,
303}
304
305impl GetProjectId for ProjectId {
306    fn project_id(&self) -> ProjectId {
307        self.clone()
308    }
309
310    fn parent_id(&self) -> Option<ProjectId> {
311        self.parent().cloned().map(ProjectId::from)
312    }
313
314    fn root_id(&self) -> ProjectId {
315        self.ancestors()
316            .last()
317            .cloned()
318            .map(ProjectId::from)
319            .unwrap()
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use std::str::FromStr;
327
328    #[test]
329    fn create_project_url() {
330        let project = Project::temp("root");
331        let url = project_url(&project);
332        println!("url = {}", url);
333
334        assert_eq!(url.scheme(), PROJECT_SCHEME);
335        assert_eq!(url.path(), "/root/");
336    }
337
338    #[test]
339    fn url_as_assemble_project() {
340        let child1 = ProjectId::from_str(":root:child1").unwrap();
341        let _child2 = ProjectId::from_str(":root:child2").unwrap();
342
343        let url = subproject_url(&child1, ":root:child1::child2", None).unwrap();
344
345        println!("url = {}", url);
346    }
347}