libcnb_data/
package_descriptor.rs

1use crate::package_descriptor::PlatformOs::Linux;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::path::PathBuf;
4use uriparse::{URIReference, URIReferenceError};
5
6/// Representation of [package.toml](https://buildpacks.io/docs/reference/config/package-config/).
7///
8/// # Example
9/// ```
10/// use libcnb_data::package_descriptor::PackageDescriptor;
11///
12/// let toml_str = r#"
13/// [buildpack]
14/// uri = "."
15///
16/// [[dependencies]]
17/// uri = "libcnb:buildpack_id"
18///
19/// [[dependencies]]
20/// uri = "../relative/path"
21///
22/// [[dependencies]]
23/// uri = "/absolute/path"
24///
25/// [[dependencies]]
26/// uri = "docker://docker.io/heroku/example:1.2.3"
27///
28/// [platform]
29/// os = "windows"
30/// "#;
31///
32/// toml::from_str::<PackageDescriptor>(toml_str).unwrap();
33/// ```
34#[derive(Debug, Deserialize, Serialize, Clone)]
35#[serde(deny_unknown_fields)]
36pub struct PackageDescriptor {
37    /// The buildpack to package.
38    pub buildpack: PackageDescriptorBuildpackReference,
39
40    /// A set of dependent buildpack locations, for packaging a composite buildpack.
41    ///
42    /// Each dependent buildpack location must correspond to an order group within the composite buildpack being packaged.
43    #[serde(default)]
44    pub dependencies: Vec<PackageDescriptorDependency>,
45
46    /// The expected runtime environment for the packaged buildpack.
47    #[serde(default)]
48    pub platform: Platform,
49}
50
51impl Default for PackageDescriptor {
52    fn default() -> Self {
53        PackageDescriptor {
54            buildpack: PackageDescriptorBuildpackReference::try_from(".")
55                .expect("a package.toml with buildpack.uri=\".\" should be valid"),
56            dependencies: Vec::new(),
57            platform: Platform::default(),
58        }
59    }
60}
61
62/// The buildpack to package.
63#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
64#[serde(deny_unknown_fields)]
65pub struct PackageDescriptorBuildpackReference {
66    /// A URL or path to an archive, or a path to a directory.
67    ///
68    /// If the `uri` field is a relative path it will be relative to the `package.toml` file.
69    #[serde(deserialize_with = "deserialize_uri_reference")]
70    #[serde(serialize_with = "serialize_uri_reference")]
71    pub uri: URIReference<'static>,
72}
73
74#[derive(Debug)]
75pub enum PackageDescriptorBuildpackError {
76    InvalidUri(String),
77}
78
79impl TryFrom<&str> for PackageDescriptorBuildpackReference {
80    type Error = PackageDescriptorBuildpackError;
81
82    fn try_from(value: &str) -> Result<Self, Self::Error> {
83        try_uri_from_str(value)
84            .map(|uri| PackageDescriptorBuildpackReference { uri })
85            .map_err(|_| PackageDescriptorBuildpackError::InvalidUri(value.to_string()))
86    }
87}
88
89/// A dependent buildpack location for packaging a composite buildpack.
90#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
91#[serde(deny_unknown_fields)]
92pub struct PackageDescriptorDependency {
93    /// A URL or path to an archive, a packaged buildpack (saved as a .cnb file), or a directory.
94    /// If the `uri` field is a relative path it will be relative to the `package.toml` file.
95    #[serde(deserialize_with = "deserialize_uri_reference")]
96    #[serde(serialize_with = "serialize_uri_reference")]
97    pub uri: URIReference<'static>,
98}
99
100#[derive(thiserror::Error, Debug)]
101pub enum PackageDescriptorDependencyError {
102    #[error("Invalid URI: {0}")]
103    InvalidUri(String),
104}
105
106impl TryFrom<PathBuf> for PackageDescriptorDependency {
107    type Error = PackageDescriptorDependencyError;
108
109    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
110        Self::try_from(value.to_string_lossy().to_string().as_str())
111    }
112}
113
114impl TryFrom<&str> for PackageDescriptorDependency {
115    type Error = PackageDescriptorDependencyError;
116
117    fn try_from(value: &str) -> Result<Self, Self::Error> {
118        try_uri_from_str(value)
119            .map(|uri| PackageDescriptorDependency { uri })
120            .map_err(|_| PackageDescriptorDependencyError::InvalidUri(value.to_string()))
121    }
122}
123
124fn try_uri_from_str(value: &str) -> Result<URIReference<'static>, URIReferenceError> {
125    URIReference::try_from(value).map(URIReference::into_owned)
126}
127
128/// The expected runtime environment for the packaged buildpack.
129#[derive(Debug, Deserialize, Serialize, Clone)]
130#[serde(deny_unknown_fields)]
131pub struct Platform {
132    /// The operating system type that the packaged buildpack will run on.
133    /// Only linux or windows is supported. If omitted, linux will be the default.
134    pub os: PlatformOs,
135}
136
137impl Default for Platform {
138    fn default() -> Self {
139        Self { os: Linux }
140    }
141}
142
143#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
144#[serde(rename_all = "lowercase")]
145pub enum PlatformOs {
146    Linux,
147    Windows,
148}
149
150// Even though `uriparse` has Serde support it only works if the value we are deserializing is an
151// map that contains URI fields like 'path', 'host', 'scheme', etc. The value from package.toml is
152// just a plain string so we need this custom deserializer that will parse the value into
153// a `URIReference`.
154fn deserialize_uri_reference<'de, D>(deserializer: D) -> Result<URIReference<'static>, D::Error>
155where
156    D: Deserializer<'de>,
157{
158    let value = String::deserialize(deserializer)?;
159    let uri = URIReference::try_from(value.as_str()).map_err(serde::de::Error::custom)?;
160    Ok(uri.into_owned())
161}
162
163// The Serde support in `uriparse` wants to serialize our `URIReference` into a map of URI fields
164// like 'path', 'host', 'scheme', etc. This custom serializer is needed to ensure the value is
165// converted into a plain string value which is what is required for the package.toml format.
166fn serialize_uri_reference<S>(uri: &URIReference, serializer: S) -> Result<S::Ok, S::Error>
167where
168    S: Serializer,
169{
170    let value = uri.to_string();
171    serializer.serialize_str(value.as_str())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::package_descriptor::PlatformOs::Windows;
178
179    #[test]
180    fn it_parses_minimal() {
181        let toml_str = r#"
182[buildpack]
183uri = "."
184"#;
185
186        let package_descriptor = toml::from_str::<PackageDescriptor>(toml_str).unwrap();
187        assert_eq!(
188            package_descriptor.buildpack,
189            PackageDescriptorBuildpackReference::try_from(".").unwrap()
190        );
191        assert_eq!(package_descriptor.platform.os, Linux);
192    }
193
194    #[test]
195    fn it_parses_with_dependencies_and_platform() {
196        let toml_str = r#"
197[buildpack]
198uri = "."
199
200[[dependencies]]
201uri = "libcnb:buildpack-id"
202
203[[dependencies]]
204uri = "../relative/path"
205
206[[dependencies]]
207uri = "/absolute/path"
208
209[[dependencies]]
210uri = "docker://docker.io/heroku/example:1.2.3"
211
212[platform]
213os = "windows"
214"#;
215
216        let package_descriptor = toml::from_str::<PackageDescriptor>(toml_str).unwrap();
217        assert_eq!(
218            package_descriptor.buildpack,
219            PackageDescriptorBuildpackReference::try_from(".").unwrap()
220        );
221        assert_eq!(package_descriptor.platform.os, Windows);
222        assert_eq!(
223            package_descriptor.dependencies,
224            [
225                PackageDescriptorDependency::try_from("libcnb:buildpack-id").unwrap(),
226                PackageDescriptorDependency::try_from("../relative/path").unwrap(),
227                PackageDescriptorDependency::try_from("/absolute/path").unwrap(),
228                PackageDescriptorDependency::try_from("docker://docker.io/heroku/example:1.2.3")
229                    .unwrap()
230            ]
231        );
232    }
233
234    #[test]
235    fn it_serializes() {
236        let package_descriptor = PackageDescriptor {
237            buildpack: PackageDescriptorBuildpackReference::try_from(".").unwrap(),
238            dependencies: vec![
239                PackageDescriptorDependency::try_from("libcnb:buildpack-id").unwrap(),
240                PackageDescriptorDependency::try_from("../relative/path").unwrap(),
241                PackageDescriptorDependency::try_from("/absolute/path").unwrap(),
242                PackageDescriptorDependency::try_from("docker://docker.io/heroku/example:1.2.3")
243                    .unwrap(),
244            ],
245            platform: Platform::default(),
246        };
247
248        let package_descriptor_contents = toml::to_string(&package_descriptor).unwrap();
249        assert_eq!(
250            package_descriptor_contents,
251            r#"
252[buildpack]
253uri = "."
254
255[[dependencies]]
256uri = "libcnb:buildpack-id"
257
258[[dependencies]]
259uri = "../relative/path"
260
261[[dependencies]]
262uri = "/absolute/path"
263
264[[dependencies]]
265uri = "docker://docker.io/heroku/example:1.2.3"
266
267[platform]
268os = "linux"
269"#
270            .trim_start()
271        );
272    }
273}