1use std::sync::Arc;
5
6use percent_encoding::percent_decode_str;
7use serde_json::Value;
8
9use crate::{
10 DesktopError, DesktopHttpMethod, DesktopProtocolRequest, DesktopProtocolResponse, Result,
11};
12
13type RouteStateHandler = dyn Fn(RouteContext<'_>) -> Result<Value> + Send + Sync;
14type ApiHandler = dyn Fn(ApiContext<'_>) -> Result<DesktopProtocolResponse> + Send + Sync;
15
16#[derive(Default)]
18pub struct RouteStateRegistry {
19 routes: Vec<RouteStateEntry>,
20}
21
22#[derive(Default)]
24pub struct ApiRouteRegistry {
25 routes: Vec<ApiRouteEntry>,
26}
27
28impl ApiRouteRegistry {
29 #[must_use]
31 pub fn new() -> Self {
32 Self { routes: Vec::new() }
33 }
34
35 pub fn route<F>(&mut self, pattern: impl AsRef<str>, handler: F) -> Result<()>
41 where
42 F: Fn(ApiContext<'_>) -> Result<DesktopProtocolResponse> + Send + Sync + 'static,
43 {
44 self.routes.push(ApiRouteEntry {
45 pattern: RoutePattern::parse(pattern.as_ref())?,
46 handler: Arc::new(handler),
47 });
48 Ok(())
49 }
50
51 pub(crate) fn resolve(
52 &self,
53 request: &DesktopProtocolRequest<'_>,
54 ) -> Result<Option<DesktopProtocolResponse>> {
55 let path = route_path(request.path);
56 for entry in &self.routes {
57 let Some(params) = entry.pattern.matches(path) else {
58 continue;
59 };
60 return (entry.handler)(ApiContext {
61 method: &request.method,
62 path,
63 params: ¶ms,
64 body: request.body,
65 })
66 .map(Some);
67 }
68 Ok(None)
69 }
70}
71
72struct ApiRouteEntry {
73 pattern: RoutePattern,
74 handler: Arc<ApiHandler>,
75}
76
77impl RouteStateRegistry {
78 pub(crate) fn has_provider(&self, path: &str) -> bool {
79 self.routes
80 .iter()
81 .any(|entry| entry.pattern.matches(path).is_some())
82 }
83
84 #[must_use]
86 pub fn new() -> Self {
87 Self { routes: Vec::new() }
88 }
89
90 pub fn route<F>(&mut self, pattern: impl AsRef<str>, handler: F) -> Result<()>
96 where
97 F: Fn(RouteContext<'_>) -> Result<Value> + Send + Sync + 'static,
98 {
99 self.routes.push(RouteStateEntry {
100 pattern: RoutePattern::parse(pattern.as_ref())?,
101 handler: Arc::new(handler),
102 });
103 Ok(())
104 }
105
106 pub(crate) fn resolve(&self, path: &str, base_state: &Value) -> Result<Option<Value>> {
107 for entry in &self.routes {
108 let Some(params) = entry.pattern.matches(path) else {
109 continue;
110 };
111 return (entry.handler)(RouteContext {
112 path,
113 params: ¶ms,
114 base_state,
115 })
116 .map(Some)
117 .map_err(|err| DesktopError::RouteProvider {
118 path: path.to_string(),
119 message: err.chain_message(),
120 });
121 }
122 Ok(None)
123 }
124}
125
126struct RouteStateEntry {
127 pattern: RoutePattern,
128 handler: Arc<RouteStateHandler>,
129}
130
131struct RoutePattern {
132 segments: Vec<RouteSegment>,
133}
134
135enum RouteSegment {
136 Literal(String),
137 Param(String),
138}
139
140pub struct RouteContext<'a> {
142 pub path: &'a str,
144 params: &'a [(String, String)],
145 pub base_state: &'a Value,
147}
148
149pub struct ApiContext<'a> {
151 pub method: &'a DesktopHttpMethod,
153 pub path: &'a str,
155 params: &'a [(String, String)],
156 pub body: &'a [u8],
158}
159
160impl ApiContext<'_> {
161 #[must_use]
163 pub fn param(&self, name: &str) -> Option<&str> {
164 self.params
165 .iter()
166 .find_map(|(key, value)| (key == name).then_some(value.as_str()))
167 }
168}
169
170impl RouteContext<'_> {
171 #[must_use]
173 pub fn param(&self, name: &str) -> Option<&str> {
174 self.params
175 .iter()
176 .find_map(|(key, value)| (key == name).then_some(value.as_str()))
177 }
178}
179
180impl RoutePattern {
181 fn parse(pattern: &str) -> Result<Self> {
182 if !pattern.starts_with('/') {
183 return Err(DesktopError::InvalidRoutePattern {
184 pattern: pattern.to_string(),
185 help: "desktop route patterns must start with '/', e.g. /contacts/:id".to_string(),
186 });
187 }
188 let trimmed = pattern.trim_matches('/');
189 let mut segments = Vec::new();
190 if !trimmed.is_empty() {
191 for segment in trimmed.split('/') {
192 if segment.is_empty() || segment == "." || segment == ".." {
193 return Err(DesktopError::InvalidRoutePattern {
194 pattern: pattern.to_string(),
195 help: "route pattern segments cannot be empty, '.', or '..'".to_string(),
196 });
197 }
198 if let Some(param) = segment.strip_prefix(':') {
199 if param.is_empty() {
200 return Err(DesktopError::InvalidRoutePattern {
201 pattern: pattern.to_string(),
202 help: "route parameter names cannot be empty".to_string(),
203 });
204 }
205 segments.push(RouteSegment::Param(param.to_string()));
206 } else {
207 segments.push(RouteSegment::Literal(segment.to_string()));
208 }
209 }
210 }
211 Ok(Self { segments })
212 }
213
214 fn matches(&self, path: &str) -> Option<Vec<(String, String)>> {
215 let trimmed = route_path(path).trim_matches('/');
216 if trimmed.is_empty() {
217 return self.segments.is_empty().then(Vec::new);
218 }
219 if trimmed.split('/').count() != self.segments.len() {
220 return None;
221 }
222 let mut params = Vec::new();
223 for (pattern, raw_segment) in self.segments.iter().zip(trimmed.split('/')) {
224 let segment = percent_decode_str(raw_segment)
225 .decode_utf8()
226 .map(|value| value.into_owned())
227 .unwrap_or_else(|_| raw_segment.to_string());
228 match pattern {
229 RouteSegment::Literal(expected) if expected == &segment => {}
230 RouteSegment::Literal(_) => return None,
231 RouteSegment::Param(name) => params.push((name.clone(), segment)),
232 }
233 }
234 Some(params)
235 }
236}
237
238pub(crate) fn route_path(request_path: &str) -> &str {
239 request_path
240 .split_once('?')
241 .map_or(request_path, |(path, _)| path)
242}