Skip to main content

haste_health/cli/
config.rs

1//! Schema and on-disk persistence for named server connection profiles
2//! (`~/.haste_health/config.toml`). The `config` *command* (create/delete/list profiles)
3//! lives in [`crate::commands::config`]; this module only owns the data.
4//!
5//! Secret material (client secrets, cached OAuth tokens) is never stored here — see
6//! [`crate::cli::secrets`] — so this file is safe to inspect, back up, or share.
7
8use haste_fhir_model::r4::generated::terminology::IssueType;
9use haste_fhir_operation_error::OperationOutcomeError;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13#[derive(Serialize, Deserialize, Debug, Default)]
14pub(crate) struct CliConfiguration {
15    pub(crate) active_profile: Option<String>,
16    pub(crate) profiles: Vec<Profile>,
17}
18
19impl CliConfiguration {
20    pub(crate) fn current_profile(&self) -> Option<&Profile> {
21        if let Some(active_profile_id) = self.active_profile.as_ref() {
22            self.profiles.iter().find(|p| &p.name == active_profile_id)
23        } else {
24            None
25        }
26    }
27}
28
29/// A named connection to a FHIR server plus how the CLI should authenticate to it.
30#[derive(Serialize, Deserialize, Debug, Clone)]
31pub(crate) struct Profile {
32    pub(crate) name: String,
33    pub(crate) r4_url: String,
34    pub(crate) oidc_discovery_uri: String,
35    pub(crate) auth: ProfileAuth,
36}
37
38/// How the CLI authenticates for a given profile. Any secret values (client secret,
39/// cached tokens) live in the separate secrets file, keyed by profile name.
40#[derive(Serialize, Deserialize, Debug, Clone)]
41pub(crate) enum ProfileAuth {
42    /// A confidential (server-to-server) client authenticated with a client secret.
43    ClientCredentails {
44        client_id: String,
45    },
46    /// A public (no secret) OIDC client authenticated by a human via the browser-based
47    /// authorization_code + PKCE flow. Run `haste-health login` to obtain tokens.
48    AuthorizationCode {
49        client_id: String,
50        redirect_uri: String,
51        scope: String,
52    },
53    Basic {
54        username: String,
55    },
56    /// No authentication; requests are sent unauthenticated.
57    Public {},
58}
59
60fn read_existing_config(location: &PathBuf) -> Result<CliConfiguration, OperationOutcomeError> {
61    let config_str = std::fs::read_to_string(location).map_err(|_| {
62        OperationOutcomeError::error(
63            IssueType::exception(),
64            format!(
65                "Failed to read config file at location '{}'",
66                location.to_string_lossy()
67            ),
68        )
69    })?;
70
71    toml::from_str::<CliConfiguration>(&config_str).map_err(|_| {
72        OperationOutcomeError::error(
73            IssueType::exception(),
74            format!(
75                "Failed to parse config file at location '{}'",
76                location.to_string_lossy()
77            ),
78        )
79    })
80}
81
82/// Loads the config file, creating a default one on disk if it doesn't exist yet.
83pub(crate) fn load_config(location: &PathBuf) -> CliConfiguration {
84    if let Ok(config) = read_existing_config(location) {
85        return config;
86    }
87
88    let config = CliConfiguration::default();
89    write_config(location, &config).expect("Failed to write default config file");
90    config
91}
92
93pub(crate) fn write_config(
94    location: &PathBuf,
95    config: &CliConfiguration,
96) -> Result<(), OperationOutcomeError> {
97    std::fs::write(location, toml::to_string(config).unwrap()).map_err(|_| {
98        OperationOutcomeError::error(
99            IssueType::exception(),
100            format!(
101                "Failed to write config file at location '{}'",
102                location.to_string_lossy()
103            ),
104        )
105    })
106}