1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use toml_edit::{value, Array, DocumentMut};
7
8#[derive(Debug, Error)]
9pub enum ConfigError {
10 #[error("failed to read {path}: {source}")]
11 Read {
12 path: PathBuf,
13 source: std::io::Error,
14 },
15 #[error("failed to write {path}: {source}")]
16 Write {
17 path: PathBuf,
18 source: std::io::Error,
19 },
20 #[error("failed to parse openauth.toml: {0}")]
21 ParseToml(#[from] toml_edit::de::Error),
22 #[error("failed to parse openauth.toml document: {0}")]
23 ParseDocument(#[from] toml_edit::TomlError),
24 #[error("failed to render openauth.toml: {0}")]
25 SerializeToml(#[from] toml_edit::ser::Error),
26 #[error("plugins.enabled must be an array")]
27 InvalidPlugins,
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(default)]
32pub struct CliConfig {
33 pub project: ProjectConfig,
34 pub database: DatabaseConfig,
35 pub security: SecurityConfig,
36 pub plugins: PluginsConfig,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(default)]
41pub struct ProjectConfig {
42 pub framework: Option<String>,
43 pub base_url: String,
44 pub base_path: String,
45 pub production: bool,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(default)]
50pub struct DatabaseConfig {
51 pub adapter: String,
52 pub provider: Option<String>,
53 pub url_env: String,
54 pub migrations_dir: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(default)]
59pub struct SecurityConfig {
60 pub secret_env: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
64#[serde(default)]
65pub struct PluginsConfig {
66 pub enabled: Vec<String>,
67}
68
69impl Default for ProjectConfig {
70 fn default() -> Self {
71 Self {
72 framework: None,
73 base_url: "http://localhost:3000/api/auth".to_owned(),
74 base_path: "/api/auth".to_owned(),
75 production: false,
76 }
77 }
78}
79
80impl Default for DatabaseConfig {
81 fn default() -> Self {
82 Self {
83 adapter: "sqlx".to_owned(),
84 provider: None,
85 url_env: "DATABASE_URL".to_owned(),
86 migrations_dir: "migrations/openauth".to_owned(),
87 }
88 }
89}
90
91impl Default for SecurityConfig {
92 fn default() -> Self {
93 Self {
94 secret_env: "OPENAUTH_SECRET".to_owned(),
95 }
96 }
97}
98
99impl CliConfig {
100 pub fn parse_str(source: &str) -> Result<Self, ConfigError> {
101 source.parse()
102 }
103
104 pub fn load(path: &Path) -> Result<Self, ConfigError> {
105 let source = fs::read_to_string(path).map_err(|source| ConfigError::Read {
106 path: path.to_path_buf(),
107 source,
108 })?;
109 Self::parse_str(&source)
110 }
111
112 pub fn load_optional(path: &Path) -> Result<Option<Self>, ConfigError> {
113 if !path.exists() {
114 return Ok(None);
115 }
116 Self::load(path).map(Some)
117 }
118
119 pub fn to_toml_string(&self) -> Result<String, ConfigError> {
120 Ok(toml_edit::ser::to_string_pretty(self)?)
121 }
122
123 pub fn write(&self, path: &Path) -> Result<(), ConfigError> {
124 let rendered = self.to_toml_string()?;
125 fs::write(path, rendered).map_err(|source| ConfigError::Write {
126 path: path.to_path_buf(),
127 source,
128 })
129 }
130
131 pub fn add_plugin_to_document(source: &str, plugin: &str) -> Result<String, ConfigError> {
132 let mut document = source.parse::<DocumentMut>()?;
133 ensure_plugin_array(&mut document)?;
134 let enabled = document["plugins"]["enabled"]
135 .as_array_mut()
136 .ok_or(ConfigError::InvalidPlugins)?;
137 if !enabled.iter().any(|item| item.as_str() == Some(plugin)) {
138 enabled.push(plugin);
139 }
140 Ok(document.to_string())
141 }
142
143 pub fn remove_plugin_from_document(source: &str, plugin: &str) -> Result<String, ConfigError> {
144 let mut document = source.parse::<DocumentMut>()?;
145 ensure_plugin_array(&mut document)?;
146 let enabled = document["plugins"]["enabled"]
147 .as_array_mut()
148 .ok_or(ConfigError::InvalidPlugins)?;
149 enabled.retain(|item| item.as_str() != Some(plugin));
150 Ok(document.to_string())
151 }
152}
153
154impl std::str::FromStr for CliConfig {
155 type Err = ConfigError;
156
157 fn from_str(source: &str) -> Result<Self, Self::Err> {
158 Ok(toml_edit::de::from_str(source)?)
159 }
160}
161
162fn ensure_plugin_array(document: &mut DocumentMut) -> Result<(), ConfigError> {
163 if document["plugins"].is_none() {
164 document["plugins"] = toml_edit::table();
165 }
166 if document["plugins"]["enabled"].is_none() {
167 document["plugins"]["enabled"] = value(Array::default());
168 }
169 if !document["plugins"]["enabled"].is_array() {
170 return Err(ConfigError::InvalidPlugins);
171 }
172 Ok(())
173}