qcs_api_client_common/configuration/
settings.rs1use std::collections::HashMap;
3use std::path::PathBuf;
4
5use figment::providers::Format;
6use figment::{Figment, providers::Toml};
7use serde::{Deserialize, Serialize};
8
9#[cfg(feature = "stubs")]
10use pyo3_stub_gen::derive::gen_stub_pyclass;
11
12use crate::configuration::error::DiscoveryError;
13use crate::configuration::oidc::{DISCOVERY_REQUIRED_SCOPE, fetch_discovery};
14use crate::configuration::tokens::default_http_client;
15
16use super::{
17 DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
18 DEFAULT_QVM_URL, LoadError, env_or_default_quilc_url, env_or_default_qvm_url,
19 expand_path_from_env_or_default,
20};
21
22pub const DEFAULT_LOGIN_SCOPES: [&str; 4] = [
31 DISCOVERY_REQUIRED_SCOPE,
32 "profile",
33 "email",
34 "offline_access",
35];
36
37pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
39pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
41
42#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
44pub struct Settings {
45 #[serde(default = "default_profile_name")]
47 pub default_profile_name: String,
48
49 #[serde(default = "default_profiles")]
51 pub profiles: HashMap<String, Profile>,
52
53 #[serde(default = "default_auth_servers")]
55 pub auth_servers: HashMap<String, AuthServer>,
56
57 #[serde(skip)]
60 pub file_path: Option<PathBuf>,
61}
62
63impl Settings {
64 pub fn load() -> Result<Self, LoadError> {
71 let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
72 #[cfg(feature = "tracing")]
73 tracing::debug!("loading QCS settings from {path:?}");
74 Self::load_from_path(&path)
75 }
76
77 pub fn load_from_path(path: &PathBuf) -> Result<Self, LoadError> {
83 let mut settings: Self = Figment::from(Toml::file(path)).extract()?;
84 settings.file_path = Some(path.into());
85 Ok(settings)
86 }
87}
88
89impl Default for Settings {
90 fn default() -> Self {
91 Self {
92 default_profile_name: default_profile_name(),
93 profiles: default_profiles(),
94 auth_servers: default_auth_servers(),
95 file_path: None,
96 }
97 }
98}
99
100fn default_profile_name() -> String {
101 DEFAULT_PROFILE_NAME.to_string()
102}
103
104fn default_profiles() -> HashMap<String, Profile> {
105 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
106}
107
108fn default_auth_servers() -> HashMap<String, AuthServer> {
109 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
115pub struct Profile {
116 #[serde(default = "default_api_url")]
118 pub api_url: String,
119 #[serde(default = "default_grpc_api_url")]
121 pub grpc_api_url: String,
122 #[serde(default = "default_profile_name")]
124 pub auth_server_name: String,
125 #[serde(default = "default_profile_name")]
127 pub credentials_name: String,
128 #[serde(default)]
130 pub applications: Applications,
131}
132
133impl Default for Profile {
134 fn default() -> Self {
135 Self {
136 api_url: DEFAULT_API_URL.to_string(),
137 grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
138 auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
139 credentials_name: DEFAULT_PROFILE_NAME.to_string(),
140 applications: Applications::default(),
141 }
142 }
143}
144
145fn default_api_url() -> String {
146 DEFAULT_API_URL.to_string()
147}
148
149fn default_grpc_api_url() -> String {
150 DEFAULT_GRPC_API_URL.to_string()
151}
152
153pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
154pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
155 "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
156
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
159#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
160#[cfg_attr(
161 feature = "python",
162 pyo3::pyclass(
163 module = "qcs_api_client_common._qcs_api_client_common.configuration",
164 eq,
165 get_all,
166 set_all,
167 from_py_object
168 )
169)]
170pub struct AuthServer {
171 pub client_id: String,
173 pub issuer: String,
184
185 pub scopes: Option<Vec<String>>,
189}
190
191impl Default for AuthServer {
192 fn default() -> Self {
193 Self {
194 client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
195 issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
196 scopes: None,
197 }
198 }
199}
200
201impl AuthServer {
202 #[must_use]
207 pub const fn new(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
208 Self {
209 client_id,
210 issuer,
211 scopes,
212 }
213 }
214
215 pub async fn new_with_discovery_supported_scopes(
224 client_id: String,
225 issuer: String,
226 ) -> Result<Self, DiscoveryError> {
227 let client = default_http_client()?;
228 let discovery = fetch_discovery(&client, &issuer).await?;
229 Ok(Self {
230 client_id,
231 issuer,
232 scopes: Some(discovery.scopes_supported),
233 })
234 }
235}
236
237#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
239pub struct Applications {
240 #[serde(default)]
242 pub pyquil: Pyquil,
243}
244
245#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
247pub struct Pyquil {
248 #[serde(default = "env_or_default_qvm_url")]
250 pub qvm_url: String,
251
252 #[serde(default = "env_or_default_quilc_url")]
254 pub quilc_url: String,
255}
256
257impl Default for Pyquil {
258 fn default() -> Self {
259 Self {
260 quilc_url: DEFAULT_QUILC_URL.to_string(),
261 qvm_url: DEFAULT_QVM_URL.to_string(),
262 }
263 }
264}
265
266#[cfg(test)]
267mod test {
268 #![allow(clippy::result_large_err, reason = "happens in figment tests")]
269
270 use std::path::PathBuf;
271
272 use super::{SETTINGS_PATH_VAR, Settings};
273
274 #[test]
275 fn returns_err_if_invalid_path_env() {
276 figment::Jail::expect_with(|jail| {
277 jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
278 Settings::load().expect_err("Should return error when a file cannot be found.");
279 Ok(())
280 });
281 }
282
283 #[test]
284 fn test_uses_defaults_incomplete_settings() {
285 figment::Jail::expect_with(|jail| {
286 let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
287 jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
288 let loaded = Settings::load().expect("should load settings");
289 let expected = Settings {
290 default_profile_name: "TEST".to_string(),
291 file_path: Some(PathBuf::from("settings.toml")),
292 ..Settings::default()
293 };
294
295 assert_eq!(loaded, expected);
296
297 Ok(())
298 });
299 }
300
301 #[test]
302 fn loads_from_env_var_path() {
303 figment::Jail::expect_with(|jail| {
304 let settings = Settings {
305 default_profile_name: "TEST".to_string(),
306 file_path: Some(PathBuf::from("secrets.toml")),
307 ..Settings::default()
308 };
309 let settings_string =
310 toml::to_string(&settings).expect("Should be able to serialize settings");
311
312 _ = jail.create_file("secrets.toml", &settings_string)?;
313 jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
314
315 assert_eq!(settings, Settings::load().unwrap());
316
317 Ok(())
318 });
319 }
320}