app_store_connect/
lib.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7mod api_key;
8mod api_token;
9pub mod bundle_api;
10pub mod certs_api;
11pub mod cli;
12pub mod device_api;
13pub mod notary_api;
14pub mod profile_api;
15
16use {
17    reqwest::blocking::{Client, ClientBuilder, RequestBuilder, Response},
18    serde_json::Value,
19    std::{path::Path, sync::Mutex},
20    thiserror::Error,
21};
22
23pub use crate::api_key::{InvalidPemPrivateKey, UnifiedApiKey};
24pub use crate::api_token::{AppStoreConnectToken, ConnectTokenEncoder, MissingApiKey};
25
26pub type Result<T> = anyhow::Result<T>;
27
28/// A client for App Store Connect API.
29///
30/// The client isn't generic. Don't get any ideas.
31pub struct AppStoreConnectClient {
32    client: Client,
33    connect_token: ConnectTokenEncoder,
34    token: Mutex<Option<AppStoreConnectToken>>,
35}
36
37impl AppStoreConnectClient {
38    pub fn from_json_path(path: &Path) -> Result<Self> {
39        let key = UnifiedApiKey::from_json_path(path)?;
40        AppStoreConnectClient::new(key.try_into()?)
41    }
42
43    /// Create a new client to the App Store Connect API.
44    pub fn new(connect_token: ConnectTokenEncoder) -> Result<Self> {
45        let client = ClientBuilder::default()
46            .user_agent("asconnect crate (https://crates.io/crates/asconnect)")
47            .build()?;
48        Ok(Self {
49            client,
50            connect_token,
51            token: Mutex::new(None),
52        })
53    }
54
55    pub fn get_token(&self) -> Result<String> {
56        let mut token = self.token.lock().unwrap();
57
58        // TODO need to handle token expiration.
59        if token.is_none() {
60            token.replace(self.connect_token.new_token(300)?);
61        }
62
63        Ok(token.as_ref().unwrap().clone())
64    }
65
66    pub fn send_request(&self, request: RequestBuilder) -> Result<Response> {
67        let request = request.build()?;
68        let method = request.method().to_string();
69        let url = request.url().to_string();
70
71        log::debug!("{} {}", request.method(), url);
72
73        let response = self.client.execute(request)?;
74
75        if response.status().is_success() {
76            Ok(response)
77        } else {
78            let body = response.bytes()?;
79
80            let message = if let Ok(value) = serde_json::from_slice::<Value>(body.as_ref()) {
81                serde_json::to_string_pretty(&value)?
82            } else {
83                String::from_utf8_lossy(body.as_ref()).into()
84            };
85
86            Err(AppStoreConnectError {
87                method,
88                url,
89                message,
90            }
91            .into())
92        }
93    }
94}
95
96#[derive(Clone, Debug, Error)]
97#[error("appstore connect error:\n{method} {url}\n{message}")]
98pub struct AppStoreConnectError {
99    method: String,
100    url: String,
101    message: String,
102}