1#![warn(missing_docs)]
2pub mod auth;
33pub mod uris;
34pub use wadl::{Error, Resource};
35
36pub const DEFAULT_USER_AGENT: &str =
38 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
39
40pub 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
66pub mod types {
68 #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
70 pub enum PackageUploadArches {
71 #[serde(rename = "source")]
73 Source,
74 #[serde(rename = "sync")]
76 Sync,
77 #[serde(untagged)]
79 Arch(String),
80 #[serde(untagged)]
82 Arches(Vec<String>),
83 }
84
85 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
89 pub enum MaybeRedacted<T> {
90 Value(T),
92 Redacted,
94 }
95
96 impl<T> MaybeRedacted<T> {
97 pub fn as_option(&self) -> Option<&T> {
99 match self {
100 Self::Value(v) => Some(v),
101 Self::Redacted => None,
102 }
103 }
104
105 pub fn into_option(self) -> Option<T> {
107 match self {
108 Self::Value(v) => Some(v),
109 Self::Redacted => None,
110 }
111 }
112
113 pub fn is_redacted(&self) -> bool {
115 matches!(self, Self::Redacted)
116 }
117
118 pub fn unwrap_or(self, default: T) -> T {
120 match self {
121 Self::Value(v) => v,
122 Self::Redacted => default,
123 }
124 }
125
126 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 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 pub fn service_root(
229 client: &dyn wadl::blocking::Client,
230 ) -> std::result::Result<ServiceRootJson, Error> {
231 ROOT.get(client)
232 }
233
234 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 pub fn service_root(
268 client: &dyn wadl::blocking::Client,
269 ) -> std::result::Result<ServiceRootJson, Error> {
270 ROOT.get(client)
271 }
272
273 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"))]
290pub 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 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 pub fn service_root(
326 client: &dyn wadl::blocking::Client,
327 ) -> std::result::Result<ServiceRootJson, Error> {
328 ROOT.get(client)
329 }
330
331 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 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 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 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 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 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 pub enum PersonOrTeam {
457 Person(Person),
459 Team(Team),
461 }
462
463 impl People {
464 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}