reinhardt_deeplink/
settings.rs1#![allow(deprecated)]
14
15use reinhardt_core::macros::settings;
16use serde::{Deserialize, Serialize};
17
18use crate::config::{
19 AndroidConfig, AndroidConfigBuilder, CustomScheme, DeeplinkConfig, IosConfig, IosConfigBuilder,
20};
21
22#[settings(fragment = true)]
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
30pub struct IosSettings {
31 #[serde(default)]
33 pub app_id: String,
34 #[serde(default)]
36 pub paths: Vec<String>,
37 #[serde(default)]
39 pub exclude_paths: Vec<String>,
40 #[serde(default)]
42 pub app_clips: Vec<String>,
43 #[serde(default)]
45 pub web_credentials: bool,
46}
47
48#[settings(fragment = true)]
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
56pub struct AndroidSettings {
57 #[serde(default)]
59 pub package_name: String,
60 #[serde(default)]
62 pub sha256_cert_fingerprints: Vec<String>,
63}
64
65#[settings(fragment = true)]
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
71pub struct CustomSchemeSettings {
72 #[serde(default)]
74 pub name: String,
75 #[serde(default)]
77 pub hosts: Vec<String>,
78 #[serde(default)]
80 pub paths: Vec<String>,
81}
82
83#[settings(fragment = true, section = "deeplink_app")]
89#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
90pub struct DeeplinkSettings {
91 #[setting(node)]
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub ios: Option<IosSettings>,
95 #[setting(node)]
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub android: Option<AndroidSettings>,
99 #[setting(node)]
101 #[serde(default)]
102 pub custom_schemes: Vec<CustomSchemeSettings>,
103}
104
105impl IosSettings {
106 fn to_config(&self) -> IosConfig {
108 let path_refs: Vec<&str> = self.paths.iter().map(String::as_str).collect();
109 let exclude_refs: Vec<&str> = self.exclude_paths.iter().map(String::as_str).collect();
110
111 let mut builder = IosConfigBuilder::new()
112 .app_id(self.app_id.clone())
113 .paths(&path_refs)
114 .exclude_paths(&exclude_refs);
115
116 for app_clip in &self.app_clips {
117 builder = builder.app_clip(app_clip.clone());
118 }
119
120 if self.web_credentials {
121 builder = builder.with_web_credentials();
122 }
123
124 builder.build()
125 }
126}
127
128impl AndroidSettings {
129 fn to_config(&self) -> AndroidConfig {
136 let fingerprint_refs: Vec<&str> = self
137 .sha256_cert_fingerprints
138 .iter()
139 .map(String::as_str)
140 .collect();
141
142 AndroidConfigBuilder::new()
143 .package_name(self.package_name.clone())
144 .sha256_fingerprints(&fingerprint_refs)
145 .build_unchecked()
146 }
147}
148
149impl From<&CustomSchemeSettings> for CustomScheme {
150 fn from(settings: &CustomSchemeSettings) -> Self {
151 CustomScheme {
152 name: settings.name.clone(),
153 hosts: settings.hosts.clone(),
154 paths: settings.paths.clone(),
155 }
156 }
157}
158
159impl From<&DeeplinkSettings> for DeeplinkConfig {
160 fn from(settings: &DeeplinkSettings) -> Self {
161 DeeplinkConfig {
162 ios: settings.ios.as_ref().map(IosSettings::to_config),
163 android: settings.android.as_ref().map(AndroidSettings::to_config),
164 custom_schemes: settings
165 .custom_schemes
166 .iter()
167 .map(CustomScheme::from)
168 .collect(),
169 }
170 }
171}
172
173pub fn create_deeplink_config_from_settings(settings: &DeeplinkSettings) -> DeeplinkConfig {
175 DeeplinkConfig::from(settings)
176}
177
178#[cfg(test)]
179mod tests {
180 use rstest::rstest;
181
182 use super::*;
183
184 const VALID_FINGERPRINT: &str = "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C";
185
186 #[rstest]
187 fn test_default_settings() {
188 let settings = DeeplinkSettings::default();
189 assert!(settings.ios.is_none());
190 assert!(settings.android.is_none());
191 assert!(settings.custom_schemes.is_empty());
192 }
193
194 #[rstest]
195 fn test_default_settings_bridge_to_empty_config() {
196 let settings = DeeplinkSettings::default();
197 let config = create_deeplink_config_from_settings(&settings);
198 assert!(!config.is_configured());
199 }
200
201 #[rstest]
202 fn test_ios_bridge() {
203 let settings = DeeplinkSettings {
204 ios: Some(IosSettings {
205 app_id: "TEAM.com.example".to_string(),
206 paths: vec!["/products/*".to_string()],
207 exclude_paths: vec!["/api/*".to_string()],
208 app_clips: vec!["TEAM.com.example.Clip".to_string()],
209 web_credentials: true,
210 }),
211 ..Default::default()
212 };
213 let config = DeeplinkConfig::from(&settings);
214 assert!(config.has_ios());
215 let ios = config.ios.expect("ios config present");
216 let json = serde_json::to_string(&ios).expect("serialize ios config");
217 assert!(json.contains("TEAM.com.example"));
218 assert!(json.contains("/products/*"));
219 assert!(json.contains("/api/*"));
220 assert!(json.contains("webcredentials"));
221 assert!(json.contains("appclips"));
222 }
223
224 #[rstest]
225 fn test_android_bridge() {
226 let settings = DeeplinkSettings {
227 android: Some(AndroidSettings {
228 package_name: "com.example.app".to_string(),
229 sha256_cert_fingerprints: vec![VALID_FINGERPRINT.to_string()],
230 }),
231 ..Default::default()
232 };
233 let config = DeeplinkConfig::from(&settings);
234 assert!(config.has_android());
235 let android = config.android.expect("android config present");
236 assert_eq!(android.statements.len(), 1);
237 assert_eq!(android.statements[0].target.package_name, "com.example.app");
238 assert_eq!(
239 android.statements[0].target.sha256_cert_fingerprints,
240 vec![VALID_FINGERPRINT.to_string()]
241 );
242 }
243
244 #[rstest]
245 fn test_custom_schemes_bridge() {
246 let settings = DeeplinkSettings {
247 custom_schemes: vec![CustomSchemeSettings {
248 name: "myapp".to_string(),
249 hosts: vec!["open".to_string()],
250 paths: vec!["/products/*".to_string()],
251 }],
252 ..Default::default()
253 };
254 let config = DeeplinkConfig::from(&settings);
255 assert!(config.has_custom_schemes());
256 assert_eq!(config.custom_schemes.len(), 1);
257 assert_eq!(config.custom_schemes[0].name, "myapp");
258 assert_eq!(config.custom_schemes[0].hosts, vec!["open".to_string()]);
259 assert_eq!(
260 config.custom_schemes[0].paths,
261 vec!["/products/*".to_string()]
262 );
263 }
264
265 #[rstest]
266 fn test_settings_roundtrip_serde() {
267 let settings = DeeplinkSettings {
268 ios: Some(IosSettings {
269 app_id: "TEAM.com.example".to_string(),
270 paths: vec!["/".to_string()],
271 exclude_paths: Vec::new(),
272 app_clips: Vec::new(),
273 web_credentials: false,
274 }),
275 android: None,
276 custom_schemes: Vec::new(),
277 };
278 let json = serde_json::to_string(&settings).expect("serialize settings");
279 let parsed: DeeplinkSettings = serde_json::from_str(&json).expect("deserialize settings");
280 assert_eq!(settings, parsed);
281 }
282}