launchpadlib/
lib.rs

1#![warn(missing_docs)]
2//! # Launchpad API
3//!
4//! This crate provides a Rust interface to the Launchpad API.
5//! It is generated from the Launchpad API WADL document.
6//!
7//! ## Usage
8//!
9//! ```rust,no_run
10//! use url::Url;
11//!
12//! #[cfg(all(feature = "api-v1_0", feature = "blocking"))]
13//! {
14//! let client = launchpadlib::blocking::Client::anonymous("just+testing");
15//! let service_root = launchpadlib::blocking::v1_0::service_root(&client).unwrap();
16//! let people = service_root.people().unwrap();
17//! let person = people.get_by_email(&client, "jelmer@jelmer.uk").unwrap();
18//! let ssh_keys = person.sshkeys(&client).unwrap().map(|k| k.unwrap().keytext).collect::<Vec<_>>();
19//! println!("SSH Keys: {:?}", ssh_keys);
20//! }
21//! ```
22//!
23//! ## Limitations and bugs
24//!
25//! * While bindings are generated from the entire WADL file, I have only used a small number of
26//!   them. Please report bugs if you run into issues.  Launchpad's WADL is incorrect in places, e.g.
27//!   claiming that certain fields are optional while they will actually be set to null. Any problems
28//!   with the WADL will impact the usability of the rust bindings.
29//!
30//! * See fixup.xsl for manual patches that are applied; this file is almost certainly incomplete.
31
32pub mod auth;
33pub mod uris;
34pub use wadl::{Error, Resource};
35
36/// The default user agent, used if none is provided
37pub const DEFAULT_USER_AGENT: &str =
38    concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
39
40/// The default Launchpad instance
41pub const DEFAULT_INSTANCE: &str = "launchpad.net";
42
43#[cfg(feature = "async")]
44pub mod r#async;
45
46#[cfg(feature = "blocking")]
47pub mod blocking;
48
49#[allow(dead_code)]
50pub(crate) trait AsTotalSize {
51    fn into_total_size(self) -> Option<usize>;
52}
53
54impl AsTotalSize for Option<usize> {
55    fn into_total_size(self) -> Option<usize> {
56        self
57    }
58}
59
60impl AsTotalSize for usize {
61    fn into_total_size(self) -> Option<usize> {
62        Some(self)
63    }
64}
65
66/// Various custom types to help massaging the LP data into proper Rust types.
67pub mod types {
68    /// Custom type to work around some peculiarities of the package_upload.display_arches field.
69    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
70    pub enum PackageUploadArches {
71        /// A sourceful upload
72        #[serde(rename = "source")]
73        Source,
74        /// When the upload comes from a Debian sync, there is no arch list.
75        #[serde(rename = "sync")]
76        Sync,
77        /// A single arch
78        #[serde(untagged)]
79        Arch(String),
80        /// Several arches for a single item. Obsolete?
81        #[serde(untagged)]
82        Arches(Vec<String>),
83    }
84
85    /// A generic wrapper type for fields that may be redacted for private projects.
86    /// Some fields in Launchpad can return "tag:launchpad.net:2008:redacted"
87    /// instead of their actual value for private projects.
88    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
89    pub enum MaybeRedacted<T> {
90        /// The actual value
91        Value(T),
92        /// A redacted value for private projects
93        Redacted,
94    }
95
96    impl<T> MaybeRedacted<T> {
97        /// Get the inner value as an Option, returning None if redacted
98        pub fn as_option(&self) -> Option<&T> {
99            match self {
100                Self::Value(v) => Some(v),
101                Self::Redacted => None,
102            }
103        }
104
105        /// Get the inner value as an Option, consuming self
106        pub fn into_option(self) -> Option<T> {
107            match self {
108                Self::Value(v) => Some(v),
109                Self::Redacted => None,
110            }
111        }
112
113        /// Check if the value is redacted
114        pub fn is_redacted(&self) -> bool {
115            matches!(self, Self::Redacted)
116        }
117
118        /// Get the inner value or a default if redacted
119        pub fn unwrap_or(self, default: T) -> T {
120            match self {
121                Self::Value(v) => v,
122                Self::Redacted => default,
123            }
124        }
125
126        /// Get the inner value or compute it from a closure if redacted
127        pub fn unwrap_or_else<F>(self, f: F) -> T
128        where
129            F: FnOnce() -> T,
130        {
131            match self {
132                Self::Value(v) => v,
133                Self::Redacted => f(),
134            }
135        }
136
137        /// Map the inner value if present
138        pub fn map<U, F>(self, f: F) -> MaybeRedacted<U>
139        where
140            F: FnOnce(T) -> U,
141        {
142            match self {
143                Self::Value(v) => MaybeRedacted::Value(f(v)),
144                Self::Redacted => MaybeRedacted::Redacted,
145            }
146        }
147    }
148
149    impl<T> Default for MaybeRedacted<T>
150    where
151        T: Default,
152    {
153        fn default() -> Self {
154            Self::Value(T::default())
155        }
156    }
157
158    impl<T> From<T> for MaybeRedacted<T> {
159        fn from(value: T) -> Self {
160            Self::Value(value)
161        }
162    }
163
164    impl<T> serde::Serialize for MaybeRedacted<T>
165    where
166        T: serde::Serialize,
167    {
168        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
169        where
170            S: serde::Serializer,
171        {
172            match self {
173                Self::Value(v) => v.serialize(serializer),
174                Self::Redacted => "tag:launchpad.net:2008:redacted".serialize(serializer),
175            }
176        }
177    }
178
179    impl<'de, T> serde::Deserialize<'de> for MaybeRedacted<T>
180    where
181        T: serde::Deserialize<'de>,
182    {
183        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184        where
185            D: serde::Deserializer<'de>,
186        {
187            #[derive(serde::Deserialize)]
188            #[serde(untagged)]
189            enum MaybeRedactedHelper<T> {
190                Value(T),
191                String(String),
192            }
193
194            match MaybeRedactedHelper::<T>::deserialize(deserializer)? {
195                MaybeRedactedHelper::Value(v) => Ok(Self::Value(v)),
196                MaybeRedactedHelper::String(s) => {
197                    if s == "tag:launchpad.net:2008:redacted" {
198                        Ok(Self::Redacted)
199                    } else {
200                        Err(serde::de::Error::custom(format!(
201                            "expected value or redacted tag, got string: {}",
202                            s
203                        )))
204                    }
205                }
206            }
207        }
208    }
209}
210
211#[cfg(feature = "api-devel")]
212pub mod devel {
213    #![allow(unused_mut)]
214    #![allow(clippy::too_many_arguments)]
215    #![allow(clippy::wrong_self_convention)]
216    #![allow(dead_code)]
217    use super::*;
218    use crate::AsTotalSize;
219    use url::Url;
220    include!(concat!(env!("OUT_DIR"), "/generated/devel.rs"));
221
222    lazy_static::lazy_static! {
223        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/devel/").unwrap());
224    }
225
226    /// Get the default service root
227    pub fn service_root(
228        client: &dyn wadl::blocking::Client,
229    ) -> std::result::Result<ServiceRootJson, Error> {
230        ROOT.get(client)
231    }
232
233    /// Get the service root for a specific host
234    ///
235    /// # Example
236    /// ```rust
237    /// let client = launchpadlib::Client::anonymous("just+testing");
238    /// let root = launchpadlib::devel::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
239    /// ```
240    pub fn service_root_for_host(
241        client: &dyn wadl::blocking::Client,
242        host: &str,
243    ) -> std::result::Result<ServiceRootJson, Error> {
244        let url = Url::parse(&format!("https://{}/devel/", host)).unwrap();
245        ServiceRoot(url).get(client)
246    }
247}
248
249#[cfg(feature = "api-beta")]
250pub mod beta {
251    #![allow(unused_mut)]
252    #![allow(clippy::too_many_arguments)]
253    #![allow(clippy::wrong_self_convention)]
254    #![allow(dead_code)]
255    use super::*;
256    use crate::AsTotalSize;
257    use url::Url;
258    include!(concat!(env!("OUT_DIR"), "/generated/beta.rs"));
259
260    lazy_static::lazy_static! {
261        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/beta/").unwrap());
262    }
263
264    /// Get the default service root
265    pub fn service_root(
266        client: &dyn wadl::blocking::Client,
267    ) -> std::result::Result<ServiceRootJson, Error> {
268        ROOT.get(client)
269    }
270
271    /// Get the service root for a specific host
272    ///
273    /// # Example
274    /// ```rust
275    /// let client = launchpadlib::Client::anonymous("just+testing");
276    /// let root = launchpadlib::beta::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
277    /// ```
278    pub fn service_root_for_host(
279        client: &dyn wadl::blocking::Client,
280        host: &str,
281    ) -> std::result::Result<ServiceRootJson, Error> {
282        let url = Url::parse(&format!("https://{}/beta/", host)).unwrap();
283        ServiceRoot(url).get(client)
284    }
285}
286
287#[cfg(feature = "api-v1_0")]
288/// Version 1.0 of the Launchpad API
289pub mod v1_0 {
290    #![allow(unused_mut)]
291    #![allow(clippy::too_many_arguments)]
292    #![allow(clippy::wrong_self_convention)]
293    #![allow(dead_code)]
294    use super::*;
295    use crate::AsTotalSize;
296    use url::Url;
297
298    include!(concat!(env!("OUT_DIR"), "/generated/1_0.rs"));
299
300    lazy_static::lazy_static! {
301        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/1.0/").unwrap());
302        static ref STATIC_RESOURCES: std::collections::HashMap<Url, Box<dyn Resource + Send + Sync>> = {
303            let mut m = std::collections::HashMap::new();
304            let root = ServiceRoot(Url::parse("https://api.launchpad.net/1.0/").unwrap());
305            m.insert(root.url().clone(), Box::new(root) as Box<dyn Resource + Send + Sync>);
306            m
307        };
308    }
309
310    /// Get the service root by URL
311    pub fn get_service_root_by_url(
312        url: &'_ Url,
313    ) -> std::result::Result<&'static ServiceRoot, Error> {
314        if url == ROOT.url() {
315            Ok(&ROOT)
316        } else {
317            Err(Error::InvalidUrl)
318        }
319    }
320
321    /// Get the default service root
322    pub fn service_root(
323        client: &dyn wadl::blocking::Client,
324    ) -> std::result::Result<ServiceRootJson, Error> {
325        ROOT.get(client)
326    }
327
328    /// Get the service root for a specific host
329    ///
330    /// # Example
331    /// ```rust
332    /// let client = launchpadlib::blocking::Client::anonymous("just+testing");
333    /// let root = launchpadlib::v1_0::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
334    /// ```
335    pub fn service_root_for_host(
336        client: &dyn wadl::blocking::Client,
337        host: &str,
338    ) -> std::result::Result<ServiceRootJson, Error> {
339        let url = Url::parse(&format!("https://{}/1.0/", host)).unwrap();
340        ServiceRoot(url).get(client)
341    }
342
343    /// Get a resource by its URL
344    pub fn get_resource_by_url(
345        url: &'_ Url,
346    ) -> std::result::Result<&'static (dyn Resource + Send + Sync), Error> {
347        if let Some(resource) = STATIC_RESOURCES.get(url) {
348            Ok(resource.as_ref())
349        } else {
350            Err(Error::InvalidUrl)
351        }
352    }
353
354    #[cfg(test)]
355    mod tests {
356        use super::*;
357
358        #[test]
359        fn test_parse_person() {
360            let json = include_str!("../testdata/person.json");
361            let person: PersonFull = serde_json::from_str(json).unwrap();
362            assert_eq!(person.display_name, "Jelmer Vernooij");
363        }
364
365        #[test]
366        fn test_parse_team() {
367            let json = include_str!("../testdata/team.json");
368            let team: TeamFull = serde_json::from_str(json).unwrap();
369            assert_eq!(team.display_name, "awsome-core");
370
371            let json = include_str!("../testdata/team2.json");
372            let _team: TeamFull = serde_json::from_str(json).unwrap();
373        }
374
375        #[test]
376        fn test_parse_bug() {
377            let json = include_str!("../testdata/bug.json");
378            let bug: BugFull = serde_json::from_str(json).unwrap();
379            assert_eq!(bug.title, "Microsoft has a majority market share");
380
381            let json = include_str!("../testdata/bug2.json");
382            let bug: BugFull = serde_json::from_str(json).unwrap();
383            assert_eq!(bug.name, None);
384            assert_eq!(bug.id, 2039729);
385        }
386
387        #[test]
388        fn test_parse_bug_tasks() {
389            let json = include_str!("../testdata/bug_tasks.json");
390            let _bug_tasks: BugTaskPage = serde_json::from_str(json).unwrap();
391        }
392    }
393
394    impl Bugs {
395        /// Get a bug by its id
396        ///
397        /// # Example
398        /// ```rust
399        /// let client = launchpadlib::blocking::Client::anonymous("just+testing");
400        /// let root = launchpadlib::v1_0::service_root(&client).unwrap();
401        /// let bug = root.bugs().unwrap().get_by_id(&client, 1).unwrap();
402        /// ```
403        pub fn get_by_id(
404            &self,
405            client: &dyn wadl::blocking::Client,
406            id: u32,
407        ) -> std::result::Result<BugFull, Error> {
408            let url = self.url().join(format!("bugs/{}", id).as_str()).unwrap();
409            Bug(url).get(client)
410        }
411    }
412
413    impl Projects {
414        /// Get a project by name
415        pub fn get_by_name(
416            &self,
417            client: &dyn wadl::blocking::Client,
418            name: &str,
419        ) -> std::result::Result<ProjectFull, Error> {
420            let url = self.url().join(name).unwrap();
421            Project(url).get(client)
422        }
423    }
424
425    impl ProjectGroups {
426        /// Get a project group by name
427        pub fn get_by_name(
428            &self,
429            client: &dyn wadl::blocking::Client,
430            name: &str,
431        ) -> std::result::Result<ProjectGroupFull, Error> {
432            let url = self.url().join(name).unwrap();
433            ProjectGroup(url).get(client)
434        }
435    }
436
437    impl Distributions {
438        /// Get a distribution by name
439        pub fn get_by_name(
440            &self,
441            client: &dyn wadl::blocking::Client,
442            name: &str,
443        ) -> std::result::Result<DistributionFull, Error> {
444            let url = self.url().join(name).unwrap();
445            Distribution(url).get(client)
446        }
447    }
448
449    /// Represents either a Person or a Team
450    pub enum PersonOrTeam {
451        /// A person
452        Person(Person),
453        /// A team
454        Team(Team),
455    }
456
457    impl People {
458        /// Get a person or team by name
459        pub fn get_by_name(
460            &self,
461            client: &dyn wadl::blocking::Client,
462            name: &str,
463        ) -> std::result::Result<PersonOrTeam, Error> {
464            let url = self.url().join(&format!("~{}", name)).unwrap();
465
466            let wadl = wadl::blocking::get_wadl_resource_by_href(client, &url)?;
467
468            let types = wadl
469                .r#type
470                .iter()
471                .filter_map(|t| t.id())
472                .collect::<Vec<_>>();
473
474            if types.contains(&"person") {
475                Ok(PersonOrTeam::Person(Person(url)))
476            } else if types.contains(&"team") {
477                Ok(PersonOrTeam::Team(Team(url)))
478            } else {
479                Err(Error::InvalidUrl)
480            }
481        }
482    }
483}