use std::collections::HashMap;
use std::path::PathBuf;
use figment::providers::Format;
use figment::{providers::Toml, Figment};
use serde::{Deserialize, Serialize};
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass;
use crate::configuration::error::DiscoveryError;
use crate::configuration::oidc::{fetch_discovery, DISCOVERY_REQUIRED_SCOPE};
use crate::configuration::tokens::default_http_client;
use super::{
env_or_default_quilc_url, env_or_default_qvm_url, expand_path_from_env_or_default, LoadError,
DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
DEFAULT_QVM_URL,
};
pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Settings {
#[serde(default = "default_profile_name")]
pub default_profile_name: String,
#[serde(default = "default_profiles")]
pub profiles: HashMap<String, Profile>,
#[serde(default = "default_auth_servers")]
pub auth_servers: HashMap<String, AuthServer>,
#[serde(skip)]
pub file_path: Option<PathBuf>,
}
impl Settings {
pub fn load() -> Result<Self, LoadError> {
let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
#[cfg(feature = "tracing")]
tracing::debug!("loading QCS settings from {path:?}");
Self::load_from_path(&path)
}
pub fn load_from_path(path: &PathBuf) -> Result<Self, LoadError> {
let mut settings: Self = Figment::from(Toml::file(path)).extract()?;
settings.file_path = Some(path.into());
Ok(settings)
}
}
impl Default for Settings {
fn default() -> Self {
Self {
default_profile_name: default_profile_name(),
profiles: default_profiles(),
auth_servers: default_auth_servers(),
file_path: None,
}
}
}
fn default_profile_name() -> String {
DEFAULT_PROFILE_NAME.to_string()
}
fn default_profiles() -> HashMap<String, Profile> {
HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
}
fn default_auth_servers() -> HashMap<String, AuthServer> {
HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Profile {
#[serde(default = "default_api_url")]
pub api_url: String,
#[serde(default = "default_grpc_api_url")]
pub grpc_api_url: String,
#[serde(default = "default_profile_name")]
pub auth_server_name: String,
#[serde(default = "default_profile_name")]
pub credentials_name: String,
#[serde(default)]
pub applications: Applications,
}
impl Default for Profile {
fn default() -> Self {
Self {
api_url: DEFAULT_API_URL.to_string(),
grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
credentials_name: DEFAULT_PROFILE_NAME.to_string(),
applications: Applications::default(),
}
}
}
fn default_api_url() -> String {
DEFAULT_API_URL.to_string()
}
fn default_grpc_api_url() -> String {
DEFAULT_GRPC_API_URL.to_string()
}
pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
"https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_api_client_common.configuration", eq, get_all, set_all)
)]
pub struct AuthServer {
pub client_id: String,
pub issuer: String,
pub scopes: Option<Vec<String>>,
}
impl Default for AuthServer {
fn default() -> Self {
Self {
client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
scopes: Some(vec![DISCOVERY_REQUIRED_SCOPE.to_string()]),
}
}
}
impl AuthServer {
#[must_use]
pub const fn new(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
Self {
client_id,
issuer,
scopes,
}
}
pub async fn new_with_discovery_supported_scopes(
client_id: String,
issuer: String,
) -> Result<Self, DiscoveryError> {
let client = default_http_client()?;
let discovery = fetch_discovery(&client, &issuer).await?;
Ok(Self {
client_id,
issuer,
scopes: Some(discovery.scopes_supported),
})
}
}
#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Applications {
#[serde(default)]
pub pyquil: Pyquil,
}
#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Pyquil {
#[serde(default = "env_or_default_qvm_url")]
pub qvm_url: String,
#[serde(default = "env_or_default_quilc_url")]
pub quilc_url: String,
}
impl Default for Pyquil {
fn default() -> Self {
Self {
quilc_url: DEFAULT_QUILC_URL.to_string(),
qvm_url: DEFAULT_QVM_URL.to_string(),
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::result_large_err, reason = "happens in figment tests")]
use std::path::PathBuf;
use super::{Settings, SETTINGS_PATH_VAR};
#[test]
fn returns_err_if_invalid_path_env() {
figment::Jail::expect_with(|jail| {
jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
Settings::load().expect_err("Should return error when a file cannot be found.");
Ok(())
});
}
#[test]
fn test_uses_defaults_incomplete_settings() {
figment::Jail::expect_with(|jail| {
let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
let loaded = Settings::load().expect("should load settings");
let expected = Settings {
default_profile_name: "TEST".to_string(),
file_path: Some(PathBuf::from("settings.toml")),
..Settings::default()
};
assert_eq!(loaded, expected);
Ok(())
});
}
#[test]
fn loads_from_env_var_path() {
figment::Jail::expect_with(|jail| {
let settings = Settings {
default_profile_name: "TEST".to_string(),
file_path: Some(PathBuf::from("secrets.toml")),
..Settings::default()
};
let settings_string =
toml::to_string(&settings).expect("Should be able to serialize settings");
_ = jail.create_file("secrets.toml", &settings_string)?;
jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
assert_eq!(settings, Settings::load().unwrap());
Ok(())
});
}
}