assemble_core/
resources.rs

1use crate::dependencies::project_dependency::{subproject_url, ProjectUrlError, PROJECT_SCHEME};
2use crate::flow::shared::Artifact;
3use crate::identifier::{InvalidId, ProjectId};
4use crate::project::{GetProjectId, VisitProject};
5use crate::Project;
6use crate::__export::TaskId;
7use crate::lazy_evaluation::Provider;
8use crate::prelude::ProjectResult;
9use crate::project::buildable::Buildable;
10
11use crate::error::PayloadError;
12use std::collections::HashSet;
13use std::path::PathBuf;
14use thiserror::Error;
15use url::Url;
16
17/// A resource location in assemble
18#[derive(Debug, Clone, Eq, PartialEq, Hash)]
19pub struct ResourceLocation {
20    project: ProjectId,
21    configuration: Option<String>,
22}
23
24impl ResourceLocation {
25    pub fn new<'a, I>(project: ProjectId, configuration: I) -> Self
26    where
27        I: Into<Option<&'a str>>,
28    {
29        Self {
30            project,
31            configuration: configuration.into().map(|s| s.to_string()),
32        }
33    }
34
35    pub fn find<'a, P, I>(
36        project: &P,
37        path: &str,
38        configuration: I,
39    ) -> Result<Self, InvalidResourceLocation>
40    where
41        P: GetProjectId,
42        I: Into<Option<&'a str>>,
43    {
44        let url = subproject_url(project, path, configuration.into().map(str::to_string))?;
45        Self::try_from(url)
46    }
47
48    pub fn project(&self) -> &ProjectId {
49        &self.project
50    }
51
52    pub fn configuration(&self) -> Option<&str> {
53        self.configuration.as_deref()
54    }
55}
56
57impl From<ResourceLocation> for Url {
58    fn from(r: ResourceLocation) -> Self {
59        subproject_url(&r.project, "", r.configuration).unwrap()
60    }
61}
62
63impl TryFrom<Url> for ResourceLocation {
64    type Error = InvalidResourceLocation;
65
66    fn try_from(value: Url) -> Result<Self, Self::Error> {
67        if value.scheme() != PROJECT_SCHEME {
68            return Err(InvalidResourceLocation::BadSchema(
69                value.scheme().to_string(),
70            ));
71        }
72
73        let path = value.path();
74        if path.ends_with('/') {
75            // use default configuration
76            let path = PathBuf::from(path);
77            let id = ProjectId::try_from(path.as_path())?;
78
79            Ok(Self::new(id, None))
80        } else {
81            // last element is configuration
82            let path = PathBuf::from(path);
83            let configuration = path.file_name().and_then(|os| os.to_str()).unwrap();
84            let project = ProjectId::try_from(path.parent().unwrap())?;
85
86            Ok(Self::new(project, configuration))
87        }
88    }
89}
90
91#[derive(Debug, Error)]
92pub enum InvalidResourceLocation {
93    #[error(
94        "Unexpected schema found, must be {:?} (found = {0:?})",
95        PROJECT_SCHEME
96    )]
97    BadSchema(String),
98    #[error(transparent)]
99    InvalidId(#[from] InvalidId),
100    #[error(transparent)]
101    ProjectUrlError(#[from] ProjectUrlError),
102    #[error("No resources could be found")]
103    NoResourceFound,
104}
105
106/// A project visitor that tries to find a resource
107pub struct ResourceLocator {
108    location: ResourceLocation,
109}
110
111impl ResourceLocator {
112    pub fn new(location: ResourceLocation) -> Self {
113        Self { location }
114    }
115}
116
117impl VisitProject<Option<Box<dyn Artifact>>> for ResourceLocator {
118    fn visit(&mut self, project: &Project) -> Option<Box<dyn Artifact>> {
119        let mut project_ptr = project.root_project();
120
121        for part in self.location.project.iter().skip(1) {
122            project_ptr = project_ptr.with(|p| p.get_subproject(part).ok().cloned())?;
123        }
124
125        let artifact = project_ptr.with(|p| {
126            let configuration = self
127                .location
128                .configuration
129                .as_ref()
130                .cloned()
131                .unwrap_or(p.variants().default());
132
133            p.variant(&configuration)
134        })?;
135        Some(Box::new(artifact.get()))
136    }
137}
138
139pub trait ProjectResourceExt {
140    /// Try to get a resource from a project.
141    fn get_resource<R>(
142        &self,
143        resource: R,
144    ) -> Result<Box<dyn Artifact>, PayloadError<InvalidResourceLocation>>
145    where
146        R: TryInto<ResourceLocation>;
147}
148
149impl ProjectResourceExt for Project {
150    fn get_resource<R>(
151        &self,
152        resource: R,
153    ) -> Result<Box<dyn Artifact>, PayloadError<InvalidResourceLocation>>
154    where
155        R: TryInto<ResourceLocation>,
156    {
157        let location = resource
158            .try_into()
159            .map_err(|_| InvalidResourceLocation::NoResourceFound)
160            .map_err(PayloadError::new)?;
161        let mut visitor = ResourceLocator::new(location);
162        self.visitor(&mut visitor)
163            .ok_or(InvalidResourceLocation::NoResourceFound)
164            .map_err(PayloadError::new)
165    }
166}
167
168impl Buildable for ResourceLocation {
169    fn get_dependencies(&self, project: &Project) -> ProjectResult<HashSet<TaskId>> {
170        let resource = project.get_resource(self.clone()).map_err(PayloadError::into)?;
171        match resource.buildable() {
172            None => Ok(HashSet::new()),
173            Some(b) => b.get_dependencies(project),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::prelude::ProjectId;
182    use crate::resources::ResourceLocation;
183    use std::str::FromStr;
184
185    #[test]
186    fn url_conversion() {
187        let resource1 = ResourceLocation::new(ProjectId::from_str(":root").unwrap(), None);
188        let as_url = Url::from(resource1.clone());
189        assert_eq!(ResourceLocation::try_from(as_url).unwrap(), resource1);
190
191        let resource2 = (ResourceLocation::find(
192            &ProjectId::from_str(":root").unwrap(),
193            "child1:child2",
194            Some("jar"),
195        ))
196        .unwrap();
197        assert_eq!(
198            resource2.project,
199            ProjectId::from_str(":root:child1:child2").unwrap()
200        );
201        let as_url = Url::from(resource2.clone());
202        assert_eq!(ResourceLocation::try_from(as_url).unwrap(), resource2);
203    }
204}