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(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 pub fn service_root(
228 client: &dyn wadl::blocking::Client,
229 ) -> std::result::Result<ServiceRootJson, Error> {
230 ROOT.get(client)
231 }
232
233 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 pub fn service_root(
266 client: &dyn wadl::blocking::Client,
267 ) -> std::result::Result<ServiceRootJson, Error> {
268 ROOT.get(client)
269 }
270
271 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")]
288pub 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 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 pub fn service_root(
323 client: &dyn wadl::blocking::Client,
324 ) -> std::result::Result<ServiceRootJson, Error> {
325 ROOT.get(client)
326 }
327
328 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 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 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 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 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 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 pub enum PersonOrTeam {
451 Person(Person),
453 Team(Team),
455 }
456
457 impl People {
458 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}