Skip to main content

reinhardt_deeplink/
router.rs

1//! Router integration for deeplink endpoints.
2//!
3//! This module provides router types and extension traits for integrating
4//! deeplink handlers with the Reinhardt routing system.
5
6use hyper::Method;
7use reinhardt_urls::routers::{ServerRouter, UnifiedRouter};
8
9use crate::config::DeeplinkConfig;
10use crate::endpoints::{AasaHandler, AssetLinksHandler};
11use crate::error::DeeplinkError;
12
13/// Dedicated router for deeplink endpoints.
14///
15/// This router handles the well-known endpoints required for mobile deep linking:
16///
17/// - `GET /.well-known/apple-app-site-association` - iOS Universal Links
18/// - `GET /.well-known/apple-app-site-association.json` - iOS Universal Links (alternative)
19/// - `GET /.well-known/assetlinks.json` - Android App Links
20///
21/// # Example
22///
23/// ```rust
24/// use reinhardt_deeplink::{DeeplinkRouter, DeeplinkConfig, IosConfig, AndroidConfig};
25///
26/// let config = DeeplinkConfig::builder()
27///     .ios(
28///         IosConfig::builder()
29///             .app_id("TEAM.com.example")
30///             .paths(&["/"])
31///             .build()
32///     )
33///     .android(
34///         AndroidConfig::builder()
35///             .package_name("com.example.app")
36///             .sha256_fingerprint("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")
37///             .build()
38///             .unwrap()
39///     )
40///     .build();
41///
42/// let router = DeeplinkRouter::new(config).unwrap();
43/// ```
44pub struct DeeplinkRouter {
45	/// The deeplink configuration.
46	config: DeeplinkConfig,
47
48	/// The underlying server router.
49	server: ServerRouter,
50}
51
52impl std::fmt::Debug for DeeplinkRouter {
53	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54		f.debug_struct("DeeplinkRouter")
55			.field("config", &self.config)
56			.field("server", &"ServerRouter { ... }")
57			.finish()
58	}
59}
60
61impl DeeplinkRouter {
62	/// Creates a new deeplink router with the given configuration.
63	///
64	/// # Errors
65	///
66	/// Returns an error if:
67	/// - iOS is configured but JSON serialization fails
68	/// - Android is configured but JSON serialization fails
69	pub fn new(config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
70		let mut server = ServerRouter::new().with_namespace("wellknown");
71
72		// Register iOS Universal Links endpoints
73		if let Some(ios_config) = &config.ios {
74			let aasa_handler = AasaHandler::new(ios_config.clone())?;
75
76			// Register at both paths (some tools expect .json extension)
77			server = server
78				.handler_with_method(
79					"/apple-app-site-association",
80					Method::GET,
81					aasa_handler.clone(),
82				)
83				.handler_with_method(
84					"/apple-app-site-association.json",
85					Method::GET,
86					aasa_handler,
87				);
88		}
89
90		// Register Android App Links endpoint
91		if let Some(android_config) = &config.android {
92			let assetlinks_handler = AssetLinksHandler::new(android_config.clone())?;
93			server =
94				server.handler_with_method("/assetlinks.json", Method::GET, assetlinks_handler);
95		}
96
97		Ok(Self { config, server })
98	}
99
100	/// Converts this router into a `ServerRouter`.
101	///
102	/// This is useful when you need to mount the deeplink router
103	/// onto another router manually.
104	pub fn into_server(self) -> ServerRouter {
105		self.server
106	}
107
108	/// Returns a reference to the underlying `ServerRouter`.
109	pub fn server(&self) -> &ServerRouter {
110		&self.server
111	}
112
113	/// Returns a reference to the configuration.
114	pub fn config(&self) -> &DeeplinkConfig {
115		&self.config
116	}
117}
118
119/// Extension trait for integrating deeplinks with `UnifiedRouter`.
120///
121/// This trait provides a convenient method to add deeplink support to
122/// any `UnifiedRouter`.
123///
124/// # Example
125///
126/// ```rust,ignore
127/// use reinhardt_urls::routers::UnifiedRouter;
128/// use reinhardt_deeplink::{DeeplinkRouterExt, DeeplinkConfig, IosConfig};
129///
130/// let config = DeeplinkConfig::builder()
131///     .ios(
132///         IosConfig::builder()
133///             .app_id("TEAM.com.example")
134///             .paths(&["/"])
135///             .build()
136///     )
137///     .build();
138///
139/// let router = UnifiedRouter::new()
140///     .with_deeplinks(config)
141///     .unwrap();
142/// ```
143pub trait DeeplinkRouterExt {
144	/// The output type after adding deeplinks.
145	type Output;
146
147	/// Adds deeplink handlers to the router.
148	///
149	/// This mounts the deeplink handlers under the `/.well-known/` path prefix.
150	///
151	/// # Errors
152	///
153	/// Returns an error if the deeplink router cannot be created.
154	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self::Output, DeeplinkError>;
155}
156
157impl DeeplinkRouterExt for UnifiedRouter {
158	type Output = Self;
159
160	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
161		let deeplink_router = DeeplinkRouter::new(config)?;
162		Ok(self.mount("/.well-known/", deeplink_router.into_server()))
163	}
164}
165
166impl DeeplinkRouterExt for ServerRouter {
167	type Output = Self;
168
169	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
170		let deeplink_router = DeeplinkRouter::new(config)?;
171		Ok(self.mount("/.well-known/", deeplink_router.into_server()))
172	}
173}
174
175#[cfg(test)]
176mod tests {
177	use rstest::rstest;
178
179	use super::*;
180	use crate::config::{AndroidConfig, IosConfig};
181
182	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";
183
184	fn create_ios_config() -> IosConfig {
185		IosConfig::builder()
186			.app_id("TEAM123456.com.example.app")
187			.paths(&["/products/*"])
188			.build()
189	}
190
191	fn create_android_config() -> AndroidConfig {
192		AndroidConfig::builder()
193			.package_name("com.example.app")
194			.sha256_fingerprint(VALID_FINGERPRINT)
195			.build()
196			.unwrap()
197	}
198
199	#[rstest]
200	fn test_router_creation_ios_only() {
201		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
202
203		let router = DeeplinkRouter::new(config).unwrap();
204		assert!(router.config().has_ios());
205		assert!(!router.config().has_android());
206	}
207
208	#[rstest]
209	fn test_router_creation_android_only() {
210		let config = DeeplinkConfig::builder()
211			.android(create_android_config())
212			.build();
213
214		let router = DeeplinkRouter::new(config).unwrap();
215		assert!(!router.config().has_ios());
216		assert!(router.config().has_android());
217	}
218
219	#[rstest]
220	fn test_router_creation_both() {
221		let config = DeeplinkConfig::builder()
222			.ios(create_ios_config())
223			.android(create_android_config())
224			.build();
225
226		let router = DeeplinkRouter::new(config).unwrap();
227		assert!(router.config().has_ios());
228		assert!(router.config().has_android());
229	}
230
231	#[rstest]
232	fn test_into_server() {
233		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
234
235		let router = DeeplinkRouter::new(config).unwrap();
236		let _server = router.into_server();
237	}
238
239	#[rstest]
240	fn test_extension_trait_unified() {
241		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
242
243		let router = UnifiedRouter::new().with_deeplinks(config).unwrap();
244
245		// Verify the router was created (we can't easily test the routes without making requests)
246		let _ = router;
247	}
248
249	#[rstest]
250	fn test_extension_trait_server() {
251		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
252
253		let router = ServerRouter::new().with_deeplinks(config).unwrap();
254
255		// Verify the router was created
256		let _ = router;
257	}
258
259	#[rstest]
260	fn test_empty_config() {
261		let config = DeeplinkConfig::default();
262		let router = DeeplinkRouter::new(config).unwrap();
263
264		// Empty config should still create a valid router
265		assert!(!router.config().is_configured());
266	}
267}