Skip to main content

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(all(feature = "api-devel", feature = "blocking"))]
212pub mod devel {
213    #![allow(unused_mut)]
214    #![allow(clippy::too_many_arguments)]
215    #![allow(clippy::wrong_self_convention)]
216    #![allow(dead_code)]
217    #![allow(missing_docs)]
218    use super::*;
219    use crate::AsTotalSize;
220    use url::Url;
221    include!(concat!(env!("OUT_DIR"), "/generated/devel.rs"));
222
223    lazy_static::lazy_static! {
224        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/devel/").unwrap());
225    }
226
227    /// Get the default service root
228    pub fn service_root(
229        client: &dyn wadl::blocking::Client,
230    ) -> std::result::Result<ServiceRootJson, Error> {
231        ROOT.get(client)
232    }
233
234    /// Get the service root for a specific host
235    ///
236    /// # Example
237    /// ```rust,no_run
238    /// let client = launchpadlib::Client::anonymous("just+testing");
239    /// let root = launchpadlib::devel::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
240    /// ```
241    pub fn service_root_for_host(
242        client: &dyn wadl::blocking::Client,
243        host: &str,
244    ) -> std::result::Result<ServiceRootJson, Error> {
245        let url = Url::parse(&format!("https://{}/devel/", host)).unwrap();
246        ServiceRoot(url).get(client)
247    }
248}
249
250#[cfg(all(feature = "api-beta", feature = "blocking"))]
251pub mod beta {
252    #![allow(unused_mut)]
253    #![allow(clippy::too_many_arguments)]
254    #![allow(clippy::wrong_self_convention)]
255    #![allow(dead_code)]
256    #![allow(missing_docs)]
257    use super::*;
258    use crate::AsTotalSize;
259    use url::Url;
260    include!(concat!(env!("OUT_DIR"), "/generated/beta.rs"));
261
262    lazy_static::lazy_static! {
263        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/beta/").unwrap());
264    }
265
266    /// Get the default service root
267    pub fn service_root(
268        client: &dyn wadl::blocking::Client,
269    ) -> std::result::Result<ServiceRootJson, Error> {
270        ROOT.get(client)
271    }
272
273    /// Get the service root for a specific host
274    ///
275    /// # Example
276    /// ```rust,no_run
277    /// let client = launchpadlib::Client::anonymous("just+testing");
278    /// let root = launchpadlib::beta::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
279    /// ```
280    pub fn service_root_for_host(
281        client: &dyn wadl::blocking::Client,
282        host: &str,
283    ) -> std::result::Result<ServiceRootJson, Error> {
284        let url = Url::parse(&format!("https://{}/beta/", host)).unwrap();
285        ServiceRoot(url).get(client)
286    }
287}
288
289#[cfg(all(feature = "api-v1_0", feature = "blocking"))]
290/// Version 1.0 of the Launchpad API
291pub mod v1_0 {
292    #![allow(unused_mut)]
293    #![allow(clippy::too_many_arguments)]
294    #![allow(clippy::wrong_self_convention)]
295    #![allow(dead_code)]
296    #![allow(missing_docs)]
297    use super::*;
298    use crate::AsTotalSize;
299    use url::Url;
300
301    include!(concat!(env!("OUT_DIR"), "/generated/1_0.rs"));
302
303    lazy_static::lazy_static! {
304        static ref ROOT: ServiceRoot = ServiceRoot(Url::parse("https://api.launchpad.net/1.0/").unwrap());
305        static ref STATIC_RESOURCES: std::collections::HashMap<Url, Box<dyn Resource + Send + Sync>> = {
306            let mut m = std::collections::HashMap::new();
307            let root = ServiceRoot(Url::parse("https://api.launchpad.net/1.0/").unwrap());
308            m.insert(root.url().clone(), Box::new(root) as Box<dyn Resource + Send + Sync>);
309            m
310        };
311    }
312
313    /// Get the service root by URL
314    pub fn get_service_root_by_url(
315        url: &'_ Url,
316    ) -> std::result::Result<&'static ServiceRoot, Error> {
317        if url == ROOT.url() {
318            Ok(&ROOT)
319        } else {
320            Err(Error::InvalidUrl)
321        }
322    }
323
324    /// Get the default service root
325    pub fn service_root(
326        client: &dyn wadl::blocking::Client,
327    ) -> std::result::Result<ServiceRootJson, Error> {
328        ROOT.get(client)
329    }
330
331    /// Get the service root for a specific host
332    ///
333    /// # Example
334    /// ```rust,no_run
335    /// let client = launchpadlib::blocking::Client::anonymous("just+testing");
336    /// let root = launchpadlib::v1_0::service_root_for_host(&client, "api.staging.launchpad.net").unwrap();
337    /// ```
338    pub fn service_root_for_host(
339        client: &dyn wadl::blocking::Client,
340        host: &str,
341    ) -> std::result::Result<ServiceRootJson, Error> {
342        let url = Url::parse(&format!("https://{}/1.0/", host)).unwrap();
343        ServiceRoot(url).get(client)
344    }
345
346    /// Get a resource by its URL
347    pub fn get_resource_by_url(
348        url: &'_ Url,
349    ) -> std::result::Result<&'static (dyn Resource + Send + Sync), Error> {
350        if let Some(resource) = STATIC_RESOURCES.get(url) {
351            Ok(resource.as_ref())
352        } else {
353            Err(Error::InvalidUrl)
354        }
355    }
356
357    #[cfg(test)]
358    mod tests {
359        use super::*;
360
361        #[test]
362        fn test_parse_person() {
363            let json = include_str!("../testdata/person.json");
364            let person: PersonFull = serde_json::from_str(json).unwrap();
365            assert_eq!(person.display_name, "Jelmer Vernooij");
366        }
367
368        #[test]
369        fn test_parse_team() {
370            let json = include_str!("../testdata/team.json");
371            let team: TeamFull = serde_json::from_str(json).unwrap();
372            assert_eq!(team.display_name, "awsome-core");
373
374            let json = include_str!("../testdata/team2.json");
375            let _team: TeamFull = serde_json::from_str(json).unwrap();
376        }
377
378        #[test]
379        #[cfg(feature = "bugs")]
380        fn test_parse_bug() {
381            let json = include_str!("../testdata/bug.json");
382            let bug: BugFull = serde_json::from_str(json).unwrap();
383            assert_eq!(bug.title, "Microsoft has a majority market share");
384
385            let json = include_str!("../testdata/bug2.json");
386            let bug: BugFull = serde_json::from_str(json).unwrap();
387            assert_eq!(bug.name, None);
388            assert_eq!(bug.id, 2039729);
389        }
390
391        #[test]
392        #[cfg(feature = "bugs")]
393        fn test_parse_bug_tasks() {
394            let json = include_str!("../testdata/bug_tasks.json");
395            let _bug_tasks: BugTaskPage = serde_json::from_str(json).unwrap();
396        }
397    }
398
399    #[cfg(feature = "bugs")]
400    impl Bugs {
401        /// Get a bug by its id
402        ///
403        /// # Example
404        /// ```rust,no_run
405        /// let client = launchpadlib::blocking::Client::anonymous("just+testing");
406        /// let root = launchpadlib::v1_0::service_root(&client).unwrap();
407        /// let bug = root.bugs().unwrap().get_by_id(&client, 1).unwrap();
408        /// ```
409        pub fn get_by_id(
410            &self,
411            client: &dyn wadl::blocking::Client,
412            id: u32,
413        ) -> std::result::Result<BugFull, Error> {
414            let url = self.url().join(format!("bugs/{}", id).as_str()).unwrap();
415            Bug(url).get(client)
416        }
417    }
418
419    impl Projects {
420        /// Get a project by name
421        pub fn get_by_name(
422            &self,
423            client: &dyn wadl::blocking::Client,
424            name: &str,
425        ) -> std::result::Result<ProjectFull, Error> {
426            let url = self.url().join(name).unwrap();
427            Project(url).get(client)
428        }
429    }
430
431    impl ProjectGroups {
432        /// Get a project group by name
433        pub fn get_by_name(
434            &self,
435            client: &dyn wadl::blocking::Client,
436            name: &str,
437        ) -> std::result::Result<ProjectGroupFull, Error> {
438            let url = self.url().join(name).unwrap();
439            ProjectGroup(url).get(client)
440        }
441    }
442
443    impl Distributions {
444        /// Get a distribution by name
445        pub fn get_by_name(
446            &self,
447            client: &dyn wadl::blocking::Client,
448            name: &str,
449        ) -> std::result::Result<DistributionFull, Error> {
450            let url = self.url().join(name).unwrap();
451            Distribution(url).get(client)
452        }
453    }
454
455    /// Represents either a Person or a Team
456    pub enum PersonOrTeam {
457        /// A person
458        Person(Person),
459        /// A team
460        Team(Team),
461    }
462
463    impl People {
464        /// Get a person or team by name
465        pub fn get_by_name(
466            &self,
467            client: &dyn wadl::blocking::Client,
468            name: &str,
469        ) -> std::result::Result<PersonOrTeam, Error> {
470            let url = self.url().join(&format!("~{}", name)).unwrap();
471
472            let wadl = wadl::blocking::get_wadl_resource_by_href(client, &url)?;
473
474            let types = wadl
475                .r#type
476                .iter()
477                .filter_map(|t| t.id())
478                .collect::<Vec<_>>();
479
480            if types.contains(&"person") {
481                Ok(PersonOrTeam::Person(Person(url)))
482            } else if types.contains(&"team") {
483                Ok(PersonOrTeam::Team(Team(url)))
484            } else {
485                Err(Error::InvalidUrl)
486            }
487        }
488    }
489}