qcs_api_client_common/configuration/
settings.rs

1//! Models and utilities for managing QCS settings.
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5use figment::providers::Format;
6use figment::{providers::Toml, Figment};
7use serde::{Deserialize, Serialize};
8
9use super::{
10    env_or_default_quilc_url, env_or_default_qvm_url, expand_path_from_env_or_default, LoadError,
11    DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
12    DEFAULT_QVM_URL,
13};
14
15/// Setting the `QCS_SETTINGS_FILE_PATH` environment variable will change which file is used for loading [`Settings`].
16pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
17/// The default path that [`Settings`] will be loaded from;
18pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
19
20/// The structure of QCS settings, typically serialized as a TOML file at [`DEFAULT_SETTINGS_PATH`].
21#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
22pub struct Settings {
23    /// The default profile to use - this should match a key of [`Settings::profiles`].
24    #[serde(default = "default_profile_name")]
25    pub default_profile_name: String,
26
27    /// All named [`Profile`]s defined in the settings file.
28    #[serde(default = "default_profiles")]
29    pub profiles: HashMap<String, Profile>,
30
31    /// All named [`AuthServer`]s defined in the settings file.
32    #[serde(default = "default_auth_servers")]
33    pub auth_servers: HashMap<String, AuthServer>,
34
35    /// The path to the settings file this [`Settings`] was loaded from,
36    /// if it was loaded from a file. This is not stored in the settings file itself.
37    #[serde(skip)]
38    pub file_path: Option<PathBuf>,
39}
40
41impl Settings {
42    /// Load [`Settings`] from the path specified by the [`SETTINGS_PATH_VAR`] environment variable if set,
43    /// or else the default path at [`DEFAULT_SETTINGS_PATH`].
44    ///
45    /// # Errors
46    ///
47    /// [`LoadError`] if the settings file cannot be loaded.
48    pub fn load() -> Result<Self, LoadError> {
49        let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
50        #[cfg(feature = "tracing")]
51        tracing::debug!("loading QCS settings from {path:?}");
52        Self::load_from_path(&path)
53    }
54
55    /// Load [`Settings`] from the path specified by `path`.
56    ///
57    /// # Errors
58    ///
59    /// [`LoadError`] if the settings file cannot be loaded.
60    pub fn load_from_path(path: &PathBuf) -> Result<Self, LoadError> {
61        let mut settings: Self = Figment::from(Toml::file(path)).extract()?;
62        settings.file_path = Some(path.into());
63        Ok(settings)
64    }
65}
66
67impl Default for Settings {
68    fn default() -> Self {
69        Self {
70            default_profile_name: default_profile_name(),
71            profiles: default_profiles(),
72            auth_servers: default_auth_servers(),
73            file_path: None,
74        }
75    }
76}
77
78fn default_profile_name() -> String {
79    DEFAULT_PROFILE_NAME.to_string()
80}
81
82fn default_profiles() -> HashMap<String, Profile> {
83    HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
84}
85
86fn default_auth_servers() -> HashMap<String, AuthServer> {
87    HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
88}
89
90/// A particular profile of [`Settings`], which defines all the configurable options
91/// for connecting to a particular QCS instance using a particular set of credentials.
92#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
93pub struct Profile {
94    /// URL of the QCS REST API.
95    #[serde(default = "default_api_url")]
96    pub api_url: String,
97    /// URL of the QCS gRPC API.
98    #[serde(default = "default_grpc_api_url")]
99    pub grpc_api_url: String,
100    /// Name of the [`AuthServer`] to use.
101    #[serde(default = "default_profile_name")]
102    pub auth_server_name: String,
103    /// Name of the [`Credential`][`super::secrets::Credential`] to use from the corresponding [`Secrets`][`super::secrets::Secrets`].
104    #[serde(default = "default_profile_name")]
105    pub credentials_name: String,
106    /// Application specific settings.
107    #[serde(default)]
108    pub applications: Applications,
109}
110
111impl Default for Profile {
112    fn default() -> Self {
113        Self {
114            api_url: DEFAULT_API_URL.to_string(),
115            grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
116            auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
117            credentials_name: DEFAULT_PROFILE_NAME.to_string(),
118            applications: Applications::default(),
119        }
120    }
121}
122
123fn default_api_url() -> String {
124    DEFAULT_API_URL.to_string()
125}
126
127fn default_grpc_api_url() -> String {
128    DEFAULT_GRPC_API_URL.to_string()
129}
130
131pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
132pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
133    "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
134
135/// OAuth 2.0 authorization server.
136#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
137#[cfg_attr(feature = "python", pyo3::pyclass)]
138pub struct AuthServer {
139    /// OAuth 2.0 client id.
140    pub client_id: String,
141    /// OAuth 2.0 issuer URL.
142    ///
143    /// This is the base URL of the identity provider.
144    /// For Okta, this usually looks like `https://example.okta.com/oauth2/default`.
145    /// For Cognito, it might look like `https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example`.
146    ///
147    /// Note that this is technically distinct from the `issuer` field in [`OidcDiscovery`],
148    /// which is the canonical URI that the identity provider uses to sign and validate tokens,
149    /// but the OpenID specification requires that they match exactly,
150    /// and that they match the `iss` claim in Tokens issued by this identity provider.
151    pub issuer: String,
152}
153
154impl Default for AuthServer {
155    fn default() -> Self {
156        Self {
157            client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
158            issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
159        }
160    }
161}
162
163impl AuthServer {
164    /// Create a new [`AuthServer`] with a ``client_id`` and ``issuer``.
165    #[must_use]
166    pub const fn new(client_id: String, issuer: String) -> Self {
167        Self { client_id, issuer }
168    }
169}
170
171/// Settings for secondary applications used by QCS SDKs.
172#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
173pub struct Applications {
174    /// Settings for use of the pyquil SDK.
175    #[serde(default)]
176    pub pyquil: Pyquil,
177}
178
179/// Settings for secondary applications used by pyquil.
180#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
181pub struct Pyquil {
182    /// URL of the QVM server.
183    #[serde(default = "env_or_default_qvm_url")]
184    pub qvm_url: String,
185
186    /// URL of the Quilc compiler server.
187    #[serde(default = "env_or_default_quilc_url")]
188    pub quilc_url: String,
189}
190
191impl Default for Pyquil {
192    fn default() -> Self {
193        Self {
194            quilc_url: DEFAULT_QUILC_URL.to_string(),
195            qvm_url: DEFAULT_QVM_URL.to_string(),
196        }
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use std::path::PathBuf;
203
204    use super::{Settings, SETTINGS_PATH_VAR};
205
206    #[test]
207    fn returns_err_if_invalid_path_env() {
208        figment::Jail::expect_with(|jail| {
209            jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
210            Settings::load().expect_err("Should return error when a file cannot be found.");
211            Ok(())
212        });
213    }
214
215    #[test]
216    fn test_uses_defaults_incomplete_settings() {
217        figment::Jail::expect_with(|jail| {
218            let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
219            jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
220            let loaded = Settings::load().expect("should load settings");
221            let expected = Settings {
222                default_profile_name: "TEST".to_string(),
223                file_path: Some(PathBuf::from("settings.toml")),
224                ..Settings::default()
225            };
226
227            assert_eq!(loaded, expected);
228
229            Ok(())
230        });
231    }
232
233    #[test]
234    fn loads_from_env_var_path() {
235        figment::Jail::expect_with(|jail| {
236            let settings = Settings {
237                default_profile_name: "TEST".to_string(),
238                file_path: Some(PathBuf::from("secrets.toml")),
239                ..Settings::default()
240            };
241            let settings_string =
242                toml::to_string(&settings).expect("Should be able to serialize settings");
243
244            _ = jail.create_file("secrets.toml", &settings_string)?;
245            jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
246
247            assert_eq!(settings, Settings::load().unwrap());
248
249            Ok(())
250        });
251    }
252}