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>>,
186}
187
188impl Default for AuthServer {
189 fn default() -> Self {
190 Self {
191 client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
192 issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
193 scopes: None,
194 }
195 }
196}
197
198impl AuthServer {
199 #[must_use]
204 pub const fn new(client_id: String, issuer: String, scopes: Option<BTreeSet<String>>) -> Self {
205 Self {
206 client_id,
207 issuer,
208 scopes,
209 }
210 }
211
212 pub async fn new_with_discovery_supported_scopes(
222 client_id: String,
223 issuer: String,
224 ) -> Result<Self, DiscoveryError> {
225 let client = default_http_client()?;
226 let discovery = fetch_discovery(&client, &issuer).await?;
227 Ok(Self {
228 client_id,
229 issuer,
230 scopes: discovery.scopes_supported,
231 })
232 }
233}
234
235#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
237pub struct Applications {
238 #[serde(default)]
240 pub pyquil: Pyquil,
241}
242
243#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
245pub struct Pyquil {
246 #[serde(default = "env_or_default_qvm_url")]
248 pub qvm_url: String,
249
250 #[serde(default = "env_or_default_quilc_url")]
252 pub quilc_url: String,
253}
254
255impl Default for Pyquil {
256 fn default() -> Self {
257 Self {
258 quilc_url: DEFAULT_QUILC_URL.to_string(),
259 qvm_url: DEFAULT_QVM_URL.to_string(),
260 }
261 }
262}
263
264#[cfg(test)]
265mod test {
266 #![allow(clippy::result_large_err, reason = "happens in figment tests")]
267
268 use std::path::PathBuf;
269
270 use super::{SETTINGS_PATH_VAR, Settings};
271
272 #[test]
273 fn returns_err_if_invalid_path_env() {
274 figment::Jail::expect_with(|jail| {
275 jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
276 Settings::load().expect_err("Should return error when a file cannot be found.");
277 Ok(())
278 });
279 }
280
281 #[test]
282 fn test_uses_defaults_incomplete_settings() {
283 figment::Jail::expect_with(|jail| {
284 let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
285 jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
286 let loaded = Settings::load().expect("should load settings");
287 let expected = Settings {
288 default_profile_name: "TEST".to_string(),
289 file_path: Some(PathBuf::from("settings.toml")),
290 ..Settings::default()
291 };
292
293 assert_eq!(loaded, expected);
294
295 Ok(())
296 });
297 }
298
299 #[test]
300 fn loads_from_env_var_path() {
301 figment::Jail::expect_with(|jail| {
302 let settings = Settings {
303 default_profile_name: "TEST".to_string(),
304 file_path: Some(PathBuf::from("secrets.toml")),
305 ..Settings::default()
306 };
307 let settings_string =
308 toml::to_string(&settings).expect("Should be able to serialize settings");
309
310 _ = jail.create_file("secrets.toml", &settings_string)?;
311 jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
312
313 assert_eq!(settings, Settings::load().unwrap());
314
315 Ok(())
316 });
317 }
318}