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}