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