qcs_api_client_common/configuration/
settings.rs1use std::collections::HashMap;
3use std::path::PathBuf;
4
5use figment::providers::Format;
6use figment::{providers::Toml, Figment};
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::{fetch_discovery, DISCOVERY_REQUIRED_SCOPE};
14use crate::configuration::tokens::default_http_client;
15
16use super::{
17 env_or_default_quilc_url, env_or_default_qvm_url, expand_path_from_env_or_default, LoadError,
18 DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
19 DEFAULT_QVM_URL,
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(module = "qcs_api_client_common.configuration", eq, get_all, set_all)
148)]
149pub struct AuthServer {
150 pub client_id: String,
152 pub issuer: String,
163
164 pub scopes: Option<Vec<String>>,
168}
169
170impl Default for AuthServer {
171 fn default() -> Self {
172 Self {
173 client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
174 issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
175 scopes: Some(vec![DISCOVERY_REQUIRED_SCOPE.to_string()]),
176 }
177 }
178}
179
180impl AuthServer {
181 #[must_use]
186 pub const fn new(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
187 Self {
188 client_id,
189 issuer,
190 scopes,
191 }
192 }
193
194 pub async fn new_with_discovery_supported_scopes(
199 client_id: String,
200 issuer: String,
201 ) -> Result<Self, DiscoveryError> {
202 let client = default_http_client()?;
203 let discovery = fetch_discovery(&client, &issuer).await?;
204 Ok(Self {
205 client_id,
206 issuer,
207 scopes: Some(discovery.scopes_supported),
208 })
209 }
210}
211
212#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
214pub struct Applications {
215 #[serde(default)]
217 pub pyquil: Pyquil,
218}
219
220#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
222pub struct Pyquil {
223 #[serde(default = "env_or_default_qvm_url")]
225 pub qvm_url: String,
226
227 #[serde(default = "env_or_default_quilc_url")]
229 pub quilc_url: String,
230}
231
232impl Default for Pyquil {
233 fn default() -> Self {
234 Self {
235 quilc_url: DEFAULT_QUILC_URL.to_string(),
236 qvm_url: DEFAULT_QVM_URL.to_string(),
237 }
238 }
239}
240
241#[cfg(test)]
242mod test {
243 use std::path::PathBuf;
244
245 use super::{Settings, SETTINGS_PATH_VAR};
246
247 #[test]
248 fn returns_err_if_invalid_path_env() {
249 figment::Jail::expect_with(|jail| {
250 jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
251 Settings::load().expect_err("Should return error when a file cannot be found.");
252 Ok(())
253 });
254 }
255
256 #[test]
257 fn test_uses_defaults_incomplete_settings() {
258 figment::Jail::expect_with(|jail| {
259 let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
260 jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
261 let loaded = Settings::load().expect("should load settings");
262 let expected = Settings {
263 default_profile_name: "TEST".to_string(),
264 file_path: Some(PathBuf::from("settings.toml")),
265 ..Settings::default()
266 };
267
268 assert_eq!(loaded, expected);
269
270 Ok(())
271 });
272 }
273
274 #[test]
275 fn loads_from_env_var_path() {
276 figment::Jail::expect_with(|jail| {
277 let settings = Settings {
278 default_profile_name: "TEST".to_string(),
279 file_path: Some(PathBuf::from("secrets.toml")),
280 ..Settings::default()
281 };
282 let settings_string =
283 toml::to_string(&settings).expect("Should be able to serialize settings");
284
285 _ = jail.create_file("secrets.toml", &settings_string)?;
286 jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
287
288 assert_eq!(settings, Settings::load().unwrap());
289
290 Ok(())
291 });
292 }
293}