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
6// The router stores and consumes the deprecated `DeeplinkConfig` during the 0.2
7// compatibility window. Remove this allowance once `DeeplinkConfig` is deleted.
8#![allow(deprecated)]
9
10use async_trait::async_trait;
11use hyper::Method;
12use reinhardt_core::endpoint::EndpointInfo;
13use reinhardt_http::{Handler, Request, Response};
14use reinhardt_urls::routers::{ServerRouter, UnifiedRouter};
15
16use crate::config::DeeplinkConfig;
17use crate::endpoints::{AasaHandler, AssetLinksHandler};
18use crate::error::DeeplinkError;
19
20#[derive(Clone)]
21struct AasaEndpoint {
22	handler: AasaHandler,
23}
24
25#[derive(Clone)]
26struct AasaJsonEndpoint {
27	handler: AasaHandler,
28}
29
30#[derive(Clone)]
31struct AssetLinksEndpoint {
32	handler: AssetLinksHandler,
33}
34
35impl EndpointInfo for AasaEndpoint {
36	fn path() -> &'static str {
37		"/apple-app-site-association"
38	}
39
40	fn method() -> Method {
41		Method::GET
42	}
43
44	fn name() -> &'static str {
45		"apple-app-site-association"
46	}
47}
48
49impl EndpointInfo for AasaJsonEndpoint {
50	fn path() -> &'static str {
51		"/apple-app-site-association.json"
52	}
53
54	fn method() -> Method {
55		Method::GET
56	}
57
58	fn name() -> &'static str {
59		"apple-app-site-association-json"
60	}
61}
62
63impl EndpointInfo for AssetLinksEndpoint {
64	fn path() -> &'static str {
65		"/assetlinks.json"
66	}
67
68	fn method() -> Method {
69		Method::GET
70	}
71
72	fn name() -> &'static str {
73		"assetlinks"
74	}
75}
76
77#[async_trait]
78impl Handler for AasaEndpoint {
79	async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
80		self.handler.handle(request).await
81	}
82}
83
84#[async_trait]
85impl Handler for AasaJsonEndpoint {
86	async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
87		self.handler.handle(request).await
88	}
89}
90
91#[async_trait]
92impl Handler for AssetLinksEndpoint {
93	async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
94		self.handler.handle(request).await
95	}
96}
97
98/// Dedicated router for deeplink endpoints.
99///
100/// This router handles the well-known endpoints required for mobile deep linking:
101///
102/// - `GET /.well-known/apple-app-site-association` - iOS Universal Links
103/// - `GET /.well-known/apple-app-site-association.json` - iOS Universal Links (alternative)
104/// - `GET /.well-known/assetlinks.json` - Android App Links
105///
106/// # Example
107///
108/// ```rust
109/// # #![allow(deprecated)]
110/// use reinhardt_deeplink::{DeeplinkRouter, DeeplinkConfig, IosConfig, AndroidConfig};
111///
112/// let config = DeeplinkConfig::builder()
113///     .ios(
114///         IosConfig::builder()
115///             .app_id("TEAM.com.example")
116///             .paths(&["/"])
117///             .build()
118///     )
119///     .android(
120///         AndroidConfig::builder()
121///             .package_name("com.example.app")
122///             .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")
123///             .build()
124///             .unwrap()
125///     )
126///     .build();
127///
128/// let router = DeeplinkRouter::new(config).unwrap();
129/// ```
130pub struct DeeplinkRouter {
131	/// The deeplink configuration.
132	config: DeeplinkConfig,
133
134	/// The underlying server router.
135	server: ServerRouter,
136}
137
138impl std::fmt::Debug for DeeplinkRouter {
139	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140		f.debug_struct("DeeplinkRouter")
141			.field("config", &self.config)
142			.field("server", &"ServerRouter { ... }")
143			.finish()
144	}
145}
146
147impl DeeplinkRouter {
148	/// Creates a new deeplink router with the given configuration.
149	///
150	/// # Errors
151	///
152	/// Returns an error if:
153	/// - iOS is configured but JSON serialization fails
154	/// - Android is configured but JSON serialization fails
155	pub fn new(config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
156		let mut server = ServerRouter::new().with_namespace("wellknown");
157
158		// Register iOS Universal Links endpoints
159		if let Some(ios_config) = &config.ios {
160			let aasa_handler = AasaHandler::new(ios_config.clone())?;
161
162			// Register at both paths (some tools expect .json extension)
163			server = server
164				.endpoint(|| AasaEndpoint {
165					handler: aasa_handler.clone(),
166				})
167				.endpoint(|| AasaJsonEndpoint {
168					handler: aasa_handler,
169				});
170		}
171
172		// Register Android App Links endpoint
173		if let Some(android_config) = &config.android {
174			let assetlinks_handler = AssetLinksHandler::new(android_config.clone())?;
175			server = server.endpoint(|| AssetLinksEndpoint {
176				handler: assetlinks_handler,
177			});
178		}
179
180		Ok(Self { config, server })
181	}
182
183	/// Converts this router into a `ServerRouter`.
184	///
185	/// This is useful when you need to mount the deeplink router
186	/// onto another router manually.
187	pub fn into_server(self) -> ServerRouter {
188		self.server
189	}
190
191	/// Returns a reference to the underlying `ServerRouter`.
192	pub fn server(&self) -> &ServerRouter {
193		&self.server
194	}
195
196	/// Returns a reference to the configuration.
197	pub fn config(&self) -> &DeeplinkConfig {
198		&self.config
199	}
200}
201
202/// Extension trait for integrating deeplinks with `UnifiedRouter`.
203///
204/// This trait provides a convenient method to add deeplink support to
205/// any `UnifiedRouter`.
206///
207/// # Example
208///
209/// ```rust,ignore
210/// use reinhardt_urls::routers::UnifiedRouter;
211/// use reinhardt_deeplink::{DeeplinkRouterExt, DeeplinkConfig, IosConfig};
212///
213/// let config = DeeplinkConfig::builder()
214///     .ios(
215///         IosConfig::builder()
216///             .app_id("TEAM.com.example")
217///             .paths(&["/"])
218///             .build()
219///     )
220///     .build();
221///
222/// let router = UnifiedRouter::new()
223///     .with_deeplinks(config)
224///     .unwrap();
225/// ```
226pub trait DeeplinkRouterExt {
227	/// The output type after adding deeplinks.
228	type Output;
229
230	/// Adds deeplink handlers to the router.
231	///
232	/// This mounts the deeplink handlers under the `/.well-known/` path prefix.
233	///
234	/// # Errors
235	///
236	/// Returns an error if the deeplink router cannot be created.
237	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self::Output, DeeplinkError>;
238}
239
240impl DeeplinkRouterExt for UnifiedRouter {
241	type Output = Self;
242
243	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
244		let deeplink_router = DeeplinkRouter::new(config)?;
245		Ok(self.mount("/.well-known/", deeplink_router.into_server()))
246	}
247}
248
249impl DeeplinkRouterExt for ServerRouter {
250	type Output = Self;
251
252	fn with_deeplinks(self, config: DeeplinkConfig) -> Result<Self, DeeplinkError> {
253		let deeplink_router = DeeplinkRouter::new(config)?;
254		Ok(self.mount("/.well-known/", deeplink_router.into_server()))
255	}
256}
257
258#[cfg(test)]
259mod tests {
260	use rstest::rstest;
261
262	use super::*;
263	use crate::config::{AndroidConfig, IosConfig};
264
265	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";
266
267	fn create_ios_config() -> IosConfig {
268		IosConfig::builder()
269			.app_id("TEAM123456.com.example.app")
270			.paths(&["/products/*"])
271			.build()
272	}
273
274	fn create_android_config() -> AndroidConfig {
275		AndroidConfig::builder()
276			.package_name("com.example.app")
277			.sha256_fingerprint(VALID_FINGERPRINT)
278			.build()
279			.unwrap()
280	}
281
282	#[rstest]
283	fn test_router_creation_ios_only() {
284		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
285
286		let router = DeeplinkRouter::new(config).unwrap();
287		assert!(router.config().has_ios());
288		assert!(!router.config().has_android());
289	}
290
291	#[rstest]
292	fn test_router_creation_android_only() {
293		let config = DeeplinkConfig::builder()
294			.android(create_android_config())
295			.build();
296
297		let router = DeeplinkRouter::new(config).unwrap();
298		assert!(!router.config().has_ios());
299		assert!(router.config().has_android());
300	}
301
302	#[rstest]
303	fn test_router_creation_both() {
304		let config = DeeplinkConfig::builder()
305			.ios(create_ios_config())
306			.android(create_android_config())
307			.build();
308
309		let router = DeeplinkRouter::new(config).unwrap();
310		assert!(router.config().has_ios());
311		assert!(router.config().has_android());
312	}
313
314	#[rstest]
315	fn test_into_server() {
316		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
317
318		let router = DeeplinkRouter::new(config).unwrap();
319		let _server = router.into_server();
320	}
321
322	#[rstest]
323	fn test_extension_trait_unified() {
324		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
325
326		let router = UnifiedRouter::new().with_deeplinks(config).unwrap();
327
328		// Verify the router was created (we can't easily test the routes without making requests)
329		let _ = router;
330	}
331
332	#[rstest]
333	fn test_extension_trait_server() {
334		let config = DeeplinkConfig::builder().ios(create_ios_config()).build();
335
336		let router = ServerRouter::new().with_deeplinks(config).unwrap();
337
338		// Verify the router was created
339		let _ = router;
340	}
341
342	#[rstest]
343	fn test_empty_config() {
344		let config = DeeplinkConfig::default();
345		let router = DeeplinkRouter::new(config).unwrap();
346
347		// Empty config should still create a valid router
348		assert!(!router.config().is_configured());
349	}
350}