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 SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
24pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
26
27#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
29pub struct Settings {
30 #[serde(default = "default_profile_name")]
32 pub default_profile_name: String,
33
34 #[serde(default = "default_profiles")]
36 pub profiles: HashMap<String, Profile>,
37
38 #[serde(default = "default_auth_servers")]
40 pub auth_servers: HashMap<String, AuthServer>,
41
42 #[serde(skip)]
45 pub file_path: Option<PathBuf>,
46}
47
48impl Settings {
49 pub fn load() -> Result<Self, LoadError> {
56 let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
57 #[cfg(feature = "tracing")]
58 tracing::debug!("loading QCS settings from {path:?}");
59 Self::load_from_path(&path)
60 }
61
62 pub fn load_from_path(path: &PathBuf) -> Result<Self, LoadError> {
68 let mut settings: Self = Figment::from(Toml::file(path)).extract()?;
69 settings.file_path = Some(path.into());
70 Ok(settings)
71 }
72}
73
74impl Default for Settings {
75 fn default() -> Self {
76 Self {
77 default_profile_name: default_profile_name(),
78 profiles: default_profiles(),
79 auth_servers: default_auth_servers(),
80 file_path: None,
81 }
82 }
83}
84
85fn default_profile_name() -> String {
86 DEFAULT_PROFILE_NAME.to_string()
87}
88
89fn default_profiles() -> HashMap<String, Profile> {
90 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
91}
92
93fn default_auth_servers() -> HashMap<String, AuthServer> {
94 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
100pub struct Profile {
101 #[serde(default = "default_api_url")]
103 pub api_url: String,
104 #[serde(default = "default_grpc_api_url")]
106 pub grpc_api_url: String,
107 #[serde(default = "default_profile_name")]
109 pub auth_server_name: String,
110 #[serde(default = "default_profile_name")]
112 pub credentials_name: String,
113 #[serde(default)]
115 pub applications: Applications,
116}
117
118impl Default for Profile {
119 fn default() -> Self {
120 Self {
121 api_url: DEFAULT_API_URL.to_string(),
122 grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
123 auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
124 credentials_name: DEFAULT_PROFILE_NAME.to_string(),
125 applications: Applications::default(),
126 }
127 }
128}
129
130fn default_api_url() -> String {
131 DEFAULT_API_URL.to_string()
132}
133
134fn default_grpc_api_url() -> String {
135 DEFAULT_GRPC_API_URL.to_string()
136}
137
138pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
139pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
140 "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
141
142#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
144#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
145#[cfg_attr(
146 feature = "python",
147 pyo3::pyclass(
148 module = "qcs_api_client_common._qcs_api_client_common.configuration",
149 eq,
150 get_all,
151 set_all,
152 from_py_object
153 )
154)]
155pub struct AuthServer {
156 pub client_id: String,
158 pub issuer: String,
169
170 pub scopes: Option<Vec<String>>,
174}
175
176impl Default for AuthServer {
177 fn default() -> Self {
178 Self {
179 client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
180 issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
181 scopes: Some(vec![DISCOVERY_REQUIRED_SCOPE.to_string()]),
182 }
183 }
184}
185
186impl AuthServer {
187 #[must_use]
192 pub const fn new(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
193 Self {
194 client_id,
195 issuer,
196 scopes,
197 }
198 }
199
200 pub async fn new_with_discovery_supported_scopes(
205 client_id: String,
206 issuer: String,
207 ) -> Result<Self, DiscoveryError> {
208 let client = default_http_client()?;
209 let discovery = fetch_discovery(&client, &issuer).await?;
210 Ok(Self {
211 client_id,
212 issuer,
213 scopes: Some(discovery.scopes_supported),
214 })
215 }
216}
217
218#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
220pub struct Applications {
221 #[serde(default)]
223 pub pyquil: Pyquil,
224}
225
226#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
228pub struct Pyquil {
229 #[serde(default = "env_or_default_qvm_url")]
231 pub qvm_url: String,
232
233 #[serde(default = "env_or_default_quilc_url")]
235 pub quilc_url: String,
236}
237
238impl Default for Pyquil {
239 fn default() -> Self {
240 Self {
241 quilc_url: DEFAULT_QUILC_URL.to_string(),
242 qvm_url: DEFAULT_QVM_URL.to_string(),
243 }
244 }
245}
246
247#[cfg(test)]
248mod test {
249 #![allow(clippy::result_large_err, reason = "happens in figment tests")]
250
251 use std::path::PathBuf;
252
253 use super::{SETTINGS_PATH_VAR, Settings};
254
255 #[test]
256 fn returns_err_if_invalid_path_env() {
257 figment::Jail::expect_with(|jail| {
258 jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
259 Settings::load().expect_err("Should return error when a file cannot be found.");
260 Ok(())
261 });
262 }
263
264 #[test]
265 fn test_uses_defaults_incomplete_settings() {
266 figment::Jail::expect_with(|jail| {
267 let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
268 jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
269 let loaded = Settings::load().expect("should load settings");
270 let expected = Settings {
271 default_profile_name: "TEST".to_string(),
272 file_path: Some(PathBuf::from("settings.toml")),
273 ..Settings::default()
274 };
275
276 assert_eq!(loaded, expected);
277
278 Ok(())
279 });
280 }
281
282 #[test]
283 fn loads_from_env_var_path() {
284 figment::Jail::expect_with(|jail| {
285 let settings = Settings {
286 default_profile_name: "TEST".to_string(),
287 file_path: Some(PathBuf::from("secrets.toml")),
288 ..Settings::default()
289 };
290 let settings_string =
291 toml::to_string(&settings).expect("Should be able to serialize settings");
292
293 _ = jail.create_file("secrets.toml", &settings_string)?;
294 jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
295
296 assert_eq!(settings, Settings::load().unwrap());
297
298 Ok(())
299 });
300 }
301}