1use super::defaults::{
2 default_case_group, default_reason_mode, default_smtp_port, default_timezone_utc_offset,
3 default_timezone_utc_offset_option, default_true,
4};
5use super::validation::validate_language_bcp47;
6use crate::error::{AppError, Result};
7use agent_first_data::normalize_utc_offset;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
11#[serde(deny_unknown_fields)]
12pub struct CaseSection {
13 #[serde(default = "default_case_group")]
14 pub default_group: String,
15}
16
17impl Default for CaseSection {
18 fn default() -> Self {
19 Self {
20 default_group: default_case_group(),
21 }
22 }
23}
24
25#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[serde(deny_unknown_fields)]
27pub struct AuditSection {
28 #[serde(default = "default_reason_mode")]
29 pub reason_mode: ReasonMode,
30}
31
32impl Default for AuditSection {
33 fn default() -> Self {
34 Self {
35 reason_mode: default_reason_mode(),
36 }
37 }
38}
39
40#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
41#[serde(rename_all = "snake_case")]
42pub enum ReasonMode {
43 Required,
44 Optional,
45}
46
47impl ReasonMode {
48 pub fn as_str(self) -> &'static str {
49 match self {
50 ReasonMode::Required => "required",
51 ReasonMode::Optional => "optional",
52 }
53 }
54
55 pub(super) fn parse(value: &str) -> Result<Self> {
56 match value {
57 "required" => Ok(Self::Required),
58 "optional" => Ok(Self::Optional),
59 _ => Err(AppError::new(
60 "invalid_request",
61 "audit.reason_mode expects required or optional",
62 )),
63 }
64 }
65}
66
67#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
68#[serde(deny_unknown_fields)]
69pub struct SmtpSection {
70 pub host: Option<String>,
71 #[serde(default = "default_smtp_port")]
72 pub port: u16,
73 #[serde(default = "default_true")]
74 pub starttls: bool,
75 #[serde(default)]
76 pub tls_wrapper: bool,
77 pub username: Option<String>,
78 pub password_secret: Option<String>,
79 pub password_secret_env: Option<String>,
80 pub from: Option<String>,
81}
82
83impl Default for SmtpSection {
84 fn default() -> Self {
85 Self {
86 host: None,
87 port: default_smtp_port(),
88 starttls: true,
89 tls_wrapper: false,
90 username: None,
91 password_secret: None,
92 password_secret_env: None,
93 from: None,
94 }
95 }
96}
97
98#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
99#[serde(deny_unknown_fields)]
100pub struct WorkspaceSection {
101 #[serde(default)]
102 pub language_bcp47: Option<String>,
103 #[serde(default = "default_timezone_utc_offset_option")]
104 pub timezone_utc_offset: Option<String>,
105}
106
107impl Default for WorkspaceSection {
108 fn default() -> Self {
109 Self {
110 language_bcp47: None,
111 timezone_utc_offset: Some(default_timezone_utc_offset()),
112 }
113 }
114}
115
116impl WorkspaceSection {
117 pub(super) fn validate(&self) -> Result<()> {
118 if let Some(language) = self.language_bcp47.as_deref() {
119 validate_language_bcp47("workspace.language_bcp47", language, "config_invalid")?;
120 }
121 if let Some(offset) = self.timezone_utc_offset.as_deref() {
122 let normalized = normalize_utc_offset(offset).ok_or_else(|| {
123 AppError::new(
124 "config_invalid",
125 "workspace.timezone_utc_offset expects UTC or a fixed offset like +08:00",
126 )
127 })?;
128 if normalized != offset {
129 return Err(AppError::new(
130 "config_invalid",
131 "workspace.timezone_utc_offset must be canonical UTC or ±HH:MM",
132 ));
133 }
134 }
135 Ok(())
136 }
137}
138
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
140pub enum TemplateLanguage {
141 #[default]
142 EnUs,
143 ZhCn,
144}
145
146impl TemplateLanguage {
147 pub const ALL: [Self; 2] = [Self::EnUs, Self::ZhCn];
148
149 pub fn as_str(self) -> &'static str {
150 match self {
151 TemplateLanguage::EnUs => "en-US",
152 TemplateLanguage::ZhCn => "zh-CN",
153 }
154 }
155
156 pub fn from_bcp47(value: &str) -> Self {
157 let lower = value.trim().to_ascii_lowercase();
158 if lower == "zh" || lower.starts_with("zh-") {
159 Self::ZhCn
160 } else {
161 Self::EnUs
162 }
163 }
164}
165
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct SmtpConfig {
168 pub host: String,
169 pub port: u16,
170 pub starttls: bool,
171 pub tls_wrapper: bool,
172 pub username: Option<String>,
173 pub password_secret: Option<String>,
174 pub from: String,
175}