proget/
lib.rs

1//! A library providing a client for the [ProGet](https://inedo.com/proget) API.
2//!
3//! This library is **heavily** a work in progress, and stability is currently **not guaranteed**.
4//! The library also needs a plethora of features to be added still - if there's something you'd
5//! like added that's missing, feel free to make an issue or send in a PR on the
6//! [GitHub repository](https://github.com/hwittenborn/proget-rust-sdk).
7//!
8//! Most use cases will involve beginning with a [`Client`]. Please start there
9//! if you're trying to find your way around the library.
10//!
11//! # Feature flags
12//! - `rustls-tls`: Use `rustls` as the TLS backend. Uses the system's native backend when not
13//!   enabled.
14//! - `indexmap`: Use [`IndexMap`] instead of [`HashMap`] for items in
15//!   [`models`].
16mod api;
17pub mod models;
18
19pub use reqwest;
20pub use semver;
21
22#[cfg(feature = "__docs")]
23use indexmap::IndexMap;
24#[cfg(feature = "__docs")]
25use std::collections::HashMap;
26
27use reqwest::{header::HeaderMap, Url};
28use std::{marker::PhantomData, ops::Deref};
29use thiserror::Error as ThisError;
30
31/// The user agent we use in requests.
32static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
33
34/// Alias for a [`std::result::Result`] with the error type always being [`crate::Error`].
35pub type Result<T> = std::result::Result<T, Error>;
36
37/// The errors that may occur throughout this crate.
38#[derive(ThisError, Debug)]
39pub enum Error {
40    /// An error while making an HTTP request.
41    #[error("{0}")]
42    Http(#[from] reqwest::Error),
43    /// An error while parsing JSON data.
44    #[error("{0}")]
45    Json(#[from] serde_json::Error),
46}
47
48// Traits to differentiate between authenticated and anonymous clients.
49mod private {
50    pub trait AuthType {}
51}
52
53/// The type needed for an authenticated [`Client`].
54#[derive(Clone)]
55pub struct Anon;
56
57/// The type needed for an anonymous [`Client`].
58#[derive(Clone)]
59pub struct Auth;
60
61impl private::AuthType for Anon {}
62impl private::AuthType for Auth {}
63
64/// The client data for [`Client`].
65#[derive(Clone)]
66struct ClientData {
67    http: reqwest::Client,
68    server_url: Url,
69}
70
71/// A struct representing a user of a ProGet instance.
72///
73/// Most methods require authentication in order to run. For the methods that don't, you can call
74/// [`Client::new_anon`] to make a new client without any authentication. If you'd like to run any
75/// authenticated calls, use [`Client::new_auth`] instead.
76///
77/// All methods on the [`Anon`] version of the client are automatically available on the [`Auth`]
78/// version, so there's no need to make two separate clients.
79//
80// This code is a bit messy - it's used this way so the documentation looks cleaner. While usually
81// you do want the code to be what looks code, having it this way makes the documentation look
82// really good, and it's not something I want to sacrafice right now.
83//
84// # How it works:
85// - When the client is a `Client<Anon>`, `client_data` is the `Some` variant, and `anon_client` is
86//   `None` (since we already have a `Client<Anon>`.
87// - When the client is a `Client<Auth>`, `client_data` is `None`, and `anon_client` is `Some`,
88//   pointing to the `Client<Anon>`.
89//
90// These generics on the `Client` don't really mean anything, they both ultimately point to
91// `ClientData`, which is what contains all the data. The `new_anon` and `new_auth` functions below
92// are what set the data in `ClientData`, with `new_auth` just setting some authentication data. We
93// just have the two separate types for extra type safety - if you call `Client::new_anon`, you
94// won't be able to call any functions from `Client<Auth>` - the error messages from Rust are also
95// amazing, and hint really good that an authenticated client from `Client::new_auth` should be
96// used instead.
97//
98// If you can find a cleaner way to implement this be my guest, PRs welcomed! :D
99#[derive(Clone)]
100pub struct Client<A: private::AuthType> {
101    client_data: Option<ClientData>,
102    anon_client: Option<Box<Client<Anon>>>,
103    _phantom: PhantomData<A>,
104}
105
106/// Functions to create and interact with ProGet without authentication.
107impl Client<Anon> {
108    /// Get the client data.
109    fn client_data(&self) -> &ClientData {
110        self.client_data.as_ref().unwrap()
111    }
112
113    /// Create a new anonymous, unauthenticated client.
114    pub fn new_anon(server_url: Url) -> Self {
115        let http = reqwest::Client::builder()
116            .user_agent(USER_AGENT)
117            .build()
118            .unwrap();
119        let client_data = ClientData { http, server_url };
120
121        Self {
122            client_data: Some(client_data),
123            anon_client: None,
124            _phantom: PhantomData,
125        }
126    }
127
128    /// Get health/status information.
129    pub async fn health(&self) -> crate::Result<models::Health> {
130        api::health(self.client_data()).await
131    }
132}
133
134/// Functions to create and interact with ProGet with authentication.
135impl Client<Auth> {
136    /// Create a new authenticated client.
137    pub fn new_auth(server_url: Url, api_token: &str) -> Self {
138        let mut headers = HeaderMap::new();
139        let auth_key = base64::encode(format!("api:{api_token}"));
140        let auth_header = format!("Basic {auth_key}");
141        headers.insert("Authorization", auth_header.parse().unwrap());
142
143        let http = reqwest::Client::builder()
144            .user_agent(USER_AGENT)
145            .default_headers(headers)
146            .build()
147            .unwrap();
148        let client_data = ClientData { http, server_url };
149        let client = Client {
150            client_data: Some(client_data),
151            anon_client: None,
152            _phantom: PhantomData,
153        };
154
155        Self {
156            client_data: None,
157            anon_client: Some(Box::new(client)),
158            _phantom: PhantomData,
159        }
160    }
161
162    /// Upload a `.deb` package.
163    ///
164    /// # Arguments
165    /// * `feed_name`: The [feed](https://docs.inedo.com/docs/proget-feeds-feed-overview) to upload the `.deb` package to.
166    /// * `component_name`: The component in the APT repository to upload the deb to. For example, this would be `bionic` in `deb https://proget.inedo.com deb-packages bionic`.
167    /// * `deb_name`: The name of the `.deb` file to register the package under (i.e. `pkg_1.0.0-1_amd64.deb`).
168    /// * `deb_data`: The binary data of the `.deb` file.
169    ///
170    /// # Errors
171    /// This function returns an error if there was an issue uploading the file.
172    pub async fn upload_deb(
173        &self,
174        feed_name: &str,
175        component_name: &str,
176        deb_name: &str,
177        deb_data: &[u8],
178    ) -> crate::Result<()> {
179        api::deb::upload_deb(
180            self.client_data(),
181            feed_name,
182            component_name,
183            deb_name,
184            deb_data,
185        )
186        .await
187    }
188}
189
190/// Automatic conversion of an [`Auth`] client into an [`Anon`] client, which allows
191/// anonymous-access functions like [`Client::health`] to be accessed from the authenticated
192/// client.
193impl Deref for Client<Auth> {
194    type Target = Client<Anon>;
195
196    fn deref(&self) -> &Self::Target {
197        self.anon_client.as_ref().unwrap()
198    }
199}