1use dtmrs_core::{BranchOp, BranchResult};
19use std::collections::HashMap;
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::Arc;
23
24#[derive(Debug, Clone)]
26pub struct BranchCtx {
27 pub gid: String,
28 pub branch_id: String,
29 pub op: BranchOp,
30 pub trans_type: String,
31}
32
33type BoxFut = Pin<Box<dyn Future<Output = BranchResult> + Send>>;
34type Handler = Arc<dyn Fn(BranchCtx) -> BoxFut + Send + Sync>;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Target {
42 Local(String),
44 Http(String),
46 Grpc(GrpcTarget),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct GrpcTarget {
57 pub endpoint: String,
59 pub path: String,
61 pub tls: bool,
64}
65
66pub fn parse_target(s: &str) -> Target {
67 if let Some(name) = s.strip_prefix("local://") {
68 return Target::Local(name.to_string());
69 }
70 for (prefix, scheme, tls) in [
75 ("grpcs://", "https", true),
76 ("grpc://", "http", false),
77 ] {
78 if let Some(rest) = s.strip_prefix(prefix) {
79 if let Some(t) = parse_grpc(rest, scheme, tls) {
82 return Target::Grpc(t);
83 }
84 }
85 }
86 Target::Http(s.to_string())
87}
88
89fn parse_grpc(rest: &str, scheme: &str, tls: bool) -> Option<GrpcTarget> {
94 let (authority, path) = rest.split_once('/')?;
95 if authority.is_empty() {
96 return None;
97 }
98 let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
99 if segs.len() != 2 {
100 return None;
101 }
102 Some(GrpcTarget {
103 endpoint: format!("{scheme}://{authority}"),
104 path: format!("/{}/{}", segs[0], segs[1]),
105 tls,
106 })
107}
108
109#[derive(Default)]
110pub struct Registry {
111 handlers: HashMap<String, Handler>,
112}
113
114impl Registry {
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn register<F, Fut>(&mut self, name: &str, f: F) -> &mut Self
121 where
122 F: Fn(BranchCtx) -> Fut + Send + Sync + 'static,
123 Fut: Future<Output = BranchResult> + Send + 'static,
124 {
125 let h: Handler = Arc::new(move |ctx| Box::pin(f(ctx)));
126 self.handlers.insert(name.to_string(), h);
127 self
128 }
129
130 pub fn get(&self, name: &str) -> Option<Handler> {
131 self.handlers.get(name).cloned()
132 }
133
134 pub fn names(&self) -> Vec<&str> {
135 self.handlers.keys().map(String::as_str).collect()
136 }
137
138 pub fn check_all(&self, targets: &[String]) -> Result<(), Vec<String>> {
143 let missing: Vec<String> = targets
144 .iter()
145 .filter_map(|t| match parse_target(t) {
146 Target::Local(n) if !self.handlers.contains_key(&n) => Some(n),
147 _ => None,
148 })
149 .collect();
150 if missing.is_empty() {
151 Ok(())
152 } else {
153 Err(missing)
154 }
155 }
156}
157
158impl std::fmt::Debug for Registry {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 f.debug_struct("Registry")
161 .field("handlers", &self.names())
162 .finish()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn 前缀区分本地与远端() {
172 assert_eq!(
173 parse_target("local://deduct"),
174 Target::Local("deduct".into())
175 );
176 assert_eq!(
177 parse_target("http://busi/deduct"),
178 Target::Http("http://busi/deduct".into())
179 );
180 assert_eq!(
182 parse_target("https://a/b"),
183 Target::Http("https://a/b".into())
184 );
185 }
186
187 #[test]
188 fn grpc地址拆成端点与方法路径() {
189 let Target::Grpc(t) = parse_target("grpc://127.0.0.1:9000/busi.Busi/Deduct") else {
190 panic!("应该认成 grpc");
191 };
192 assert_eq!(t.endpoint, "http://127.0.0.1:9000");
194 assert_eq!(t.path, "/busi.Busi/Deduct");
195 assert!(!t.tls, "grpc:// 是明文");
196 }
197
198 #[test]
199 fn grpcs走tls且端点是https() {
200 let Target::Grpc(t) = parse_target("grpcs://busi.internal:9000/busi.Busi/Deduct") else {
201 panic!("应该认成 grpc");
202 };
203 assert_eq!(t.endpoint, "https://busi.internal:9000");
204 assert_eq!(t.path, "/busi.Busi/Deduct");
205 assert!(t.tls, "grpcs:// 必须走 TLS");
206 }
207
208 #[test]
212 fn grpcs绝不能静默降级成明文() {
213 for s in [
214 "grpcs://a:1/p.S/M",
215 "grpcs://a:1/bad", "grpcs://", ] {
218 if let Target::Grpc(t) = parse_target(s) {
219 assert!(t.tls, "{s} 认成了 grpc 却没开 TLS —— 这是静默降级成明文");
220 assert!(
221 t.endpoint.starts_with("https://"),
222 "{s} 的端点不是 https:{}",
223 t.endpoint
224 );
225 }
226 }
227 }
228
229 #[test]
230 fn 畸形grpc地址不猜而是落回http() {
231 for bad in [
234 "grpc://127.0.0.1:9000/onlyservice",
235 "grpc://127.0.0.1:9000/",
236 "grpc://127.0.0.1:9000/a/b/c",
237 "grpc:///a/b",
238 "grpc://noslash",
239 ] {
240 assert!(
241 matches!(parse_target(bad), Target::Http(_)),
242 "{bad} 不该被当成合法 grpc 地址"
243 );
244 }
245 }
246
247 #[tokio::test]
248 async fn 注册与调用() {
249 let mut r = Registry::new();
250 r.register("ok", |_ctx| async { BranchResult::Success });
251 let h = r.get("ok").expect("应该能查到");
252 let ctx = BranchCtx {
253 gid: "g".into(),
254 branch_id: "01".into(),
255 op: BranchOp::Action,
256 trans_type: "saga".into(),
257 };
258 assert_eq!(h(ctx).await, BranchResult::Success);
259 assert!(r.get("nope").is_none());
260 }
261
262 #[test]
263 fn 提交前能查出漏注册的分支() {
264 let mut r = Registry::new();
265 r.register("a", |_| async { BranchResult::Success });
266 let targets = vec![
267 "local://a".to_string(),
268 "local://missing".to_string(),
269 "http://x/y".to_string(),
270 ];
271 let err = r.check_all(&targets).unwrap_err();
272 assert_eq!(err, vec!["missing"], "只报本地漏的,http 不管");
273 assert!(r.check_all(&["local://a".to_string()]).is_ok());
274 }
275}