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
9use crate::configuration::error::DiscoveryError;
10use crate::configuration::oidc::{fetch_discovery, DISCOVERY_REQUIRED_SCOPE};
11use crate::configuration::tokens::default_http_client;
12
13use super::{
14 env_or_default_quilc_url, env_or_default_qvm_url, expand_path_from_env_or_default, LoadError,
15 DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
16 DEFAULT_QVM_URL,
17};
18
19pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
21pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
23
24#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
26pub struct Settings {
27 #[serde(default = "default_profile_name")]
29 pub default_profile_name: String,
30
31 #[serde(default = "default_profiles")]
33 pub profiles: HashMap<String, Profile>,
34
35 #[serde(default = "default_auth_servers")]
37 pub auth_servers: HashMap<String, AuthServer>,
38
39 #[serde(skip)]
42 pub file_path: Option<PathBuf>,
43}
44
45impl Settings {
46 pub fn load() -> Result<Self, LoadError> {
53 let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
54 #[cfg(feature = "tracing")]
55 tracing::debug!("loading QCS settings from {path:?}");
56 Self::load_from_path(&path)
57 }
58
59 pub fn load_from_path(path: &PathBuf) -> Result<Self, LoadError> {
65 let mut settings: Self = Figment::from(Toml::file(path)).extract()?;
66 settings.file_path = Some(path.into());
67 Ok(settings)
68 }
69}
70
71impl Default for Settings {
72 fn default() -> Self {
73 Self {
74 default_profile_name: default_profile_name(),
75 profiles: default_profiles(),
76 auth_servers: default_auth_servers(),
77 file_path: None,
78 }
79 }
80}
81
82fn default_profile_name() -> String {
83 DEFAULT_PROFILE_NAME.to_string()
84}
85
86fn default_profiles() -> HashMap<String, Profile> {
87 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
88}
89
90fn default_auth_servers() -> HashMap<String, AuthServer> {
91 HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
97pub struct Profile {
98 #[serde(default = "default_api_url")]
100 pub api_url: String,
101 #[serde(default = "default_grpc_api_url")]
103 pub grpc_api_url: String,
104 #[serde(default = "default_profile_name")]
106 pub auth_server_name: String,
107 #[serde(default = "default_profile_name")]
109 pub credentials_name: String,
110 #[serde(default)]
112 pub applications: Applications,
113}
114
115impl Default for Profile {
116 fn default() -> Self {
117 Self {
118 api_url: DEFAULT_API_URL.to_string(),
119 grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
120 auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
121 credentials_name: DEFAULT_PROFILE_NAME.to_string(),
122 applications: Applications::default(),
123 }
124 }
125}
126
127fn default_api_url() -> String {
128 DEFAULT_API_URL.to_string()
129}
130
131fn default_grpc_api_url() -> String {
132 DEFAULT_GRPC_API_URL.to_string()
133}
134
135pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
136pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
137 "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
138
139#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
141#[cfg_attr(feature = "python", pyo3::pyclass)]
142pub struct AuthServer {
143 pub client_id: String,
145 pub issuer: String,
156
157 pub scopes: Option<Vec<String>>,
161}
162
163impl Default for AuthServer {
164 fn default() -> Self {
165 Self {
166 client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
167 issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
168 scopes: Some(vec![DISCOVERY_REQUIRED_SCOPE.to_string()]),
169 }
170 }
171}
172
173impl AuthServer {
174 #[must_use]
179 pub const fn new(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
180 Self {
181 client_id,
182 issuer,
183 scopes,
184 }
185 }
186
187 pub async fn new_with_discovery_supported_scopes(
192 client_id: String,
193 issuer: String,
194 ) -> Result<Self, DiscoveryError> {
195 let client = default_http_client()?;
196 let discovery = fetch_discovery(&client, &issuer).await?;
197 Ok(Self {
198 client_id,
199 issuer,
200 scopes: Some(discovery.scopes_supported),
201 })
202 }
203}
204
205#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
207pub struct Applications {
208 #[serde(default)]
210 pub pyquil: Pyquil,
211}
212
213#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
215pub struct Pyquil {
216 #[serde(default = "env_or_default_qvm_url")]
218 pub qvm_url: String,
219
220 #[serde(default = "env_or_default_quilc_url")]
222 pub quilc_url: String,
223}
224
225impl Default for Pyquil {
226 fn default() -> Self {
227 Self {
228 quilc_url: DEFAULT_QUILC_URL.to_string(),
229 qvm_url: DEFAULT_QVM_URL.to_string(),
230 }
231 }
232}
233
234#[cfg(test)]
235mod test {
236 use std::path::PathBuf;
237
238 use super::{Settings, SETTINGS_PATH_VAR};
239
240 #[test]
241 fn returns_err_if_invalid_path_env() {
242 figment::Jail::expect_with(|jail| {
243 jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
244 Settings::load().expect_err("Should return error when a file cannot be found.");
245 Ok(())
246 });
247 }
248
249 #[test]
250 fn test_uses_defaults_incomplete_settings() {
251 figment::Jail::expect_with(|jail| {
252 let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
253 jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
254 let loaded = Settings::load().expect("should load settings");
255 let expected = Settings {
256 default_profile_name: "TEST".to_string(),
257 file_path: Some(PathBuf::from("settings.toml")),
258 ..Settings::default()
259 };
260
261 assert_eq!(loaded, expected);
262
263 Ok(())
264 });
265 }
266
267 #[test]
268 fn loads_from_env_var_path() {
269 figment::Jail::expect_with(|jail| {
270 let settings = Settings {
271 default_profile_name: "TEST".to_string(),
272 file_path: Some(PathBuf::from("secrets.toml")),
273 ..Settings::default()
274 };
275 let settings_string =
276 toml::to_string(&settings).expect("Should be able to serialize settings");
277
278 _ = jail.create_file("secrets.toml", &settings_string)?;
279 jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
280
281 assert_eq!(settings, Settings::load().unwrap());
282
283 Ok(())
284 });
285 }
286}